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