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