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