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