992653d7e31d9a74226712ca0f4b00fa601791e9
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / htmlarea.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2002-2004 interactivetools.com, inc.
5 * (c) 2003-2004 dynarch.com
6 * (c) 2004-2012 Stanislas Rolland <typo3(arobas)sjbr.ca>
7 * All rights reserved
8 *
9 * This script is part of the TYPO3 project. The TYPO3 project is
10 * free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * The GNU General Public License can be found at
16 * http://www.gnu.org/copyleft/gpl.html.
17 * A copy is found in the textfile GPL.txt and important notices to the license
18 * from the author is found in LICENSE.txt distributed with these scripts.
19 *
20 *
21 * This script is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * This script is a modified version of a script published under the htmlArea License.
27 * A copy of the htmlArea License may be found in the textfile HTMLAREA_LICENSE.txt.
28 *
29 * This copyright notice MUST APPEAR in all copies of the script!
30 ***************************************************************/
31 /*
32 * Main script of TYPO3 htmlArea RTE
33 */
34 // Avoid re-initialization on AJax call when HTMLArea object was already initialized
35 if (typeof(HTMLArea) == 'undefined') {
36 // Establish HTMLArea name space
37 Ext.namespace('HTMLArea.CSS', 'HTMLArea.util.TYPO3', 'HTMLArea.util.Tips', 'HTMLArea.util.Color', 'Ext.ux.form', 'Ext.ux.menu', 'Ext.ux.Toolbar');
38 Ext.apply(HTMLArea, {
39 /***************************************************
40 * COMPILED REGULAR EXPRESSIONS *
41 ***************************************************/
42 RE_htmlTag : /<.[^<>]*?>/g,
43 RE_tagName : /(<\/|<)\s*([^ \t\n>]+)/ig,
44 RE_head : /<head>((.|\n)*?)<\/head>/i,
45 RE_body : /<body>((.|\n)*?)<\/body>/i,
46 // This expression is deprecated as of TYPO3 4.7
47 Reg_body : new RegExp('<\/?(body)[^>]*>', 'gi'),
48 reservedClassNames : /htmlarea/,
49 RE_email : /([0-9a-z]+([a-z0-9_-]*[0-9a-z])*){1}(\.[0-9a-z]+([a-z0-9_-]*[0-9a-z])*)*@([0-9a-z]+([a-z0-9_-]*[0-9a-z])*\.)+[a-z]{2,9}/i,
50 RE_url : /(([^:/?#]+):\/\/)?(([a-z0-9_]+:[a-z0-9_]+@)?[a-z0-9_-]{2,}(\.[a-z0-9_-]{2,})+\.[a-z]{2,5}(:[0-9]+)?(\/\S+)*\/?)/i,
51 RE_numberOrPunctuation : /[0-9.(),;:!¡?¿%#$'"_+=\\\/-]*/g,
52 /***************************************************
53 * BROWSER IDENTIFICATION *
54 ***************************************************/
55 isIEBeforeIE9: Ext.isIE6 || Ext.isIE7 || Ext.isIE8 || (Ext.isIE && typeof(document.documentMode) !== 'undefined' && document.documentMode < 9),
56 /***************************************************
57 * LOCALIZATION *
58 ***************************************************/
59 localize: function (label, plural) {
60 var i = plural || 0;
61 var localized = HTMLArea.I18N.dialogs[label] || HTMLArea.I18N.tooltips[label] || HTMLArea.I18N.msg[label] || '';
62 if (typeof localized === 'object' && typeof localized[i] !== 'undefined') {
63 localized = localized[i]['target'];
64 }
65 return localized;
66 },
67 /***************************************************
68 * INITIALIZATION *
69 ***************************************************/
70 init: function () {
71 if (!HTMLArea.isReady) {
72 // Apply global configuration settings
73 Ext.apply(HTMLArea, RTEarea[0]);
74 Ext.applyIf(HTMLArea, {
75 editorSkin : HTMLArea.editorUrl + 'skins/default/',
76 editorCSS : HTMLArea.editorUrl + 'skins/default/htmlarea.css'
77 });
78 if (!Ext.isString(HTMLArea.editedContentCSS)) {
79 HTMLArea.editedContentCSS = HTMLArea.editorSkin + 'htmlarea-edited-content.css';
80 }
81 HTMLArea.isReady = true;
82 HTMLArea.appendToLog('', 'HTMLArea', 'init', 'Editor url set to: ' + HTMLArea.editorUrl, 'info');
83 HTMLArea.appendToLog('', 'HTMLArea', 'init', 'Editor skin CSS set to: ' + HTMLArea.editorCSS, 'info');
84 HTMLArea.appendToLog('', 'HTMLArea', 'init', 'Editor content skin CSS set to: ' + HTMLArea.editedContentCSS, 'info');
85 }
86 },
87 /*
88 * Create an editor when HTMLArea is loaded and when Ext is ready
89 *
90 * @param string editorId: the id of the editor
91 *
92 * @return boolean false if successful
93 */
94 initEditor: function (editorId) {
95 if (document.getElementById('pleasewait' + editorId)) {
96 if (HTMLArea.checkSupportedBrowser()) {
97 document.getElementById('pleasewait' + editorId).style.display = 'block';
98 document.getElementById('editorWrap' + editorId).style.visibility = 'hidden';
99 if (!HTMLArea.isReady) {
100 HTMLArea.initEditor.defer(150, null, [editorId]);
101 } else {
102 // Create an editor for the textarea
103 var editor = new HTMLArea.Editor(Ext.apply(new HTMLArea.Config(editorId), RTEarea[editorId]));
104 editor.generate();
105 return false;
106 }
107 } else {
108 document.getElementById('pleasewait' + editorId).style.display = 'none';
109 document.getElementById('editorWrap' + editorId).style.visibility = 'visible';
110 }
111 }
112 return true;
113 },
114 /*
115 * Check if the client agent is supported
116 *
117 * @return boolean true if the client is supported
118 */
119 checkSupportedBrowser: function () {
120 return Ext.isGecko || Ext.isWebKit || Ext.isOpera || Ext.isIE;
121 },
122 /*
123 * Write message to JavaScript console
124 *
125 * @param string editorId: the id of the editor issuing the message
126 * @param string objectName: the name of the object issuing the message
127 * @param string functionName: the name of the function issuing the message
128 * @param string text: the text of the message
129 * @param string type: the type of message: 'log', 'info', 'warn' or 'error'
130 *
131 * @return void
132 */
133 appendToLog: function (editorId, objectName, functionName, text, type) {
134 var str = 'RTE[' + editorId + '][' + objectName + '::' + functionName + ']: ' + text;
135 if (typeof(type) === 'undefined') {
136 var type = 'info';
137 }
138 if (typeof(console) !== 'undefined' && typeof(console) === 'object') {
139 // If console is TYPO3.Backend.DebugConsole, write only error messages
140 if (Ext.isFunction(console.addTab)) {
141 if (type === 'error') {
142 console[type](str);
143 }
144 } else {
145 console[type](str);
146 }
147 }
148 }
149 });
150 /***************************************************
151 * EDITOR CONFIGURATION
152 ***************************************************/
153 HTMLArea.Config = function (editorId) {
154 this.editorId = editorId;
155 // if the site is secure, create a secure iframe
156 this.useHTTPS = false;
157 // for Mozilla
158 this.useCSS = false;
159 this.enableMozillaExtension = true;
160 this.disableEnterParagraphs = false;
161 this.disableObjectResizing = false;
162 this.removeTrailingBR = true;
163 // style included in the iframe document
164 this.editedContentStyle = HTMLArea.editedContentCSS;
165 // content style
166 this.pageStyle = "";
167 // Maximum attempts at accessing the stylesheets
168 this.styleSheetsMaximumAttempts = 20;
169 // Remove tags (must be a regular expression)
170 this.htmlRemoveTags = /none/i;
171 // Remove tags and their contents (must be a regular expression)
172 this.htmlRemoveTagsAndContents = /none/i;
173 // Remove comments
174 this.htmlRemoveComments = false;
175 // Array of custom tags
176 this.customTags = [];
177 // BaseURL to be included in the iframe document
178 this.baseURL = document.baseURI;
179 // IE does not support document.baseURI
180 // Since document.URL is incorrect when using realurl, get first base tag and extract href
181 if (!this.baseURL) {
182 var baseTags = document.getElementsByTagName ('base');
183 if (baseTags.length > 0) {
184 this.baseURL = baseTags[0].href;
185 } else {
186 this.baseURL = document.URL;
187 }
188 }
189 if (this.baseURL && this.baseURL.match(/(.*\:\/\/.*\/)[^\/]*/)) {
190 this.baseURL = RegExp.$1;
191 }
192 // URL-s
193 this.popupURL = "popups/";
194 // DocumentType
195 this.documentType = '<!DOCTYPE html\r'
196 + ' PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\r'
197 + ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r';
198 this.blankDocument = '<html><head></head><body></body></html>';
199 // Hold the configuration of buttons and hot keys registered by plugins
200 this.buttonsConfig = {};
201 this.hotKeyList = {};
202 // Default configurations for toolbar items
203 this.configDefaults = {
204 all: {
205 xtype: 'htmlareabutton',
206 disabledClass: 'buttonDisabled',
207 textMode: false,
208 selection: false,
209 dialog: false,
210 hidden: false,
211 hideMode: 'display'
212 },
213 htmlareabutton: {
214 cls: 'button',
215 overCls: 'buttonHover',
216 // Erratic behaviour of click event in WebKit and IE browsers
217 clickEvent: (Ext.isWebKit || Ext.isIE) ? 'mousedown' : 'click'
218 },
219 htmlareacombo: {
220 cls: 'select',
221 typeAhead: true,
222 lastQuery: '',
223 triggerAction: 'all',
224 editable: !Ext.isIE,
225 selectOnFocus: !Ext.isIE,
226 validationEvent: false,
227 validateOnBlur: false,
228 submitValue: false,
229 forceSelection: true,
230 mode: 'local',
231 storeRoot: 'options',
232 storeFields: [ { name: 'text'}, { name: 'value'}],
233 valueField: 'value',
234 displayField: 'text',
235 labelSeparator: '',
236 hideLabel: true,
237 tpl: '<tpl for="."><div ext:qtip="{value}" style="text-align:left;font-size:11px;" class="x-combo-list-item">{text}</div></tpl>'
238 }
239 };
240 };
241 HTMLArea.Config = Ext.extend(HTMLArea.Config, {
242 /**
243 * Registers a button for inclusion in the toolbar, adding some standard configuration properties for the ExtJS widgets
244 *
245 * @param object buttonConfiguration: the configuration object of the button:
246 * id : unique id for the button
247 * tooltip : tooltip for the button
248 * textMode : enable in text mode
249 * context : disable if not inside one of listed elements
250 * hidden : hide in menu and show only in context menu
251 * selection : disable if there is no selection
252 * hotkey : hotkey character
253 * dialog : if true, the button opens a dialogue
254 * dimensions : the opening dimensions object of the dialogue window: { width: nn, height: mm }
255 * and potentially other ExtJS config properties (will be forwarded)
256 *
257 * @return boolean true if the button was successfully registered
258 */
259 registerButton: function (config) {
260 config.itemId = config.id;
261 if (Ext.type(this.buttonsConfig[config.id])) {
262 HTMLArea.appendToLog('', 'HTMLArea.Config', 'registerButton', 'A toolbar item with the same Id: ' + config.id + ' already exists and will be overidden.', 'warn');
263 }
264 // Apply defaults
265 config = Ext.applyIf(config, this.configDefaults['all']);
266 config = Ext.applyIf(config, this.configDefaults[config.xtype]);
267 // Set some additional properties
268 switch (config.xtype) {
269 case 'htmlareacombo':
270 if (config.options) {
271 // Create combo array store
272 config.store = new Ext.data.ArrayStore({
273 autoDestroy: true,
274 fields: config.storeFields,
275 data: config.options
276 });
277 } else if (config.storeUrl) {
278 // Create combo json store
279 config.store = new Ext.data.JsonStore({
280 autoDestroy: true,
281 autoLoad: true,
282 root: config.storeRoot,
283 fields: config.storeFields,
284 url: config.storeUrl
285 });
286 }
287 config.hideLabel = Ext.isEmpty(config.fieldLabel) || Ext.isIE6;
288 config.helpTitle = config.tooltip;
289 break;
290 default:
291 if (!config.iconCls) {
292 config.iconCls = config.id;
293 }
294 break;
295 }
296 config.cmd = config.id;
297 config.tooltip = { title: config.tooltip };
298 this.buttonsConfig[config.id] = config;
299 return true;
300 },
301 /*
302 * Register a hotkey with the editor configuration.
303 */
304 registerHotKey: function (hotKeyConfiguration) {
305 if (Ext.isDefined(this.hotKeyList[hotKeyConfiguration.id])) {
306 HTMLArea.appendToLog('', 'HTMLArea.Config', 'registerHotKey', 'A hotkey with the same key ' + hotKeyConfiguration.id + ' already exists and will be overidden.', 'warn');
307 }
308 if (Ext.isDefined(hotKeyConfiguration.cmd) && !Ext.isEmpty(hotKeyConfiguration.cmd) && Ext.isDefined(this.buttonsConfig[hotKeyConfiguration.cmd])) {
309 this.hotKeyList[hotKeyConfiguration.id] = hotKeyConfiguration;
310 return true;
311 } else {
312 HTMLArea.appendToLog('', 'HTMLArea.Config', 'registerHotKey', 'A hotkey with key ' + hotKeyConfiguration.id + ' could not be registered because toolbar item with id ' + hotKeyConfiguration.cmd + ' was not registered.', 'warn');
313 return false;
314 }
315 },
316 /*
317 * Get the configured document type for dialogue windows
318 */
319 getDocumentType: function () {
320 return this.documentType;
321 }
322 });
323 /***************************************************
324 * TOOLBAR COMPONENTS
325 ***************************************************/
326 /*
327 * Ext.ux.HTMLAreaButton extends Ext.Button
328 */
329 Ext.ux.HTMLAreaButton = Ext.extend(Ext.Button, {
330 /*
331 * Component initialization
332 */
333 initComponent: function () {
334 Ext.ux.HTMLAreaButton.superclass.initComponent.call(this);
335 this.addEvents(
336 /*
337 * @event HTMLAreaEventHotkey
338 * Fires when the button hotkey is pressed
339 */
340 'HTMLAreaEventHotkey',
341 /*
342 * @event HTMLAreaEventContextMenu
343 * Fires when the button is triggered from the context menu
344 */
345 'HTMLAreaEventContextMenu'
346 );
347 this.addListener({
348 afterrender: {
349 fn: this.initEventListeners,
350 single: true
351 }
352 });
353 },
354 /*
355 * Initialize listeners
356 */
357 initEventListeners: function () {
358 this.addListener({
359 HTMLAreaEventHotkey: {
360 fn: this.onHotKey
361 },
362 HTMLAreaEventContextMenu: {
363 fn: this.onButtonClick
364 }
365 });
366 this.setHandler(this.onButtonClick, this);
367 // Monitor toolbar updates in order to refresh the state of the button
368 this.mon(this.getToolbar(), 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar, this);
369 },
370 /*
371 * Get a reference to the editor
372 */
373 getEditor: function() {
374 return RTEarea[this.ownerCt.editorId].editor;
375 },
376 /*
377 * Get a reference to the toolbar
378 */
379 getToolbar: function() {
380 return this.ownerCt;
381 },
382 /*
383 * Add properties and function to set button active or not depending on current selection
384 */
385 inactive: true,
386 activeClass: 'buttonActive',
387 setInactive: function (inactive) {
388 this.inactive = inactive;
389 return inactive ? this.removeClass(this.activeClass) : this.addClass(this.activeClass);
390 },
391 /*
392 * Determine if the button should be enabled based on the current selection and context configuration property
393 */
394 isInContext: function (mode, selectionEmpty, ancestors) {
395 var editor = this.getEditor();
396 var inContext = true;
397 if (mode === 'wysiwyg' && this.context) {
398 var attributes = [],
399 contexts = [];
400 if (/(.*)\[(.*?)\]/.test(this.context)) {
401 contexts = RegExp.$1.split(',');
402 attributes = RegExp.$2.split(',');
403 } else {
404 contexts = this.context.split(',');
405 }
406 contexts = new RegExp( '^(' + contexts.join('|') + ')$', 'i');
407 var matchAny = contexts.test('*');
408 Ext.each(ancestors, function (ancestor) {
409 inContext = matchAny || contexts.test(ancestor.nodeName);
410 if (inContext) {
411 Ext.each(attributes, function (attribute) {
412 inContext = eval("ancestor." + attribute);
413 return inContext;
414 });
415 }
416 return !inContext;
417 });
418 }
419 return inContext && (!this.selection || !selectionEmpty);
420 },
421 /*
422 * Handler invoked when the button is clicked
423 */
424 onButtonClick: function (button, event, key) {
425 if (!this.disabled) {
426 if (!this.plugins[this.action](this.getEditor(), key || this.itemId) && event) {
427 event.stopEvent();
428 }
429 if (Ext.isOpera) {
430 this.getEditor().focus();
431 }
432 if (this.dialog) {
433 this.setDisabled(true);
434 } else {
435 this.getToolbar().update();
436 }
437 }
438 return false;
439 },
440 /*
441 * Handler invoked when the hotkey configured for this button is pressed
442 */
443 onHotKey: function (key, event) {
444 return this.onButtonClick(this, event, key);
445 },
446 /*
447 * Handler invoked when the toolbar is updated
448 */
449 onUpdateToolbar: function (mode, selectionEmpty, ancestors, endPointsInSameBlock) {
450 this.setDisabled(mode === 'textmode' && !this.textMode);
451 if (!this.disabled) {
452 if (!this.noAutoUpdate) {
453 this.setDisabled(!this.isInContext(mode, selectionEmpty, ancestors));
454 }
455 this.plugins['onUpdateToolbar'](this, mode, selectionEmpty, ancestors, endPointsInSameBlock);
456 }
457 }
458 });
459 Ext.reg('htmlareabutton', Ext.ux.HTMLAreaButton);
460 /*
461 * Ext.ux.Toolbar.HTMLAreaToolbarText extends Ext.Toolbar.TextItem
462 */
463 Ext.ux.Toolbar.HTMLAreaToolbarText = Ext.extend(Ext.Toolbar.TextItem, {
464 /*
465 * Constructor
466 */
467 initComponent: function () {
468 Ext.ux.Toolbar.HTMLAreaToolbarText.superclass.initComponent.call(this);
469 this.addListener({
470 afterrender: {
471 fn: this.initEventListeners,
472 single: true
473 }
474 });
475 },
476 /*
477 * Initialize listeners
478 */
479 initEventListeners: function () {
480 // Monitor toolbar updates in order to refresh the state of the button
481 this.mon(this.getToolbar(), 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar, this);
482 },
483 /*
484 * Get a reference to the editor
485 */
486 getEditor: function() {
487 return RTEarea[this.ownerCt.editorId].editor;
488 },
489 /*
490 * Get a reference to the toolbar
491 */
492 getToolbar: function() {
493 return this.ownerCt;
494 },
495 /*
496 * Handler invoked when the toolbar is updated
497 */
498 onUpdateToolbar: function (mode, selectionEmpty, ancestors, endPointsInSameBlock) {
499 this.setDisabled(mode === 'textmode' && !this.textMode);
500 if (!this.disabled) {
501 this.plugins['onUpdateToolbar'](this, mode, selectionEmpty, ancestors, endPointsInSameBlock);
502 }
503 }
504 });
505 Ext.reg('htmlareatoolbartext', Ext.ux.Toolbar.HTMLAreaToolbarText);
506 /*
507 * Ext.ux.form.HTMLAreaCombo extends Ext.form.ComboBox
508 */
509 Ext.ux.form.HTMLAreaCombo = Ext.extend(Ext.form.ComboBox, {
510 /*
511 * Constructor
512 */
513 initComponent: function () {
514 Ext.ux.form.HTMLAreaCombo.superclass.initComponent.call(this);
515 this.addEvents(
516 /*
517 * @event HTMLAreaEventHotkey
518 * Fires when a hotkey configured for the combo is pressed
519 */
520 'HTMLAreaEventHotkey'
521 );
522 this.addListener({
523 afterrender: {
524 fn: this.initEventListeners,
525 single: true
526 }
527 });
528 },
529 /*
530 * Initialize listeners
531 */
532 initEventListeners: function () {
533 this.addListener({
534 select: {
535 fn: this.onComboSelect
536 },
537 specialkey: {
538 fn: this.onSpecialKey
539 },
540 HTMLAreaEventHotkey: {
541 fn: this.onHotKey
542 },
543 beforedestroy: {
544 fn: this.onBeforeDestroy,
545 single: true
546 }
547 });
548 // Monitor toolbar updates in order to refresh the state of the combo
549 this.mon(this.getToolbar(), 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar, this);
550 // Monitor framework becoming ready
551 this.mon(this.getToolbar().ownerCt, 'HTMLAreaEventFrameworkReady', this.onFrameworkReady, this);
552 },
553 /*
554 * Get a reference to the editor
555 */
556 getEditor: function() {
557 return RTEarea[this.ownerCt.editorId].editor;
558 },
559 /*
560 * Get a reference to the toolbar
561 */
562 getToolbar: function() {
563 return this.ownerCt;
564 },
565 /*
566 * Handler invoked when an item is selected in the dropdown list
567 */
568 onComboSelect: function (combo, record, index) {
569 if (!combo.disabled) {
570 var editor = this.getEditor();
571 // In IE, reclaim lost focus on the editor iframe and restore the bookmarked selection
572 if (Ext.isIE) {
573 if (!Ext.isEmpty(this.savedRange)) {
574 editor.getSelection().selectRange(this.savedRange);
575 this.savedRange = null;
576 }
577 }
578 // Invoke the plugin onChange handler
579 this.plugins[this.action](editor, combo, record, index);
580 // In IE, bookmark the updated selection as the editor will be loosing focus
581 if (Ext.isIE) {
582 this.savedRange = editor.getSelection().createRange();
583 this.triggered = true;
584 }
585 if (Ext.isOpera) {
586 editor.focus();
587 }
588 this.getToolbar().update();
589 }
590 return false;
591 },
592 /*
593 * Handler invoked when the trigger element is clicked
594 * In IE, need to reclaim lost focus for the editor in order to restore the selection
595 */
596 onTriggerClick: function () {
597 Ext.ux.form.HTMLAreaCombo.superclass.onTriggerClick.call(this);
598 // In IE, avoid focus being stolen and selection being lost
599 if (Ext.isIE) {
600 this.triggered = true;
601 this.getEditor().focus();
602 }
603 },
604 /*
605 * Handler invoked when the list of options is clicked in
606 */
607 onViewClick: function (doFocus) {
608 // Avoid stealing focus from the editor
609 Ext.ux.form.HTMLAreaCombo.superclass.onViewClick.call(this, false);
610 },
611 /*
612 * Handler invoked in IE when the mouse moves out of the editor iframe
613 */
614 saveSelection: function (event) {
615 var editor = this.getEditor();
616 if (editor.document.hasFocus()) {
617 this.savedRange = editor.getSelection().createRange();
618 }
619 },
620 /*
621 * Handler invoked in IE when the editor gets the focus back
622 */
623 restoreSelection: function (event) {
624 if (!Ext.isEmpty(this.savedRange) && this.triggered) {
625 this.getEditor().getSelection().selectRange(this.savedRange);
626 this.triggered = false;
627 }
628 },
629 /*
630 * Handler invoked when the enter key is pressed while the combo has focus
631 */
632 onSpecialKey: function (combo, event) {
633 if (event.getKey() == event.ENTER) {
634 event.stopEvent();
635 }
636 return false;
637 },
638 /*
639 * Handler invoked when a hot key configured for this dropdown list is pressed
640 */
641 onHotKey: function (key) {
642 if (!this.disabled) {
643 this.plugins.onHotKey(this.getEditor(), key);
644 if (Ext.isOpera) {
645 this.getEditor().focus();
646 }
647 this.getToolbar().update();
648 }
649 return false;
650 },
651 /*
652 * Handler invoked when the toolbar is updated
653 */
654 onUpdateToolbar: function (mode, selectionEmpty, ancestors, endPointsInSameBlock) {
655 this.setDisabled(mode === 'textmode' && !this.textMode);
656 if (!this.disabled) {
657 this.plugins['onUpdateToolbar'](this, mode, selectionEmpty, ancestors, endPointsInSameBlock);
658 }
659 },
660 /*
661 * The iframe must have been rendered
662 */
663 onFrameworkReady: function () {
664 var iframe = this.getEditor().iframe;
665 // Close the combo on a click in the iframe
666 // Note: ExtJS is monitoring events only on the parent window
667 this.mon(Ext.get(iframe.document.documentElement), 'click', this.collapse, this);
668 // Special handling for combo stealing focus in IE
669 if (Ext.isIE) {
670 // Take a bookmark in case the editor looses focus by activation of this combo
671 this.mon(iframe.getEl(), 'mouseleave', this.saveSelection, this);
672 // Restore the selection if combo was triggered
673 this.mon(iframe.getEl(), 'focus', this.restoreSelection, this);
674 }
675 },
676 /*
677 * Cleanup
678 */
679 onBeforeDestroy: function () {
680 this.savedRange = null;
681 this.getStore().removeAll();
682 this.getStore().destroy();
683 }
684 });
685 Ext.reg('htmlareacombo', Ext.ux.form.HTMLAreaCombo);
686 /***************************************************
687 * EDITOR FRAMEWORK
688 ***************************************************/
689 /*
690 * HTMLArea.Toolbar extends Ext.Container
691 */
692 HTMLArea.Toolbar = Ext.extend(Ext.Container, {
693 /*
694 * Constructor
695 */
696 initComponent: function () {
697 HTMLArea.Toolbar.superclass.initComponent.call(this);
698 this.addEvents(
699 /*
700 * @event HTMLAreaEventToolbarUpdate
701 * Fires when the toolbar is updated
702 */
703 'HTMLAreaEventToolbarUpdate'
704 );
705 // Build the deferred toolbar update task
706 this.updateLater = new Ext.util.DelayedTask(this.update, this);
707 // Add the toolbar items
708 this.addItems();
709 this.addListener({
710 afterrender: {
711 fn: this.initEventListeners,
712 single: true
713 }
714 });
715 },
716 /*
717 * Initialize listeners
718 */
719 initEventListeners: function () {
720 this.addListener({
721 beforedestroy: {
722 fn: this.onBeforeDestroy,
723 single: true
724 }
725 });
726 // Monitor editor becoming ready
727 this.mon(this.getEditor(), 'HTMLAreaEventEditorReady', this.update, this, {single: true});
728 },
729 /*
730 * editorId should be set in config
731 */
732 editorId: null,
733 /*
734 * Get a reference to the editor
735 */
736 getEditor: function() {
737 return RTEarea[this.editorId].editor;
738 },
739 /*
740 * Create the toolbar items based on editor toolbar configuration
741 */
742 addItems: function () {
743 var editor = this.getEditor();
744 // Walk through the editor toolbar configuration nested arrays: [ toolbar [ row [ group ] ] ]
745 var firstOnRow = true;
746 var firstInGroup = true;
747 Ext.each(editor.config.toolbar, function (row) {
748 if (!firstOnRow) {
749 // If a visible item was added to the previous line
750 this.add({
751 xtype: 'tbspacer',
752 cls: 'x-form-clear-left'
753 });
754 }
755 firstOnRow = true;
756 // Add the groups
757 Ext.each(row, function (group) {
758 // To do: this.config.keepButtonGroupTogether ...
759 if (!firstOnRow && !firstInGroup) {
760 // If a visible item was added to the line
761 this.add({
762 xtype: 'tbseparator',
763 cls: 'separator'
764 });
765 }
766 firstInGroup = true;
767 // Add each item
768 Ext.each(group, function (item) {
769 if (item == 'space') {
770 this.add({
771 xtype: 'tbspacer',
772 cls: 'space'
773 });
774 } else {
775 // Get the item's config as registered by some plugin
776 var itemConfig = editor.config.buttonsConfig[item];
777 if (!Ext.isEmpty(itemConfig)) {
778 itemConfig.id = this.editorId + '-' + itemConfig.id;
779 this.add(itemConfig);
780 firstInGroup = firstInGroup && itemConfig.hidden;
781 firstOnRow = firstOnRow && firstInGroup;
782 }
783 }
784 return true;
785 }, this);
786 return true;
787 }, this);
788 return true;
789 }, this);
790 this.add({
791 xtype: 'tbspacer',
792 cls: 'x-form-clear-left'
793 });
794 },
795 /*
796 * Retrieve a toolbar item by itemId
797 */
798 getButton: function (buttonId) {
799 return this.find('itemId', buttonId)[0];
800 },
801 /*
802 * Update the state of the toolbar
803 */
804 update: function() {
805 var editor = this.getEditor(),
806 mode = editor.getMode(),
807 selection = editor.getSelection(),
808 selectionEmpty = true,
809 ancestors = null,
810 endPointsInSameBlock = true;
811 if (editor.getMode() === 'wysiwyg') {
812 selectionEmpty = selection.isEmpty();
813 ancestors = selection.getAllAncestors();
814 endPointsInSameBlock = selection.endPointsInSameBlock();
815 }
816 this.fireEvent('HTMLAreaEventToolbarUpdate', mode, selectionEmpty, ancestors, endPointsInSameBlock);
817 },
818 /*
819 * Cleanup
820 */
821 onBeforeDestroy: function () {
822 this.removeAll(true);
823 return true;
824 }
825 });
826 Ext.reg('htmlareatoolbar', HTMLArea.Toolbar);
827 /*
828 * HTMLArea.Iframe extends Ext.BoxComponent
829 */
830 HTMLArea.Iframe = Ext.extend(Ext.BoxComponent, {
831 /*
832 * Constructor
833 */
834 initComponent: function () {
835 HTMLArea.Iframe.superclass.initComponent.call(this);
836 this.addEvents(
837 /*
838 * @event HTMLAreaEventIframeReady
839 * Fires when the iframe style sheets become accessible
840 */
841 'HTMLAreaEventIframeReady',
842 /*
843 * @event HTMLAreaEventWordCountChange
844 * Fires when the word count may have changed
845 */
846 'HTMLAreaEventWordCountChange'
847 );
848 this.addListener({
849 afterrender: {
850 fn: this.initEventListeners,
851 single: true
852 },
853 beforedestroy: {
854 fn: this.onBeforeDestroy,
855 single: true
856 }
857 });
858 this.config = this.getEditor().config;
859 this.htmlRenderer = new HTMLArea.DOM.Walker({
860 keepComments: !this.config.htmlRemoveComments,
861 removeTags: this.config.htmlRemoveTags,
862 removeTagsAndContents: this.config.htmlRemoveTagsAndContents,
863 baseUrl: this.config.baseURL
864 });
865 if (!this.config.showStatusBar) {
866 this.addClass('noStatusBar');
867 }
868 },
869 /*
870 * Initialize event listeners and the document after the iframe has rendered
871 */
872 initEventListeners: function () {
873 this.initStyleChangeEventListener();
874 if (Ext.isOpera) {
875 this.mon(this.getEl(), 'load', this.initializeIframe , this, {single: true});
876 } else {
877 this.initializeIframe();
878 }
879 },
880 /*
881 * The editor iframe may become hidden with style.display = "none" on some parent div
882 * This breaks the editor in Firefox: the designMode attribute needs to be reset after the style.display of the container div is reset to "block"
883 * In all browsers, it breaks the evaluation of the framework dimensions
884 */
885 initStyleChangeEventListener: function () {
886 if (this.isNested && Ext.isGecko) {
887 var options = {
888 stopEvent: true,
889 delay: 50
890 };
891 Ext.each(this.nestedParentElements.sorted, function (nested) {
892 this.mon(
893 Ext.get(nested),
894 Ext.isIE ? 'propertychange' : 'DOMAttrModified',
895 this.onNestedShow,
896 this,
897 options
898 );
899 }, this);
900 }
901 },
902 /*
903 * editorId should be set in config
904 */
905 editorId: null,
906 /*
907 * Get a reference to the editor
908 */
909 getEditor: function() {
910 return RTEarea[this.editorId].editor;
911 },
912 /*
913 * Get a reference to the toolbar
914 */
915 getToolbar: function () {
916 return this.ownerCt.getTopToolbar();
917 },
918 /*
919 * Get a reference to the statusBar
920 */
921 getStatusBar: function () {
922 return this.ownerCt.getBottomToolbar();
923 },
924 /*
925 * Get a reference to a button
926 */
927 getButton: function (buttonId) {
928 return this.getToolbar().getButton(buttonId);
929 },
930 /*
931 * Flag set to true when the iframe becomes usable for editing
932 */
933 ready: false,
934 /*
935 * Create the iframe element at rendering time
936 */
937 onRender: function (ct, position){
938 // from Ext.Component
939 if (!this.el && this.autoEl) {
940 if (Ext.isString(this.autoEl)) {
941 this.el = document.createElement(this.autoEl);
942 } else {
943 // ExtJS Default method will not work with iframe element
944 this.el = Ext.DomHelper.append(ct, this.autoEl, true);
945 }
946 if (!this.el.id) {
947 this.el.id = this.getId();
948 }
949 }
950 // from Ext.BoxComponent
951 if (this.resizeEl){
952 this.resizeEl = Ext.get(this.resizeEl);
953 }
954 if (this.positionEl){
955 this.positionEl = Ext.get(this.positionEl);
956 }
957 },
958 /*
959 * Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
960 */
961 initializeIframe: function () {
962 var iframe = this.getEl().dom;
963 // All browsers
964 if (!iframe || (!iframe.contentWindow && !iframe.contentDocument)) {
965 this.initializeIframe.defer(50, this);
966 // All except WebKit
967 } else if (iframe.contentWindow && !Ext.isWebKit && (!iframe.contentWindow.document || !iframe.contentWindow.document.documentElement)) {
968 this.initializeIframe.defer(50, this);
969 // WebKit
970 } else if (Ext.isWebKit && (!iframe.contentDocument.documentElement || !iframe.contentDocument.body)) {
971 this.initializeIframe.defer(50, this);
972 } else {
973 this.document = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
974 this.getEditor().document = this.document;
975 this.getEditor()._doc = this.document;
976 this.getEditor()._iframe = iframe;
977 this.initializeCustomTags();
978 this.createHead();
979 // Style the document body
980 Ext.get(this.document.body).addClass('htmlarea-content-body');
981 // Start listening to things happening in the iframe
982 // For some unknown reason, this is too early for Opera
983 if (!Ext.isOpera) {
984 this.startListening();
985 }
986 // Hide the iframe
987 this.hide();
988 // Set iframe ready
989 this.ready = true;
990 this.fireEvent('HTMLAreaEventIframeReady');
991 }
992 },
993 /*
994 * Create one of each of the configured custom tags so they are properly parsed by the walker when using IE
995 * See: http://en.wikipedia.org/wiki/HTML5_Shiv
996 *
997 * @return void
998 */
999 initializeCustomTags: function () {
1000 if (HTMLArea.isIEBeforeIE9) {
1001 Ext.each(this.config.customTags, function (tag) {
1002 this.document.createElement(tag);
1003 }, this);
1004 }
1005 },
1006 /*
1007 * Build the iframe document head
1008 */
1009 createHead: function () {
1010 var head = this.document.getElementsByTagName('head')[0];
1011 if (!head) {
1012 head = this.document.createElement('head');
1013 this.document.documentElement.appendChild(head);
1014 }
1015 if (this.config.baseURL) {
1016 var base = this.document.getElementsByTagName('base')[0];
1017 if (!base) {
1018 base = this.document.createElement('base');
1019 base.href = this.config.baseURL;
1020 head.appendChild(base);
1021 }
1022 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Iframe baseURL set to: ' + base.href, 'info');
1023 }
1024 var link0 = this.document.getElementsByTagName('link')[0];
1025 if (!link0) {
1026 link0 = this.document.createElement('link');
1027 link0.rel = 'stylesheet';
1028 link0.type = 'text/css';
1029 // Firefox 3.0.1 does not apply the base URL while Firefox 3.6.8 does so. Do not know in what version this was fixed.
1030 // Therefore, for versions before 3.6.8, we prepend the url with the base, if the url is not absolute
1031 link0.href = ((Ext.isGecko && navigator.productSub < 2010072200 && !/^http(s?):\/{2}/.test(this.config.editedContentStyle)) ? this.config.baseURL : '') + this.config.editedContentStyle;
1032 head.appendChild(link0);
1033 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Skin CSS set to: ' + link0.href, 'info');
1034 }
1035 if (this.config.pageStyle) {
1036 var link = this.document.getElementsByTagName('link')[1];
1037 if (!link) {
1038 link = this.document.createElement('link');
1039 link.rel = 'stylesheet';
1040 link.type = 'text/css';
1041 link.href = ((Ext.isGecko && navigator.productSub < 2010072200 && !/^https?:\/{2}/.test(this.config.pageStyle)) ? this.config.baseURL : '') + this.config.pageStyle;
1042 head.appendChild(link);
1043 }
1044 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link.href, 'info');
1045 }
1046 },
1047 /*
1048 * Focus on the iframe
1049 */
1050 focus: function () {
1051 try {
1052 if (Ext.isWebKit) {
1053 this.getEl().dom.focus();
1054 } else {
1055 this.getEl().dom.contentWindow.focus();
1056 }
1057 } catch(e) { }
1058 },
1059 /*
1060 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1061 * Should be set in config
1062 */
1063 isNested: false,
1064 /*
1065 * All nested tabs and inline levels in the sorting order they were applied
1066 * Should be set in config
1067 */
1068 nestedParentElements: {},
1069 /*
1070 * Set designMode
1071 *
1072 * @param boolean on: if true set designMode to on, otherwise set to off
1073 *
1074 * @rturn void
1075 */
1076 setDesignMode: function (on) {
1077 if (on) {
1078 if (!Ext.isIE) {
1079 if (Ext.isGecko) {
1080 // In Firefox, we can't set designMode when we are in a hidden TYPO3 tab or inline element
1081 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1082 this.document.designMode = 'on';
1083 this.setOptions();
1084 }
1085 } else {
1086 this.document.designMode = 'on';
1087 this.setOptions();
1088 }
1089 }
1090 if (Ext.isIE || Ext.isWebKit) {
1091 this.document.body.contentEditable = true;
1092 }
1093 } else {
1094 if (!Ext.isIE) {
1095 this.document.designMode = 'off';
1096 }
1097 if (Ext.isIE || Ext.isWebKit) {
1098 this.document.body.contentEditable = false;
1099 }
1100 }
1101 },
1102 /*
1103 * Set editing mode options (if we can... raises exception in Firefox 3)
1104 *
1105 * @return void
1106 */
1107 setOptions: function () {
1108 if (!Ext.isIE) {
1109 try {
1110 if (this.document.queryCommandEnabled('insertBrOnReturn')) {
1111 this.document.execCommand('insertBrOnReturn', false, this.config.disableEnterParagraphs);
1112 }
1113 if (this.document.queryCommandEnabled('styleWithCSS')) {
1114 this.document.execCommand('styleWithCSS', false, this.config.useCSS);
1115 } else if (Ext.isGecko && this.document.queryCommandEnabled('useCSS')) {
1116 this.document.execCommand('useCSS', false, !this.config.useCSS);
1117 }
1118 if (Ext.isGecko) {
1119 if (this.document.queryCommandEnabled('enableObjectResizing')) {
1120 this.document.execCommand('enableObjectResizing', false, !this.config.disableObjectResizing);
1121 }
1122 if (this.document.queryCommandEnabled('enableInlineTableEditing')) {
1123 this.document.execCommand('enableInlineTableEditing', false, (this.config.buttons.table && this.config.buttons.table.enableHandles) ? true : false);
1124 }
1125 }
1126 } catch(e) {}
1127 }
1128 },
1129 /*
1130 * Handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
1131 */
1132 onNestedShow: function (event, target) {
1133 var styleEvent = true;
1134 // In older versions of Gecko attrName is not set and refering to it causes a non-catchable crash
1135 if ((Ext.isGecko && navigator.productSub > 2007112700) || Ext.isOpera) {
1136 styleEvent = (event.browserEvent.attrName == 'style');
1137 } else if (Ext.isIE) {
1138 styleEvent = (event.browserEvent.propertyName == 'style.display');
1139 }
1140 if (styleEvent && this.nestedParentElements.sorted.indexOf(target.id) != -1 && (target.style.display == '' || target.style.display == 'block')) {
1141 // Check if all container nested elements are displayed
1142 if (HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1143 if (this.getEditor().getMode() === 'wysiwyg') {
1144 if (Ext.isGecko) {
1145 this.setDesignMode(true);
1146 }
1147 this.fireEvent('show');
1148 } else {
1149 this.ownerCt.textAreaContainer.fireEvent('show');
1150 }
1151 this.getToolbar().update();
1152 return false;
1153 }
1154 }
1155 },
1156 /*
1157 * Instance of DOM walker
1158 */
1159 htmlRenderer: {},
1160 /*
1161 * Get the HTML content of the iframe
1162 */
1163 getHTML: function () {
1164 return this.htmlRenderer.render(this.document.body, false);
1165 },
1166 /*
1167 * Start listening to things happening in the iframe
1168 */
1169 startListening: function () {
1170 // Create keyMap so that plugins may bind key handlers
1171 this.keyMap = new Ext.KeyMap(Ext.get(this.document.documentElement), [], (Ext.isIE || Ext.isWebKit) ? 'keydown' : 'keypress');
1172 // Special keys map
1173 this.keyMap.addBinding([
1174 {
1175 key: [Ext.EventObject.DOWN, Ext.EventObject.UP, Ext.EventObject.LEFT, Ext.EventObject.RIGHT],
1176 alt: false,
1177 handler: this.onArrow,
1178 scope: this
1179 },
1180 {
1181 key: Ext.EventObject.TAB,
1182 ctrl: false,
1183 alt: false,
1184 handler: this.onTab,
1185 scope: this
1186 },
1187 {
1188 key: Ext.EventObject.SPACE,
1189 ctrl: true,
1190 shift: false,
1191 alt: false,
1192 handler: this.onCtrlSpace,
1193 scope: this
1194 }
1195 ]);
1196 if (Ext.isGecko || Ext.isIE) {
1197 this.keyMap.addBinding(
1198 {
1199 key: [Ext.EventObject.BACKSPACE, Ext.EventObject.DELETE],
1200 alt: false,
1201 handler: this.onBackSpace,
1202 scope: this
1203 });
1204 }
1205 if (!Ext.isIE && !this.config.disableEnterParagraphs) {
1206 this.keyMap.addBinding(
1207 {
1208 key: Ext.EventObject.ENTER,
1209 shift: false,
1210 handler: this.onEnter,
1211 scope: this
1212 });
1213 }
1214 if (Ext.isWebKit) {
1215 this.keyMap.addBinding(
1216 {
1217 key: Ext.EventObject.ENTER,
1218 alt: false,
1219 handler: this.onWebKitEnter,
1220 scope: this
1221 });
1222 }
1223 // Hot key map (on keydown for all browsers)
1224 var hotKeys = '';
1225 Ext.iterate(this.config.hotKeyList, function (key) {
1226 if (key.length == 1) {
1227 hotKeys += key.toUpperCase();
1228 }
1229 });
1230 // Make hot key map available, even if empty, so that plugins may add bindings
1231 this.hotKeyMap = new Ext.KeyMap(Ext.get(this.document.documentElement));
1232 if (!Ext.isEmpty(hotKeys)) {
1233 this.hotKeyMap.addBinding({
1234 key: hotKeys,
1235 ctrl: true,
1236 shift: false,
1237 alt: false,
1238 handler: this.onHotKey,
1239 scope: this
1240 });
1241 }
1242 this.mon(Ext.get(this.document.documentElement), (Ext.isIE || Ext.isWebKit) ? 'keydown' : 'keypress', this.onAnyKey, this);
1243 this.mon(Ext.get(this.document.documentElement), 'mouseup', this.onMouse, this);
1244 this.mon(Ext.get(this.document.documentElement), 'click', this.onMouse, this);
1245 if (Ext.isGecko) {
1246 this.mon(Ext.get(this.document.documentElement), 'paste', this.onPaste, this);
1247 }
1248 this.mon(Ext.get(this.document.documentElement), 'drop', this.onDrop, this);
1249 if (Ext.isWebKit) {
1250 this.mon(Ext.get(this.document.body), 'dragend', this.onDrop, this);
1251 }
1252 },
1253 /*
1254 * Handler for other key events
1255 */
1256 onAnyKey: function(event) {
1257 if (this.inhibitKeyboardInput(event)) {
1258 return false;
1259 }
1260 this.fireEvent('HTMLAreaEventWordCountChange', 100);
1261 if (!event.altKey && !event.ctrlKey) {
1262 // Detect URL in non-IE browsers
1263 if (!Ext.isIE && (event.getKey() != Ext.EventObject.ENTER || (event.shiftKey && !Ext.isWebKit))) {
1264 this.getEditor().getSelection().detectURL(event);
1265 }
1266 // Handle option+SPACE for Mac users
1267 if (Ext.isMac && event.browserEvent.charCode == 160) {
1268 return this.onOptionSpace(event.browserEvent.charCode, event);
1269 }
1270 }
1271 return true;
1272 },
1273 /*
1274 * On any key input event, check if input is currently inhibited
1275 */
1276 inhibitKeyboardInput: function (event) {
1277 // Inhibit key events while server-based cleaning is being processed
1278 if (this.getEditor().inhibitKeyboardInput) {
1279 event.stopEvent();
1280 return true;
1281 } else {
1282 return false;
1283 }
1284 },
1285 /*
1286 * Handler for mouse events
1287 */
1288 onMouse: function (event, target) {
1289 // In WebKit, select the image when it is clicked
1290 if (Ext.isWebKit && /^(img)$/i.test(target.nodeName) && event.browserEvent.type == 'click') {
1291 this.getEditor().getSelection().selectNode(target);
1292 }
1293 this.getToolbar().updateLater.delay(100);
1294 return true;
1295 },
1296 /*
1297 * Handler for paste operations in Gecko
1298 */
1299 onPaste: function (event) {
1300 // Make src and href urls absolute
1301 if (Ext.isGecko) {
1302 HTMLArea.DOM.makeUrlsAbsolute.defer(50, this, [this.getEditor().document.body, this.config.baseURL, this.htmlRenderer]);
1303 }
1304 },
1305 /*
1306 * Handler for drag and drop operations
1307 */
1308 onDrop: function (event, target) {
1309 // Clean up span elements added by WebKit
1310 if (Ext.isWebKit) {
1311 this.getEditor().cleanAppleStyleSpans.defer(50, this.getEditor(), [this.getEditor().document.body]);
1312 }
1313 // Make src url absolute in Firefox
1314 if (Ext.isGecko) {
1315 HTMLArea.DOM.makeUrlsAbsolute.defer(50, this, [target, this.config.baseURL, this.htmlRenderer]);
1316 }
1317 this.getToolbar().updateLater.delay(100);
1318 },
1319 /*
1320 * Handler for UP, DOWN, LEFT and RIGHT keys
1321 */
1322 onArrow: function () {
1323 this.getToolbar().updateLater.delay(100);
1324 return true;
1325 },
1326 /*
1327 * Handler for TAB and SHIFT-TAB keys
1328 *
1329 * If available, BlockElements plugin will handle the TAB key
1330 */
1331 onTab: function (key, event) {
1332 if (this.inhibitKeyboardInput(event)) {
1333 return false;
1334 }
1335 var keyName = (event.shiftKey ? 'SHIFT-' : '') + 'TAB';
1336 if (this.config.hotKeyList[keyName] && this.config.hotKeyList[keyName].cmd) {
1337 var button = this.getButton(this.config.hotKeyList[keyName].cmd);
1338 if (button) {
1339 event.stopEvent();
1340 button.fireEvent('HTMLAreaEventHotkey', keyName, event);
1341 return false;
1342 }
1343 }
1344 return true;
1345 },
1346 /*
1347 * Handler for BACKSPACE and DELETE keys
1348 */
1349 onBackSpace: function (key, event) {
1350 if (this.inhibitKeyboardInput(event)) {
1351 return false;
1352 }
1353 if ((!Ext.isIE && !event.shiftKey) || Ext.isIE) {
1354 if (this.getEditor().getSelection().handleBackSpace()) {
1355 event.stopEvent();
1356 }
1357 }
1358 // Update the toolbar state after some time
1359 this.getToolbar().updateLater.delay(200);
1360 return false;
1361 },
1362 /*
1363 * Handler for ENTER key in non-IE browsers
1364 */
1365 onEnter: function (key, event) {
1366 if (this.inhibitKeyboardInput(event)) {
1367 return false;
1368 }
1369 this.getEditor().getSelection().detectURL(event);
1370 if (this.getEditor().getSelection().checkInsertParagraph()) {
1371 event.stopEvent();
1372 }
1373 // Update the toolbar state after some time
1374 this.getToolbar().updateLater.delay(200);
1375 return false;
1376 },
1377 /*
1378 * Handler for ENTER key in WebKit browsers
1379 */
1380 onWebKitEnter: function (key, event) {
1381 if (this.inhibitKeyboardInput(event)) {
1382 return false;
1383 }
1384 if (event.shiftKey || this.config.disableEnterParagraphs) {
1385 var editor = this.getEditor();
1386 editor.getSelection().detectURL(event);
1387 if (Ext.isSafari) {
1388 var brNode = editor.document.createElement('br');
1389 editor.insertNodeAtSelection(brNode);
1390 brNode.parentNode.normalize();
1391 // Selection issue when an URL was detected
1392 if (editor._unlinkOnUndo) {
1393 brNode = brNode.parentNode.parentNode.insertBefore(brNode, brNode.parentNode.nextSibling);
1394 }
1395 if (!brNode.nextSibling || !/\S+/i.test(brNode.nextSibling.textContent)) {
1396 var secondBrNode = editor.document.createElement('br');
1397 secondBrNode = brNode.parentNode.appendChild(secondBrNode);
1398 }
1399 editor.getSelection().selectNode(brNode, false);
1400 event.stopEvent();
1401 }
1402 }
1403 // Update the toolbar state after some time
1404 this.getToolbar().updateLater.delay(200);
1405 return false;
1406 },
1407 /*
1408 * Handler for CTRL-SPACE keys
1409 */
1410 onCtrlSpace: function (key, event) {
1411 if (this.inhibitKeyboardInput(event)) {
1412 return false;
1413 }
1414 this.getEditor().getSelection().insertHtml('&nbsp;');
1415 event.stopEvent();
1416 return false;
1417 },
1418 /*
1419 * Handler for OPTION-SPACE keys on Mac
1420 */
1421 onOptionSpace: function (key, event) {
1422 if (this.inhibitKeyboardInput(event)) {
1423 return false;
1424 }
1425 this.getEditor().getSelection().insertHtml('&nbsp;');
1426 event.stopEvent();
1427 return false;
1428 },
1429 /*
1430 * Handler for configured hotkeys
1431 */
1432 onHotKey: function (key, event) {
1433 if (this.inhibitKeyboardInput(event)) {
1434 return false;
1435 }
1436 var hotKey = String.fromCharCode(key).toLowerCase();
1437 this.getButton(this.config.hotKeyList[hotKey].cmd).fireEvent('HTMLAreaEventHotkey', hotKey, event);
1438 return false;
1439 },
1440 /*
1441 * Cleanup
1442 */
1443 onBeforeDestroy: function () {
1444 // ExtJS KeyMap object makes IE leak memory
1445 // Nullify EXTJS private handlers
1446 Ext.each(this.keyMap.bindings, function (binding, index) {
1447 this.keyMap.bindings[index] = null;
1448 }, this);
1449 this.keyMap.handleKeyDown = null;
1450 Ext.each(this.hotKeyMap.bindings, function (binding, index) {
1451 this.hotKeyMap.bindings[index] = null;
1452 }, this);
1453 this.hotKeyMap.handleKeyDown = null;
1454 this.keyMap.disable();
1455 this.hotKeyMap.disable();
1456 // Cleaning references to DOM in order to avoid IE memory leaks
1457 Ext.get(this.document.body).purgeAllListeners();
1458 Ext.get(this.document.body).dom = null;
1459 Ext.get(this.document.documentElement).purgeAllListeners();
1460 Ext.get(this.document.documentElement).dom = null;
1461 this.document = null;
1462 this.getEditor().document = null;
1463 this.getEditor()._doc = null;
1464 this.getEditor()._iframe = null;
1465 Ext.each(this.nestedParentElements.sorted, function (nested) {
1466 Ext.get(nested).purgeAllListeners();
1467 Ext.get(nested).dom = null;
1468 });
1469 Ext.destroy(this.autoEl, this.el, this.resizeEl, this.positionEl);
1470 return true;
1471 }
1472 });
1473 Ext.reg('htmlareaiframe', HTMLArea.Iframe);
1474 /*
1475 * HTMLArea.StatusBar extends Ext.Container
1476 */
1477 HTMLArea.StatusBar = Ext.extend(Ext.Container, {
1478 /*
1479 * Constructor
1480 */
1481 initComponent: function () {
1482 HTMLArea.StatusBar.superclass.initComponent.call(this);
1483 // Build the deferred word count update task
1484 this.updateWordCountLater = new Ext.util.DelayedTask(this.updateWordCount, this);
1485 this.addListener({
1486 render: {
1487 fn: this.addComponents,
1488 single: true
1489 },
1490 afterrender: {
1491 fn: this.initEventListeners,
1492 single: true
1493 }
1494 });
1495 },
1496 /*
1497 * Initialize listeners
1498 */
1499 initEventListeners: function () {
1500 this.addListener({
1501 beforedestroy: {
1502 fn: this.onBeforeDestroy,
1503 single: true
1504 }
1505 });
1506 // Monitor toolbar updates in order to refresh the contents of the statusbar
1507 // The toolbar must have been rendered
1508 this.mon(this.ownerCt.toolbar, 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar, this);
1509 // Monitor editor changing mode
1510 this.mon(this.getEditor(), 'HTMLAreaEventModeChange', this.onModeChange, this);
1511 // Monitor word count change
1512 this.mon(this.ownerCt.iframe, 'HTMLAreaEventWordCountChange', this.onWordCountChange, this);
1513 },
1514 /*
1515 * editorId should be set in config
1516 */
1517 editorId: null,
1518 /*
1519 * Get a reference to the editor
1520 */
1521 getEditor: function() {
1522 return RTEarea[this.editorId].editor;
1523 },
1524 /*
1525 * Create span elements to display when the status bar tree or a message when the editor is in text mode
1526 */
1527 addComponents: function () {
1528 this.statusBarWordCount = Ext.DomHelper.append(this.getEl(), {
1529 id: this.editorId + '-statusBarWordCount',
1530 tag: 'span',
1531 cls: 'statusBarWordCount',
1532 html: '&nbsp;'
1533 }, true);
1534 this.statusBarTree = Ext.DomHelper.append(this.getEl(), {
1535 id: this.editorId + '-statusBarTree',
1536 tag: 'span',
1537 cls: 'statusBarTree',
1538 html: HTMLArea.localize('Path') + ': '
1539 }, true).setVisibilityMode(Ext.Element.DISPLAY).setVisible(true);
1540 this.statusBarTextMode = Ext.DomHelper.append(this.getEl(), {
1541 id: this.editorId + '-statusBarTextMode',
1542 tag: 'span',
1543 cls: 'statusBarTextMode',
1544 html: HTMLArea.localize('TEXT_MODE')
1545 }, true).setVisibilityMode(Ext.Element.DISPLAY).setVisible(false);
1546 },
1547 /*
1548 * Clear the status bar tree
1549 */
1550 clear: function () {
1551 this.statusBarTree.removeAllListeners();
1552 Ext.each(this.statusBarTree.query('a'), function (node) {
1553 Ext.QuickTips.unregister(node);
1554 Ext.get(node).dom.ancestor = null;
1555 Ext.destroy(node);
1556 });
1557 this.statusBarTree.update('');
1558 this.setSelection(null);
1559 },
1560 /*
1561 * Flag indicating that the status bar should not be updated on this toolbar update
1562 */
1563 noUpdate: false,
1564 /*
1565 * Update the status bar
1566 */
1567 onUpdateToolbar: function (mode, selectionEmpty, ancestors, endPointsInSameBlock) {
1568 if (mode === 'wysiwyg' && !this.noUpdate) {
1569 var text,
1570 language,
1571 languageObject = this.getEditor().getPlugin('Language'),
1572 classes = new Array(),
1573 classText;
1574 this.clear();
1575 var path = Ext.DomHelper.append(this.statusBarTree, {
1576 tag: 'span',
1577 html: HTMLArea.localize('Path') + ': '
1578 },true);
1579 Ext.each(ancestors, function (ancestor, index) {
1580 if (!ancestor) {
1581 return true;
1582 }
1583 text = ancestor.nodeName.toLowerCase();
1584 // Do not show any id generated by ExtJS
1585 if (ancestor.id && text !== 'body' && ancestor.id.substr(0, 7) !== 'ext-gen') {
1586 text += '#' + ancestor.id;
1587 }
1588 if (languageObject && languageObject.getLanguageAttribute) {
1589 language = languageObject.getLanguageAttribute(ancestor);
1590 if (language != 'none') {
1591 text += '[' + language + ']';
1592 }
1593 }
1594 if (ancestor.className) {
1595 classText = '';
1596 classes = ancestor.className.trim().split(' ');
1597 for (var j = 0, n = classes.length; j < n; ++j) {
1598 if (!HTMLArea.reservedClassNames.test(classes[j])) {
1599 classText += '.' + classes[j];
1600 }
1601 }
1602 text += classText;
1603 }
1604 var element = Ext.DomHelper.insertAfter(path, {
1605 tag: 'a',
1606 href: '#',
1607 'ext:qtitle': HTMLArea.localize('statusBarStyle'),
1608 'ext:qtip': ancestor.style.cssText.split(';').join('<br />'),
1609 html: text
1610 }, true);
1611 // Ext.DomHelper does not honour the custom attribute
1612 element.dom.ancestor = ancestor;
1613 element.on('click', this.onClick, this);
1614 element.on('mousedown', this.onClick, this);
1615 if (!Ext.isOpera) {
1616 element.on('contextmenu', this.onContextMenu, this);
1617 }
1618 if (index) {
1619 Ext.DomHelper.insertAfter(element, {
1620 tag: 'span',
1621 html: String.fromCharCode(0xbb)
1622 });
1623 }
1624 }, this);
1625 }
1626 this.updateWordCount();
1627 this.noUpdate = false;
1628 },
1629 /*
1630 * Handler when the word count may have changed
1631 */
1632 onWordCountChange: function(delay) {
1633 this.updateWordCountLater.delay(delay ? delay : 0);
1634 },
1635 /*
1636 * Update the word count
1637 */
1638 updateWordCount: function() {
1639 var wordCount = 0;
1640 if (this.getEditor().getMode() == 'wysiwyg') {
1641 // Get the html content
1642 var text = this.getEditor().getHTML();
1643 if (!Ext.isEmpty(text)) {
1644 // Replace html tags with spaces
1645 text = text.replace(HTMLArea.RE_htmlTag, ' ');
1646 // Replace html space entities
1647 text = text.replace(/&nbsp;|&#160;/gi, ' ');
1648 // Remove numbers and punctuation
1649 text = text.replace(HTMLArea.RE_numberOrPunctuation, '');
1650 // Get the number of word
1651 wordCount = text.split(/\S\s+/g).length - 1;
1652 }
1653 }
1654 // Update the word count of the status bar
1655 this.statusBarWordCount.dom.innerHTML = wordCount ? ( wordCount + ' ' + HTMLArea.localize((wordCount == 1) ? 'word' : 'words')) : '&nbsp;';
1656 },
1657 /*
1658 * Adapt status bar to current editor mode
1659 *
1660 * @param string mode: the mode to which the editor got switched to
1661 */
1662 onModeChange: function (mode) {
1663 switch (mode) {
1664 case 'wysiwyg':
1665 this.statusBarTextMode.setVisible(false);
1666 this.statusBarTree.setVisible(true);
1667 break;
1668 case 'textmode':
1669 default:
1670 this.statusBarTree.setVisible(false);
1671 this.statusBarTextMode.setVisible(true);
1672 break;
1673 }
1674 },
1675 /*
1676 * Refrence to the element last selected on the status bar
1677 */
1678 selected: null,
1679 /*
1680 * Get the status bar selection
1681 */
1682 getSelection: function() {
1683 return this.selected;
1684 },
1685 /*
1686 * Set the status bar selection
1687 *
1688 * @param object element: set the status bar selection to the given element
1689 */
1690 setSelection: function (element) {
1691 this.selected = element ? element : null;
1692 },
1693 /*
1694 * Select the element that was clicked in the status bar and set the status bar selection
1695 */
1696 selectElement: function (element) {
1697 var editor = this.getEditor();
1698 element.blur();
1699 if (!Ext.isIE) {
1700 if (/^(img|table)$/i.test(element.ancestor.nodeName)) {
1701 editor.getSelection().selectNode(element.ancestor);
1702 } else {
1703 editor.getSelection().selectNodeContents(element.ancestor);
1704 }
1705 } else {
1706 if (/^(img|table)$/i.test(element.ancestor.nodeName)) {
1707 var range = editor.document.body.createControlRange();
1708 range.addElement(element.ancestor);
1709 range.select();
1710 } else {
1711 editor.getSelection().selectNode(element.ancestor);
1712 }
1713 }
1714 this.setSelection(element.ancestor);
1715 this.noUpdate = true;
1716 editor.toolbar.update();
1717 },
1718 /*
1719 * Click handler
1720 */
1721 onClick: function (event, element) {
1722 this.selectElement(element);
1723 event.stopEvent();
1724 return false;
1725 },
1726 /*
1727 * ContextMenu handler
1728 */
1729 onContextMenu: function (event, target) {
1730 this.selectElement(target);
1731 return this.getEditor().getPlugin('ContextMenu') ? this.getEditor().getPlugin('ContextMenu').show(event, target.ancestor) : false;
1732 },
1733 /*
1734 * Cleanup
1735 */
1736 onBeforeDestroy: function() {
1737 this.clear();
1738 this.removeAll(true);
1739 Ext.destroy(this.statusBarTree, this.statusBarTextMode);
1740 return true;
1741 }
1742 });
1743 Ext.reg('htmlareastatusbar', HTMLArea.StatusBar);
1744 /*
1745 * HTMLArea.Framework extends Ext.Panel
1746 */
1747 HTMLArea.Framework = Ext.extend(Ext.Panel, {
1748 /*
1749 * Constructor
1750 */
1751 initComponent: function () {
1752 HTMLArea.Framework.superclass.initComponent.call(this);
1753 // Set some references
1754 this.toolbar = this.getTopToolbar();
1755 this.statusBar = this.getBottomToolbar();
1756 this.iframe = this.getComponent('iframe');
1757 this.textAreaContainer = this.getComponent('textAreaContainer');
1758 this.addEvents(
1759 /*
1760 * @event HTMLAreaEventFrameworkReady
1761 * Fires when the iframe is ready and all components are rendered
1762 */
1763 'HTMLAreaEventFrameworkReady'
1764 );
1765 this.addListener({
1766 beforedestroy: {
1767 fn: this.onBeforeDestroy,
1768 single: true
1769 }
1770 });
1771 // Monitor iframe becoming ready
1772 this.mon(this.iframe, 'HTMLAreaEventIframeReady', this.onIframeReady, this, {single: true});
1773 // Let the framefork render itself, but it will fail to do so if inside a hidden tab or inline element
1774 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1775 this.render(this.textArea.parent(), this.textArea.id);
1776 } else {
1777 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1778 var parentElements = [].concat(this.nestedParentElements.sorted);
1779 // Walk through all nested tabs and inline levels to get correct sizes
1780 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].render(args[0].textArea.parent(), args[0].textArea.id)', [this]);
1781 }
1782 },
1783 /*
1784 * Initiate events monitoring
1785 */
1786 initEventListeners: function () {
1787 // Make the framework resizable, if configured by the user
1788 this.makeResizable();
1789 // Monitor textArea container becoming shown or hidden as it may change the height of the status bar
1790 this.mon(this.textAreaContainer, 'show', this.resizable ? this.onTextAreaShow : this.onWindowResize, this);
1791 // Monitor iframe becoming shown or hidden as it may change the height of the status bar
1792 this.mon(this.iframe, 'show', this.resizable ? this.onIframeShow : this.onWindowResize, this);
1793 // Monitor window resizing
1794 Ext.EventManager.onWindowResize(this.onWindowResize, this);
1795 // If the textarea is inside a form, on reset, re-initialize the HTMLArea content and update the toolbar
1796 var form = this.textArea.dom.form;
1797 if (form) {
1798 if (Ext.isFunction(form.onreset)) {
1799 if (typeof(form.htmlAreaPreviousOnReset) == 'undefined') {
1800 form.htmlAreaPreviousOnReset = [];
1801 }
1802 form.htmlAreaPreviousOnReset.push(form.onreset);
1803 }
1804 this.mon(Ext.get(form), 'reset', this.onReset, this);
1805 }
1806 this.addListener({
1807 resize: {
1808 fn: this.onFrameworkResize
1809 }
1810 });
1811 },
1812 /*
1813 * editorId should be set in config
1814 */
1815 editorId: null,
1816 /*
1817 * Get a reference to the editor
1818 */
1819 getEditor: function() {
1820 return RTEarea[this.editorId].editor;
1821 },
1822 /*
1823 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1824 * Should be set in config
1825 */
1826 isNested: false,
1827 /*
1828 * All nested tabs and inline levels in the sorting order they were applied
1829 * Should be set in config
1830 */
1831 nestedParentElements: {},
1832 /*
1833 * Flag set to true when the framework is ready
1834 */
1835 ready: false,
1836 /*
1837 * All nested tabs and inline levels in the sorting order they were applied
1838 * Should be set in config
1839 */
1840 nestedParentElements: {},
1841 /*
1842 * Whether the framework should be made resizable
1843 * May be set in config
1844 */
1845 resizable: false,
1846 /*
1847 * Maximum height to which the framework may resized (in pixels)
1848 * May be set in config
1849 */
1850 maxHeight: 2000,
1851 /*
1852 * Initial textArea dimensions
1853 * Should be set in config
1854 */
1855 textAreaInitialSize: {
1856 width: 0,
1857 contextWidth: 0,
1858 height: 0
1859 },
1860 /*
1861 * doLayout will fail if inside a hidden tab or inline element
1862 */
1863 doLayout: function () {
1864 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1865 HTMLArea.Framework.superclass.doLayout.call(this);
1866 } else {
1867 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1868 var parentElements = [].concat(this.nestedParentElements.sorted);
1869 // Walk through all nested tabs and inline levels to get correct sizes
1870 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'HTMLArea.Framework.superclass.doLayout.call(args[0])', [this]);
1871 }
1872 },
1873 /*
1874 * onLayout will fail if inside a hidden tab or inline element
1875 */
1876 onLayout: function () {
1877 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1878 HTMLArea.Framework.superclass.onLayout.call(this);
1879 } else {
1880 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1881 var parentElements = [].concat(this.nestedParentElements.sorted);
1882 // Walk through all nested tabs and inline levels to get correct sizes
1883 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'HTMLArea.Framework.superclass.onLayout.call(args[0])', [this]);
1884 }
1885 },
1886 /*
1887 * Make the framework resizable, if configured
1888 */
1889 makeResizable: function () {
1890 if (this.resizable) {
1891 this.addClass('resizable');
1892 this.resizer = new Ext.Resizable(this.getEl(), {
1893 minWidth: 300,
1894 maxHeight: this.maxHeight,
1895 dynamic: false
1896 });
1897 this.resizer.on('resize', this.onHtmlAreaResize, this);
1898 }
1899 },
1900 /*
1901 * Resize the framework when the resizer handles are used
1902 */
1903 onHtmlAreaResize: function (resizer, width, height, event) {
1904 // Set width first as it may change the height of the toolbar and of the statusBar
1905 this.setWidth(width);
1906 // Set height of iframe and textarea
1907 this.iframe.setHeight(this.getInnerHeight());
1908 this.textArea.setSize(this.getInnerWidth(), this.getInnerHeight());
1909 },
1910 /*
1911 * Size the iframe according to initial textarea size as set by Page and User TSConfig
1912 */
1913 onWindowResize: function (width, height) {
1914 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1915 this.resizeFramework(width, height);
1916 } else {
1917 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1918 var parentElements = [].concat(this.nestedParentElements.sorted);
1919 // Walk through all nested tabs and inline levels to get correct sizes
1920 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].resizeFramework(args[1], args[2])', [this, width, height]);
1921 }
1922 },
1923 /*
1924 * Resize the framework to its initial size
1925 */
1926 resizeFramework: function (width, height) {
1927 var frameworkHeight = parseInt(this.textAreaInitialSize.height);
1928 if (this.textAreaInitialSize.width.indexOf('%') === -1) {
1929 // Width is specified in pixels
1930 var frameworkWidth = parseInt(this.textAreaInitialSize.width) - this.getFrameWidth();
1931 } else {
1932 // Width is specified in %
1933 if (Ext.isNumber(width)) {
1934 // Framework sizing on actual window resize
1935 var frameworkWidth = parseInt(((width - this.textAreaInitialSize.wizardsWidth - (this.fullScreen ? 10 : Ext.getScrollBarWidth()) - this.getBox().x - 15) * parseInt(this.textAreaInitialSize.width))/100);
1936 } else {
1937 // Initial framework sizing
1938 var frameworkWidth = parseInt(((HTMLArea.util.TYPO3.getWindowSize().width - this.textAreaInitialSize.wizardsWidth - (this.fullScreen ? 10 : Ext.getScrollBarWidth()) - this.getBox().x - 15) * parseInt(this.textAreaInitialSize.width))/100);
1939 }
1940 }
1941 if (this.resizable) {
1942 this.resizer.resizeTo(frameworkWidth, frameworkHeight);
1943 } else {
1944 this.setSize(frameworkWidth, frameworkHeight);
1945 this.doLayout();
1946 }
1947 },
1948 /*
1949 * Resize the framework components
1950 */
1951 onFrameworkResize: function () {
1952 this.iframe.setSize(this.getInnerWidth(), this.getInnerHeight());
1953 this.textArea.setSize(this.getInnerWidth(), this.getInnerHeight());
1954 },
1955 /*
1956 * Adjust the height to the changing size of the statusbar when the textarea is shown
1957 */
1958 onTextAreaShow: function () {
1959 this.iframe.setHeight(this.getInnerHeight());
1960 this.textArea.setHeight(this.getInnerHeight());
1961 },
1962 /*
1963 * Adjust the height to the changing size of the statusbar when the iframe is shown
1964 */
1965 onIframeShow: function () {
1966 if (this.getInnerHeight() <= 0) {
1967 this.onWindowResize();
1968 } else {
1969 this.iframe.setHeight(this.getInnerHeight());
1970 this.textArea.setHeight(this.getInnerHeight());
1971 }
1972 },
1973 /*
1974 * Calculate the height available for the editing iframe
1975 */
1976 getInnerHeight: function () {
1977 return this.getSize().height - this.toolbar.getHeight() - this.statusBar.getHeight() - 5;
1978 },
1979 /*
1980 * Fire the editor when all components of the framework are rendered and ready
1981 */
1982 onIframeReady: function () {
1983 this.ready = this.rendered && this.toolbar.rendered && this.statusBar.rendered && this.textAreaContainer.rendered;
1984 if (this.ready) {
1985 this.initEventListeners();
1986 this.textAreaContainer.show();
1987 if (!this.getEditor().config.showStatusBar) {
1988 this.statusBar.hide();
1989 }
1990 // Set the initial size of the framework
1991 this.onWindowResize();
1992 this.fireEvent('HTMLAreaEventFrameworkReady');
1993 } else {
1994 this.onIframeReady.defer(50, this);
1995 }
1996 },
1997 /*
1998 * Handler invoked if we are inside a form and the form is reset
1999 * On reset, re-initialize the HTMLArea content and update the toolbar
2000 */
2001 onReset: function (event) {
2002 this.getEditor().setHTML(this.textArea.getValue());
2003 this.toolbar.update();
2004 // Invoke previous reset handlers, if any
2005 var htmlAreaPreviousOnReset = event.getTarget().dom.htmlAreaPreviousOnReset;
2006 if (typeof(htmlAreaPreviousOnReset) != 'undefined') {
2007 Ext.each(htmlAreaPreviousOnReset, function (onReset) {
2008 onReset();
2009 return true;
2010 });
2011 }
2012 },
2013 /*
2014 * Cleanup on framework destruction
2015 */
2016 onBeforeDestroy: function () {
2017 Ext.EventManager.removeResizeListener(this.onWindowResize, this);
2018 // Cleaning references to DOM in order to avoid IE memory leaks
2019 var form = this.textArea.dom.form;
2020 if (form) {
2021 form.htmlAreaPreviousOnReset = null;
2022 Ext.get(form).dom = null;
2023 }
2024 Ext.getBody().dom = null;
2025 // ExtJS is not releasing any resources when the iframe is unloaded
2026 this.toolbar.destroy();
2027 this.statusBar.destroy();
2028 this.removeAll(true);
2029 if (this.resizer) {
2030 this.resizer.destroy();
2031 }
2032 return true;
2033 }
2034 });
2035 Ext.reg('htmlareaframework', HTMLArea.Framework);
2036 /***************************************************
2037 * HTMLArea.Editor extends Ext.util.Observable
2038 ***************************************************/
2039 HTMLArea.Editor = Ext.extend(Ext.util.Observable, {
2040 /*
2041 * HTMLArea.Editor constructor
2042 */
2043 constructor: function (config) {
2044 HTMLArea.Editor.superclass.constructor.call(this, {});
2045 // Save the config
2046 this.config = config;
2047 // Establish references to this editor
2048 this.editorId = this.config.editorId;
2049 RTEarea[this.editorId].editor = this;
2050 // Get textarea size and wizard context
2051 this.textArea = Ext.get(this.config.id);
2052 this.textAreaInitialSize = {
2053 width: this.config.RTEWidthOverride ? this.config.RTEWidthOverride : this.textArea.getStyle('width'),
2054 height: this.config.fullScreen ? HTMLArea.util.TYPO3.getWindowSize().height - 20 : this.textArea.getStyle('height'),
2055 wizardsWidth: 0
2056 };
2057 // TYPO3 Inline elements and tabs
2058 this.nestedParentElements = {
2059 all: this.config.tceformsNested,
2060 sorted: HTMLArea.util.TYPO3.simplifyNested(this.config.tceformsNested)
2061 };
2062 this.isNested = !Ext.isEmpty(this.nestedParentElements.sorted);
2063 // If in BE, get width of wizards
2064 if (Ext.get('typo3-docheader')) {
2065 this.wizards = this.textArea.parent().parent().next();
2066 if (this.wizards) {
2067 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
2068 this.textAreaInitialSize.wizardsWidth = this.wizards.getWidth();
2069 } else {
2070 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
2071 var parentElements = [].concat(this.nestedParentElements.sorted);
2072 // Walk through all nested tabs and inline levels to get correct size
2073 this.textAreaInitialSize.wizardsWidth = HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].getWidth()', [this.wizards]);
2074 }
2075 // Hide the wizards so that they do not move around while the editor framework is being sized
2076 this.wizards.hide();
2077 }
2078 }
2079 // Plugins register
2080 this.plugins = {};
2081 // Register the plugins included in the configuration
2082 Ext.iterate(this.config.plugin, function (plugin) {
2083 if (this.config.plugin[plugin]) {
2084 this.registerPlugin(plugin);
2085 }
2086 }, this);
2087 // Create Ajax object
2088 this.ajax = new HTMLArea.Ajax({
2089 editor: this
2090 });
2091 // Initialize keyboard input inhibit flag
2092 this.inhibitKeyboardInput = false;
2093 this.addEvents(
2094 /*
2095 * @event HTMLAreaEventEditorReady
2096 * Fires when initialization of the editor is complete
2097 */
2098 'HTMLAreaEventEditorReady',
2099 /*
2100 * @event HTMLAreaEventModeChange
2101 * Fires when the editor changes mode
2102 */
2103 'HTMLAreaEventModeChange'
2104 );
2105 },
2106 /*
2107 * Flag set to true when the editor initialization has completed
2108 */
2109 ready: false,
2110 /*
2111 * The current mode of the editor: 'wysiwyg' or 'textmode'
2112 */
2113 mode: 'textmode',
2114 /*
2115 * Determine whether the editor document is currently contentEditable
2116 *
2117 * @return boolean true, if the document is contentEditable
2118 */
2119 isEditable: function () {
2120 return Ext.isIE ? this.document.body.contentEditable : (this.document.designMode === 'on');
2121 },
2122 /*
2123 * The selection object
2124 */
2125 selection: null,
2126 getSelection: function () {
2127 if (!this.selection) {
2128 this.selection = new HTMLArea.DOM.Selection({
2129 editor: this
2130 });
2131 }
2132 return this.selection;
2133 },
2134 /*
2135 * The bookmark object
2136 */
2137 bookMark: null,
2138 getBookMark: function () {
2139 if (!this.bookMark) {
2140 this.bookMark = new HTMLArea.DOM.BookMark({
2141 editor: this
2142 });
2143 }
2144 return this.bookMark;
2145 },
2146 /*
2147 * The DOM node object
2148 */
2149 domNode: null,
2150 getDomNode: function () {
2151 if (!this.domNode) {
2152 this.domNode = new HTMLArea.DOM.Node({
2153 editor: this
2154 });
2155 }
2156 return this.domNode;
2157 },
2158 /*
2159 * Create the htmlArea framework
2160 */
2161 generate: function () {
2162 // Create the editor framework
2163 this.htmlArea = new HTMLArea.Framework({
2164 id: this.editorId + '-htmlArea',
2165 layout: 'anchor',
2166 baseCls: 'htmlarea',
2167 editorId: this.editorId,
2168 textArea: this.textArea,
2169 textAreaInitialSize: this.textAreaInitialSize,
2170 fullScreen: this.config.fullScreen,
2171 resizable: this.config.resizable,
2172 maxHeight: this.config.maxHeight,
2173 isNested: this.isNested,
2174 nestedParentElements: this.nestedParentElements,
2175 // The toolbar
2176 tbar: {
2177 xtype: 'htmlareatoolbar',
2178 id: this.editorId + '-toolbar',
2179 anchor: '100%',
2180 layout: 'form',
2181 cls: 'toolbar',
2182 editorId: this.editorId
2183 },
2184 items: [{
2185 // The iframe
2186 xtype: 'htmlareaiframe',
2187 itemId: 'iframe',
2188 anchor: '100%',
2189 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
2190 height: parseInt(this.textAreaInitialSize.height),
2191 autoEl: {
2192 id: this.editorId + '-iframe',
2193 tag: 'iframe',
2194 cls: 'editorIframe',
2195 src: Ext.isGecko ? 'javascript:void(0);' : (Ext.isWebKit ? 'javascript: \'' + HTMLArea.htmlEncode(this.config.documentType + this.config.blankDocument) + '\'' : HTMLArea.editorUrl + 'popups/blank.html')
2196 },
2197 isNested: this.isNested,
2198 nestedParentElements: this.nestedParentElements,
2199 editorId: this.editorId
2200 },{
2201 // Box container for the textarea
2202 xtype: 'box',
2203 itemId: 'textAreaContainer',
2204 anchor: '100%',
2205 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
2206 // Let the framework swallow the textarea and throw it back
2207 listeners: {
2208 afterrender: {
2209 fn: function (textAreaContainer) {
2210 this.originalParent = this.textArea.parent().dom;
2211 textAreaContainer.getEl().appendChild(this.textArea);
2212 },
2213 single: true,
2214 scope: this
2215 },
2216 beforedestroy: {
2217 fn: function (textAreaContainer) {
2218 this.originalParent.appendChild(this.textArea.dom);
2219 return true;
2220 },
2221 single: true,
2222 scope: this
2223 }
2224 }
2225 }
2226 ],
2227 // The status bar
2228 bbar: {
2229 xtype: 'htmlareastatusbar',
2230 anchor: '100%',
2231 cls: 'statusBar',
2232 editorId: this.editorId
2233 }
2234 });
2235 // Set some references
2236 this.toolbar = this.htmlArea.getTopToolbar();
2237 this.statusBar = this.htmlArea.getBottomToolbar();
2238 this.iframe = this.htmlArea.getComponent('iframe');
2239 this.textAreaContainer = this.htmlArea.getComponent('textAreaContainer');
2240 // Get triggered when the framework becomes ready
2241 this.relayEvents(this.htmlArea, ['HTMLAreaEventFrameworkReady']);
2242 this.on('HTMLAreaEventFrameworkReady', this.onFrameworkReady, this, {single: true});
2243 },
2244 /*
2245 * Initialize the editor
2246 */
2247 onFrameworkReady: function () {
2248 // Initialize editor mode
2249 this.setMode('wysiwyg');
2250 // Create the selection object
2251 this.getSelection();
2252 // Create the bookmark object
2253 this.getBookMark();
2254 // Create the DOM node object
2255 this.getDomNode();
2256 // Initiate events listening
2257 this.initEventsListening();
2258 // Generate plugins
2259 this.generatePlugins();
2260 // Make the editor visible
2261 this.show();
2262 // Make the wizards visible again
2263 if (this.wizards) {
2264 this.wizards.show();
2265 }
2266 // Focus on the first editor that is not hidden
2267 Ext.iterate(RTEarea, function (editorId, RTE) {
2268 if (!Ext.isDefined(RTE.editor) || (RTE.editor.isNested && !HTMLArea.util.TYPO3.allElementsAreDisplayed(RTE.editor.nestedParentElements.sorted))) {
2269 return true;
2270 } else {
2271 RTE.editor.focus();
2272 return false;
2273 }
2274 }, this);
2275 this.ready = true;
2276 this.fireEvent('HTMLAreaEventEditorReady');
2277 this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
2278 },
2279 /*
2280 * Set editor mode
2281 *
2282 * @param string mode: 'textmode' or 'wysiwyg'
2283 *
2284 * @return void
2285 */
2286 setMode: function (mode) {
2287 switch (mode) {
2288 case 'textmode':
2289 this.textArea.set({ value: this.getHTML() }, false);
2290 this.iframe.setDesignMode(false);
2291 this.iframe.hide();
2292 this.textAreaContainer.show();
2293 this.mode = mode;
2294 break;
2295 case 'wysiwyg':
2296 try {
2297 this.document.body.innerHTML = this.getHTML();
2298 } catch(e) {
2299 this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
2300 TYPO3.Dialog.ErrorDialog({
2301 title: 'htmlArea RTE',
2302 msg: HTMLArea.localize('HTML-document-not-well-formed')
2303 });
2304 break;
2305 }
2306 this.textAreaContainer.hide();
2307 this.iframe.show();
2308 this.iframe.setDesignMode(true);
2309 this.mode = mode;
2310 break;
2311 }
2312 this.fireEvent('HTMLAreaEventModeChange', this.mode);
2313 this.focus();
2314 Ext.iterate(this.plugins, function(pluginId) {
2315 this.getPlugin(pluginId).onMode(this.mode);
2316 }, this);
2317 },
2318 /*
2319 * Get current editor mode
2320 */
2321 getMode: function () {
2322 return this.mode;
2323 },
2324 /*
2325 * Retrieve the HTML
2326 * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
2327 *
2328 * @return string the textual html content from the current editing mode
2329 */
2330 getHTML: function () {
2331 switch (this.mode) {
2332 case 'wysiwyg':
2333 return this.iframe.getHTML();
2334 case 'textmode':
2335 // Collapse repeated spaces non-editable in wysiwyg
2336 // Replace leading and trailing spaces non-editable in wysiwyg
2337 return this.textArea.getValue().
2338 replace(/[\x20]+/g, '\x20').
2339 replace(/^\x20/g, '&nbsp;').
2340 replace(/\x20$/g, '&nbsp;');
2341 default:
2342 return '';
2343 }
2344 },
2345 /*
2346 * Retrieve raw HTML
2347 *
2348 * @return string the textual html content from the current editing mode
2349 */
2350 getInnerHTML: function () {
2351 switch (this.mode) {
2352 case 'wysiwyg':
2353 return this.document.body.innerHTML;
2354 case 'textmode':
2355 return this.textArea.getValue();
2356 default:
2357 return '';
2358 }
2359 },
2360 /*
2361 * Replace the html content
2362 *
2363 * @param string html: the textual html
2364 *
2365 * @return void
2366 */
2367 setHTML: function (html) {
2368 switch (this.mode) {
2369 case 'wysiwyg':
2370 this.document.body.innerHTML = html;
2371 break;
2372 case 'textmode':
2373 this.textArea.set({ value: html }, false);;
2374 break;
2375 }
2376 },
2377 /*
2378 * Instantiate the specified plugin and register it with the editor
2379 *
2380 * @param string plugin: the name of the plugin
2381 *
2382 * @return boolean true if the plugin was successfully registered
2383 */
2384 registerPlugin: function (pluginName) {
2385 var plugin = HTMLArea[pluginName],
2386 isRegistered = false;
2387 if (typeof(plugin) !== 'undefined' && Ext.isFunction(plugin)) {
2388 var pluginInstance = new plugin(this, pluginName);
2389 if (pluginInstance) {
2390 var pluginInformation = pluginInstance.getPluginInformation();
2391 pluginInformation.instance = pluginInstance;
2392 this.plugins[pluginName] = pluginInformation;
2393 isRegistered = true;
2394 }
2395 }
2396 if (!isRegistered) {
2397 this.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName + '.', 'warn');
2398 }
2399 return isRegistered;
2400 },
2401 /*
2402 * Generate registered plugins
2403 */
2404 generatePlugins: function () {
2405 Ext.iterate(this.plugins, function (pluginId) {
2406 var plugin = this.getPlugin(pluginId);
2407 plugin.onGenerate();
2408 }, this);
2409 },
2410 /*
2411 * Get the instance of the specified plugin, if it exists
2412 *
2413 * @param string pluginName: the name of the plugin
2414 * @return object the plugin instance or null
2415 */
2416 getPlugin: function(pluginName) {
2417 return (this.plugins[pluginName] ? this.plugins[pluginName].instance : null);
2418 },
2419 /*
2420 * Unregister the instance of the specified plugin
2421 *
2422 * @param string pluginName: the name of the plugin
2423 * @return void
2424 */
2425 unRegisterPlugin: function(pluginName) {
2426 delete this.plugins[pluginName].instance;
2427 delete this.plugins[pluginName];
2428 },
2429 /*
2430 * Update the edito toolbar
2431 */
2432 updateToolbar: function (noStatus) {
2433 this.toolbar.update(noStatus);
2434 },
2435 /*
2436 * Focus on the editor
2437 */
2438 focus: function () {
2439 switch (this.getMode()) {
2440 case 'wysiwyg':
2441 this.iframe.focus();
2442 break;
2443 case 'textmode':
2444 this.textArea.focus();
2445 break;
2446 }
2447 },
2448 /*
2449 * Scroll the editor window to the current caret position
2450 */
2451 scrollToCaret: function () {
2452 if (!Ext.isIE) {
2453 var e = this.getSelection().getParentElement(),
2454 w = this.iframe.getEl().dom.contentWindow ? this.iframe.getEl().dom.contentWindow : window,
2455 h = w.innerHeight || w.height,
2456 d = this.document,
2457 t = d.documentElement.scrollTop || d.body.scrollTop;
2458 if (e.offsetTop > h+t || e.offsetTop < t) {
2459 this.getSelection().getParentElement().scrollIntoView();
2460 }
2461 }
2462 },
2463 /*
2464 * Add listeners
2465 */
2466 initEventsListening: function () {
2467 if (Ext.isOpera) {
2468 this.iframe.startListening();
2469 }
2470 // Add unload handler
2471 var iframe = this.iframe.getEl().dom;
2472 Ext.EventManager.on(iframe.contentWindow ? iframe.contentWindow : iframe.contentDocument, 'unload', this.onUnload, this, {single: true});
2473 },
2474 /*
2475 * Make the editor framework visible
2476 */
2477 show: function () {
2478 document.getElementById('pleasewait' + this.editorId).style.display = 'none';
2479 document.getElementById('editorWrap' + this.editorId).style.visibility = 'visible';
2480 },
2481 /*
2482 * Append an entry at the end of the troubleshooting log
2483 *
2484 * @param string functionName: the name of the editor function writing to the log
2485 * @param string text: the text of the message
2486 * @param string type: the type of message
2487 *
2488 * @return void
2489 */
2490 appendToLog: function (objectName, functionName, text, type) {
2491 HTMLArea.appendToLog(this.editorId, objectName, functionName, text, type);
2492 },
2493 /*
2494 * Iframe unload handler: Update the textarea for submission and cleanup
2495 */
2496 onUnload: function (event) {
2497 // Save the HTML content into the original textarea for submit, back/forward, etc.
2498 if (this.ready) {
2499 this.textArea.set({
2500 value: this.getHTML()
2501 }, false);
2502 }
2503 // Cleanup
2504 Ext.TaskMgr.stopAll();
2505 // ExtJS is not releasing any resources when the iframe is unloaded
2506 this.htmlArea.destroy();
2507 Ext.iterate(this.plugins, function (pluginId) {
2508 this.unRegisterPlugin(pluginId);
2509 }, this);
2510 this.purgeListeners();
2511 // Cleaning references to DOM in order to avoid IE memory leaks
2512 if (this.wizards) {
2513 this.wizards.dom = null;
2514 this.textArea.parent().parent().dom = null;
2515 this.textArea.parent().dom = null;
2516 }
2517 this.textArea.dom = null;
2518 RTEarea[this.editorId].editor = null;
2519 }
2520 });
2521 HTMLArea.Ajax = function (config) {
2522 Ext.apply(this, config);
2523 };
2524 HTMLArea.Ajax = Ext.extend(HTMLArea.Ajax, {
2525 /*
2526 * Load a Javascript file asynchronously
2527 *
2528 * @param string url: url of the file to load
2529 * @param function callBack: the callBack function
2530 * @param object scope: scope of the callbacks
2531 *
2532 * @return boolean true on success of the request submission
2533 */
2534 getJavascriptFile: function (url, callback, scope) {
2535 var success = false;
2536 var self = this;
2537 Ext.Ajax.request({
2538 method: 'GET',
2539 url: url,
2540 callback: callback,
2541 success: function (response) {
2542 success = true;
2543 },
2544 failure: function (response) {
2545 self.editor.inhibitKeyboardInput = false;
2546 self.editor.appendToLog('HTMLArea.Ajax', 'getJavascriptFile', 'Unable to get ' + url + ' . Server reported ' + response.status, 'error');
2547 },
2548 scope: scope
2549 });
2550 return success;
2551 },
2552 /*
2553 * Post data to the server
2554 *
2555 * @param string url: url to post data to
2556 * @param object data: data to be posted
2557 * @param function callback: function that will handle the response returned by the server
2558 * @param object scope: scope of the callbacks
2559 *
2560 * @return boolean true on success
2561 */
2562 postData: function (url, data, callback, scope) {
2563 var success = false;
2564 var self = this;
2565 data.charset = this.editor.config.typo3ContentCharset ? this.editor.config.typo3ContentCharset : 'utf-8';
2566 var params = '';
2567 Ext.iterate(data, function (parameter, value) {
2568 params += (params.length ? '&' : '') + parameter + '=' + encodeURIComponent(value);
2569 });
2570 params += this.editor.config.RTEtsConfigParams;
2571 Ext.Ajax.request({
2572 method: 'POST',
2573 headers: {
2574 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
2575 },
2576 url: url,
2577 params: params,
2578 callback: Ext.isFunction(callback) ? callback: function (options, success, response) {
2579 if (!success) {
2580 self.editor.appendToLog('HTMLArea.Ajax', 'postData', 'Post request to ' + url + ' failed. Server reported ' + response.status, 'error');
2581 }
2582 },
2583 success: function (response) {
2584 success = true;
2585 },
2586 failure: function (response) {
2587 self.editor.appendToLog('HTMLArea.Ajax', 'postData', 'Unable to post ' + url + ' . Server reported ' + response.status, 'error');
2588 },
2589 scope: scope
2590 });
2591 return success;
2592 }
2593 });
2594 /***************************************************
2595 * HTMLArea.util.TYPO3: Utility functions for dealing with tabs and inline elements in TYPO3 forms
2596 ***************************************************/
2597 HTMLArea.util.TYPO3 = function () {
2598 return {
2599 /*
2600 * Simplify the array of nested levels. Create an indexed array with the correct names of the elements.
2601 *
2602 * @param object nested: The array with the nested levels
2603 * @return object The simplified array
2604 * @author Oliver Hader <oh@inpublica.de>
2605 */
2606 simplifyNested: function(nested) {
2607 var i, type, level, elementId, max, simplifiedNested=[],
2608 elementIdSuffix = {
2609 tab: '-DIV',
2610 inline: '_fields',
2611 flex: '-content'
2612 };
2613 if (nested && nested.length) {
2614 if (nested[0][0]=='inline') {
2615 nested = inline.findContinuedNestedLevel(nested, nested[0][1]);
2616 }
2617 for (i=0, max=nested.length; i<max; i++) {
2618 type = nested[i][0];
2619 level = nested[i][1];
2620 elementId = level + elementIdSuffix[type];
2621 if (Ext.get(elementId)) {
2622 simplifiedNested.push(elementId);
2623 }
2624 }
2625 }
2626 return simplifiedNested;
2627 },
2628 /*
2629 * Access an inline relational element or tab menu and make it "accessible".
2630 * If a parent or ancestor object has the style "display: none", offsetWidth & offsetHeight are '0'.
2631 *
2632 * @params arry parentElements: array of parent elements id's; note that this input array will be modified
2633 * @params object callbackFunc: A function to be called, when the embedded objects are "accessible".
2634 * @params array args: array of arguments
2635 * @return object An object returned by the callbackFunc.
2636 * @author Oliver Hader <oh@inpublica.de>
2637 */
2638 accessParentElements: function (parentElements, callbackFunc, args) {
2639 var result = {};
2640 if (parentElements.length) {
2641 var currentElement = parentElements.pop();
2642 currentElement = Ext.get(currentElement);
2643 var actionRequired = (currentElement.getStyle('display') == 'none');
2644 if (actionRequired) {
2645 var originalStyles = currentElement.getStyles('visibility', 'position', 'top', 'display');
2646 currentElement.setStyle({
2647 visibility: 'hidden',
2648 position: 'absolute',
2649 top: '-10000px',
2650 display: ''
2651 });
2652 }
2653 result = this.accessParentElements(parentElements, callbackFunc, args);
2654 if (actionRequired) {
2655 currentElement.setStyle(originalStyles);
2656 }
2657 } else {
2658 result = eval(callbackFunc);
2659 }
2660 return result;
2661 },
2662 /*
2663 * Check if all elements in input array are currently displayed
2664 *
2665 * @param array elements: array of element id's
2666 * @return boolean true if all elements are displayed
2667 */
2668 allElementsAreDisplayed: function(elements) {
2669 var allDisplayed = true;
2670 Ext.each(elements, function (element) {
2671 allDisplayed = Ext.get(element).getStyle('display') != 'none';
2672 return allDisplayed;
2673 });
2674 return allDisplayed;
2675 },
2676 /*
2677 * Get current size of window
2678 *
2679 * @return object width and height of window
2680 */
2681 getWindowSize: function () {
2682 if (Ext.isIE) {
2683 var size = Ext.getBody().getSize();
2684 } else {
2685 var size = {
2686 width: window.innerWidth,
2687 height: window.innerHeight
2688 };
2689 }
2690 // Subtract the docheader height from the calculated window height
2691 var docHeader = Ext.get('typo3-docheader');
2692 if (docHeader) {
2693 size.height -= docHeader.getHeight();
2694 docHeader.dom = null;
2695 }
2696 return size;
2697 }
2698 }
2699 }();
2700 /*
2701 ***********************************************
2702 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2703 ***********************************************
2704 */
2705 HTMLArea.Editor.prototype.forceRedraw = function() {
2706 this.appendToLog('HTMLArea.Editor', 'forceRedraw', 'Reference to deprecated method', 'warn');
2707 this.htmlArea.doLayout();
2708 };
2709 /*
2710 * Surround the currently selected HTML source code with the given tags.
2711 * Delete the selection, if any.
2712 *
2713 ***********************************************
2714 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2715 ***********************************************
2716 */
2717 HTMLArea.Editor.prototype.surroundHTML = function(startTag,endTag) {
2718 this.appendToLog('HTMLArea.Editor', 'surroundHTML', 'Reference to deprecated method', 'warn');
2719 this.getSelection().surroundHtml(startTag, endTag);
2720 };
2721
2722 /*
2723 * Change the tag name of a node.
2724 *
2725 ***********************************************
2726 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2727 ***********************************************
2728 */
2729 HTMLArea.Editor.prototype.convertNode = function(el,newTagName) {
2730 this.appendToLog('HTMLArea.Editor', 'surroundHTML', 'Reference to deprecated method', 'warn');
2731 return HTMLArea.DOM.convertNode(el, newTagName);
2732 };
2733
2734 /*
2735 * This function removes the given markup element
2736 *
2737 * @param object element: the inline element to be removed, content and selection being preserved
2738 *
2739 * @return void
2740 *
2741 ***********************************************
2742 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2743 ***********************************************
2744 */
2745 HTMLArea.Editor.prototype.removeMarkup = function(element) {
2746 this.appendToLog('HTMLArea.Editor', 'removeMarkup', 'Reference to deprecated method', 'warn');
2747 this.getDomNode().removeMarkup(element);
2748 };
2749 /*
2750 * Return true if we have some selected content
2751 *
2752 ***********************************************
2753 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2754 ***********************************************
2755 */
2756 HTMLArea.Editor.prototype.hasSelectedText = function() {
2757 this.appendToLog('HTMLArea.Editor', 'hasSelectedText', 'Reference to deprecated method', 'warn');
2758 return !this.getSelection().isEmpty();
2759 };
2760
2761 /*
2762 * Get an array with all the ancestor nodes of the selection
2763 *
2764 ***********************************************
2765 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2766 ***********************************************
2767 */
2768 HTMLArea.Editor.prototype.getAllAncestors = function() {
2769 this.appendToLog('HTMLArea.Editor', 'getAllAncestors', 'Reference to deprecated method', 'warn');
2770 return this.getSelection().getAllAncestors();
2771 };
2772
2773 /*
2774 * Get the block elements containing the start and the end points of the selection
2775 *
2776 ***********************************************
2777 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2778 ***********************************************
2779 */
2780 HTMLArea.Editor.prototype.getEndBlocks = function(selection) {
2781 this.appendToLog('HTMLArea.Editor', 'getEndBlocks', 'Reference to deprecated method', 'warn');
2782 return this.getSelection().getEndBlocks();
2783 };
2784
2785 /*
2786 * This function determines if the end poins of the current selection are within the same block
2787 *
2788 * @return boolean true if the end points of the current selection are inside the same block element
2789 *
2790 ***********************************************
2791 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2792 ***********************************************
2793 */
2794 HTMLArea.Editor.prototype.endPointsInSameBlock = function() {
2795 this.appendToLog('HTMLArea.Editor', 'endPointsInSameBlock', 'Reference to deprecated method', 'warn');
2796 return this.getSelection().endPointsInSameBlock();
2797 };
2798
2799 /*
2800 * Get the deepest ancestor of the selection that is of the specified type
2801 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2802 *
2803 ***********************************************
2804 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2805 ***********************************************
2806 */
2807 HTMLArea.Editor.prototype._getFirstAncestor = function(sel,types) {
2808 this.appendToLog('HTMLArea.Editor', '_getFirstAncestor', 'Reference to deprecated method', 'warn');
2809 return this.getSelection().getFirstAncestorOfType(types);
2810 };
2811 /*
2812 * Get the node whose contents are currently fully selected
2813 *
2814 * @param array selection: the current selection
2815 * @param array range: the range of the current selection
2816 * @param array ancestors: the array of ancestors node of the current selection
2817 *
2818 * @return object the fully selected node, if any, null otherwise
2819 *
2820 ***********************************************
2821 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2822 ***********************************************
2823 */
2824 HTMLArea.Editor.prototype.getFullySelectedNode = function (selection, range, ancestors) {
2825 this.appendToLog('HTMLArea.Editor', 'getFullySelectedNode', 'Reference to deprecated method', 'warn');
2826 return this.getSelection().getFullySelectedNode();
2827 };
2828 /*
2829 * Intercept some native execCommand commands
2830 *
2831 ***********************************************
2832 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2833 ***********************************************
2834 */
2835 HTMLArea.Editor.prototype.execCommand = function(cmdID, UI, param) {
2836 this.appendToLog('HTMLArea.Editor', 'execCommand', 'Reference to deprecated method', 'warn');
2837 return this.getSelection().execCommand(cmdID, UI, param);
2838 };
2839
2840 /***************************************************
2841 * UTILITY FUNCTIONS
2842 ***************************************************/
2843 Ext.apply(HTMLArea.util, {
2844 /*
2845 * Perform HTML encoding of some given string
2846 * Borrowed in part from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2847 */
2848 htmlDecode: function (str) {
2849 str = str.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
2850 str = str.replace(/&nbsp;/g, '\xA0'); // Decimal 160, non-breaking-space
2851 str = str.replace(/&quot;/g, '\x22');
2852 str = str.replace(/&#39;/g, "'");
2853 str = str.replace(/&amp;/g, '&');
2854 return str;
2855 },
2856 htmlEncode: function (str) {
2857 if (typeof(str) != 'string') {
2858 str = str.toString();
2859 }
2860 str = str.replace(/&/g, '&amp;');
2861 str = str.replace(/</g, '&lt;').replace(/>/g, '&gt;');
2862 str = str.replace(/\xA0/g, '&nbsp;'); // Decimal 160, non-breaking-space
2863 str = str.replace(/\x22/g, '&quot;'); // \x22 means '"'
2864 return str;
2865 }
2866 });
2867 /*
2868 ***********************************************
2869 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2870 ***********************************************
2871 */
2872 HTMLArea.htmlDecode = HTMLArea.util.htmlDecode;
2873 /*
2874 ***********************************************
2875 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.7 *
2876 ***********************************************
2877 */
2878 HTMLArea.htmlEncode = HTMLArea.util.htmlEncode;
2879
2880 /*****************************************************************
2881 * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
2882 *****************************************************************/
2883 HTMLArea.DOM = function () {
2884 return {
2885 /***************************************************
2886 * DOM-RELATED CONSTANTS
2887 ***************************************************/
2888 // DOM node types
2889 ELEMENT_NODE: 1,
2890 ATTRIBUTE_NODE: 2,
2891 TEXT_NODE: 3,
2892 CDATA_SECTION_NODE: 4,
2893 ENTITY_REFERENCE_NODE: 5,
2894 ENTITY_NODE: 6,
2895 PROCESSING_INSTRUCTION_NODE: 7,
2896 COMMENT_NODE: 8,
2897 DOCUMENT_NODE: 9,
2898 DOCUMENT_TYPE_NODE: 10,
2899 DOCUMENT_FRAGMENT_NODE: 11,
2900 NOTATION_NODE: 12,
2901 /***************************************************
2902 * DOM-RELATED REGULAR EXPRESSIONS
2903 ***************************************************/
2904 RE_blockTags: /^(address|article|aside|body|blockquote|caption|dd|div|dl|dt|fieldset|footer|form|header|hr|h1|h2|h3|h4|h5|h6|iframe|li|ol|p|pre|nav|noscript|section|table|tbody|td|tfoot|th|thead|tr|ul)$/i,
2905 RE_noClosingTag: /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i,
2906 RE_bodyTag: new RegExp('<\/?(body)[^>]*>', 'gi'),
2907 /***************************************************
2908 * STATIC METHODS ON DOM NODE
2909 ***************************************************/
2910 /*
2911 * Determine whether an element node is a block element
2912 *
2913 * @param object element: the element node
2914 *
2915 * @return boolean true, if the element node is a block element
2916 */
2917 isBlockElement: function (element) {
2918 return element && element.nodeType === HTMLArea.DOM.ELEMENT_NODE && HTMLArea.DOM.RE_blockTags.test(element.nodeName);
2919 },
2920 /*
2921 * Determine whether an element node needs a closing tag
2922 *
2923 * @param object element: the element node
2924 *
2925 * @return boolean true, if the element node needs a closing tag
2926 */
2927 needsClosingTag: function (element) {
2928 return element && element.nodeType === HTMLArea.DOM.ELEMENT_NODE && !HTMLArea.DOM.RE_noClosingTag.test(element.nodeName);
2929 },
2930 /*
2931 * Gets the class names assigned to a node, reserved classes removed
2932 *
2933 * @param object node: the node
2934 * @return array array of class names on the node, reserved classes removed
2935 */
2936 getClassNames: function (node) {
2937 var classNames = [];
2938 if (node) {
2939 if (node.className && /\S/.test(node.className)) {
2940 classNames = node.className.trim().split(' ');
2941 }
2942 if (HTMLArea.reservedClassNames.test(node.className)) {
2943 var cleanClassNames = [];
2944 var j = -1;
2945 for (var i = 0; i < classNames.length; ++i) {
2946 if (!HTMLArea.reservedClassNames.test(classNames[i])) {
2947 cleanClassNames[++j] = classNames[i];
2948 }
2949 }
2950 classNames = cleanClassNames;
2951 }
2952 }
2953 return classNames;
2954 },
2955 /*
2956 * Check if a class name is in the class attribute of a node
2957 *
2958 * @param object node: the node
2959 * @param string className: the class name to look for
2960 * @param boolean substring: if true, look for a class name starting with the given string
2961 * @return boolean true if the class name was found, false otherwise
2962 */
2963 hasClass: function (node, className, substring) {
2964 var found = false;
2965 if (node && node.className) {
2966 var classes = node.className.trim().split(' ');
2967 for (var i = classes.length; --i >= 0;) {
2968 found = ((classes[i] == className) || (substring && classes[i].indexOf(className) == 0));
2969 if (found) {
2970 break;
2971 }
2972 }
2973 }
2974 return found;
2975 },
2976 /*
2977 * Add a class name to the class attribute of a node
2978 *
2979 * @param object node: the node
2980 * @param string className: the name of the class to be added
2981 * @return void
2982 */
2983 addClass: function (node, className) {
2984 if (node) {
2985 HTMLArea.DOM.removeClass(node, className);
2986 // Remove classes configured to be incompatible with the class to be added
2987 if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && Ext.isFunction(HTMLArea.classesXOR[className].test)) {
2988 var classNames = node.className.trim().split(' ');
2989 for (var i = classNames.length; --i >= 0;) {
2990 if (HTMLArea.classesXOR[className].test(classNames[i])) {
2991 HTMLArea.DOM.removeClass(node, classNames[i]);
2992 }
2993 }
2994 }
2995 if (node.className) {
2996 node.className += ' ' + className;
2997 } else {
2998 node.className = className;
2999 }
3000 }
3001 },
3002 /*
3003 * Remove a class name from the class attribute of a node
3004 *
3005 * @param object node: the node
3006 * @param string className: the class name to removed
3007 * @param boolean substring: if true, remove the class names starting with the given string
3008 * @return void
3009 */
3010 removeClass: function (node, className, substring) {
3011 if (node && node.className) {
3012 var classes = node.className.trim().split(' ');
3013 var newClasses = [];
3014 for (var i = classes.length; --i >= 0;) {
3015 if ((!substring && classes[i] != className) || (substring && classes[i].indexOf(className) != 0)) {
3016 newClasses[newClasses.length] = classes[i];
3017 }
3018 }
3019 if (newClasses.length) {
3020 node.className = newClasses.join(' ');
3021 } else {
3022 if (!Ext.isOpera) {
3023 node.removeAttribute('class');
3024 if (Ext.isIE) {
3025 node.removeAttribute('className');
3026 }
3027 } else {
3028 node.className = '';
3029 }
3030 }
3031 }
3032 },
3033 /*
3034 * Get the innerText of a given node
3035 *
3036 * @param object node: the node
3037 *
3038 * @return string the text inside the node
3039 */
3040 getInnerText: function (node) {
3041 return HTMLArea.isIEBeforeIE9 ? node.innerText : node.textContent;;
3042 },
3043 /*
3044 * Get the block ancestors of a node within a given block
3045 *
3046 * @param object node: the given node
3047 * @param object withinBlock: the containing node
3048 *
3049 * @return array array of block ancestors
3050 */
3051 getBlockAncestors: function (node, withinBlock) {
3052 var ancestors = [];
3053 var ancestor = node;
3054 while (ancestor && (ancestor.nodeType === HTMLArea.DOM.ELEMENT_NODE) && !/^(body)$/i.test(ancestor.nodeName) && ancestor != withinBlock) {
3055 if (HTMLArea.DOM.isBlockElement(ancestor)) {
3056 ancestors.unshift(ancestor);
3057 }
3058 ancestor = ancestor.parentNode;
3059 }
3060 ancestors.unshift(ancestor);
3061 return ancestors;
3062 },
3063 /*
3064 * Get the deepest element ancestor of a given node that is of one of the specified types
3065 *
3066 * @param object node: the given node
3067 * @param array types: an array of nodeNames
3068 *
3069 * @return object the found ancestor of one of the given types or null
3070 */
3071 getFirstAncestorOfType: function (node, types) {
3072 var ancestor = null,
3073 parent = node;
3074 if (!Ext.isEmpty(types)) {
3075 if (Ext.isString(types)) {
3076 var types = [types];
3077 }
3078 types = new RegExp( '^(' + types.join('|') + ')$', 'i');
3079 while (parent && parent.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
3080 if (types.test(parent.nodeName)) {
3081 ancestor = parent;
3082 break;
3083 }
3084 parent = parent.parentNode;
3085 }
3086 }
3087 return ancestor;
3088 },
3089 /*
3090 * Determine whether a given node has any allowed attributes
3091 *
3092 * @param object node: the DOM node
3093 * @param array allowedAttributes: array of allowed attribute names
3094 *
3095 * @return boolean true if the node has one of the allowed attributes
3096 */
3097 hasAllowedAttributes: function (node, allowedAttributes) {
3098 var value,
3099 hasAllowedAttributes = false;
3100 if (Ext.isString(allowedAttributes)) {
3101 allowedAttributes = [allowedAttributes];
3102 }
3103 allowedAttributes = allowedAttributes || [];
3104 for (var i = allowedAttributes.length; --i >= 0;) {
3105 value = node.getAttribute(allowedAttributes[i]);
3106 if (value) {
3107 if (allowedAttributes[i] === 'style') {
3108