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