2c733dc5fee452939be6213584a9fd01a52bc382
[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 baseUrl: this.config.baseURL
815 });
816 if (!this.config.showStatusBar) {
817 this.addClass('noStatusBar');
818 }
819 },
820 /*
821 * Initialize event listeners and the document after the iframe has rendered
822 */
823 initEventListeners: function () {
824 this.initStyleChangeEventListener();
825 if (Ext.isOpera) {
826 this.mon(this.getEl(), 'load', this.initializeIframe , this, {single: true});
827 } else {
828 this.initializeIframe();
829 }
830 },
831 /*
832 * The editor iframe may become hidden with style.display = "none" on some parent div
833 * 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"
834 * In all browsers, it breaks the evaluation of the framework dimensions
835 */
836 initStyleChangeEventListener: function () {
837 if (this.isNested && !Ext.isWebKit) {
838 var options = {
839 stopEvent: true
840 };
841 if (Ext.isGecko) {
842 options.delay = 50;
843 }
844 Ext.each(this.nestedParentElements.sorted, function (nested) {
845 if (!Ext.isGecko) {
846 options.target = Ext.get(nested);
847 }
848 this.mon(
849 Ext.get(nested),
850 Ext.isIE ? 'propertychange' : 'DOMAttrModified',
851 this.onNestedShow,
852 this,
853 options
854 );
855 }, this);
856 }
857 },
858 /*
859 * editorId should be set in config
860 */
861 editorId: null,
862 /*
863 * Get a reference to the editor
864 */
865 getEditor: function() {
866 return RTEarea[this.editorId].editor;
867 },
868 /*
869 * Get a reference to the toolbar
870 */
871 getToolbar: function () {
872 return this.ownerCt.getTopToolbar();
873 },
874 /*
875 * Get a reference to the statusBar
876 */
877 getStatusBar: function () {
878 return this.ownerCt.getBottomToolbar();
879 },
880 /*
881 * Get a reference to a button
882 */
883 getButton: function (buttonId) {
884 return this.getToolbar().getButton(buttonId);
885 },
886 /*
887 * Flag set to true when the iframe becomes usable for editing
888 */
889 ready: false,
890 /*
891 * Create the iframe element at rendering time
892 */
893 onRender: function (ct, position){
894 // from Ext.Component
895 if (!this.el && this.autoEl) {
896 if (Ext.isString(this.autoEl)) {
897 this.el = document.createElement(this.autoEl);
898 } else {
899 // ExtJS Default method will not work with iframe element
900 this.el = Ext.DomHelper.append(ct, this.autoEl, true);
901 }
902 if (!this.el.id) {
903 this.el.id = this.getId();
904 }
905 }
906 // from Ext.BoxComponent
907 if (this.resizeEl){
908 this.resizeEl = Ext.get(this.resizeEl);
909 }
910 if (this.positionEl){
911 this.positionEl = Ext.get(this.positionEl);
912 }
913 },
914 /*
915 * Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
916 */
917 initializeIframe: function () {
918 var iframe = this.getEl().dom;
919 // All browsers
920 if (!iframe || (!iframe.contentWindow && !iframe.contentDocument)) {
921 this.initializeIframe.defer(50, this);
922 // All except WebKit
923 } else if (iframe.contentWindow && !Ext.isWebKit && (!iframe.contentWindow.document || !iframe.contentWindow.document.documentElement)) {
924 this.initializeIframe.defer(50, this);
925 // WebKit
926 } else if (Ext.isWebKit && (!iframe.contentDocument.documentElement || !iframe.contentDocument.body)) {
927 this.initializeIframe.defer(50, this);
928 } else {
929 this.document = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
930 this.getEditor().document = this.document;
931 this.getEditor()._doc = this.document;
932 this.getEditor()._iframe = iframe;
933 this.createHead();
934 // Style the document body
935 Ext.get(this.document.body).addClass('htmlarea-content-body');
936 // Start listening to things happening in the iframe
937 // For some unknown reason, this is too early for Opera
938 if (!Ext.isOpera) {
939 this.startListening();
940 }
941 // Hide the iframe
942 this.hide();
943 // Set iframe ready
944 this.ready = true;
945 this.fireEvent('HTMLAreaEventIframeReady');
946 }
947 },
948 /*
949 * Build the iframe document head
950 */
951 createHead: function () {
952 var head = this.document.getElementsByTagName('head')[0];
953 if (!head) {
954 head = this.document.createElement('head');
955 this.document.documentElement.appendChild(head);
956 }
957 if (this.config.baseURL) {
958 var base = this.document.getElementsByTagName('base')[0];
959 if (!base) {
960 base = this.document.createElement('base');
961 base.href = this.config.baseURL;
962 head.appendChild(base);
963 }
964 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Iframe baseURL set to: ' + base.href, 'info');
965 }
966 var link0 = this.document.getElementsByTagName('link')[0];
967 if (!link0) {
968 link0 = this.document.createElement('link');
969 link0.rel = 'stylesheet';
970 link0.type = 'text/css';
971 // 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.
972 // Therefore, for versions before 3.6.8, we prepend the url with the base, if the url is not absolute
973 link0.href = ((Ext.isGecko && navigator.productSub < 2010072200 && !/^http(s?):\/{2}/.test(this.config.editedContentStyle)) ? this.config.baseURL : '') + this.config.editedContentStyle;
974 head.appendChild(link0);
975 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Skin CSS set to: ' + link0.href, 'info');
976 }
977 if (this.config.defaultPageStyle) {
978 var link = this.document.getElementsByTagName('link')[1];
979 if (!link) {
980 link = this.document.createElement('link');
981 link.rel = 'stylesheet';
982 link.type = 'text/css';
983 link.href = ((Ext.isGecko && navigator.productSub < 2010072200 && !/^https?:\/{2}/.test(this.config.defaultPageStyle)) ? this.config.baseURL : '') + this.config.defaultPageStyle;
984 head.appendChild(link);
985 }
986 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Override CSS set to: ' + link.href, 'info');
987 }
988 if (this.config.pageStyle) {
989 var link = this.document.getElementsByTagName('link')[2];
990 if (!link) {
991 link = this.document.createElement('link');
992 link.rel = 'stylesheet';
993 link.type = 'text/css';
994 link.href = ((Ext.isGecko && navigator.productSub < 2010072200 && !/^https?:\/{2}/.test(this.config.pageStyle)) ? this.config.baseURL : '') + this.config.pageStyle;
995 head.appendChild(link);
996 }
997 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link.href, 'info');
998 }
999 },
1000 /*
1001 * Focus on the iframe
1002 */
1003 focus: function () {
1004 try {
1005 if (Ext.isWebKit) {
1006 this.getEl().dom.focus();
1007 } else {
1008 this.getEl().dom.contentWindow.focus();
1009 }
1010 } catch(e) { }
1011 },
1012 /*
1013 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1014 * Should be set in config
1015 */
1016 isNested: false,
1017 /*
1018 * All nested tabs and inline levels in the sorting order they were applied
1019 * Should be set in config
1020 */
1021 nestedParentElements: {},
1022 /*
1023 * Set designMode
1024 *
1025 * @param boolean on: if true set designMode to on, otherwise set to off
1026 *
1027 * @rturn void
1028 */
1029 setDesignMode: function (on) {
1030 if (on) {
1031 if (!Ext.isIE) {
1032 if (Ext.isGecko) {
1033 // In Firefox, we can't set designMode when we are in a hidden TYPO3 tab or inline element
1034 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1035 this.document.designMode = 'on';
1036 this.setOptions();
1037 }
1038 } else {
1039 this.document.designMode = 'on';
1040 this.setOptions();
1041 }
1042 }
1043 if (Ext.isIE || Ext.isWebKit) {
1044 this.document.body.contentEditable = true;
1045 }
1046 } else {
1047 if (!Ext.isIE) {
1048 this.document.designMode = 'off';
1049 }
1050 if (Ext.isIE || Ext.isWebKit) {
1051 this.document.body.contentEditable = false;
1052 }
1053 }
1054 },
1055 /*
1056 * Set editing mode options (if we can... raises exception in Firefox 3)
1057 *
1058 * @return void
1059 */
1060 setOptions: function () {
1061 if (!Ext.isIE) {
1062 try {
1063 if (this.document.queryCommandEnabled('insertBrOnReturn')) {
1064 this.document.execCommand('insertBrOnReturn', false, this.config.disableEnterParagraphs);
1065 }
1066 if (this.document.queryCommandEnabled('styleWithCSS')) {
1067 this.document.execCommand('styleWithCSS', false, this.config.useCSS);
1068 } else if (Ext.isGecko && this.document.queryCommandEnabled('useCSS')) {
1069 this.document.execCommand('useCSS', false, !this.config.useCSS);
1070 }
1071 if (Ext.isGecko) {
1072 if (this.document.queryCommandEnabled('enableObjectResizing')) {
1073 this.document.execCommand('enableObjectResizing', false, !this.config.disableObjectResizing);
1074 }
1075 if (this.document.queryCommandEnabled('enableInlineTableEditing')) {
1076 this.document.execCommand('enableInlineTableEditing', false, (this.config.buttons.table && this.config.buttons.table.enableHandles) ? true : false);
1077 }
1078 }
1079 } catch(e) {}
1080 }
1081 },
1082 /*
1083 * Handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
1084 */
1085 onNestedShow: function (event, target) {
1086 var styleEvent = true;
1087 // In older versions of Gecko attrName is not set and refering to it causes a non-catchable crash
1088 if ((Ext.isGecko && navigator.productSub > 2007112700) || Ext.isOpera) {
1089 styleEvent = (event.browserEvent.attrName == 'style');
1090 } else if (Ext.isIE) {
1091 styleEvent = (event.browserEvent.propertyName == 'style.display');
1092 }
1093 if (styleEvent && this.nestedParentElements.sorted.indexOf(target.id) != -1 && (target.style.display == '' || target.style.display == 'block')) {
1094 // Check if all container nested elements are displayed
1095 if (HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1096 if (this.getEditor().getMode() === 'wysiwyg') {
1097 if (Ext.isGecko) {
1098 this.setDesignMode(true);
1099 }
1100 this.fireEvent('show');
1101 } else {
1102 this.ownerCt.textAreaContainer.fireEvent('show');
1103 }
1104 this.getToolbar().update();
1105 return false;
1106 }
1107 }
1108 },
1109 /*
1110 * Instance of DOM walker
1111 */
1112 htmlRenderer: {},
1113 /*
1114 * Get the HTML content of the iframe
1115 */
1116 getHTML: function () {
1117 return this.htmlRenderer.render(this.document.body, false);
1118 },
1119 /*
1120 * Start listening to things happening in the iframe
1121 */
1122 startListening: function () {
1123 // Create keyMap so that plugins may bind key handlers
1124 this.keyMap = new Ext.KeyMap(Ext.get(this.document.documentElement), [], (Ext.isIE || Ext.isWebKit) ? 'keydown' : 'keypress');
1125 // Special keys map
1126 this.keyMap.addBinding([
1127 {
1128 key: [Ext.EventObject.DOWN, Ext.EventObject.UP, Ext.EventObject.LEFT, Ext.EventObject.RIGHT],
1129 alt: false,
1130 handler: this.onArrow,
1131 scope: this
1132 },
1133 {
1134 key: Ext.EventObject.TAB,
1135 ctrl: false,
1136 alt: false,
1137 handler: this.onTab,
1138 scope: this
1139 },
1140 {
1141 key: Ext.EventObject.SPACE,
1142 ctrl: true,
1143 shift: false,
1144 alt: false,
1145 handler: this.onCtrlSpace,
1146 scope: this
1147 }
1148 ]);
1149 if (Ext.isGecko || Ext.isIE) {
1150 this.keyMap.addBinding(
1151 {
1152 key: [Ext.EventObject.BACKSPACE, Ext.EventObject.DELETE],
1153 alt: false,
1154 handler: this.onBackSpace,
1155 scope: this
1156 });
1157 }
1158 if (!Ext.isIE && !this.config.disableEnterParagraphs) {
1159 this.keyMap.addBinding(
1160 {
1161 key: Ext.EventObject.ENTER,
1162 shift: false,
1163 handler: this.onEnter,
1164 scope: this
1165 });
1166 }
1167 if (Ext.isWebKit) {
1168 this.keyMap.addBinding(
1169 {
1170 key: Ext.EventObject.ENTER,
1171 alt: false,
1172 handler: this.onWebKitEnter,
1173 scope: this
1174 });
1175 }
1176 // Hot key map (on keydown for all browsers)
1177 var hotKeys = '';
1178 Ext.iterate(this.config.hotKeyList, function (key) {
1179 if (key.length == 1) {
1180 hotKeys += key.toUpperCase();
1181 }
1182 });
1183 // Make hot key map available, even if empty, so that plugins may add bindings
1184 this.hotKeyMap = new Ext.KeyMap(Ext.get(this.document.documentElement));
1185 if (!Ext.isEmpty(hotKeys)) {
1186 this.hotKeyMap.addBinding({
1187 key: hotKeys,
1188 ctrl: true,
1189 shift: false,
1190 alt: false,
1191 handler: this.onHotKey,
1192 scope: this
1193 });
1194 }
1195 this.mon(Ext.get(this.document.documentElement), (Ext.isIE || Ext.isWebKit) ? 'keydown' : 'keypress', this.onAnyKey, this);
1196 this.mon(Ext.get(this.document.documentElement), 'mouseup', this.onMouse, this);
1197 this.mon(Ext.get(this.document.documentElement), 'click', this.onMouse, this);
1198 if (Ext.isGecko) {
1199 this.mon(Ext.get(this.document.documentElement), 'paste', this.onPaste, this);
1200 }
1201 this.mon(Ext.get(this.document.documentElement), 'drop', this.onDrop, this);
1202 if (Ext.isWebKit) {
1203 this.mon(Ext.get(this.document.body), 'dragend', this.onDrop, this);
1204 }
1205 },
1206 /*
1207 * Handler for other key events
1208 */
1209 onAnyKey: function(event) {
1210 if (this.inhibitKeyboardInput(event)) {
1211 return false;
1212 }
1213 this.fireEvent('HTMLAreaEventWordCountChange', 100);
1214 if (!event.altKey && !event.ctrlKey) {
1215 // Detect URL in non-IE browsers
1216 if (!Ext.isIE && (event.getKey() != Ext.EventObject.ENTER || (event.shiftKey && !Ext.isWebKit))) {
1217 this.getEditor()._detectURL(event);
1218 }
1219 // Handle option+SPACE for Mac users
1220 if (Ext.isMac && event.browserEvent.charCode == 160) {
1221 return this.onOptionSpace(event.browserEvent.charCode, event);
1222 }
1223 }
1224 return true;
1225 },
1226 /*
1227 * On any key input event, check if input is currently inhibited
1228 */
1229 inhibitKeyboardInput: function (event) {
1230 // Inhibit key events while server-based cleaning is being processed
1231 if (this.getEditor().inhibitKeyboardInput) {
1232 event.stopEvent();
1233 return true;
1234 } else {
1235 return false;
1236 }
1237 },
1238 /*
1239 * Handler for mouse events
1240 */
1241 onMouse: function (event, target) {
1242 // In WebKit, select the image when it is clicked
1243 if (Ext.isWebKit && /^(img)$/i.test(target.nodeName) && event.browserEvent.type == 'click') {
1244 this.getEditor().selectNode(target);
1245 }
1246 this.getToolbar().updateLater.delay(100);
1247 return true;
1248 },
1249 /*
1250 * Handler for paste operations in Gecko
1251 */
1252 onPaste: function (event) {
1253 // Make src and href urls absolute
1254 if (Ext.isGecko) {
1255 HTMLArea.DOM.makeUrlsAbsolute.defer(50, this, [this.getEditor().document.body, this.config.baseURL, this.htmlRenderer]);
1256 }
1257 },
1258 /*
1259 * Handler for drag and drop operations
1260 */
1261 onDrop: function (event, target) {
1262 // Clean up span elements added by WebKit
1263 if (Ext.isWebKit) {
1264 this.getEditor().cleanAppleStyleSpans.defer(50, this.getEditor(), [this.getEditor().document.body]);
1265 }
1266 // Make src url absolute in Firefox
1267 if (Ext.isGecko) {
1268 HTMLArea.DOM.makeUrlsAbsolute.defer(50, this, [target, this.config.baseURL, this.htmlRenderer]);
1269 }
1270 this.getToolbar().updateLater.delay(100);
1271 },
1272 /*
1273 * Handler for UP, DOWN, LEFT and RIGHT keys
1274 */
1275 onArrow: function () {
1276 this.getToolbar().updateLater.delay(100);
1277 return true;
1278 },
1279 /*
1280 * Handler for TAB and SHIFT-TAB keys
1281 *
1282 * If available, BlockElements plugin will handle the TAB key
1283 */
1284 onTab: function (key, event) {
1285 if (this.inhibitKeyboardInput(event)) {
1286 return false;
1287 }
1288 var keyName = (event.shiftKey ? 'SHIFT-' : '') + 'TAB';
1289 if (this.config.hotKeyList[keyName] && this.config.hotKeyList[keyName].cmd) {
1290 var button = this.getButton(this.config.hotKeyList[keyName].cmd);
1291 if (button) {
1292 event.stopEvent();
1293 button.fireEvent('HTMLAreaEventHotkey', keyName, event);
1294 return false;
1295 }
1296 }
1297 return true;
1298 },
1299 /*
1300 * Handler for BACKSPACE and DELETE keys
1301 */
1302 onBackSpace: function (key, event) {
1303 if (this.inhibitKeyboardInput(event)) {
1304 return false;
1305 }
1306 if ((!Ext.isIE && !event.shiftKey) || Ext.isIE) {
1307 if (this.getEditor()._checkBackspace()) {
1308 event.stopEvent();
1309 }
1310 }
1311 // Update the toolbar state after some time
1312 this.getToolbar().updateLater.delay(200);
1313 return false;
1314 },
1315 /*
1316 * Handler for ENTER key in non-IE browsers
1317 */
1318 onEnter: function (key, event) {
1319 if (this.inhibitKeyboardInput(event)) {
1320 return false;
1321 }
1322 this.getEditor()._detectURL(event);
1323 if (this.getEditor()._checkInsertP()) {
1324 event.stopEvent();
1325 }
1326 // Update the toolbar state after some time
1327 this.getToolbar().updateLater.delay(200);
1328 return false;
1329 },
1330 /*
1331 * Handler for ENTER key in WebKit browsers
1332 */
1333 onWebKitEnter: function (key, event) {
1334 if (this.inhibitKeyboardInput(event)) {
1335 return false;
1336 }
1337 if (event.shiftKey || this.config.disableEnterParagraphs) {
1338 var editor = this.getEditor();
1339 editor._detectURL(event);
1340 if (Ext.isSafari) {
1341 var brNode = editor.document.createElement('br');
1342 editor.insertNodeAtSelection(brNode);
1343 brNode.parentNode.normalize();
1344 // Selection issue when an URL was detected
1345 if (editor._unlinkOnUndo) {
1346 brNode = brNode.parentNode.parentNode.insertBefore(brNode, brNode.parentNode.nextSibling);
1347 }
1348 if (!brNode.nextSibling || !/\S+/i.test(brNode.nextSibling.textContent)) {
1349 var secondBrNode = editor.document.createElement('br');
1350 secondBrNode = brNode.parentNode.appendChild(secondBrNode);
1351 }
1352 editor.selectNode(brNode, false);
1353 event.stopEvent();
1354 }
1355 }
1356 // Update the toolbar state after some time
1357 this.getToolbar().updateLater.delay(200);
1358 return false;
1359 },
1360 /*
1361 * Handler for CTRL-SPACE keys
1362 */
1363 onCtrlSpace: function (key, event) {
1364 if (this.inhibitKeyboardInput(event)) {
1365 return false;
1366 }
1367 this.getEditor().insertHTML('&nbsp;');
1368 event.stopEvent();
1369 return false;
1370 },
1371 /*
1372 * Handler for OPTION-SPACE keys on Mac
1373 */
1374 onOptionSpace: function (key, event) {
1375 if (this.inhibitKeyboardInput(event)) {
1376 return false;
1377 }
1378 this.getEditor().insertHTML('&nbsp;');
1379 event.stopEvent();
1380 return false;
1381 },
1382 /*
1383 * Handler for configured hotkeys
1384 */
1385 onHotKey: function (key, event) {
1386 if (this.inhibitKeyboardInput(event)) {
1387 return false;
1388 }
1389 var hotKey = String.fromCharCode(key).toLowerCase();
1390 this.getButton(this.config.hotKeyList[hotKey].cmd).fireEvent('HTMLAreaEventHotkey', hotKey, event);
1391 return false;
1392 },
1393 /*
1394 * Cleanup
1395 */
1396 onBeforeDestroy: function () {
1397 // ExtJS KeyMap object makes IE leak memory
1398 // Nullify EXTJS private handlers
1399 Ext.each(this.keyMap.bindings, function (binding, index) {
1400 this.keyMap.bindings[index] = null;
1401 }, this);
1402 this.keyMap.handleKeyDown = null;
1403 Ext.each(this.hotKeyMap.bindings, function (binding, index) {
1404 this.hotKeyMap.bindings[index] = null;
1405 }, this);
1406 this.hotKeyMap.handleKeyDown = null;
1407 this.keyMap.disable();
1408 this.hotKeyMap.disable();
1409 // Cleaning references to DOM in order to avoid IE memory leaks
1410 Ext.get(this.document.body).purgeAllListeners();
1411 Ext.get(this.document.body).dom = null;
1412 Ext.get(this.document.documentElement).purgeAllListeners();
1413 Ext.get(this.document.documentElement).dom = null;
1414 this.document = null;
1415 this.getEditor().document = null;
1416 this.getEditor()._doc = null;
1417 this.getEditor()._iframe = null;
1418 Ext.each(this.nestedParentElements.sorted, function (nested) {
1419 Ext.get(nested).purgeAllListeners();
1420 Ext.get(nested).dom = null;
1421 });
1422 Ext.destroy(this.autoEl, this.el, this.resizeEl, this.positionEl);
1423 return true;
1424 }
1425 });
1426 Ext.reg('htmlareaiframe', HTMLArea.Iframe);
1427 /*
1428 * HTMLArea.StatusBar extends Ext.Container
1429 */
1430 HTMLArea.StatusBar = Ext.extend(Ext.Container, {
1431 /*
1432 * Constructor
1433 */
1434 initComponent: function () {
1435 HTMLArea.StatusBar.superclass.initComponent.call(this);
1436 // Build the deferred word count update task
1437 this.updateWordCountLater = new Ext.util.DelayedTask(this.updateWordCount, this);
1438 this.addListener({
1439 render: {
1440 fn: this.addComponents,
1441 single: true
1442 },
1443 afterrender: {
1444 fn: this.initEventListeners,
1445 single: true
1446 }
1447 });
1448 },
1449 /*
1450 * Initialize listeners
1451 */
1452 initEventListeners: function () {
1453 this.addListener({
1454 beforedestroy: {
1455 fn: this.onBeforeDestroy,
1456 single: true
1457 }
1458 });
1459 // Monitor toolbar updates in order to refresh the contents of the statusbar
1460 // The toolbar must have been rendered
1461 this.mon(this.ownerCt.toolbar, 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar, this);
1462 // Monitor editor changing mode
1463 this.mon(this.getEditor(), 'HTMLAreaEventModeChange', this.onModeChange, this);
1464 // Monitor word count change
1465 this.mon(this.ownerCt.iframe, 'HTMLAreaEventWordCountChange', this.onWordCountChange, this);
1466 },
1467 /*
1468 * editorId should be set in config
1469 */
1470 editorId: null,
1471 /*
1472 * Get a reference to the editor
1473 */
1474 getEditor: function() {
1475 return RTEarea[this.editorId].editor;
1476 },
1477 /*
1478 * Create span elements to display when the status bar tree or a message when the editor is in text mode
1479 */
1480 addComponents: function () {
1481 this.statusBarWordCount = Ext.DomHelper.append(this.getEl(), {
1482 id: this.editorId + '-statusBarWordCount',
1483 tag: 'span',
1484 cls: 'statusBarWordCount',
1485 html: '&nbsp;'
1486 }, true);
1487 this.statusBarTree = Ext.DomHelper.append(this.getEl(), {
1488 id: this.editorId + '-statusBarTree',
1489 tag: 'span',
1490 cls: 'statusBarTree',
1491 html: HTMLArea.localize('Path') + ': '
1492 }, true).setVisibilityMode(Ext.Element.DISPLAY).setVisible(true);
1493 this.statusBarTextMode = Ext.DomHelper.append(this.getEl(), {
1494 id: this.editorId + '-statusBarTextMode',
1495 tag: 'span',
1496 cls: 'statusBarTextMode',
1497 html: HTMLArea.localize('TEXT_MODE')
1498 }, true).setVisibilityMode(Ext.Element.DISPLAY).setVisible(false);
1499 },
1500 /*
1501 * Clear the status bar tree
1502 */
1503 clear: function () {
1504 this.statusBarTree.removeAllListeners();
1505 Ext.each(this.statusBarTree.query('a'), function (node) {
1506 Ext.QuickTips.unregister(node);
1507 Ext.get(node).dom.ancestor = null;
1508 Ext.destroy(node);
1509 });
1510 this.statusBarTree.update('');
1511 this.setSelection(null);
1512 },
1513 /*
1514 * Flag indicating that the status bar should not be updated on this toolbar update
1515 */
1516 noUpdate: false,
1517 /*
1518 * Update the status bar
1519 */
1520 onUpdateToolbar: function (mode, selectionEmpty, ancestors, endPointsInSameBlock) {
1521 if (mode === 'wysiwyg' && !this.noUpdate) {
1522 var text,
1523 language,
1524 languageObject = this.getEditor().getPlugin('Language'),
1525 classes = new Array(),
1526 classText;
1527 this.clear();
1528 var path = Ext.DomHelper.append(this.statusBarTree, {
1529 tag: 'span',
1530 html: HTMLArea.localize('Path') + ': '
1531 },true);
1532 Ext.each(ancestors, function (ancestor, index) {
1533 if (!ancestor) {
1534 return true;
1535 }
1536 text = ancestor.nodeName.toLowerCase();
1537 // Do not show any id generated by ExtJS
1538 if (ancestor.id && text !== 'body' && ancestor.id.substr(0, 7) !== 'ext-gen') {
1539 text += '#' + ancestor.id;
1540 }
1541 if (languageObject && languageObject.getLanguageAttribute) {
1542 language = languageObject.getLanguageAttribute(ancestor);
1543 if (language != 'none') {
1544 text += '[' + language + ']';
1545 }
1546 }
1547 if (ancestor.className) {
1548 classText = '';
1549 classes = ancestor.className.trim().split(' ');
1550 for (var j = 0, n = classes.length; j < n; ++j) {
1551 if (!HTMLArea.reservedClassNames.test(classes[j])) {
1552 classText += '.' + classes[j];
1553 }
1554 }
1555 text += classText;
1556 }
1557 var element = Ext.DomHelper.insertAfter(path, {
1558 tag: 'a',
1559 href: '#',
1560 'ext:qtitle': HTMLArea.localize('statusBarStyle'),
1561 'ext:qtip': ancestor.style.cssText.split(';').join('<br />'),
1562 html: text
1563 }, true);
1564 // Ext.DomHelper does not honour the custom attribute
1565 element.dom.ancestor = ancestor;
1566 element.on('click', this.onClick, this);
1567 element.on('mousedown', this.onClick, this);
1568 if (!Ext.isOpera) {
1569 element.on('contextmenu', this.onContextMenu, this);
1570 }
1571 if (index) {
1572 Ext.DomHelper.insertAfter(element, {
1573 tag: 'span',
1574 html: String.fromCharCode(0xbb)
1575 });
1576 }
1577 }, this);
1578 }
1579 this.updateWordCount();
1580 this.noUpdate = false;
1581 },
1582 /*
1583 * Handler when the word count may have changed
1584 */
1585 onWordCountChange: function(delay) {
1586 this.updateWordCountLater.delay(delay ? delay : 0);
1587 },
1588 /*
1589 * Update the word count
1590 */
1591 updateWordCount: function() {
1592 var wordCount = 0;
1593 if (this.getEditor().getMode() == 'wysiwyg') {
1594 // Get the html content
1595 var text = this.getEditor().getHTML();
1596 if (!Ext.isEmpty(text)) {
1597 // Replace html tags with spaces
1598 text = text.replace(HTMLArea.RE_htmlTag, ' ');
1599 // Replace html space entities
1600 text = text.replace(/&nbsp;|&#160;/gi, ' ');
1601 // Remove numbers and punctuation
1602 text = text.replace(HTMLArea.RE_numberOrPunctuation, '');
1603 // Get the number of word
1604 wordCount = text.split(/\S\s+/g).length - 1;
1605 }
1606 }
1607 // Update the word count of the status bar
1608 this.statusBarWordCount.dom.innerHTML = wordCount ? ( wordCount + ' ' + HTMLArea.localize((wordCount == 1) ? 'word' : 'words')) : '&nbsp;';
1609 },
1610 /*
1611 * Adapt status bar to current editor mode
1612 *
1613 * @param string mode: the mode to which the editor got switched to
1614 */
1615 onModeChange: function (mode) {
1616 switch (mode) {
1617 case 'wysiwyg':
1618 this.statusBarTextMode.setVisible(false);
1619 this.statusBarTree.setVisible(true);
1620 break;
1621 case 'textmode':
1622 default:
1623 this.statusBarTree.setVisible(false);
1624 this.statusBarTextMode.setVisible(true);
1625 break;
1626 }
1627 },
1628 /*
1629 * Refrence to the element last selected on the status bar
1630 */
1631 selected: null,
1632 /*
1633 * Get the status bar selection
1634 */
1635 getSelection: function() {
1636 return this.selected;
1637 },
1638 /*
1639 * Set the status bar selection
1640 *
1641 * @param object element: set the status bar selection to the given element
1642 */
1643 setSelection: function(element) {
1644 this.selected = element ? element : null;
1645 },
1646 /*
1647 * Select the element that was clicked in the status bar and set the status bar selection
1648 */
1649 selectElement: function (element) {
1650 var editor = this.getEditor();
1651 element.blur();
1652 if (!Ext.isIE) {
1653 if (/^(img|table)$/i.test(element.ancestor.nodeName)) {
1654 editor.selectNode(element.ancestor);
1655 } else {
1656 editor.selectNodeContents(element.ancestor);
1657 }
1658 } else {
1659 if (/^(img|table)$/i.test(element.ancestor.nodeName)) {
1660 var range = editor.document.body.createControlRange();
1661 range.addElement(element.ancestor);
1662 range.select();
1663 } else {
1664 editor.selectNode(element.ancestor);
1665 }
1666 }
1667 this.setSelection(element.ancestor);
1668 this.noUpdate = true;
1669 editor.toolbar.update();
1670 },
1671 /*
1672 * Click handler
1673 */
1674 onClick: function (event, element) {
1675 this.selectElement(element);
1676 event.stopEvent();
1677 return false;
1678 },
1679 /*
1680 * ContextMenu handler
1681 */
1682 onContextMenu: function (event, target) {
1683 this.selectElement(target);
1684 return this.getEditor().getPlugin('ContextMenu') ? this.getEditor().getPlugin('ContextMenu').show(event, target.ancestor) : false;
1685 },
1686 /*
1687 * Cleanup
1688 */
1689 onBeforeDestroy: function() {
1690 this.clear();
1691 this.removeAll(true);
1692 Ext.destroy(this.statusBarTree, this.statusBarTextMode);
1693 return true;
1694 }
1695 });
1696 Ext.reg('htmlareastatusbar', HTMLArea.StatusBar);
1697 /*
1698 * HTMLArea.Framework extends Ext.Panel
1699 */
1700 HTMLArea.Framework = Ext.extend(Ext.Panel, {
1701 /*
1702 * Constructor
1703 */
1704 initComponent: function () {
1705 HTMLArea.Framework.superclass.initComponent.call(this);
1706 // Set some references
1707 this.toolbar = this.getTopToolbar();
1708 this.statusBar = this.getBottomToolbar();
1709 this.iframe = this.getComponent('iframe');
1710 this.textAreaContainer = this.getComponent('textAreaContainer');
1711 this.addEvents(
1712 /*
1713 * @event HTMLAreaEventFrameworkReady
1714 * Fires when the iframe is ready and all components are rendered
1715 */
1716 'HTMLAreaEventFrameworkReady'
1717 );
1718 this.addListener({
1719 beforedestroy: {
1720 fn: this.onBeforeDestroy,
1721 single: true
1722 }
1723 });
1724 // Monitor iframe becoming ready
1725 this.mon(this.iframe, 'HTMLAreaEventIframeReady', this.onIframeReady, this, {single: true});
1726 // Let the framefork render itself, but it will fail to do so if inside a hidden tab or inline element
1727 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1728 this.render(this.textArea.parent(), this.textArea.id);
1729 } else {
1730 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1731 var parentElements = [].concat(this.nestedParentElements.sorted);
1732 // Walk through all nested tabs and inline levels to get correct sizes
1733 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].render(args[0].textArea.parent(), args[0].textArea.id)', [this]);
1734 }
1735 },
1736 /*
1737 * Initiate events monitoring
1738 */
1739 initEventListeners: function () {
1740 // Make the framework resizable, if configured by the user
1741 this.makeResizable();
1742 // Monitor textArea container becoming shown or hidden as it may change the height of the status bar
1743 this.mon(this.textAreaContainer, 'show', this.resizable ? this.onTextAreaShow : this.onWindowResize, this);
1744 // Monitor iframe becoming shown or hidden as it may change the height of the status bar
1745 this.mon(this.iframe, 'show', this.resizable ? this.onIframeShow : this.onWindowResize, this);
1746 // Monitor window resizing
1747 Ext.EventManager.onWindowResize(this.onWindowResize, this);
1748 // If the textarea is inside a form, on reset, re-initialize the HTMLArea content and update the toolbar
1749 var form = this.textArea.dom.form;
1750 if (form) {
1751 if (Ext.isFunction(form.onreset)) {
1752 if (typeof(form.htmlAreaPreviousOnReset) == 'undefined') {
1753 form.htmlAreaPreviousOnReset = [];
1754 }
1755 form.htmlAreaPreviousOnReset.push(form.onreset);
1756 }
1757 this.mon(Ext.get(form), 'reset', this.onReset, this);
1758 }
1759 this.addListener({
1760 resize: {
1761 fn: this.onFrameworkResize
1762 }
1763 });
1764 },
1765 /*
1766 * editorId should be set in config
1767 */
1768 editorId: null,
1769 /*
1770 * Get a reference to the editor
1771 */
1772 getEditor: function() {
1773 return RTEarea[this.editorId].editor;
1774 },
1775 /*
1776 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1777 * Should be set in config
1778 */
1779 isNested: false,
1780 /*
1781 * All nested tabs and inline levels in the sorting order they were applied
1782 * Should be set in config
1783 */
1784 nestedParentElements: {},
1785 /*
1786 * Flag set to true when the framework is ready
1787 */
1788 ready: false,
1789 /*
1790 * All nested tabs and inline levels in the sorting order they were applied
1791 * Should be set in config
1792 */
1793 nestedParentElements: {},
1794 /*
1795 * Whether the framework should be made resizable
1796 * May be set in config
1797 */
1798 resizable: false,
1799 /*
1800 * Maximum height to which the framework may resized (in pixels)
1801 * May be set in config
1802 */
1803 maxHeight: 2000,
1804 /*
1805 * Initial textArea dimensions
1806 * Should be set in config
1807 */
1808 textAreaInitialSize: {
1809 width: 0,
1810 contextWidth: 0,
1811 height: 0
1812 },
1813 /*
1814 * doLayout will fail if inside a hidden tab or inline element
1815 */
1816 doLayout: function () {
1817 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1818 HTMLArea.Framework.superclass.doLayout.call(this);
1819 } else {
1820 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1821 var parentElements = [].concat(this.nestedParentElements.sorted);
1822 // Walk through all nested tabs and inline levels to get correct sizes
1823 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'HTMLArea.Framework.superclass.doLayout.call(args[0])', [this]);
1824 }
1825 },
1826 /*
1827 * Make the framework resizable, if configured
1828 */
1829 makeResizable: function () {
1830 if (this.resizable) {
1831 this.addClass('resizable');
1832 this.resizer = new Ext.Resizable(this.getEl(), {
1833 minWidth: 300,
1834 maxHeight: this.maxHeight,
1835 dynamic: false
1836 });
1837 this.resizer.on('resize', this.onHtmlAreaResize, this);
1838 }
1839 },
1840 /*
1841 * Resize the framework when the resizer handles are used
1842 */
1843 onHtmlAreaResize: function (resizer, width, height, event) {
1844 // Set width first as it may change the height of the toolbar and of the statusBar
1845 this.setWidth(width);
1846 // Set height of iframe and textarea
1847 this.iframe.setHeight(this.getInnerHeight());
1848 this.textArea.setSize(this.getInnerWidth(), this.getInnerHeight());
1849 },
1850 /*
1851 * Size the iframe according to initial textarea size as set by Page and User TSConfig
1852 */
1853 onWindowResize: function (width, height) {
1854 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
1855 this.resizeFramework(width, height);
1856 } else {
1857 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1858 var parentElements = [].concat(this.nestedParentElements.sorted);
1859 // Walk through all nested tabs and inline levels to get correct sizes
1860 HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].resizeFramework(args[1], args[2])', [this, width, height]);
1861 }
1862 },
1863 /*
1864 * Resize the framework to its initial size
1865 */
1866 resizeFramework: function (width, height) {
1867 var frameworkHeight = parseInt(this.textAreaInitialSize.height);
1868 if (this.textAreaInitialSize.width.indexOf('%') === -1) {
1869 // Width is specified in pixels
1870 var frameworkWidth = parseInt(this.textAreaInitialSize.width) - this.getFrameWidth();
1871 } else {
1872 // Width is specified in %
1873 if (Ext.isNumber(width)) {
1874 // Framework sizing on actual window resize
1875 var frameworkWidth = parseInt(((width - this.textAreaInitialSize.wizardsWidth - (this.fullScreen ? 10 : Ext.getScrollBarWidth()) - this.getBox().x - 15) * parseInt(this.textAreaInitialSize.width))/100);
1876 } else {
1877 // Initial framework sizing
1878 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);
1879 }
1880 }
1881 if (this.resizable) {
1882 this.resizer.resizeTo(frameworkWidth, frameworkHeight);
1883 } else {
1884 this.setSize(frameworkWidth, frameworkHeight);
1885 }
1886 },
1887 /*
1888 * Resize the framework components
1889 */
1890 onFrameworkResize: function () {
1891 // For unknown reason, in Chrome 7, this following is the only way to set the height of the iframe
1892 if (Ext.isChrome) {
1893 this.iframe.getResizeEl().dom.setAttribute('style', 'width:' + this.getInnerWidth() + 'px; height:' + this.getInnerHeight() + 'px;');
1894 } else {
1895 this.iframe.setSize(this.getInnerWidth(), this.getInnerHeight());
1896 }
1897 this.textArea.setSize(this.getInnerWidth(), this.getInnerHeight());
1898 },
1899 /*
1900 * Adjust the height to the changing size of the statusbar when the textarea is shown
1901 */
1902 onTextAreaShow: function () {
1903 this.iframe.setHeight(this.getInnerHeight());
1904 this.textArea.setHeight(this.getInnerHeight());
1905 },
1906 /*
1907 * Adjust the height to the changing size of the statusbar when the iframe is shown
1908 */
1909 onIframeShow: function () {
1910 if (this.getInnerHeight() <= 0) {
1911 this.onWindowResize();
1912 } else {
1913 // For unknown reason, in Chrome 7, this following is the only way to set the height of the iframe
1914 if (Ext.isChrome) {
1915 this.iframe.getResizeEl().dom.setAttribute('style', 'width:' + this.getInnerWidth() + 'px; height:' + this.getInnerHeight() + 'px;');
1916 } else {
1917 this.iframe.setHeight(this.getInnerHeight());
1918 }
1919 this.textArea.setHeight(this.getInnerHeight());
1920 }
1921 },
1922 /*
1923 * Calculate the height available for the editing iframe
1924 */
1925 getInnerHeight: function () {
1926 return this.getSize().height - this.toolbar.getHeight() - this.statusBar.getHeight() - 5;
1927 },
1928 /*
1929 * Fire the editor when all components of the framework are rendered and ready
1930 */
1931 onIframeReady: function () {
1932 this.ready = this.rendered && this.toolbar.rendered && this.statusBar.rendered && this.textAreaContainer.rendered;
1933 if (this.ready) {
1934 this.initEventListeners();
1935 this.textAreaContainer.show();
1936 if (!this.getEditor().config.showStatusBar) {
1937 this.statusBar.hide();
1938 }
1939 // Set the initial size of the framework
1940 this.onWindowResize();
1941 this.fireEvent('HTMLAreaEventFrameworkReady');
1942 } else {
1943 this.onIframeReady.defer(50, this);
1944 }
1945 },
1946 /*
1947 * Handler invoked if we are inside a form and the form is reset
1948 * On reset, re-initialize the HTMLArea content and update the toolbar
1949 */
1950 onReset: function (event) {
1951 this.getEditor().setHTML(this.textArea.getValue());
1952 this.toolbar.update();
1953 // Invoke previous reset handlers, if any
1954 var htmlAreaPreviousOnReset = event.getTarget().dom.htmlAreaPreviousOnReset;
1955 if (typeof(htmlAreaPreviousOnReset) != 'undefined') {
1956 Ext.each(htmlAreaPreviousOnReset, function (onReset) {
1957 onReset();
1958 return true;
1959 });
1960 }
1961 },
1962 /*
1963 * Cleanup on framework destruction
1964 */
1965 onBeforeDestroy: function () {
1966 Ext.EventManager.removeResizeListener(this.onWindowResize, this);
1967 // Cleaning references to DOM in order to avoid IE memory leaks
1968 var form = this.textArea.dom.form;
1969 if (form) {
1970 form.htmlAreaPreviousOnReset = null;
1971 Ext.get(form).dom = null;
1972 }
1973 Ext.getBody().dom = null;
1974 // ExtJS is not releasing any resources when the iframe is unloaded
1975 this.toolbar.destroy();
1976 this.statusBar.destroy();
1977 this.removeAll(true);
1978 if (this.resizer) {
1979 this.resizer.destroy();
1980 }
1981 return true;
1982 }
1983 });
1984 Ext.reg('htmlareaframework', HTMLArea.Framework);
1985 /***************************************************
1986 * HTMLArea.Editor extends Ext.util.Observable
1987 ***************************************************/
1988 HTMLArea.Editor = Ext.extend(Ext.util.Observable, {
1989 /*
1990 * HTMLArea.Editor constructor
1991 */
1992 constructor: function (config) {
1993 HTMLArea.Editor.superclass.constructor.call(this, {});
1994 // Save the config
1995 this.config = config;
1996 // Establish references to this editor
1997 this.editorId = this.config.editorId;
1998 RTEarea[this.editorId].editor = this;
1999 // Get textarea size and wizard context
2000 this.textArea = Ext.get(this.config.id);
2001 this.textAreaInitialSize = {
2002 width: this.config.RTEWidthOverride ? this.config.RTEWidthOverride : this.textArea.getStyle('width'),
2003 height: this.config.fullScreen ? HTMLArea.util.TYPO3.getWindowSize().height - 20 : this.textArea.getStyle('height'),
2004 wizardsWidth: 0
2005 };
2006 // TYPO3 Inline elements and tabs
2007 this.nestedParentElements = {
2008 all: this.config.tceformsNested,
2009 sorted: HTMLArea.util.TYPO3.simplifyNested(this.config.tceformsNested)
2010 };
2011 this.isNested = !Ext.isEmpty(this.nestedParentElements.sorted);
2012 // If in BE, get width of wizards
2013 if (Ext.get('typo3-docheader')) {
2014 this.wizards = this.textArea.parent().parent().next();
2015 if (this.wizards) {
2016 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
2017 this.textAreaInitialSize.wizardsWidth = this.wizards.getWidth();
2018 } else {
2019 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
2020 var parentElements = [].concat(this.nestedParentElements.sorted);
2021 // Walk through all nested tabs and inline levels to get correct size
2022 this.textAreaInitialSize.wizardsWidth = HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].getWidth()', [this.wizards]);
2023 }
2024 // Hide the wizards so that they do not move around while the editor framework is being sized
2025 this.wizards.hide();
2026 }
2027 }
2028 // Plugins register
2029 this.plugins = {};
2030 // Register the plugins included in the configuration
2031 Ext.iterate(this.config.plugin, function (plugin) {
2032 if (this.config.plugin[plugin]) {
2033 this.registerPlugin(plugin);
2034 }
2035 }, this);
2036 // Create Ajax object
2037 this.ajax = new HTMLArea.Ajax({
2038 editor: this
2039 });
2040 // Initialize keyboard input inhibit flag
2041 this.inhibitKeyboardInput = false;
2042 this.addEvents(
2043 /*
2044 * @event HTMLAreaEventEditorReady
2045 * Fires when initialization of the editor is complete
2046 */
2047 'HTMLAreaEventEditorReady',
2048 /*
2049 * @event HTMLAreaEventModeChange
2050 * Fires when the editor changes mode
2051 */
2052 'HTMLAreaEventModeChange'
2053 );
2054 },
2055 /*
2056 * Flag set to true when the editor initialization has completed
2057 */
2058 ready: false,
2059 /*
2060 * The current mode of the editor: 'wysiwyg' or 'textmode'
2061 */
2062 mode: 'textmode',
2063 /*
2064 * Create the htmlArea framework
2065 */
2066 generate: function () {
2067 // Create the editor framework
2068 this.htmlArea = new HTMLArea.Framework({
2069 id: this.editorId + '-htmlArea',
2070 layout: 'anchor',
2071 baseCls: 'htmlarea',
2072 editorId: this.editorId,
2073 textArea: this.textArea,
2074 textAreaInitialSize: this.textAreaInitialSize,
2075 fullScreen: this.config.fullScreen,
2076 resizable: this.config.resizable,
2077 maxHeight: this.config.maxHeight,
2078 isNested: this.isNested,
2079 nestedParentElements: this.nestedParentElements,
2080 // The toolbar
2081 tbar: {
2082 xtype: 'htmlareatoolbar',
2083 id: this.editorId + '-toolbar',
2084 anchor: '100%',
2085 layout: 'form',
2086 cls: 'toolbar',
2087 editorId: this.editorId
2088 },
2089 items: [{
2090 // The iframe
2091 xtype: 'htmlareaiframe',
2092 itemId: 'iframe',
2093 anchor: '100%',
2094 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
2095 height: parseInt(this.textAreaInitialSize.height),
2096 autoEl: {
2097 id: this.editorId + '-iframe',
2098 tag: 'iframe',
2099 cls: 'editorIframe',
2100 src: (Ext.isGecko || Ext.isWebKit) ? 'javascript:void(0);' : HTMLArea.editorUrl + 'popups/blank.html'
2101 },
2102 isNested: this.isNested,
2103 nestedParentElements: this.nestedParentElements,
2104 editorId: this.editorId
2105 },{
2106 // Box container for the textarea
2107 xtype: 'box',
2108 itemId: 'textAreaContainer',
2109 anchor: '100%',
2110 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
2111 // Let the framework swallow the textarea and throw it back
2112 listeners: {
2113 afterrender: {
2114 fn: function (textAreaContainer) {
2115 this.originalParent = this.textArea.parent().dom;
2116 textAreaContainer.getEl().appendChild(this.textArea);
2117 },
2118 single: true,
2119 scope: this
2120 },
2121 beforedestroy: {
2122 fn: function (textAreaContainer) {
2123 this.originalParent.appendChild(this.textArea.dom);
2124 return true;
2125 },
2126 single: true,
2127 scope: this
2128 }
2129 }
2130 }
2131 ],
2132 // The status bar
2133 bbar: {
2134 xtype: 'htmlareastatusbar',
2135 anchor: '100%',
2136 cls: 'statusBar',
2137 editorId: this.editorId
2138 }
2139 });
2140 // Set some references
2141 this.toolbar = this.htmlArea.getTopToolbar();
2142 this.statusBar = this.htmlArea.getBottomToolbar();
2143 this.iframe = this.htmlArea.getComponent('iframe');
2144 this.textAreaContainer = this.htmlArea.getComponent('textAreaContainer');
2145 // Get triggered when the framework becomes ready
2146 this.relayEvents(this.htmlArea, ['HTMLAreaEventFrameworkReady']);
2147 this.on('HTMLAreaEventFrameworkReady', this.onFrameworkReady, this, {single: true});
2148 },
2149 /*
2150 * Initialize the editor
2151 */
2152 onFrameworkReady: function () {
2153 // Initialize editor mode
2154 this.setMode('wysiwyg');
2155 // Initiate events listening
2156 this.initEventsListening();
2157 // Generate plugins
2158 this.generatePlugins();
2159 // Make the editor visible
2160 this.show();
2161 // Make the wizards visible again
2162 if (this.wizards) {
2163 this.wizards.show();
2164 }
2165 // Focus on the first editor that is not hidden
2166 Ext.iterate(RTEarea, function (editorId, RTE) {
2167 if (!Ext.isDefined(RTE.editor) || (RTE.editor.isNested && !HTMLArea.util.TYPO3.allElementsAreDisplayed(RTE.editor.nestedParentElements.sorted))) {
2168 return true;
2169 } else {
2170 RTE.editor.focus();
2171 return false;
2172 }
2173 }, this);
2174 this.ready = true;
2175 this.fireEvent('HTMLAreaEventEditorReady');
2176 this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
2177 },
2178 /*
2179 * Set editor mode
2180 *
2181 * @param string mode: 'textmode' or 'wysiwyg'
2182 *
2183 * @return void
2184 */
2185 setMode: function (mode) {
2186 switch (mode) {
2187 case 'textmode':
2188 this.textArea.set({ value: this.getHTML() }, false);
2189 this.iframe.setDesignMode(false);
2190 this.iframe.hide();
2191 this.textAreaContainer.show();
2192 this.mode = mode;
2193 break;
2194 case 'wysiwyg':
2195 try {
2196 this.document.body.innerHTML = this.getHTML();
2197 } catch(e) {
2198 this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
2199 TYPO3.Dialog.ErrorDialog({
2200 title: 'htmlArea RTE',
2201 msg: HTMLArea.localize('HTML-document-not-well-formed')
2202 });
2203 break;
2204 }
2205 this.textAreaContainer.hide();
2206 this.iframe.show();
2207 this.iframe.setDesignMode(true);
2208 this.mode = mode;
2209 break;
2210 }
2211 this.fireEvent('HTMLAreaEventModeChange', this.mode);
2212 this.focus();
2213 Ext.iterate(this.plugins, function(pluginId) {
2214 this.getPlugin(pluginId).onMode(this.mode);
2215 }, this);
2216 },
2217 /*
2218 * Get current editor mode
2219 */
2220 getMode: function () {
2221 return this.mode;
2222 },
2223 /*
2224 * Retrieve the HTML
2225 * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
2226 *
2227 * @return string the textual html content from the current editing mode
2228 */
2229 getHTML: function () {
2230 switch (this.mode) {
2231 case 'wysiwyg':
2232 return this.iframe.getHTML();
2233 case 'textmode':
2234 return this.textArea.getValue();
2235 default:
2236 return '';
2237 }
2238 },
2239 /*
2240 * Retrieve raw HTML
2241 *
2242 * @return string the textual html content from the current editing mode
2243 */
2244 getInnerHTML: function () {
2245 switch (this.mode) {
2246 case 'wysiwyg':
2247 return this.document.body.innerHTML;
2248 case 'textmode':
2249 return this.textArea.getValue();
2250 default:
2251 return '';
2252 }
2253 },
2254 /*
2255 * Replace the html content
2256 *
2257 * @param string html: the textual html
2258 *
2259 * @return void
2260 */
2261 setHTML: function (html) {
2262 switch (this.mode) {
2263 case 'wysiwyg':
2264 this.document.body.innerHTML = html;
2265 break;
2266 case 'textmode':
2267 this.textArea.set({ value: html }, false);;
2268 break;
2269 }
2270 },
2271 /*
2272 * Instantiate the specified plugin and register it with the editor
2273 *
2274 * @param string plugin: the name of the plugin
2275 *
2276 * @return boolean true if the plugin was successfully registered
2277 */
2278 registerPlugin: function (pluginName) {
2279 var plugin = HTMLArea[pluginName],
2280 isRegistered = false;
2281 if (typeof(plugin) !== 'undefined' && Ext.isFunction(plugin)) {
2282 var pluginInstance = new plugin(this, pluginName);
2283 if (pluginInstance) {
2284 var pluginInformation = pluginInstance.getPluginInformation();
2285 pluginInformation.instance = pluginInstance;
2286 this.plugins[pluginName] = pluginInformation;
2287 isRegistered = true;
2288 }
2289 }
2290 if (!isRegistered) {
2291 this.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName + '.', 'warn');
2292 }
2293 return isRegistered;
2294 },
2295 /*
2296 * Generate registered plugins
2297 */
2298 generatePlugins: function () {
2299 Ext.iterate(this.plugins, function (pluginId) {
2300 var plugin = this.getPlugin(pluginId);
2301 plugin.onGenerate();
2302 }, this);
2303 },
2304 /*
2305 * Get the instance of the specified plugin, if it exists
2306 *
2307 * @param string pluginName: the name of the plugin
2308 * @return object the plugin instance or null
2309 */
2310 getPlugin: function(pluginName) {
2311 return (this.plugins[pluginName] ? this.plugins[pluginName].instance : null);
2312 },
2313 /*
2314 * Unregister the instance of the specified plugin
2315 *
2316 * @param string pluginName: the name of the plugin
2317 * @return void
2318 */
2319 unRegisterPlugin: function(pluginName) {
2320 delete this.plugins[pluginName].instance;
2321 delete this.plugins[pluginName];
2322 },
2323 /*
2324 * Focus on the editor
2325 */
2326 focus: function () {
2327 switch (this.getMode()) {
2328 case 'wysiwyg':
2329 this.iframe.focus();
2330 break;
2331 case 'textmode':
2332 this.textArea.focus();
2333 break;
2334 }
2335 },
2336 /*
2337 * Add listeners
2338 */
2339 initEventsListening: function () {
2340 if (Ext.isOpera) {
2341 this.iframe.startListening();
2342 }
2343 // Add unload handler
2344 var iframe = this.iframe.getEl().dom;
2345 Ext.EventManager.on(iframe.contentWindow ? iframe.contentWindow : iframe.contentDocument, 'unload', this.onUnload, this, {single: true});
2346 },
2347 /*
2348 * Make the editor framework visible
2349 */
2350 show: function () {
2351 document.getElementById('pleasewait' + this.editorId).style.display = 'none';
2352 document.getElementById('editorWrap' + this.editorId).style.visibility = 'visible';
2353 },
2354 /*
2355 * Append an entry at the end of the troubleshooting log
2356 *
2357 * @param string functionName: the name of the editor function writing to the log
2358 * @param string text: the text of the message
2359 * @param string type: the type of message
2360 *
2361 * @return void
2362 */
2363 appendToLog: function (objectName, functionName, text, type) {
2364 HTMLArea.appendToLog(this.editorId, objectName, functionName, text, type);
2365 },
2366 /*
2367 * Iframe unload handler: Update the textarea for submission and cleanup
2368 */
2369 onUnload: function (event) {
2370 // Save the HTML content into the original textarea for submit, back/forward, etc.
2371 if (this.ready) {
2372 this.textArea.set({
2373 value: this.getHTML()
2374 }, false);
2375 }
2376 // Cleanup
2377 Ext.TaskMgr.stopAll();
2378 // ExtJS is not releasing any resources when the iframe is unloaded
2379 this.htmlArea.destroy();
2380 Ext.iterate(this.plugins, function (pluginId) {
2381 this.unRegisterPlugin(pluginId);
2382 }, this);
2383 this.purgeListeners();
2384 // Cleaning references to DOM in order to avoid IE memory leaks
2385 if (this.wizards) {
2386 this.wizards.dom = null;
2387 this.textArea.parent().parent().dom = null;
2388 this.textArea.parent().dom = null;
2389 }
2390 this.textArea.dom = null;
2391 RTEarea[this.editorId].editor = null;
2392 }
2393 });
2394 HTMLArea.Ajax = function (config) {
2395 Ext.apply(this, config);
2396 };
2397 HTMLArea.Ajax = Ext.extend(HTMLArea.Ajax, {
2398 /*
2399 * Load a Javascript file asynchronously
2400 *
2401 * @param string url: url of the file to load
2402 * @param function callBack: the callBack function
2403 * @param object scope: scope of the callbacks
2404 *
2405 * @return boolean true on success of the request submission
2406 */
2407 getJavascriptFile: function (url, callback, scope) {
2408 var success = false;
2409 var self = this;
2410 Ext.Ajax.request({
2411 method: 'GET',
2412 url: url,
2413 callback: callback,
2414 success: function (response) {
2415 success = true;
2416 },
2417 failure: function (response) {
2418 self.editor.inhibitKeyboardInput = false;
2419 self.editor.appendToLog('HTMLArea.Ajax', 'getJavascriptFile', 'Unable to get ' + url + ' . Server reported ' + response.status, 'error');
2420 },
2421 scope: scope
2422 });
2423 return success;
2424 },
2425 /*
2426 * Post data to the server
2427 *
2428 * @param string url: url to post data to
2429 * @param object data: data to be posted
2430 * @param function callback: function that will handle the response returned by the server
2431 * @param object scope: scope of the callbacks
2432 *
2433 * @return boolean true on success
2434 */
2435 postData: function (url, data, callback, scope) {
2436 var success = false;
2437 var self = this;
2438 data.charset = this.editor.config.typo3ContentCharset ? this.editor.config.typo3ContentCharset : 'utf-8';
2439 var params = '';
2440 Ext.iterate(data, function (parameter, value) {
2441 params += (params.length ? '&' : '') + parameter + '=' + encodeURIComponent(value);
2442 });
2443 params += this.editor.config.RTEtsConfigParams;
2444 Ext.Ajax.request({
2445 method: 'POST',
2446 headers: {
2447 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
2448 },
2449 url: url,
2450 params: params,
2451 callback: Ext.isFunction(callback) ? callback: function (options, success, response) {
2452 if (!success) {
2453 self.editor.appendToLog('HTMLArea.Ajax', 'postData', 'Post request to ' + url + ' failed. Server reported ' + response.status, 'error');
2454 }
2455 },
2456 success: function (response) {
2457 success = true;
2458 },
2459 failure: function (response) {
2460 self.editor.appendToLog('HTMLArea.Ajax', 'postData', 'Unable to post ' + url + ' . Server reported ' + response.status, 'error');
2461 },
2462 scope: scope
2463 });
2464 return success;
2465 }
2466 });
2467 /***************************************************
2468 * HTMLArea.util.TYPO3: Utility functions for dealing with tabs and inline elements in TYPO3 forms
2469 ***************************************************/
2470 HTMLArea.util.TYPO3 = function () {
2471 return {
2472 /*
2473 * Simplify the array of nested levels. Create an indexed array with the correct names of the elements.
2474 *
2475 * @param object nested: The array with the nested levels
2476 * @return object The simplified array
2477 * @author Oliver Hader <oh@inpublica.de>
2478 */
2479 simplifyNested: function(nested) {
2480 var i, type, level, elementId, max, simplifiedNested=[],
2481 elementIdSuffix = {
2482 tab: '-DIV',
2483 inline: '_fields',
2484 flex: '-content'
2485 };
2486 if (nested && nested.length) {
2487 if (nested[0][0]=='inline') {
2488 nested = inline.findContinuedNestedLevel(nested, nested[0][1]);
2489 }
2490 for (i=0, max=nested.length; i<max; i++) {
2491 type = nested[i][0];
2492 level = nested[i][1];
2493 elementId = level + elementIdSuffix[type];
2494 if (Ext.get(elementId)) {
2495 simplifiedNested.push(elementId);
2496 }
2497 }
2498 }
2499 return simplifiedNested;
2500 },
2501 /*
2502 * Access an inline relational element or tab menu and make it "accessible".
2503 * If a parent or ancestor object has the style "display: none", offsetWidth & offsetHeight are '0'.
2504 *
2505 * @params arry parentElements: array of parent elements id's; note that this input array will be modified
2506 * @params object callbackFunc: A function to be called, when the embedded objects are "accessible".
2507 * @params array args: array of arguments
2508 * @return object An object returned by the callbackFunc.
2509 * @author Oliver Hader <oh@inpublica.de>
2510 */
2511 accessParentElements: function (parentElements, callbackFunc, args) {
2512 var result = {};
2513 if (parentElements.length) {
2514 var currentElement = parentElements.pop();
2515 currentElement = Ext.get(currentElement);
2516 var actionRequired = (currentElement.getStyle('display') == 'none');
2517 if (actionRequired) {
2518 var originalStyles = currentElement.getStyles('visibility', 'position', 'top', 'display');
2519 currentElement.setStyle({
2520 visibility: 'hidden',
2521 position: 'absolute',
2522 top: '-10000px',
2523 display: ''
2524 });
2525 }
2526 result = this.accessParentElements(parentElements, callbackFunc, args);
2527 if (actionRequired) {
2528 currentElement.setStyle(originalStyles);
2529 }
2530 } else {
2531 result = eval(callbackFunc);
2532 }
2533 return result;
2534 },
2535 /*
2536 * Check if all elements in input array are currently displayed
2537 *
2538 * @param array elements: array of element id's
2539 * @return boolean true if all elements are displayed
2540 */
2541 allElementsAreDisplayed: function(elements) {
2542 var allDisplayed = true;
2543 Ext.each(elements, function (element) {
2544 allDisplayed = Ext.get(element).getStyle('display') != 'none';
2545 return allDisplayed;
2546 });
2547 return allDisplayed;
2548 },
2549 /*
2550 * Get current size of window
2551 *
2552 * @return object width and height of window
2553 */
2554 getWindowSize: function () {
2555 if (Ext.isIE) {
2556 var size = Ext.getBody().getSize();
2557 } else {
2558 var size = {
2559 width: window.innerWidth,
2560 height: window.innerHeight
2561 };
2562 }
2563 // Subtract the docheader height from the calculated window height
2564 var docHeader = Ext.get('typo3-docheader');
2565 if (docHeader) {
2566 size.height -= docHeader.getHeight();
2567 docHeader.dom = null;
2568 }
2569 return size;
2570 }
2571 }
2572 }();
2573 /***************************************************
2574 * EDITOR UTILITIES
2575 ***************************************************/
2576 HTMLArea.getInnerText = function(el) {
2577 var txt = '', i;
2578 if(el.firstChild) {
2579 for(i=el.firstChild;i;i =i.nextSibling) {
2580 if(i.nodeType == 3) txt += i.data;
2581 else if(i.nodeType == 1) txt += HTMLArea.getInnerText(i);
2582 }
2583 } else {
2584 if(el.nodeType == 3) txt = el.data;
2585 }
2586 return txt;
2587 };
2588
2589 HTMLArea.Editor.prototype.forceRedraw = function() {
2590 this.htmlArea.doLayout();
2591 };
2592
2593 HTMLArea.Editor.prototype.updateToolbar = function(noStatus) {
2594 this.toolbar.update(noStatus);
2595 };
2596 /***************************************************
2597 * DOM TREE MANIPULATION
2598 ***************************************************/
2599
2600 /*
2601 * Surround the currently selected HTML source code with the given tags.
2602 * Delete the selection, if any.
2603 */
2604 HTMLArea.Editor.prototype.surroundHTML = function(startTag,endTag) {
2605 this.insertHTML(startTag + this.getSelectedHTML().replace(HTMLArea.Reg_body, "") + endTag);
2606 };
2607
2608 /*
2609 * Change the tag name of a node.
2610 */
2611 HTMLArea.Editor.prototype.convertNode = function(el,newTagName) {
2612 var newel = this.document.createElement(newTagName), p = el.parentNode;
2613 while (el.firstChild) newel.appendChild(el.firstChild);
2614 p.insertBefore(newel, el);
2615 p.removeChild(el);
2616 return newel;
2617 };
2618
2619 /*
2620 * Find a parent of an element with a specified tag
2621 */
2622 HTMLArea.getElementObject = function(el,tagName) {
2623 var oEl = el;
2624 while (oEl != null && oEl.nodeName.toLowerCase() != tagName) oEl = oEl.parentNode;
2625 return oEl;
2626 };
2627
2628 /*
2629 * This function removes the given markup element
2630 *
2631 * @param object element: the inline element to be removed, content being preserved
2632 *
2633 * @return void
2634 */
2635 HTMLArea.Editor.prototype.removeMarkup = function(element) {
2636 var bookmark = this.getBookmark(this._createRange(this._getSelection()));
2637 var parent = element.parentNode;
2638 while (element.firstChild) {
2639 parent.insertBefore(element.firstChild, element);
2640 }
2641 parent.removeChild(element);
2642 this.selectRange(this.moveToBookmark(bookmark));
2643 };
2644
2645 /*
2646 * This function verifies if the element has any allowed attributes
2647 *
2648 * @param object element: the DOM element
2649 * @param array allowedAttributes: array of allowed attribute names
2650 *
2651 * @return boolean true if the element has one of the allowed attributes
2652 */
2653 HTMLArea.hasAllowedAttributes = function(element,allowedAttributes) {
2654 var value;
2655 for (var i = allowedAttributes.length; --i >= 0;) {
2656 value = element.getAttribute(allowedAttributes[i]);
2657 if (value) {
2658 if (allowedAttributes[i] == "style" && element.style.cssText) {
2659 return true;
2660 } else {
2661 return true;
2662 }
2663 }
2664 }
2665 return false;
2666 };
2667
2668 /***************************************************
2669 * SELECTIONS AND RANGES
2670 ***************************************************/
2671
2672 /*
2673 * Return true if we have some selected content
2674 */
2675 HTMLArea.Editor.prototype.hasSelectedText = function() {
2676 return this.getSelectedHTML() != "";
2677 };
2678
2679 /*
2680 * Get an array with all the ancestor nodes of the selection.
2681 */
2682 HTMLArea.Editor.prototype.getAllAncestors = function() {
2683 var p = this.getParentElement();
2684 var a = [];
2685 while (p && (p.nodeType === 1) && (p.nodeName.toLowerCase() !== "body")) {
2686 a.push(p);
2687 p = p.parentNode;
2688 }
2689 a.push(this.document.body);
2690 return a;
2691 };
2692
2693 /*
2694 * Get the block ancestors of an element within a given block
2695 */
2696 HTMLArea.Editor.prototype.getBlockAncestors = function(element, withinBlock) {
2697 var ancestors = new Array();
2698 var ancestor = element;
2699 while (ancestor && (ancestor.nodeType === 1) && !/^(body)$/i.test(ancestor.nodeName) && ancestor != withinBlock) {
2700 if (HTMLArea.isBlockElement(ancestor)) {
2701 ancestors.unshift(ancestor);
2702 }
2703 ancestor = ancestor.parentNode;
2704 }
2705 ancestors.unshift(ancestor);
2706 return ancestors;
2707 };
2708
2709 /*
2710 * Get the block elements containing the start and the end points of the selection
2711 */
2712 HTMLArea.Editor.prototype.getEndBlocks = function(selection) {
2713 var range = this._createRange(selection);
2714 if (!Ext.isIE) {
2715 var parentStart = range.startContainer;
2716 if (/^(body)$/i.test(parentStart.nodeName)) {
2717 parentStart = parentStart.firstChild;
2718 }
2719 var parentEnd = range.endContainer;
2720 if (/^(body)$/i.test(parentEnd.nodeName)) {
2721 parentEnd = parentEnd.lastChild;
2722 }
2723 } else {
2724 if (selection.type !== "Control" ) {
2725 var rangeEnd = range.duplicate();
2726 range.collapse(true);
2727 var parentStart = range.parentElement();
2728 rangeEnd.collapse(false);
2729 var parentEnd = rangeEnd.parentElement();
2730 } else {
2731 var parentStart = range.item(0);
2732 var parentEnd = parentStart;
2733 }
2734 }
2735 while (parentStart && !HTMLArea.isBlockElement(parentStart)) {
2736 parentStart = parentStart.parentNode;
2737 }
2738 while (parentEnd && !HTMLArea.isBlockElement(parentEnd)) {
2739 parentEnd = parentEnd.parentNode;
2740 }
2741 return { start : parentStart,
2742 end : parentEnd
2743 };
2744 };
2745
2746 /*
2747 * This function determines if the end poins of the current selection are within the same block
2748 *
2749 * @return boolean true if the end points of the current selection are inside the same block element
2750 */
2751 HTMLArea.Editor.prototype.endPointsInSameBlock = function() {
2752 var selection = this._getSelection();
2753 if (this._selectionEmpty(selection)) {
2754 return true;
2755 } else {
2756 var parent = this.getParentElement(selection);
2757 var endBlocks = this.getEndBlocks(selection);
2758 return (endBlocks.start === endBlocks.end && !/^(table|thead|tbody|tfoot|tr)$/i.test(parent.nodeName));
2759 }
2760 };
2761
2762 /*
2763 * Get the deepest ancestor of the selection that is of the specified type
2764 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2765 */
2766 HTMLArea.Editor.prototype._getFirstAncestor = function(sel,types) {
2767 var prnt = this._activeElement(sel);
2768 if (prnt == null) {
2769 try {
2770 prnt = (Ext.isIE ? this._createRange(sel).parentElement() : this._createRange(sel).commonAncestorContainer);
2771 } catch(e) {
2772 return null;
2773 }
2774 }
2775 if (typeof(types) == 'string') types = [types];
2776
2777 while (prnt) {
2778 if (prnt.nodeType == 1) {
2779 if (types == null) return prnt;
2780 for (var i = 0; i < types.length; i++) {
2781 if(prnt.tagName.toLowerCase() == types[i]) return prnt;
2782 }
2783 if(prnt.tagName.toLowerCase() == 'body') break;
2784 if(prnt.tagName.toLowerCase() == 'table') break;
2785 }
2786 prnt = prnt.parentNode;
2787 }
2788 return null;
2789 };
2790 /*
2791 * Get the node whose contents are currently fully selected
2792 *
2793 * @param array selection: the current selection
2794 * @param array range: the range of the current selection
2795 * @param array ancestors: the array of ancestors node of the current selection
2796 *
2797 * @return object the fully selected node, if any, null otherwise
2798 */
2799 HTMLArea.Editor.prototype.getFullySelectedNode = function (selection, range, ancestors) {
2800 var node, fullNodeSelected = false;
2801 if (!selection) {
2802 var selection = this._getSelection();
2803 }
2804 if (!this._selectionEmpty(selection)) {
2805 if (!range) {
2806 var range = this._createRange(selection);
2807 }
2808 if (!ancestors) {
2809 var ancestors = this.getAllAncestors();
2810 }
2811 Ext.each(ancestors, function (ancestor) {
2812 if (Ext.isIE) {
2813 fullNodeSelected = (selection.type !== 'Control' && ancestor.innerText == range.text) || (selection.type === 'Control' && ancestor.innerText == range.item(0).text);
2814 } else {
2815 fullNodeSelected = (ancestor.textContent == range.toString());
2816 }
2817 if (fullNodeSelected) {
2818 node = ancestor;
2819 return false;
2820 }
2821 });
2822 // Working around bug with WebKit selection
2823 if (Ext.isWebKit && !fullNodeSelected) {
2824 var statusBarSelection = this.statusBar ? this.statusBar.getSelection() : null;
2825 if (statusBarSelection && statusBarSelection.textContent == range.toString()) {
2826 fullNodeSelected = true;
2827 node = statusBarSelection;
2828 }
2829 }
2830 }
2831 return fullNodeSelected ? node : null;
2832 };
2833 /***************************************************
2834 * Category: EVENT HANDLERS
2835 ***************************************************/
2836
2837 /*
2838 * Intercept some native execCommand commands
2839 */
2840 HTMLArea.Editor.prototype.execCommand = function(cmdID, UI, param) {
2841 this.focus();
2842 switch (cmdID) {
2843 default:
2844 try {
2845 this.document.execCommand(cmdID, UI, param);
2846 } catch(e) {
2847 this.appendToLog('HTMLArea.Editor', 'execCommand', e + ' by execCommand(' + cmdID + ')', 'error');
2848 }
2849 }
2850 this.toolbar.update();
2851 return false;
2852 };
2853
2854 HTMLArea.Editor.prototype.scrollToCaret = function() {
2855 if (!Ext.isIE) {
2856 var e = this.getParentElement(),
2857 w = this._iframe.contentWindow ? this._iframe.contentWindow : window,
2858 h = w.innerHeight || w.height,
2859 d = this.document,
2860 t = d.documentElement.scrollTop || d.body.scrollTop;
2861 if (e.offsetTop > h+t || e.offsetTop < t) {
2862 this.getParentElement().scrollIntoView();
2863 }
2864 }
2865 };
2866 /***************************************************
2867 * UTILITY FUNCTIONS
2868 ***************************************************/
2869
2870 /*
2871 * Check if the client agent is supported
2872 */
2873 HTMLArea.checkSupportedBrowser = function() {
2874 return Ext.isGecko || Ext.isWebKit || Ext.isOpera || Ext.isIE;
2875 };
2876 /*
2877 * Remove a class name from the class attribute of an element
2878 *
2879 * @param object el: the element
2880 * @param string className: the class name to remove
2881 * @param boolean substring: if true, remove the first class name starting with the given string
2882 * @return void
2883 ***********************************************
2884 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2885 ***********************************************
2886 */
2887 HTMLArea._removeClass = function(el, className, substring) {
2888 HTMLArea.DOM.removeClass(el, className, substring);
2889 };
2890 /*
2891 * Add a class name to the class attribute
2892 ***********************************************
2893 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2894 ***********************************************
2895 */
2896 HTMLArea._addClass = function(el, className) {
2897 HTMLArea.DOM.addClass(el, className);
2898 };
2899 /*
2900 * Check if a class name is in the class attribute of an element
2901 *
2902 * @param object el: the element
2903 * @param string className: the class name to look for
2904 * @param boolean substring: if true, look for a class name starting with the given string
2905 * @return boolean true if the class name was found
2906 ***********************************************
2907 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2908 ***********************************************
2909 */
2910 HTMLArea._hasClass = function(el, className, substring) {
2911 return HTMLArea.DOM.hasClass(el, className, substring);
2912 };
2913
2914 HTMLArea.isBlockElement = function(el) { return el && el.nodeType == 1 && HTMLArea.RE_blockTags.test(el.nodeName.toLowerCase()); };
2915 HTMLArea.needsClosingTag = function(el) { return el && el.nodeType == 1 && !HTMLArea.RE_noClosingTag.test(el.tagName.toLowerCase()); };
2916
2917 /*
2918 * Perform HTML encoding of some given string
2919 * Borrowed in part from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2920 */
2921 HTMLArea.htmlDecode = function(str) {
2922 str = str.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
2923 str = str.replace(/&nbsp;/g, "\xA0"); // Decimal 160, non-breaking-space
2924 str = str.replace(/&quot;/g, "\x22");
2925 str = str.replace(/&#39;/g, "'") ;
2926 str = str.replace(/&amp;/g, "&");
2927 return str;
2928 };
2929 HTMLArea.htmlEncode = function(str) {
2930 if (typeof(str) != 'string') str = str.toString(); // we don't need regexp for that, but.. so be it for now.
2931 str = str.replace(/&/g, "&amp;");
2932 str = str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
2933 str = str.replace(/\xA0/g, "&nbsp;"); // Decimal 160, non-breaking-space
2934 str = str.replace(/\x22/g, "&quot;"); // \x22 means '"'
2935 return str;
2936 };
2937 /*
2938 * Retrieve the HTML code from the given node.
2939 * This is a replacement for getting innerHTML, using standard DOM calls.
2940 * Wrapper catches a Mozilla-Exception with non well-formed html source code.
2941 ***********************************************
2942 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2943 ***********************************************
2944 */
2945 HTMLArea.getHTML = function(root, outputRoot, editor){
2946 try {
2947 return editor.iframe.htmlRenderer.render(root, outputRoot);
2948 } catch(e) {
2949 editor.appendToLog('HTMLArea', 'getHTML', 'The HTML document is not well-formed.', 'warn');
2950 TYPO3.Dialog.ErrorDialog({
2951 title: 'htmlArea RTE',
2952 msg: HTMLArea.localize('HTML-document-not-well-formed')
2953 });
2954 return editor.document.body.innerHTML;
2955 }
2956 };
2957 HTMLArea.getPrevNode = function(node) {
2958 if(!node) return null;
2959 if(node.previousSibling) return node.previousSibling;
2960 if(node.parentNode) return node.parentNode;
2961 return null;
2962 };
2963
2964 HTMLArea.getNextNode = function(node) {
2965 if(!node) return null;
2966 if(node.nextSibling) return node.nextSibling;
2967 if(node.parentNode) return node.parentNode;
2968 return null;
2969 };
2970
2971 HTMLArea.removeFromParent = function(el) {
2972 if(!el.parentNode) return;
2973 var pN = el.parentNode;
2974 pN.removeChild(el);
2975 return el;
2976 };
2977 /*****************************************************************
2978 * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
2979 *****************************************************************/
2980 HTMLArea.DOM = function () {
2981 return {
2982 /***************************************************
2983 * DOM-RELATED CONSTANTS
2984 ***************************************************/
2985 // DOM node types
2986 ELEMENT_NODE: 1,
2987 ATTRIBUTE_NODE: 2,
2988 TEXT_NODE: 3,
2989 CDATA_SECTION_NODE: 4,
2990 ENTITY_REFERENCE_NODE: 5,
2991 ENTITY_NODE: 6,
2992 PROCESSING_INSTRUCTION_NODE: 7,
2993 COMMENT_NODE: 8,
2994 DOCUMENT_NODE: 9,
2995 DOCUMENT_TYPE_NODE: 10,
2996 DOCUMENT_FRAGMENT_NODE: 11,
2997 NOTATION_NODE: 12,
2998 /*
2999 * Gets the class names assigned to a node, reserved classes removed
3000 *
3001 * @param object node: the node
3002 * @return array array of class names on the node, reserved classes removed
3003 */
3004 getClassNames: function (node) {
3005 var classNames = [];
3006 if (node) {
3007 if (node.className && /\S/.test(node.className)) {
3008 classNames = node.className.trim().split(' ');
3009 }
3010 if (HTMLArea.reservedClassNames.test(node.className)) {
3011 var cleanClassNames = [];
3012 var j = -1;
3013 for (var i = 0; i < classNames.length; ++i) {
3014 if (!HTMLArea.reservedClassNames.test(classNames[i])) {
3015 cleanClassNames[++j] = classNames[i];
3016 }
3017 }
3018 classNames = cleanClassNames;
3019 }
3020 }
3021 return classNames;
3022 },
3023 /*
3024 * Check if a class name is in the class attribute of a node
3025 *
3026 * @param object node: the node
3027 * @param string className: the class name to look for
3028 * @param boolean substring: if true, look for a class name starting with the given string
3029 * @return boolean true if the class name was found, false otherwise
3030 */
3031 hasClass: function (node, className, substring) {
3032 var found = false;
3033 if (node && node.className) {
3034 var classes = node.className.trim().split(' ');
3035 for (var i = classes.length; --i >= 0;) {
3036 found = ((classes[i] == className) || (substring && classes[i].indexOf(className) == 0));
3037 if (found) {
3038 break;
3039 }
3040 }
3041 }
3042 return found;
3043 },
3044 /*
3045 * Add a class name to the class attribute of a node
3046 *
3047 * @param object node: the node
3048 * @param string className: the name of the class to be added
3049 * @return void
3050 */
3051 addClass: function (node, className) {
3052 if (node) {
3053 HTMLArea.DOM.removeClass(node, className);
3054 // Remove classes configured to be incompatible with the class to be added
3055 if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && Ext.isFunction(HTMLArea.classesXOR[className].test)) {
3056 var classNames = node.className.trim().split(' ');
3057 for (var i = classNames.length; --i >= 0;) {
3058 if (HTMLArea.classesXOR[className].test(classNames[i])) {
3059 HTMLArea.DOM.removeClass(node, classNames[i]);
3060 }
3061 }
3062 }
3063 if (node.className) {
3064 node.className += ' ' + className;
3065 } else {
3066 node.className = className;
3067 }
3068 }
3069 },
3070 /*
3071 * Remove a class name from the class attribute of a node
3072 *
3073 * @param object node: the node
3074 * @param string className: the class name to removed
3075 * @param boolean substring: if true, remove the class names starting with the given string
3076 * @return void
3077 */
3078 removeClass: function (node, className, substring) {
3079 if (node && node.className) {
3080 var classes = node.className.trim().split(' ');
3081 var newClasses = [];
3082 for (var i = classes.length; --i >= 0;) {
3083 if ((!substring && classes[i] != className) || (substring && classes[i].indexOf(className) != 0)) {
3084 newClasses[newClasses.length] = classes[i];
3085 }
3086 }
3087 if (newClasses.length) {
3088 node.className = newClasses.join(' ');
3089 } else {
3090 if (!Ext.isOpera) {
3091 node.removeAttribute('class');
3092 if (Ext.isIE) {
3093 node.removeAttribute('className');
3094 }
3095 }