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