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