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