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