1 /***************************************************************
4 * (c) 2002-2004 interactivetools.com, inc.
5 * (c) 2003-2004 dynarch.com
6 * (c) 2004-2011 Stanislas Rolland <typo3(arobas)sjbr.ca>
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.
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.
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.
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.
29 * This copyright notice MUST APPEAR in all copies of the script!
30 ***************************************************************/
32 * Main script of TYPO3 htmlArea RTE
34 // Avoid re-initialization on AJax call when HTMLArea object was already initialized
35 if (typeof(HTMLArea
) == 'undefined') {
36 // Establish HTMLArea name space
37 Ext
.namespace('HTMLArea.CSS', 'HTMLArea.util.TYPO3', 'HTMLArea.util.Tips', 'HTMLArea.util.Color', 'Ext.ux.form', 'Ext.ux.menu', 'Ext.ux.Toolbar');
39 /***************************************************
40 * COMPILED REGULAR EXPRESSIONS *
41 ***************************************************/
42 RE_htmlTag
: /<.[^<>]*?>/g,
43 RE_tagName
: /(<\/|<)\s*([^ \t\n>]+)/ig,
44 RE_head
: /<head>((.|\n)*?)<\/head>/i,
45 RE_body
: /<body>((.|\n)*?)<\/body>/i,
46 Reg_body
: new RegExp('<\/?(body)[^>]*>', 'gi'),
47 reservedClassNames
: /htmlarea/,
48 RE_email
: /([0-9a-z]+([a-z0-9_-]*[0-9a-z])*){1}(\.[0-9a-z]+([a-z0-9_-]*[0-9a-z])*)*@([0-9a-z]+([a-z0-9_-]*[0-9a-z])*\.)+[a-z]{2,9}/i,
49 RE_url
: /(([^:/?#]+):\/\/)?(([a-z0-9_]+:[a-z0-9_]+@)?[a-z0-9_-]{2,}(\.[a-z0-9_-]{2,})+\.[a-z]{2,5}(:[0-9]+)?(\/\S+)*\/?)/i,
50 RE_blockTags
: /^(body|p|h1|h2|h3|h4|h5|h6|ul|ol|pre|dl|dt|dd|div|noscript|blockquote|form|hr|table|caption|fieldset|address|td|tr|th|li|tbody|thead|tfoot|iframe)$/i,
51 RE_closingTags
: /^(p|blockquote|a|li|ol|ul|dl|dt|td|th|tr|tbody|thead|tfoot|caption|colgroup|table|div|b|bdo|big|cite|code|del|dfn|em|i|ins|kbd|label|q|samp|small|span|strike|strong|sub|sup|tt|u|var|abbr|acronym|font|center|object|embed|style|script|title|head)$/i,
52 RE_noClosingTag
: /^(img|br|hr|col|input|area|base|link|meta|param)$/i,
53 RE_numberOrPunctuation
: /[0-9.(),;:!¡?¿%#$'"_+=\\\/-]*/g,
54 /***************************************************
56 ***************************************************/
57 localize: function (label
, plural
) {
59 var localized
= HTMLArea
.I18N
.dialogs
[label
] || HTMLArea
.I18N
.tooltips
[label
] || HTMLArea
.I18N
.msg
[label
] || '';
60 if (typeof localized
=== 'object' && typeof localized
[i
] !== 'undefined') {
61 localized
= localized
[i
]['target'];
65 /***************************************************
67 ***************************************************/
69 // Apply global configuration settings
70 Ext
.apply(HTMLArea
, RTEarea
[0]);
71 Ext
.applyIf(HTMLArea
, {
72 editorSkin
: HTMLArea
.editorUrl
+ 'skins/default/',
73 editorCSS
: HTMLArea
.editorUrl
+ 'skins/default/htmlarea.css'
75 if (!Ext
.isString(HTMLArea
.editedContentCSS
)) {
76 HTMLArea
.editedContentCSS
= HTMLArea
.editorSkin
+ 'htmlarea-edited-content.css';
78 HTMLArea
.isReady
= true;
79 HTMLArea
.appendToLog('', 'HTMLArea', 'init', 'Editor url set to: ' + HTMLArea
.editorUrl
, 'info');
80 HTMLArea
.appendToLog('', 'HTMLArea', 'init', 'Editor skin CSS set to: ' + HTMLArea
.editorCSS
, 'info');
81 HTMLArea
.appendToLog('', 'HTMLArea', 'init', 'Editor content skin CSS set to: ' + HTMLArea
.editedContentCSS
, 'info');
84 * Write message to JavaScript console
86 * @param string editorId: the id of the editor issuing the message
87 * @param string objectName: the name of the object issuing the message
88 * @param string functionName: the name of the function issuing the message
89 * @param string text: the text of the message
90 * @param string type: the type of message: 'log', 'info', 'warn' or 'error'
94 appendToLog: function (editorId
, objectName
, functionName
, text
, type
) {
95 var str
= 'RTE[' + editorId
+ '][' + objectName
+ '::' + functionName
+ ']: ' + text
;
96 if (typeof(type
) === 'undefined') {
99 if (typeof(console
) !== 'undefined' && typeof(console
) === 'object') {
100 // If console is TYPO3.Backend.DebugConsole, write only error messages
101 if (Ext
.isFunction(console
.addTab
)) {
102 if (type
=== 'error') {
111 /***************************************************
112 * EDITOR CONFIGURATION
113 ***************************************************/
114 HTMLArea
.Config = function (editorId
) {
115 this.editorId
= editorId
;
116 // if the site is secure, create a secure iframe
117 this.useHTTPS
= false;
120 this.enableMozillaExtension
= true;
121 this.disableEnterParagraphs
= false;
122 this.disableObjectResizing
= false;
123 this.removeTrailingBR
= true;
124 // style included in the iframe document
125 this.editedContentStyle
= HTMLArea
.editedContentCSS
;
128 // Maximum attempts at accessing the stylesheets
129 this.styleSheetsMaximumAttempts
= 20;
130 // Remove tags (must be a regular expression)
131 this.htmlRemoveTags
= /none/i;
132 // Remove tags and their contents (must be a regular expression)
133 this.htmlRemoveTagsAndContents
= /none/i;
135 this.htmlRemoveComments
= false;
136 // Custom tags (must be a regular expression)
137 this.customTags
= /none/i;
138 // BaseURL to be included in the iframe document
139 this.baseURL
= document
.baseURI
|| document
.URL
;
140 if (this.baseURL
&& this.baseURL
.match(/(.*\:\/\/.*\/)[^\/]*/)) {
141 this.baseURL
= RegExp
.$1;
144 this.popupURL
= "popups/";
146 this.documentType
= '<!DOCTYPE html\r'
147 + ' PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\r'
148 + ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r';
149 // Hold the configuration of buttons and hot keys registered by plugins
150 this.buttonsConfig
= {};
151 this.hotKeyList
= {};
152 // Default configurations for toolbar items
153 this.configDefaults
= {
155 xtype
: 'htmlareabutton',
156 disabledClass
: 'buttonDisabled',
165 overCls
: 'buttonHover',
166 // Erratic behaviour of click event in WebKit and IE browsers
167 clickEvent
: (Ext
.isWebKit
|| Ext
.isIE
) ? 'mousedown' : 'click'
173 triggerAction
: 'all',
175 selectOnFocus
: !Ext
.isIE
,
176 validationEvent
: false,
177 validateOnBlur
: false,
179 forceSelection
: true,
181 storeRoot
: 'options',
182 storeFields
: [ { name
: 'text'}, { name
: 'value'}],
184 displayField
: 'text',
187 tpl
: '<tpl for="."><div ext:qtip="{value}" style="text-align:left;font-size:11px;" class="x-combo-list-item">{text}</div></tpl>'
191 HTMLArea
.Config
= Ext
.extend(HTMLArea
.Config
, {
193 * Registers a button for inclusion in the toolbar, adding some standard configuration properties for the ExtJS widgets
195 * @param object buttonConfiguration: the configuration object of the button:
196 * id : unique id for the button
197 * tooltip : tooltip for the button
198 * textMode : enable in text mode
199 * context : disable if not inside one of listed elements
200 * hidden : hide in menu and show only in context menu
201 * selection : disable if there is no selection
202 * hotkey : hotkey character
203 * dialog : if true, the button opens a dialogue
204 * dimensions : the opening dimensions object of the dialogue window: { width: nn, height: mm }
205 * and potentially other ExtJS config properties (will be forwarded)
207 * @return boolean true if the button was successfully registered
209 registerButton: function (config
) {
210 config
.itemId
= config
.id
;
211 if (Ext
.type(this.buttonsConfig
[config
.id
])) {
212 HTMLArea
.appendToLog('', 'HTMLArea.Config', 'registerButton', 'A toolbar item with the same Id: ' + config
.id
+ ' already exists and will be overidden.', 'warn');
215 config
= Ext
.applyIf(config
, this.configDefaults
['all']);
216 config
= Ext
.applyIf(config
, this.configDefaults
[config
.xtype
]);
217 // Set some additional properties
218 switch (config
.xtype
) {
219 case 'htmlareacombo':
220 if (config
.options
) {
221 // Create combo array store
222 config
.store
= new Ext
.data
.ArrayStore({
224 fields
: config
.storeFields
,
227 } else if (config
.storeUrl
) {
228 // Create combo json store
229 config
.store
= new Ext
.data
.JsonStore({
232 root
: config
.storeRoot
,
233 fields
: config
.storeFields
,
237 config
.hideLabel
= Ext
.isEmpty(config
.fieldLabel
) || Ext
.isIE6
;
238 config
.helpTitle
= config
.tooltip
;
241 if (!config
.iconCls
) {
242 config
.iconCls
= config
.id
;
246 config
.cmd
= config
.id
;
247 config
.tooltip
= { title
: config
.tooltip
};
248 this.buttonsConfig
[config
.id
] = config
;
252 * Register a hotkey with the editor configuration.
254 registerHotKey: function (hotKeyConfiguration
) {
255 if (Ext
.isDefined(this.hotKeyList
[hotKeyConfiguration
.id
])) {
256 HTMLArea
.appendToLog('', 'HTMLArea.Config', 'registerHotKey', 'A hotkey with the same key ' + hotKeyConfiguration
.id
+ ' already exists and will be overidden.', 'warn');
258 if (Ext
.isDefined(hotKeyConfiguration
.cmd
) && !Ext
.isEmpty(hotKeyConfiguration
.cmd
) && Ext
.isDefined(this.buttonsConfig
[hotKeyConfiguration
.cmd
])) {
259 this.hotKeyList
[hotKeyConfiguration
.id
] = hotKeyConfiguration
;
262 HTMLArea
.appendToLog('', 'HTMLArea.Config', 'registerHotKey', 'A hotkey with key ' + hotKeyConfiguration
.id
+ ' could not be registered because toolbar item with id ' + hotKeyConfiguration
.cmd
+ ' was not registered.', 'warn');
267 * Get the configured document type for dialogue windows
269 getDocumentType: function () {
270 return this.documentType
;
273 /***************************************************
275 ***************************************************/
277 * Ext.ux.HTMLAreaButton extends Ext.Button
279 Ext
.ux
.HTMLAreaButton
= Ext
.extend(Ext
.Button
, {
281 * Component initialization
283 initComponent: function () {
284 Ext
.ux
.HTMLAreaButton
.superclass
.initComponent
.call(this);
287 * @event HTMLAreaEventHotkey
288 * Fires when the button hotkey is pressed
290 'HTMLAreaEventHotkey',
292 * @event HTMLAreaEventContextMenu
293 * Fires when the button is triggered from the context menu
295 'HTMLAreaEventContextMenu'
299 fn
: this.initEventListeners
,
305 * Initialize listeners
307 initEventListeners: function () {
309 HTMLAreaEventHotkey
: {
312 HTMLAreaEventContextMenu
: {
313 fn
: this.onButtonClick
316 this.setHandler(this.onButtonClick
, this);
317 // Monitor toolbar updates in order to refresh the state of the button
318 this.mon(this.getToolbar(), 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar
, this);
321 * Get a reference to the editor
323 getEditor: function() {
324 return RTEarea
[this.ownerCt
.editorId
].editor
;
327 * Get a reference to the toolbar
329 getToolbar: function() {
333 * Add properties and function to set button active or not depending on current selection
336 activeClass
: 'buttonActive',
337 setInactive: function (inactive
) {
338 this.inactive
= inactive
;
339 return inactive
? this.removeClass(this.activeClass
) : this.addClass(this.activeClass
);
342 * Determine if the button should be enabled based on the current selection and context configuration property
344 isInContext: function (mode
, selectionEmpty
, ancestors
) {
345 var editor
= this.getEditor();
346 var inContext
= true;
347 if (mode
=== 'wysiwyg' && this.context
) {
350 if (/(.*)\[(.*?)\]/.test(this.context
)) {
351 contexts
= RegExp
.$1.split(',');
352 attributes
= RegExp
.$2.split(',');
354 contexts
= this.context
.split(',');
356 contexts
= new RegExp( '^(' + contexts
.join('|') + ')$', 'i');
357 var matchAny
= contexts
.test('*');
358 Ext
.each(ancestors
, function (ancestor
) {
359 inContext
= matchAny
|| contexts
.test(ancestor
.nodeName
);
361 Ext
.each(attributes
, function (attribute
) {
362 inContext
= eval("ancestor." + attribute
);
369 return inContext
&& (!this.selection
|| !selectionEmpty
);
372 * Handler invoked when the button is clicked
374 onButtonClick: function (button
, event
, key
) {
375 if (!this.disabled
) {
376 if (!this.plugins
[this.action
](this.getEditor(), key
|| this.itemId
) && event
) {
380 this.getEditor().focus();
383 this.setDisabled(true);
385 this.getToolbar().update();
391 * Handler invoked when the hotkey configured for this button is pressed
393 onHotKey: function (key
, event
) {
394 return this.onButtonClick(this, event
, key
);
397 * Handler invoked when the toolbar is updated
399 onUpdateToolbar: function (mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
) {
400 this.setDisabled(mode
=== 'textmode' && !this.textMode
);
401 if (!this.disabled
) {
402 if (!this.noAutoUpdate
) {
403 this.setDisabled(!this.isInContext(mode
, selectionEmpty
, ancestors
));
405 this.plugins
['onUpdateToolbar'](this, mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
);
409 Ext
.reg('htmlareabutton', Ext
.ux
.HTMLAreaButton
);
411 * Ext.ux.Toolbar.HTMLAreaToolbarText extends Ext.Toolbar.TextItem
413 Ext
.ux
.Toolbar
.HTMLAreaToolbarText
= Ext
.extend(Ext
.Toolbar
.TextItem
, {
417 initComponent: function () {
418 Ext
.ux
.Toolbar
.HTMLAreaToolbarText
.superclass
.initComponent
.call(this);
421 fn
: this.initEventListeners
,
427 * Initialize listeners
429 initEventListeners: function () {
430 // Monitor toolbar updates in order to refresh the state of the button
431 this.mon(this.getToolbar(), 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar
, this);
434 * Get a reference to the editor
436 getEditor: function() {
437 return RTEarea
[this.ownerCt
.editorId
].editor
;
440 * Get a reference to the toolbar
442 getToolbar: function() {
446 * Handler invoked when the toolbar is updated
448 onUpdateToolbar: function (mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
) {
449 this.setDisabled(mode
=== 'textmode' && !this.textMode
);
450 if (!this.disabled
) {
451 this.plugins
['onUpdateToolbar'](this, mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
);
455 Ext
.reg('htmlareatoolbartext', Ext
.ux
.Toolbar
.HTMLAreaToolbarText
);
457 * Ext.ux.form.HTMLAreaCombo extends Ext.form.ComboBox
459 Ext
.ux
.form
.HTMLAreaCombo
= Ext
.extend(Ext
.form
.ComboBox
, {
463 initComponent: function () {
464 Ext
.ux
.form
.HTMLAreaCombo
.superclass
.initComponent
.call(this);
467 * @event HTMLAreaEventHotkey
468 * Fires when a hotkey configured for the combo is pressed
470 'HTMLAreaEventHotkey'
474 fn
: this.initEventListeners
,
480 * Initialize listeners
482 initEventListeners: function () {
485 fn
: this.onComboSelect
488 fn
: this.onSpecialKey
490 HTMLAreaEventHotkey
: {
494 fn
: this.onBeforeDestroy
,
498 // Monitor toolbar updates in order to refresh the state of the combo
499 this.mon(this.getToolbar(), 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar
, this);
500 // Monitor framework becoming ready
501 this.mon(this.getToolbar().ownerCt
, 'HTMLAreaEventFrameworkReady', this.onFrameworkReady
, this);
504 * Get a reference to the editor
506 getEditor: function() {
507 return RTEarea
[this.ownerCt
.editorId
].editor
;
510 * Get a reference to the toolbar
512 getToolbar: function() {
516 * Handler invoked when an item is selected in the dropdown list
518 onComboSelect: function (combo
, record
, index
) {
519 if (!combo
.disabled
) {
520 var editor
= this.getEditor();
521 // In IE, reclaim lost focus on the editor iframe and restore the bookmarked selection
524 if (!Ext
.isEmpty(this.savedRange
)) {
525 editor
.selectRange(this.savedRange
);
526 this.savedRange
= null;
529 // Invoke the plugin onChange handler
530 this.plugins
[this.action
](editor
, combo
, record
, index
);
531 // In IE, bookmark the updated selection as the editor will be loosing focus
534 this.savedRange
= editor
._createRange(editor
._getSelection());
535 this.triggered
= true;
540 this.getToolbar().update();
545 * Handler invoked when the trigger element is clicked
546 * In IE, need to reclaim lost focus for the editor in order to restore the selection
548 onTriggerClick: function () {
549 Ext
.ux
.form
.HTMLAreaCombo
.superclass
.onTriggerClick
.call(this);
550 // In IE, avoid focus being stolen and selection being lost
552 this.triggered
= true;
553 this.getEditor().focus();
557 * Handler invoked when the list of options is clicked in
559 onViewClick: function (doFocus
) {
560 // Avoid stealing focus from the editor
561 Ext
.ux
.form
.HTMLAreaCombo
.superclass
.onViewClick
.call(this, false);
564 * Handler invoked in IE when the mouse moves out of the editor iframe
566 saveSelection: function (event
) {
567 var editor
= this.getEditor();
568 if (editor
.document
.hasFocus()) {
569 this.savedRange
= editor
._createRange(editor
._getSelection());
573 * Handler invoked in IE when the editor gets the focus back
575 restoreSelection: function (event
) {
576 if (!Ext
.isEmpty(this.savedRange
) && this.triggered
) {
577 this.getEditor().selectRange(this.savedRange
);
578 this.triggered
= false;
582 * Handler invoked when the enter key is pressed while the combo has focus
584 onSpecialKey: function (combo
, event
) {
585 if (event
.getKey() == event
.ENTER
) {
591 * Handler invoked when a hot key configured for this dropdown list is pressed
593 onHotKey: function (key
) {
594 if (!this.disabled
) {
595 this.plugins
.onHotKey(this.getEditor(), key
);
597 this.getEditor().focus();
599 this.getToolbar().update();
604 * Handler invoked when the toolbar is updated
606 onUpdateToolbar: function (mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
) {
607 this.setDisabled(mode
=== 'textmode' && !this.textMode
);
608 if (!this.disabled
) {
609 this.plugins
['onUpdateToolbar'](this, mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
);
613 * The iframe must have been rendered
615 onFrameworkReady: function () {
616 var iframe
= this.getEditor().iframe
;
617 // Close the combo on a click in the iframe
618 // Note: ExtJS is monitoring events only on the parent window
619 this.mon(Ext
.get(iframe
.document
.documentElement
), 'click', this.collapse
, this);
620 // Special handling for combo stealing focus in IE
622 // Take a bookmark in case the editor looses focus by activation of this combo
623 this.mon(iframe
.getEl(), 'mouseleave', this.saveSelection
, this);
624 // Restore the selection if combo was triggered
625 this.mon(iframe
.getEl(), 'focus', this.restoreSelection
, this);
631 onBeforeDestroy: function () {
632 this.savedRange
= null;
633 this.getStore().removeAll();
634 this.getStore().destroy();
637 Ext
.reg('htmlareacombo', Ext
.ux
.form
.HTMLAreaCombo
);
638 /***************************************************
640 ***************************************************/
642 * HTMLArea.Toolbar extends Ext.Container
644 HTMLArea
.Toolbar
= Ext
.extend(Ext
.Container
, {
648 initComponent: function () {
649 HTMLArea
.Toolbar
.superclass
.initComponent
.call(this);
652 * @event HTMLAreaEventToolbarUpdate
653 * Fires when the toolbar is updated
655 'HTMLAreaEventToolbarUpdate'
657 // Build the deferred toolbar update task
658 this.updateLater
= new Ext
.util
.DelayedTask(this.update
, this);
659 // Add the toolbar items
663 fn
: this.initEventListeners
,
669 * Initialize listeners
671 initEventListeners: function () {
674 fn
: this.onBeforeDestroy
,
678 // Monitor editor becoming ready
679 this.mon(this.getEditor(), 'HTMLAreaEventEditorReady', this.update
, this, {single
: true});
682 * editorId should be set in config
686 * Get a reference to the editor
688 getEditor: function() {
689 return RTEarea
[this.editorId
].editor
;
692 * Create the toolbar items based on editor toolbar configuration
694 addItems: function () {
695 var editor
= this.getEditor();
696 // Walk through the editor toolbar configuration nested arrays: [ toolbar [ row [ group ] ] ]
697 var firstOnRow
= true;
698 var firstInGroup
= true;
699 Ext
.each(editor
.config
.toolbar
, function (row
) {
701 // If a visible item was added to the previous line
704 cls
: 'x-form-clear-left'
709 Ext
.each(row
, function (group
) {
710 // To do: this.config.keepButtonGroupTogether ...
711 if (!firstOnRow
&& !firstInGroup
) {
712 // If a visible item was added to the line
714 xtype
: 'tbseparator',
720 Ext
.each(group
, function (item
) {
721 if (item
== 'space') {
727 // Get the item's config as registered by some plugin
728 var itemConfig
= editor
.config
.buttonsConfig
[item
];
729 if (!Ext
.isEmpty(itemConfig
)) {
730 itemConfig
.id
= this.editorId
+ '-' + itemConfig
.id
;
731 this.add(itemConfig
);
732 firstInGroup
= firstInGroup
&& itemConfig
.hidden
;
733 firstOnRow
= firstOnRow
&& firstInGroup
;
744 cls
: 'x-form-clear-left'
748 * Retrieve a toolbar item by itemId
750 getButton: function (buttonId
) {
751 return this.find('itemId', buttonId
)[0];
754 * Update the state of the toolbar
757 var editor
= this.getEditor(),
758 mode
= editor
.getMode(),
759 selectionEmpty
= true,
761 endPointsInSameBlock
= true;
762 if (editor
.getMode() === 'wysiwyg') {
763 selectionEmpty
= editor
._selectionEmpty(editor
._getSelection());
764 ancestors
= editor
.getAllAncestors();
765 endPointsInSameBlock
= editor
.endPointsInSameBlock();
767 this.fireEvent('HTMLAreaEventToolbarUpdate', mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
);
772 onBeforeDestroy: function () {
773 this.removeAll(true);
777 Ext
.reg('htmlareatoolbar', HTMLArea
.Toolbar
);
779 * HTMLArea.Iframe extends Ext.BoxComponent
781 HTMLArea
.Iframe
= Ext
.extend(Ext
.BoxComponent
, {
785 initComponent: function () {
786 HTMLArea
.Iframe
.superclass
.initComponent
.call(this);
789 * @event HTMLAreaEventIframeReady
790 * Fires when the iframe style sheets become accessible
792 'HTMLAreaEventIframeReady',
794 * @event HTMLAreaEventWordCountChange
795 * Fires when the word count may have changed
797 'HTMLAreaEventWordCountChange'
801 fn
: this.initEventListeners
,
805 fn
: this.onBeforeDestroy
,
809 this.config
= this.getEditor().config
;
810 this.htmlRenderer
= new HTMLArea
.DOM
.Walker({
811 keepComments
: !this.config
.htmlRemoveComments
,
812 removeTags
: this.config
.htmlRemoveTags
,
813 removeTagsAndContents
: this.config
.htmlRemoveTagsAndContents
,
814 baseUrl
: this.config
.baseURL
816 if (!this.config
.showStatusBar
) {
817 this.addClass('noStatusBar');
821 * Initialize event listeners and the document after the iframe has rendered
823 initEventListeners: function () {
824 this.initStyleChangeEventListener();
826 this.mon(this.getEl(), 'load', this.initializeIframe
, this, {single
: true});
828 this.initializeIframe();
832 * The editor iframe may become hidden with style.display = "none" on some parent div
833 * This breaks the editor in Firefox: the designMode attribute needs to be reset after the style.display of the container div is reset to "block"
834 * In all browsers, it breaks the evaluation of the framework dimensions
836 initStyleChangeEventListener: function () {
837 if (this.isNested
&& !Ext
.isWebKit
) {
844 Ext
.each(this.nestedParentElements
.sorted
, function (nested
) {
846 options
.target
= Ext
.get(nested
);
850 Ext
.isIE
? 'propertychange' : 'DOMAttrModified',
859 * editorId should be set in config
863 * Get a reference to the editor
865 getEditor: function() {
866 return RTEarea
[this.editorId
].editor
;
869 * Get a reference to the toolbar
871 getToolbar: function () {
872 return this.ownerCt
.getTopToolbar();
875 * Get a reference to the statusBar
877 getStatusBar: function () {
878 return this.ownerCt
.getBottomToolbar();
881 * Get a reference to a button
883 getButton: function (buttonId
) {
884 return this.getToolbar().getButton(buttonId
);
887 * Flag set to true when the iframe becomes usable for editing
891 * Create the iframe element at rendering time
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
);
899 // ExtJS Default method will not work with iframe element
900 this.el
= Ext
.DomHelper
.append(ct
, this.autoEl
, true);
903 this.el
.id
= this.getId();
906 // from Ext.BoxComponent
908 this.resizeEl
= Ext
.get(this.resizeEl
);
910 if (this.positionEl
){
911 this.positionEl
= Ext
.get(this.positionEl
);
915 * Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
917 initializeIframe: function () {
918 var iframe
= this.getEl().dom
;
920 if (!iframe
|| (!iframe
.contentWindow
&& !iframe
.contentDocument
)) {
921 this.initializeIframe
.defer(50, this);
923 } else if (iframe
.contentWindow
&& !Ext
.isWebKit
&& (!iframe
.contentWindow
.document
|| !iframe
.contentWindow
.document
.documentElement
)) {
924 this.initializeIframe
.defer(50, this);
926 } else if (Ext
.isWebKit
&& (!iframe
.contentDocument
.documentElement
|| !iframe
.contentDocument
.body
)) {
927 this.initializeIframe
.defer(50, this);
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
;
934 // Style the document body
935 Ext
.get(this.document
.body
).addClass('htmlarea-content-body');
936 // Start listening to things happening in the iframe
937 // For some unknown reason, this is too early for Opera
939 this.startListening();
945 this.fireEvent('HTMLAreaEventIframeReady');
949 * Build the iframe document head
951 createHead: function () {
952 var head
= this.document
.getElementsByTagName('head')[0];
954 head
= this.document
.createElement('head');
955 this.document
.documentElement
.appendChild(head
);
957 if (this.config
.baseURL
) {
958 var base
= this.document
.getElementsByTagName('base')[0];
960 base
= this.document
.createElement('base');
961 base
.href
= this.config
.baseURL
;
962 head
.appendChild(base
);
964 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Iframe baseURL set to: ' + base
.href
, 'info');
966 var link0
= this.document
.getElementsByTagName('link')[0];
968 link0
= this.document
.createElement('link');
969 link0
.rel
= 'stylesheet';
970 link0
.type
= 'text/css';
971 // Firefox 3.0.1 does not apply the base URL while Firefox 3.6.8 does so. Do not know in what version this was fixed.
972 // Therefore, for versions before 3.6.8, we prepend the url with the base, if the url is not absolute
973 link0
.href
= ((Ext
.isGecko
&& navigator
.productSub
< 2010072200 && !/^http(s?):\/{2}/.test(this.config
.editedContentStyle
)) ? this.config
.baseURL
: '') + this.config
.editedContentStyle
;
974 head
.appendChild(link0
);
975 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Skin CSS set to: ' + link0
.href
, 'info');
977 if (this.config
.defaultPageStyle
) {
978 var link
= this.document
.getElementsByTagName('link')[1];
980 link
= this.document
.createElement('link');
981 link
.rel
= 'stylesheet';
982 link
.type
= 'text/css';
983 link
.href
= ((Ext
.isGecko
&& navigator
.productSub
< 2010072200 && !/^https?:\/{2}/.test(this.config
.defaultPageStyle
)) ? this.config
.baseURL
: '') + this.config
.defaultPageStyle
;
984 head
.appendChild(link
);
986 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Override CSS set to: ' + link
.href
, 'info');
988 if (this.config
.pageStyle
) {
989 var link
= this.document
.getElementsByTagName('link')[2];
991 link
= this.document
.createElement('link');
992 link
.rel
= 'stylesheet';
993 link
.type
= 'text/css';
994 link
.href
= ((Ext
.isGecko
&& navigator
.productSub
< 2010072200 && !/^https?:\/{2}/.test(this.config
.pageStyle
)) ? this.config
.baseURL
: '') + this.config
.pageStyle
;
995 head
.appendChild(link
);
997 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link
.href
, 'info');
1001 * Focus on the iframe
1003 focus: function () {
1006 this.getEl().dom
.focus();
1008 this.getEl().dom
.contentWindow
.focus();
1013 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1014 * Should be set in config
1018 * All nested tabs and inline levels in the sorting order they were applied
1019 * Should be set in config
1021 nestedParentElements
: {},
1025 * @param boolean on: if true set designMode to on, otherwise set to off
1029 setDesignMode: function (on
) {
1033 // In Firefox, we can't set designMode when we are in a hidden TYPO3 tab or inline element
1034 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1035 this.document
.designMode
= 'on';
1039 this.document
.designMode
= 'on';
1043 if (Ext
.isIE
|| Ext
.isWebKit
) {
1044 this.document
.body
.contentEditable
= true;
1048 this.document
.designMode
= 'off';
1050 if (Ext
.isIE
|| Ext
.isWebKit
) {
1051 this.document
.body
.contentEditable
= false;
1056 * Set editing mode options (if we can... raises exception in Firefox 3)
1060 setOptions: function () {
1063 if (this.document
.queryCommandEnabled('insertBrOnReturn')) {
1064 this.document
.execCommand('insertBrOnReturn', false, this.config
.disableEnterParagraphs
);
1066 if (this.document
.queryCommandEnabled('styleWithCSS')) {
1067 this.document
.execCommand('styleWithCSS', false, this.config
.useCSS
);
1068 } else if (Ext
.isGecko
&& this.document
.queryCommandEnabled('useCSS')) {
1069 this.document
.execCommand('useCSS', false, !this.config
.useCSS
);
1072 if (this.document
.queryCommandEnabled('enableObjectResizing')) {
1073 this.document
.execCommand('enableObjectResizing', false, !this.config
.disableObjectResizing
);
1075 if (this.document
.queryCommandEnabled('enableInlineTableEditing')) {
1076 this.document
.execCommand('enableInlineTableEditing', false, (this.config
.buttons
.table
&& this.config
.buttons
.table
.enableHandles
) ? true : false);
1083 * Handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
1085 onNestedShow: function (event
, target
) {
1086 var styleEvent
= true;
1087 // In older versions of Gecko attrName is not set and refering to it causes a non-catchable crash
1088 if ((Ext
.isGecko
&& navigator
.productSub
> 2007112700) || Ext
.isOpera
) {
1089 styleEvent
= (event
.browserEvent
.attrName
== 'style');
1090 } else if (Ext
.isIE
) {
1091 styleEvent
= (event
.browserEvent
.propertyName
== 'style.display');
1093 if (styleEvent
&& this.nestedParentElements
.sorted
.indexOf(target
.id
) != -1 && (target
.style
.display
== '' || target
.style
.display
== 'block')) {
1094 // Check if all container nested elements are displayed
1095 if (HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1096 if (this.getEditor().getMode() === 'wysiwyg') {
1098 this.setDesignMode(true);
1100 this.fireEvent('show');
1102 this.ownerCt
.textAreaContainer
.fireEvent('show');
1104 this.getToolbar().update();
1110 * Instance of DOM walker
1114 * Get the HTML content of the iframe
1116 getHTML: function () {
1117 return this.htmlRenderer
.render(this.document
.body
, false);
1120 * Start listening to things happening in the iframe
1122 startListening: function () {
1123 // Create keyMap so that plugins may bind key handlers
1124 this.keyMap
= new Ext
.KeyMap(Ext
.get(this.document
.documentElement
), [], (Ext
.isIE
|| Ext
.isWebKit
) ? 'keydown' : 'keypress');
1126 this.keyMap
.addBinding([
1128 key
: [Ext
.EventObject
.DOWN
, Ext
.EventObject
.UP
, Ext
.EventObject
.LEFT
, Ext
.EventObject
.RIGHT
],
1130 handler
: this.onArrow
,
1134 key
: Ext
.EventObject
.TAB
,
1137 handler
: this.onTab
,
1141 key
: Ext
.EventObject
.SPACE
,
1145 handler
: this.onCtrlSpace
,
1149 if (Ext
.isGecko
|| Ext
.isIE
) {
1150 this.keyMap
.addBinding(
1152 key
: [Ext
.EventObject
.BACKSPACE
, Ext
.EventObject
.DELETE
],
1154 handler
: this.onBackSpace
,
1158 if (!Ext
.isIE
&& !this.config
.disableEnterParagraphs
) {
1159 this.keyMap
.addBinding(
1161 key
: Ext
.EventObject
.ENTER
,
1163 handler
: this.onEnter
,
1168 this.keyMap
.addBinding(
1170 key
: Ext
.EventObject
.ENTER
,
1172 handler
: this.onWebKitEnter
,
1176 // Hot key map (on keydown for all browsers)
1178 Ext
.iterate(this.config
.hotKeyList
, function (key
) {
1179 if (key
.length
== 1) {
1180 hotKeys
+= key
.toUpperCase();
1183 // Make hot key map available, even if empty, so that plugins may add bindings
1184 this.hotKeyMap
= new Ext
.KeyMap(Ext
.get(this.document
.documentElement
));
1185 if (!Ext
.isEmpty(hotKeys
)) {
1186 this.hotKeyMap
.addBinding({
1191 handler
: this.onHotKey
,
1195 this.mon(Ext
.get(this.document
.documentElement
), (Ext
.isIE
|| Ext
.isWebKit
) ? 'keydown' : 'keypress', this.onAnyKey
, this);
1196 this.mon(Ext
.get(this.document
.documentElement
), 'mouseup', this.onMouse
, this);
1197 this.mon(Ext
.get(this.document
.documentElement
), 'click', this.onMouse
, this);
1199 this.mon(Ext
.get(this.document
.documentElement
), 'paste', this.onPaste
, this);
1201 this.mon(Ext
.get(this.document
.documentElement
), 'drop', this.onDrop
, this);
1203 this.mon(Ext
.get(this.document
.body
), 'dragend', this.onDrop
, this);
1207 * Handler for other key events
1209 onAnyKey: function(event
) {
1210 if (this.inhibitKeyboardInput(event
)) {
1213 this.fireEvent('HTMLAreaEventWordCountChange', 100);
1214 if (!event
.altKey
&& !event
.ctrlKey
) {
1215 // Detect URL in non-IE browsers
1216 if (!Ext
.isIE
&& (event
.getKey() != Ext
.EventObject
.ENTER
|| (event
.shiftKey
&& !Ext
.isWebKit
))) {
1217 this.getEditor()._detectURL(event
);
1219 // Handle option+SPACE for Mac users
1220 if (Ext
.isMac
&& event
.browserEvent
.charCode
== 160) {
1221 return this.onOptionSpace(event
.browserEvent
.charCode
, event
);
1227 * On any key input event, check if input is currently inhibited
1229 inhibitKeyboardInput: function (event
) {
1230 // Inhibit key events while server-based cleaning is being processed
1231 if (this.getEditor().inhibitKeyboardInput
) {
1239 * Handler for mouse events
1241 onMouse: function (event
, target
) {
1242 // In WebKit, select the image when it is clicked
1243 if (Ext
.isWebKit
&& /^(img)$/i.test(target
.nodeName
) && event
.browserEvent
.type
== 'click') {
1244 this.getEditor().selectNode(target
);
1246 this.getToolbar().updateLater
.delay(100);
1250 * Handler for paste operations in Gecko
1252 onPaste: function (event
) {
1253 // Make src and href urls absolute
1255 HTMLArea
.DOM
.makeUrlsAbsolute
.defer(50, this, [this.getEditor().document
.body
, this.config
.baseURL
, this.htmlRenderer
]);
1259 * Handler for drag and drop operations
1261 onDrop: function (event
, target
) {
1262 // Clean up span elements added by WebKit
1264 this.getEditor().cleanAppleStyleSpans
.defer(50, this.getEditor(), [this.getEditor().document
.body
]);
1266 // Make src url absolute in Firefox
1268 HTMLArea
.DOM
.makeUrlsAbsolute
.defer(50, this, [target
, this.config
.baseURL
, this.htmlRenderer
]);
1270 this.getToolbar().updateLater
.delay(100);
1273 * Handler for UP, DOWN, LEFT and RIGHT keys
1275 onArrow: function () {
1276 this.getToolbar().updateLater
.delay(100);
1280 * Handler for TAB and SHIFT-TAB keys
1282 * If available, BlockElements plugin will handle the TAB key
1284 onTab: function (key
, event
) {
1285 if (this.inhibitKeyboardInput(event
)) {
1288 var keyName
= (event
.shiftKey
? 'SHIFT-' : '') + 'TAB';
1289 if (this.config
.hotKeyList
[keyName
] && this.config
.hotKeyList
[keyName
].cmd
) {
1290 var button
= this.getButton(this.config
.hotKeyList
[keyName
].cmd
);
1293 button
.fireEvent('HTMLAreaEventHotkey', keyName
, event
);
1300 * Handler for BACKSPACE and DELETE keys
1302 onBackSpace: function (key
, event
) {
1303 if (this.inhibitKeyboardInput(event
)) {
1306 if ((!Ext
.isIE
&& !event
.shiftKey
) || Ext
.isIE
) {
1307 if (this.getEditor()._checkBackspace()) {
1311 // Update the toolbar state after some time
1312 this.getToolbar().updateLater
.delay(200);
1316 * Handler for ENTER key in non-IE browsers
1318 onEnter: function (key
, event
) {
1319 if (this.inhibitKeyboardInput(event
)) {
1322 this.getEditor()._detectURL(event
);
1323 if (this.getEditor()._checkInsertP()) {
1326 // Update the toolbar state after some time
1327 this.getToolbar().updateLater
.delay(200);
1331 * Handler for ENTER key in WebKit browsers
1333 onWebKitEnter: function (key
, event
) {
1334 if (this.inhibitKeyboardInput(event
)) {
1337 if (event
.shiftKey
|| this.config
.disableEnterParagraphs
) {
1338 var editor
= this.getEditor();
1339 editor
._detectURL(event
);
1341 var brNode
= editor
.document
.createElement('br');
1342 editor
.insertNodeAtSelection(brNode
);
1343 brNode
.parentNode
.normalize();
1344 // Selection issue when an URL was detected
1345 if (editor
._unlinkOnUndo
) {
1346 brNode
= brNode
.parentNode
.parentNode
.insertBefore(brNode
, brNode
.parentNode
.nextSibling
);
1348 if (!brNode
.nextSibling
|| !/\S+/i.test(brNode
.nextSibling
.textContent
)) {
1349 var secondBrNode
= editor
.document
.createElement('br');
1350 secondBrNode
= brNode
.parentNode
.appendChild(secondBrNode
);
1352 editor
.selectNode(brNode
, false);
1356 // Update the toolbar state after some time
1357 this.getToolbar().updateLater
.delay(200);
1361 * Handler for CTRL-SPACE keys
1363 onCtrlSpace: function (key
, event
) {
1364 if (this.inhibitKeyboardInput(event
)) {
1367 this.getEditor().insertHTML(' ');
1372 * Handler for OPTION-SPACE keys on Mac
1374 onOptionSpace: function (key
, event
) {
1375 if (this.inhibitKeyboardInput(event
)) {
1378 this.getEditor().insertHTML(' ');
1383 * Handler for configured hotkeys
1385 onHotKey: function (key
, event
) {
1386 if (this.inhibitKeyboardInput(event
)) {
1389 var hotKey
= String
.fromCharCode(key
).toLowerCase();
1390 this.getButton(this.config
.hotKeyList
[hotKey
].cmd
).fireEvent('HTMLAreaEventHotkey', hotKey
, event
);
1396 onBeforeDestroy: function () {
1397 // ExtJS KeyMap object makes IE leak memory
1398 // Nullify EXTJS private handlers
1399 Ext
.each(this.keyMap
.bindings
, function (binding
, index
) {
1400 this.keyMap
.bindings
[index
] = null;
1402 this.keyMap
.handleKeyDown
= null;
1403 Ext
.each(this.hotKeyMap
.bindings
, function (binding
, index
) {
1404 this.hotKeyMap
.bindings
[index
] = null;
1406 this.hotKeyMap
.handleKeyDown
= null;
1407 this.keyMap
.disable();
1408 this.hotKeyMap
.disable();
1409 // Cleaning references to DOM in order to avoid IE memory leaks
1410 Ext
.get(this.document
.body
).purgeAllListeners();
1411 Ext
.get(this.document
.body
).dom
= null;
1412 Ext
.get(this.document
.documentElement
).purgeAllListeners();
1413 Ext
.get(this.document
.documentElement
).dom
= null;
1414 this.document
= null;
1415 this.getEditor().document
= null;
1416 this.getEditor()._doc
= null;
1417 this.getEditor()._iframe
= null;
1418 Ext
.each(this.nestedParentElements
.sorted
, function (nested
) {
1419 Ext
.get(nested
).purgeAllListeners();
1420 Ext
.get(nested
).dom
= null;
1422 Ext
.destroy(this.autoEl
, this.el
, this.resizeEl
, this.positionEl
);
1426 Ext
.reg('htmlareaiframe', HTMLArea
.Iframe
);
1428 * HTMLArea.StatusBar extends Ext.Container
1430 HTMLArea
.StatusBar
= Ext
.extend(Ext
.Container
, {
1434 initComponent: function () {
1435 HTMLArea
.StatusBar
.superclass
.initComponent
.call(this);
1436 // Build the deferred word count update task
1437 this.updateWordCountLater
= new Ext
.util
.DelayedTask(this.updateWordCount
, this);
1440 fn
: this.addComponents
,
1444 fn
: this.initEventListeners
,
1450 * Initialize listeners
1452 initEventListeners: function () {
1455 fn
: this.onBeforeDestroy
,
1459 // Monitor toolbar updates in order to refresh the contents of the statusbar
1460 // The toolbar must have been rendered
1461 this.mon(this.ownerCt
.toolbar
, 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar
, this);
1462 // Monitor editor changing mode
1463 this.mon(this.getEditor(), 'HTMLAreaEventModeChange', this.onModeChange
, this);
1464 // Monitor word count change
1465 this.mon(this.ownerCt
.iframe
, 'HTMLAreaEventWordCountChange', this.onWordCountChange
, this);
1468 * editorId should be set in config
1472 * Get a reference to the editor
1474 getEditor: function() {
1475 return RTEarea
[this.editorId
].editor
;
1478 * Create span elements to display when the status bar tree or a message when the editor is in text mode
1480 addComponents: function () {
1481 this.statusBarWordCount
= Ext
.DomHelper
.append(this.getEl(), {
1482 id
: this.editorId
+ '-statusBarWordCount',
1484 cls
: 'statusBarWordCount',
1487 this.statusBarTree
= Ext
.DomHelper
.append(this.getEl(), {
1488 id
: this.editorId
+ '-statusBarTree',
1490 cls
: 'statusBarTree',
1491 html
: HTMLArea
.localize('Path') + ': '
1492 }, true).setVisibilityMode(Ext
.Element
.DISPLAY
).setVisible(true);
1493 this.statusBarTextMode
= Ext
.DomHelper
.append(this.getEl(), {
1494 id
: this.editorId
+ '-statusBarTextMode',
1496 cls
: 'statusBarTextMode',
1497 html
: HTMLArea
.localize('TEXT_MODE')
1498 }, true).setVisibilityMode(Ext
.Element
.DISPLAY
).setVisible(false);
1501 * Clear the status bar tree
1503 clear: function () {
1504 this.statusBarTree
.removeAllListeners();
1505 Ext
.each(this.statusBarTree
.query('a'), function (node
) {
1506 Ext
.QuickTips
.unregister(node
);
1507 Ext
.get(node
).dom
.ancestor
= null;
1510 this.statusBarTree
.update('');
1511 this.setSelection(null);
1514 * Flag indicating that the status bar should not be updated on this toolbar update
1518 * Update the status bar
1520 onUpdateToolbar: function (mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
) {
1521 if (mode
=== 'wysiwyg' && !this.noUpdate
) {
1524 languageObject
= this.getEditor().getPlugin('Language'),
1525 classes
= new Array(),
1528 var path
= Ext
.DomHelper
.append(this.statusBarTree
, {
1530 html
: HTMLArea
.localize('Path') + ': '
1532 Ext
.each(ancestors
, function (ancestor
, index
) {
1536 text
= ancestor
.nodeName
.toLowerCase();
1537 // Do not show any id generated by ExtJS
1538 if (ancestor
.id
&& text
!== 'body' && ancestor
.id
.substr(0, 7) !== 'ext-gen') {
1539 text
+= '#' + ancestor
.id
;
1541 if (languageObject
&& languageObject
.getLanguageAttribute
) {
1542 language
= languageObject
.getLanguageAttribute(ancestor
);
1543 if (language
!= 'none') {
1544 text
+= '[' + language
+ ']';
1547 if (ancestor
.className
) {
1549 classes
= ancestor
.className
.trim().split(' ');
1550 for (var j
= 0, n
= classes
.length
; j
< n
; ++j
) {
1551 if (!HTMLArea
.reservedClassNames
.test(classes
[j
])) {
1552 classText
+= '.' + classes
[j
];
1557 var element
= Ext
.DomHelper
.insertAfter(path
, {
1560 'ext:qtitle': HTMLArea
.localize('statusBarStyle'),
1561 'ext:qtip': ancestor
.style
.cssText
.split(';').join('<br />'),
1564 // Ext.DomHelper does not honour the custom attribute
1565 element
.dom
.ancestor
= ancestor
;
1566 element
.on('click', this.onClick
, this);
1567 element
.on('mousedown', this.onClick
, this);
1569 element
.on('contextmenu', this.onContextMenu
, this);
1572 Ext
.DomHelper
.insertAfter(element
, {
1574 html
: String
.fromCharCode(0xbb)
1579 this.updateWordCount();
1580 this.noUpdate
= false;
1583 * Handler when the word count may have changed
1585 onWordCountChange: function(delay
) {
1586 this.updateWordCountLater
.delay(delay
? delay
: 0);
1589 * Update the word count
1591 updateWordCount: function() {
1593 if (this.getEditor().getMode() == 'wysiwyg') {
1594 // Get the html content
1595 var text
= this.getEditor().getHTML();
1596 if (!Ext
.isEmpty(text
)) {
1597 // Replace html tags with spaces
1598 text
= text
.replace(HTMLArea
.RE_htmlTag
, ' ');
1599 // Replace html space entities
1600 text
= text
.replace(/ | /gi, ' ');
1601 // Remove numbers and punctuation
1602 text
= text
.replace(HTMLArea
.RE_numberOrPunctuation
, '');
1603 // Get the number of word
1604 wordCount
= text
.split(/\S\s+/g).length
- 1;
1607 // Update the word count of the status bar
1608 this.statusBarWordCount
.dom
.innerHTML
= wordCount
? ( wordCount
+ ' ' + HTMLArea
.localize((wordCount
== 1) ? 'word' : 'words')) : ' ';
1611 * Adapt status bar to current editor mode
1613 * @param string mode: the mode to which the editor got switched to
1615 onModeChange: function (mode
) {
1618 this.statusBarTextMode
.setVisible(false);
1619 this.statusBarTree
.setVisible(true);
1623 this.statusBarTree
.setVisible(false);
1624 this.statusBarTextMode
.setVisible(true);
1629 * Refrence to the element last selected on the status bar
1633 * Get the status bar selection
1635 getSelection: function() {
1636 return this.selected
;
1639 * Set the status bar selection
1641 * @param object element: set the status bar selection to the given element
1643 setSelection: function(element
) {
1644 this.selected
= element
? element
: null;
1647 * Select the element that was clicked in the status bar and set the status bar selection
1649 selectElement: function (element
) {
1650 var editor
= this.getEditor();
1653 if (/^(img)$/i.test(element
.ancestor
.nodeName
)) {
1654 editor
.selectNode(element
.ancestor
);
1656 editor
.selectNodeContents(element
.ancestor
);
1659 if (/^(img|table)$/i.test(element
.ancestor
.nodeName
)) {
1660 var range
= editor
.document
.body
.createControlRange();
1661 range
.addElement(element
.ancestor
);
1664 editor
.selectNode(element
.ancestor
);
1667 this.setSelection(element
.ancestor
);
1668 this.noUpdate
= true;
1669 editor
.toolbar
.update();
1674 onClick: function (event
, element
) {
1675 this.selectElement(element
);
1680 * ContextMenu handler
1682 onContextMenu: function (event
, target
) {
1683 this.selectElement(target
);
1684 return this.getEditor().getPlugin('ContextMenu') ? this.getEditor().getPlugin('ContextMenu').show(event
, target
.ancestor
) : false;
1689 onBeforeDestroy: function() {
1691 this.removeAll(true);
1692 Ext
.destroy(this.statusBarTree
, this.statusBarTextMode
);
1696 Ext
.reg('htmlareastatusbar', HTMLArea
.StatusBar
);
1698 * HTMLArea.Framework extends Ext.Panel
1700 HTMLArea
.Framework
= Ext
.extend(Ext
.Panel
, {
1704 initComponent: function () {
1705 HTMLArea
.Framework
.superclass
.initComponent
.call(this);
1706 // Set some references
1707 this.toolbar
= this.getTopToolbar();
1708 this.statusBar
= this.getBottomToolbar();
1709 this.iframe
= this.getComponent('iframe');
1710 this.textAreaContainer
= this.getComponent('textAreaContainer');
1713 * @event HTMLAreaEventFrameworkReady
1714 * Fires when the iframe is ready and all components are rendered
1716 'HTMLAreaEventFrameworkReady'
1720 fn
: this.onBeforeDestroy
,
1724 // Monitor iframe becoming ready
1725 this.mon(this.iframe
, 'HTMLAreaEventIframeReady', this.onIframeReady
, this, {single
: true});
1726 // Let the framefork render itself, but it will fail to do so if inside a hidden tab or inline element
1727 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1728 this.render(this.textArea
.parent(), this.textArea
.id
);
1730 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1731 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
1732 // Walk through all nested tabs and inline levels to get correct sizes
1733 HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'args[0].render(args[0].textArea.parent(), args[0].textArea.id)', [this]);
1737 * Initiate events monitoring
1739 initEventListeners: function () {
1740 // Make the framework resizable, if configured by the user
1741 this.makeResizable();
1742 // Monitor textArea container becoming shown or hidden as it may change the height of the status bar
1743 this.mon(this.textAreaContainer
, 'show', this.resizable
? this.onTextAreaShow
: this.onWindowResize
, this);
1744 // Monitor iframe becoming shown or hidden as it may change the height of the status bar
1745 this.mon(this.iframe
, 'show', this.resizable
? this.onIframeShow
: this.onWindowResize
, this);
1746 // Monitor window resizing
1747 Ext
.EventManager
.onWindowResize(this.onWindowResize
, this);
1748 // If the textarea is inside a form, on reset, re-initialize the HTMLArea content and update the toolbar
1749 var form
= this.textArea
.dom
.form
;
1751 if (Ext
.isFunction(form
.onreset
)) {
1752 if (typeof(form
.htmlAreaPreviousOnReset
) == 'undefined') {
1753 form
.htmlAreaPreviousOnReset
= [];
1755 form
.htmlAreaPreviousOnReset
.push(form
.onreset
);
1757 this.mon(Ext
.get(form
), 'reset', this.onReset
, this);
1761 fn
: this.onFrameworkResize
1766 * editorId should be set in config
1770 * Get a reference to the editor
1772 getEditor: function() {
1773 return RTEarea
[this.editorId
].editor
;
1776 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1777 * Should be set in config
1781 * All nested tabs and inline levels in the sorting order they were applied
1782 * Should be set in config
1784 nestedParentElements
: {},
1786 * Flag set to true when the framework is ready
1790 * All nested tabs and inline levels in the sorting order they were applied
1791 * Should be set in config
1793 nestedParentElements
: {},
1795 * Whether the framework should be made resizable
1796 * May be set in config
1800 * Maximum height to which the framework may resized (in pixels)
1801 * May be set in config
1805 * Initial textArea dimensions
1806 * Should be set in config
1808 textAreaInitialSize
: {
1814 * doLayout will fail if inside a hidden tab or inline element
1816 doLayout: function () {
1817 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1818 HTMLArea
.Framework
.superclass
.doLayout
.call(this);
1820 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1821 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
1822 // Walk through all nested tabs and inline levels to get correct sizes
1823 HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'HTMLArea.Framework.superclass.doLayout.call(args[0])', [this]);
1827 * Make the framework resizable, if configured
1829 makeResizable: function () {
1830 if (this.resizable
) {
1831 this.addClass('resizable');
1832 this.resizer
= new Ext
.Resizable(this.getEl(), {
1834 maxHeight
: this.maxHeight
,
1837 this.resizer
.on('resize', this.onHtmlAreaResize
, this);
1841 * Resize the framework when the resizer handles are used
1843 onHtmlAreaResize: function (resizer
, width
, height
, event
) {
1844 // Set width first as it may change the height of the toolbar and of the statusBar
1845 this.setWidth(width
);
1846 // Set height of iframe and textarea
1847 this.iframe
.setHeight(this.getInnerHeight());
1848 this.textArea
.setSize(this.getInnerWidth(), this.getInnerHeight());
1851 * Size the iframe according to initial textarea size as set by Page and User TSConfig
1853 onWindowResize: function (width
, height
) {
1854 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1855 this.resizeFramework(width
, height
);
1857 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1858 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
1859 // Walk through all nested tabs and inline levels to get correct sizes
1860 HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'args[0].resizeFramework(args[1], args[2])', [this, width
, height
]);
1864 * Resize the framework to its initial size
1866 resizeFramework: function (width
, height
) {
1867 var frameworkHeight
= parseInt(this.textAreaInitialSize
.height
);
1868 if (this.textAreaInitialSize
.width
.indexOf('%') === -1) {
1869 // Width is specified in pixels
1870 var frameworkWidth
= parseInt(this.textAreaInitialSize
.width
) - this.getFrameWidth();
1872 // Width is specified in %
1873 if (Ext
.isNumber(width
)) {
1874 // Framework sizing on actual window resize
1875 var frameworkWidth
= parseInt(((width
- this.textAreaInitialSize
.wizardsWidth
- (this.fullScreen
? 10 : Ext
.getScrollBarWidth()) - this.getBox().x
- 15) * parseInt(this.textAreaInitialSize
.width
))/100);
1877 // Initial framework sizing
1878 var frameworkWidth
= parseInt(((HTMLArea
.util
.TYPO3
.getWindowSize().width
- this.textAreaInitialSize
.wizardsWidth
- (this.fullScreen
? 10 : Ext
.getScrollBarWidth()) - this.getBox().x
- 15) * parseInt(this.textAreaInitialSize
.width
))/100);
1881 if (this.resizable
) {
1882 this.resizer
.resizeTo(frameworkWidth
, frameworkHeight
);
1884 this.setSize(frameworkWidth
, frameworkHeight
);
1888 * Resize the framework components
1890 onFrameworkResize: function () {
1891 // For unknown reason, in Chrome 7, this following is the only way to set the height of the iframe
1893 this.iframe
.getResizeEl().dom
.setAttribute('style', 'width:' + this.getInnerWidth() + 'px; height:' + this.getInnerHeight() + 'px;');
1895 this.iframe
.setSize(this.getInnerWidth(), this.getInnerHeight());
1897 this.textArea
.setSize(this.getInnerWidth(), this.getInnerHeight());
1900 * Adjust the height to the changing size of the statusbar when the textarea is shown
1902 onTextAreaShow: function () {
1903 this.iframe
.setHeight(this.getInnerHeight());
1904 this.textArea
.setHeight(this.getInnerHeight());
1907 * Adjust the height to the changing size of the statusbar when the iframe is shown
1909 onIframeShow: function () {
1910 if (this.getInnerHeight() <= 0) {
1911 this.onWindowResize();
1913 // For unknown reason, in Chrome 7, this following is the only way to set the height of the iframe
1915 this.iframe
.getResizeEl().dom
.setAttribute('style', 'width:' + this.getInnerWidth() + 'px; height:' + this.getInnerHeight() + 'px;');
1917 this.iframe
.setHeight(this.getInnerHeight());
1919 this.textArea
.setHeight(this.getInnerHeight());
1923 * Calculate the height available for the editing iframe
1925 getInnerHeight: function () {
1926 return this.getSize().height
- this.toolbar
.getHeight() - this.statusBar
.getHeight() - 5;
1929 * Fire the editor when all components of the framework are rendered and ready
1931 onIframeReady: function () {
1932 this.ready
= this.rendered
&& this.toolbar
.rendered
&& this.statusBar
.rendered
&& this.textAreaContainer
.rendered
;
1934 this.initEventListeners();
1935 this.textAreaContainer
.show();
1936 if (!this.getEditor().config
.showStatusBar
) {
1937 this.statusBar
.hide();
1939 // Set the initial size of the framework
1940 this.onWindowResize();
1941 this.fireEvent('HTMLAreaEventFrameworkReady');
1943 this.onIframeReady
.defer(50, this);
1947 * Handler invoked if we are inside a form and the form is reset
1948 * On reset, re-initialize the HTMLArea content and update the toolbar
1950 onReset: function (event
) {
1951 this.getEditor().setHTML(this.textArea
.getValue());
1952 this.toolbar
.update();
1953 // Invoke previous reset handlers, if any
1954 var htmlAreaPreviousOnReset
= event
.getTarget().dom
.htmlAreaPreviousOnReset
;
1955 if (typeof(htmlAreaPreviousOnReset
) != 'undefined') {
1956 Ext
.each(htmlAreaPreviousOnReset
, function (onReset
) {
1963 * Cleanup on framework destruction
1965 onBeforeDestroy: function () {
1966 Ext
.EventManager
.removeResizeListener(this.onWindowResize
, this);
1967 // Cleaning references to DOM in order to avoid IE memory leaks
1968 var form
= this.textArea
.dom
.form
;
1970 form
.htmlAreaPreviousOnReset
= null;
1971 Ext
.get(form
).dom
= null;
1973 Ext
.getBody().dom
= null;
1974 // ExtJS is not releasing any resources when the iframe is unloaded
1975 this.toolbar
.destroy();
1976 this.statusBar
.destroy();
1977 this.removeAll(true);
1979 this.resizer
.destroy();
1984 Ext
.reg('htmlareaframework', HTMLArea
.Framework
);
1985 /***************************************************
1986 * HTMLArea.Editor extends Ext.util.Observable
1987 ***************************************************/
1988 HTMLArea
.Editor
= Ext
.extend(Ext
.util
.Observable
, {
1990 * HTMLArea.Editor constructor
1992 constructor: function (config
) {
1993 HTMLArea
.Editor
.superclass
.constructor.call(this, {});
1995 this.config
= config
;
1996 // Establish references to this editor
1997 this.editorId
= this.config
.editorId
;
1998 RTEarea
[this.editorId
].editor
= this;
1999 // Get textarea size and wizard context
2000 this.textArea
= Ext
.get(this.config
.id
);
2001 this.textAreaInitialSize
= {
2002 width
: this.config
.RTEWidthOverride
? this.config
.RTEWidthOverride
: this.textArea
.getStyle('width'),
2003 height
: this.config
.fullScreen
? HTMLArea
.util
.TYPO3
.getWindowSize().height
- 20 : this.textArea
.getStyle('height'),
2006 // TYPO3 Inline elements and tabs
2007 this.nestedParentElements
= {
2008 all
: this.config
.tceformsNested
,
2009 sorted
: HTMLArea
.util
.TYPO3
.simplifyNested(this.config
.tceformsNested
)
2011 this.isNested
= !Ext
.isEmpty(this.nestedParentElements
.sorted
);
2012 // If in BE, get width of wizards
2013 if (Ext
.get('typo3-docheader')) {
2014 this.wizards
= this.textArea
.parent().parent().next();
2016 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
2017 this.textAreaInitialSize
.wizardsWidth
= this.wizards
.getWidth();
2019 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
2020 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
2021 // Walk through all nested tabs and inline levels to get correct size
2022 this.textAreaInitialSize
.wizardsWidth
= HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'args[0].getWidth()', [this.wizards
]);
2024 // Hide the wizards so that they do not move around while the editor framework is being sized
2025 this.wizards
.hide();
2030 // Register the plugins included in the configuration
2031 Ext
.iterate(this.config
.plugin
, function (plugin
) {
2032 if (this.config
.plugin
[plugin
]) {
2033 this.registerPlugin(plugin
);
2036 // Create Ajax object
2037 this.ajax
= new HTMLArea
.Ajax({
2040 // Initialize keyboard input inhibit flag
2041 this.inhibitKeyboardInput
= false;
2044 * @event HTMLAreaEventEditorReady
2045 * Fires when initialization of the editor is complete
2047 'HTMLAreaEventEditorReady',
2049 * @event HTMLAreaEventModeChange
2050 * Fires when the editor changes mode
2052 'HTMLAreaEventModeChange'
2056 * Flag set to true when the editor initialization has completed
2060 * The current mode of the editor: 'wysiwyg' or 'textmode'
2064 * Create the htmlArea framework
2066 generate: function () {
2067 // Create the editor framework
2068 this.htmlArea
= new HTMLArea
.Framework({
2069 id
: this.editorId
+ '-htmlArea',
2071 baseCls
: 'htmlarea',
2072 editorId
: this.editorId
,
2073 textArea
: this.textArea
,
2074 textAreaInitialSize
: this.textAreaInitialSize
,
2075 fullScreen
: this.config
.fullScreen
,
2076 resizable
: this.config
.resizable
,
2077 maxHeight
: this.config
.maxHeight
,
2078 isNested
: this.isNested
,
2079 nestedParentElements
: this.nestedParentElements
,
2082 xtype
: 'htmlareatoolbar',
2083 id
: this.editorId
+ '-toolbar',
2087 editorId
: this.editorId
2091 xtype
: 'htmlareaiframe',
2094 width
: (this.textAreaInitialSize
.width
.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize
.width
) : 300,
2095 height
: parseInt(this.textAreaInitialSize
.height
),
2097 id
: this.editorId
+ '-iframe',
2099 cls
: 'editorIframe',
2100 src
: (Ext
.isGecko
|| Ext
.isWebKit
) ? 'javascript:void(0);' : HTMLArea
.editorUrl
+ 'popups/blank.html'
2102 isNested
: this.isNested
,
2103 nestedParentElements
: this.nestedParentElements
,
2104 editorId
: this.editorId
2106 // Box container for the textarea
2108 itemId
: 'textAreaContainer',
2110 width
: (this.textAreaInitialSize
.width
.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize
.width
) : 300,
2111 // Let the framework swallow the textarea and throw it back
2114 fn: function (textAreaContainer
) {
2115 this.originalParent
= this.textArea
.parent().dom
;
2116 textAreaContainer
.getEl().appendChild(this.textArea
);
2122 fn: function (textAreaContainer
) {
2123 this.originalParent
.appendChild(this.textArea
.dom
);
2134 xtype
: 'htmlareastatusbar',
2137 editorId
: this.editorId
2140 // Set some references
2141 this.toolbar
= this.htmlArea
.getTopToolbar();
2142 this.statusBar
= this.htmlArea
.getBottomToolbar();
2143 this.iframe
= this.htmlArea
.getComponent('iframe');
2144 this.textAreaContainer
= this.htmlArea
.getComponent('textAreaContainer');
2145 // Get triggered when the framework becomes ready
2146 this.relayEvents(this.htmlArea
, ['HTMLAreaEventFrameworkReady']);
2147 this.on('HTMLAreaEventFrameworkReady', this.onFrameworkReady
, this, {single
: true});
2150 * Initialize the editor
2152 onFrameworkReady: function () {
2153 // Initialize editor mode
2154 this.setMode('wysiwyg');
2155 // Initiate events listening
2156 this.initEventsListening();
2158 this.generatePlugins();
2159 // Make the editor visible
2161 // Make the wizards visible again
2163 this.wizards
.show();
2165 // Focus on the first editor that is not hidden
2166 Ext
.iterate(RTEarea
, function (editorId
, RTE
) {
2167 if (!Ext
.isDefined(RTE
.editor
) || (RTE
.editor
.isNested
&& !HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(RTE
.editor
.nestedParentElements
.sorted
))) {
2175 this.fireEvent('HTMLAreaEventEditorReady');
2176 this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
2181 * @param string mode: 'textmode' or 'wysiwyg'
2185 setMode: function (mode
) {
2188 this.textArea
.set({ value
: this.getHTML() }, false);
2189 this.iframe
.setDesignMode(false);
2191 this.textAreaContainer
.show();
2196 this.document
.body
.innerHTML
= this.getHTML();
2198 this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
2199 TYPO3
.Dialog
.ErrorDialog({
2200 title
: 'htmlArea RTE',
2201 msg
: HTMLArea
.localize('HTML-document-not-well-formed')
2205 this.textAreaContainer
.hide();
2207 this.iframe
.setDesignMode(true);
2211 this.fireEvent('HTMLAreaEventModeChange', this.mode
);
2213 Ext
.iterate(this.plugins
, function(pluginId
) {
2214 this.getPlugin(pluginId
).onMode(this.mode
);
2218 * Get current editor mode
2220 getMode: function () {
2225 * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
2227 * @return string the textual html content from the current editing mode
2229 getHTML: function () {
2230 switch (this.mode
) {
2232 return this.iframe
.getHTML();
2234 return this.textArea
.getValue();
2242 * @return string the textual html content from the current editing mode
2244 getInnerHTML: function () {
2245 switch (this.mode
) {
2247 return this.document
.body
.innerHTML
;
2249 return this.textArea
.getValue();
2255 * Replace the html content
2257 * @param string html: the textual html
2261 setHTML: function (html
) {
2262 switch (this.mode
) {
2264 this.document
.body
.innerHTML
= html
;
2267 this.textArea
.set({ value
: html
}, false);;
2272 * Instantiate the specified plugin and register it with the editor
2274 * @param string plugin: the name of the plugin
2276 * @return boolean true if the plugin was successfully registered
2278 registerPlugin: function (pluginName
) {
2279 var plugin
= HTMLArea
[pluginName
],
2280 isRegistered
= false;
2281 if (typeof(plugin
) !== 'undefined' && Ext
.isFunction(plugin
)) {
2282 var pluginInstance
= new plugin(this, pluginName
);
2283 if (pluginInstance
) {
2284 var pluginInformation
= pluginInstance
.getPluginInformation();
2285 pluginInformation
.instance
= pluginInstance
;
2286 this.plugins
[pluginName
] = pluginInformation
;
2287 isRegistered
= true;
2290 if (!isRegistered
) {
2291 this.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName
+ '.', 'warn');
2293 return isRegistered
;
2296 * Generate registered plugins
2298 generatePlugins: function () {
2299 Ext
.iterate(this.plugins
, function (pluginId
) {
2300 var plugin
= this.getPlugin(pluginId
);
2301 plugin
.onGenerate();
2305 * Get the instance of the specified plugin, if it exists
2307 * @param string pluginName: the name of the plugin
2308 * @return object the plugin instance or null
2310 getPlugin: function(pluginName
) {
2311 return (this.plugins
[pluginName
] ? this.plugins
[pluginName
].instance
: null);
2314 * Unregister the instance of the specified plugin
2316 * @param string pluginName: the name of the plugin
2319 unRegisterPlugin: function(pluginName
) {
2320 delete this.plugins
[pluginName
].instance
;
2321 delete this.plugins
[pluginName
];
2324 * Focus on the editor
2326 focus: function () {
2327 switch (this.getMode()) {
2329 this.iframe
.focus();
2332 this.textArea
.focus();
2339 initEventsListening: function () {
2341 this.iframe
.startListening();
2343 // Add unload handler
2344 var iframe
= this.iframe
.getEl().dom
;
2345 Ext
.EventManager
.on(iframe
.contentWindow
? iframe
.contentWindow
: iframe
.contentDocument
, 'unload', this.onUnload
, this, {single
: true});
2348 * Make the editor framework visible
2351 document
.getElementById('pleasewait' + this.editorId
).style
.display
= 'none';
2352 document
.getElementById('editorWrap' + this.editorId
).style
.visibility
= 'visible';
2355 * Append an entry at the end of the troubleshooting log
2357 * @param string functionName: the name of the editor function writing to the log
2358 * @param string text: the text of the message
2359 * @param string type: the type of message
2363 appendToLog: function (objectName
, functionName
, text
, type
) {
2364 HTMLArea
.appendToLog(this.editorId
, objectName
, functionName
, text
, type
);
2367 * Iframe unload handler: Update the textarea for submission and cleanup
2369 onUnload: function (event
) {
2370 // Save the HTML content into the original textarea for submit, back/forward, etc.
2373 value
: this.getHTML()
2377 Ext
.TaskMgr
.stopAll();
2378 // ExtJS is not releasing any resources when the iframe is unloaded
2379 this.htmlArea
.destroy();
2380 Ext
.iterate(this.plugins
, function (pluginId
) {
2381 this.unRegisterPlugin(pluginId
);
2383 this.purgeListeners();
2384 // Cleaning references to DOM in order to avoid IE memory leaks
2386 this.wizards
.dom
= null;
2387 this.textArea
.parent().parent().dom
= null;
2388 this.textArea
.parent().dom
= null;
2390 this.textArea
.dom
= null;
2391 RTEarea
[this.editorId
].editor
= null;
2394 HTMLArea
.Ajax = function (config
) {
2395 Ext
.apply(this, config
);
2397 HTMLArea
.Ajax
= Ext
.extend(HTMLArea
.Ajax
, {
2399 * Load a Javascript file asynchronously
2401 * @param string url: url of the file to load
2402 * @param function callBack: the callBack function
2403 * @param object scope: scope of the callbacks
2405 * @return boolean true on success of the request submission
2407 getJavascriptFile: function (url
, callback
, scope
) {
2408 var success
= false;
2414 success: function (response
) {
2417 failure: function (response
) {
2418 self
.editor
.inhibitKeyboardInput
= false;
2419 self
.editor
.appendToLog('HTMLArea.Ajax', 'getJavascriptFile', 'Unable to get ' + url
+ ' . Server reported ' + response
.status
, 'error');
2426 * Post data to the server
2428 * @param string url: url to post data to
2429 * @param object data: data to be posted
2430 * @param function callback: function that will handle the response returned by the server
2431 * @param object scope: scope of the callbacks
2433 * @return boolean true on success
2435 postData: function (url
, data
, callback
, scope
) {
2436 var success
= false;
2438 data
.charset
= this.editor
.config
.typo3ContentCharset
? this.editor
.config
.typo3ContentCharset
: 'utf-8';
2440 Ext
.iterate(data
, function (parameter
, value
) {
2441 params
+= (params
.length
? '&' : '') + parameter
+ '=' + encodeURIComponent(value
);
2443 params
+= this.editor
.config
.RTEtsConfigParams
;
2447 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
2451 callback
: Ext
.isFunction(callback
) ? callback: function (options
, success
, response
) {
2453 self
.editor
.appendToLog('HTMLArea.Ajax', 'postData', 'Post request to ' + url
+ ' failed. Server reported ' + response
.status
, 'error');
2456 success: function (response
) {
2459 failure: function (response
) {
2460 self
.editor
.appendToLog('HTMLArea.Ajax', 'postData', 'Unable to post ' + url
+ ' . Server reported ' + response
.status
, 'error');
2467 /***************************************************
2468 * HTMLArea.util.TYPO3: Utility functions for dealing with tabs and inline elements in TYPO3 forms
2469 ***************************************************/
2470 HTMLArea
.util
.TYPO3 = function () {
2473 * Simplify the array of nested levels. Create an indexed array with the correct names of the elements.
2475 * @param object nested: The array with the nested levels
2476 * @return object The simplified array
2477 * @author Oliver Hader <oh@inpublica.de>
2479 simplifyNested: function(nested
) {
2480 var i
, type
, level
, elementId
, max
, simplifiedNested
=[],
2486 if (nested
&& nested
.length
) {
2487 if (nested
[0][0]=='inline') {
2488 nested
= inline
.findContinuedNestedLevel(nested
, nested
[0][1]);
2490 for (i
=0, max
=nested
.length
; i
<max
; i
++) {
2491 type
= nested
[i
][0];
2492 level
= nested
[i
][1];
2493 elementId
= level
+ elementIdSuffix
[type
];
2494 if (Ext
.get(elementId
)) {
2495 simplifiedNested
.push(elementId
);
2499 return simplifiedNested
;
2502 * Access an inline relational element or tab menu and make it "accessible".
2503 * If a parent or ancestor object has the style "display: none", offsetWidth & offsetHeight are '0'.
2505 * @params arry parentElements: array of parent elements id's; note that this input array will be modified
2506 * @params object callbackFunc: A function to be called, when the embedded objects are "accessible".
2507 * @params array args: array of arguments
2508 * @return object An object returned by the callbackFunc.
2509 * @author Oliver Hader <oh@inpublica.de>
2511 accessParentElements: function (parentElements
, callbackFunc
, args
) {
2513 if (parentElements
.length
) {
2514 var currentElement
= parentElements
.pop();
2515 currentElement
= Ext
.get(currentElement
);
2516 var actionRequired
= (currentElement
.getStyle('display') == 'none');
2517 if (actionRequired
) {
2518 var originalStyles
= currentElement
.getStyles('visibility', 'position', 'top', 'display');
2519 currentElement
.setStyle({
2520 visibility
: 'hidden',
2521 position
: 'absolute',
2526 result
= this.accessParentElements(parentElements
, callbackFunc
, args
);
2527 if (actionRequired
) {
2528 currentElement
.setStyle(originalStyles
);
2531 result
= eval(callbackFunc
);
2536 * Check if all elements in input array are currently displayed
2538 * @param array elements: array of element id's
2539 * @return boolean true if all elements are displayed
2541 allElementsAreDisplayed: function(elements
) {
2542 var allDisplayed
= true;
2543 Ext
.each(elements
, function (element
) {
2544 allDisplayed
= Ext
.get(element
).getStyle('display') != 'none';
2545 return allDisplayed
;
2547 return allDisplayed
;
2550 * Get current size of window
2552 * @return object width and height of window
2554 getWindowSize: function () {
2556 var size
= Ext
.getBody().getSize();
2559 width
: window
.innerWidth
,
2560 height
: window
.innerHeight
2563 // Subtract the docheader height from the calculated window height
2564 var docHeader
= Ext
.get('typo3-docheader');
2566 size
.height
-= docHeader
.getHeight();
2567 docHeader
.dom
= null;
2573 /***************************************************
2575 ***************************************************/
2576 HTMLArea
.getInnerText = function(el
) {
2579 for(i
=el
.firstChild
;i
;i
=i
.nextSibling
) {
2580 if(i
.nodeType
== 3) txt
+= i
.data
;
2581 else if(i
.nodeType
== 1) txt
+= HTMLArea
.getInnerText(i
);
2584 if(el
.nodeType
== 3) txt
= el
.data
;
2589 HTMLArea
.Editor
.prototype.forceRedraw = function() {
2590 this.htmlArea
.doLayout();
2593 HTMLArea
.Editor
.prototype.updateToolbar = function(noStatus
) {
2594 this.toolbar
.update(noStatus
);
2596 /***************************************************
2597 * DOM TREE MANIPULATION
2598 ***************************************************/
2601 * Surround the currently selected HTML source code with the given tags.
2602 * Delete the selection, if any.
2604 HTMLArea
.Editor
.prototype.surroundHTML = function(startTag
,endTag
) {
2605 this.insertHTML(startTag
+ this.getSelectedHTML().replace(HTMLArea
.Reg_body
, "") + endTag
);
2609 * Change the tag name of a node.
2611 HTMLArea
.Editor
.prototype.convertNode = function(el
,newTagName
) {
2612 var newel
= this.document
.createElement(newTagName
), p
= el
.parentNode
;
2613 while (el
.firstChild
) newel
.appendChild(el
.firstChild
);
2614 p
.insertBefore(newel
, el
);
2620 * Find a parent of an element with a specified tag
2622 HTMLArea
.getElementObject = function(el
,tagName
) {
2624 while (oEl
!= null && oEl
.nodeName
.toLowerCase() != tagName
) oEl
= oEl
.parentNode
;
2629 * This function removes the given markup element
2631 * @param object element: the inline element to be removed, content being preserved
2635 HTMLArea
.Editor
.prototype.removeMarkup = function(element
) {
2636 var bookmark
= this.getBookmark(this._createRange(this._getSelection()));
2637 var parent
= element
.parentNode
;
2638 while (element
.firstChild
) {
2639 parent
.insertBefore(element
.firstChild
, element
);
2641 parent
.removeChild(element
);
2642 this.selectRange(this.moveToBookmark(bookmark
));
2646 * This function verifies if the element has any allowed attributes
2648 * @param object element: the DOM element
2649 * @param array allowedAttributes: array of allowed attribute names
2651 * @return boolean true if the element has one of the allowed attributes
2653 HTMLArea
.hasAllowedAttributes = function(element
,allowedAttributes
) {
2655 for (var i
= allowedAttributes
.length
; --i
>= 0;) {
2656 value
= element
.getAttribute(allowedAttributes
[i
]);
2658 if (allowedAttributes
[i
] == "style" && element
.style
.cssText
) {
2668 /***************************************************
2669 * SELECTIONS AND RANGES
2670 ***************************************************/
2673 * Return true if we have some selected content
2675 HTMLArea
.Editor
.prototype.hasSelectedText = function() {
2676 return this.getSelectedHTML() != "";
2680 * Get an array with all the ancestor nodes of the selection.
2682 HTMLArea
.Editor
.prototype.getAllAncestors = function() {
2683 var p
= this.getParentElement();
2685 while (p
&& (p
.nodeType
=== 1) && (p
.nodeName
.toLowerCase() !== "body")) {
2689 a
.push(this.document
.body
);
2694 * Get the block ancestors of an element within a given block
2696 HTMLArea
.Editor
.prototype.getBlockAncestors = function(element
, withinBlock
) {
2697 var ancestors
= new Array();
2698 var ancestor
= element
;
2699 while (ancestor
&& (ancestor
.nodeType
=== 1) && !/^(body)$/i.test(ancestor
.nodeName
) && ancestor
!= withinBlock
) {
2700 if (HTMLArea
.isBlockElement(ancestor
)) {
2701 ancestors
.unshift(ancestor
);
2703 ancestor
= ancestor
.parentNode
;
2705 ancestors
.unshift(ancestor
);
2710 * Get the block elements containing the start and the end points of the selection
2712 HTMLArea
.Editor
.prototype.getEndBlocks = function(selection
) {
2713 var range
= this._createRange(selection
);
2715 var parentStart
= range
.startContainer
;
2716 if (/^(body)$/i.test(parentStart
.nodeName
)) {
2717 parentStart
= parentStart
.firstChild
;
2719 var parentEnd
= range
.endContainer
;
2720 if (/^(body)$/i.test(parentEnd
.nodeName
)) {
2721 parentEnd
= parentEnd
.lastChild
;
2724 if (selection
.type
!== "Control" ) {
2725 var rangeEnd
= range
.duplicate();
2726 range
.collapse(true);
2727 var parentStart
= range
.parentElement();
2728 rangeEnd
.collapse(false);
2729 var parentEnd
= rangeEnd
.parentElement();
2731 var parentStart
= range
.item(0);
2732 var parentEnd
= parentStart
;
2735 while (parentStart
&& !HTMLArea
.isBlockElement(parentStart
)) {
2736 parentStart
= parentStart
.parentNode
;
2738 while (parentEnd
&& !HTMLArea
.isBlockElement(parentEnd
)) {
2739 parentEnd
= parentEnd
.parentNode
;
2741 return { start
: parentStart
,
2747 * This function determines if the end poins of the current selection are within the same block
2749 * @return boolean true if the end points of the current selection are inside the same block element
2751 HTMLArea
.Editor
.prototype.endPointsInSameBlock = function() {
2752 var selection
= this._getSelection();
2753 if (this._selectionEmpty(selection
)) {
2756 var parent
= this.getParentElement(selection
);
2757 var endBlocks
= this.getEndBlocks(selection
);
2758 return (endBlocks
.start
=== endBlocks
.end
&& !/^(table|thead|tbody|tfoot|tr)$/i.test(parent
.nodeName
));
2763 * Get the deepest ancestor of the selection that is of the specified type
2764 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2766 HTMLArea
.Editor
.prototype._getFirstAncestor = function(sel
,types
) {
2767 var prnt
= this._activeElement(sel
);
2770 prnt
= (Ext
.isIE
? this._createRange(sel
).parentElement() : this._createRange(sel
).commonAncestorContainer
);
2775 if (typeof(types
) == 'string') types
= [types
];
2778 if (prnt
.nodeType
== 1) {
2779 if (types
== null) return prnt
;
2780 for (var i
= 0; i
< types
.length
; i
++) {
2781 if(prnt
.tagName
.toLowerCase() == types
[i
]) return prnt
;
2783 if(prnt
.tagName
.toLowerCase() == 'body') break;
2784 if(prnt
.tagName
.toLowerCase() == 'table') break;
2786 prnt
= prnt
.parentNode
;
2791 * Get the node whose contents are currently fully selected
2793 * @param array selection: the current selection
2794 * @param array range: the range of the current selection
2795 * @param array ancestors: the array of ancestors node of the current selection
2797 * @return object the fully selected node, if any, null otherwise
2799 HTMLArea
.Editor
.prototype.getFullySelectedNode = function (selection
, range
, ancestors
) {
2800 var node
, fullNodeSelected
= false;
2802 var selection
= this._getSelection();
2804 if (!this._selectionEmpty(selection
)) {
2806 var range
= this._createRange(selection
);
2809 var ancestors
= this.getAllAncestors();
2811 Ext
.each(ancestors
, function (ancestor
) {
2813 fullNodeSelected
= (selection
.type
!== 'Control' && ancestor
.innerText
== range
.text
) || (selection
.type
=== 'Control' && ancestor
.innerText
== range
.item(0).text
);
2815 fullNodeSelected
= (ancestor
.textContent
== range
.toString());
2817 if (fullNodeSelected
) {
2822 // Working around bug with WebKit selection
2823 if (Ext
.isWebKit
&& !fullNodeSelected
) {
2824 var statusBarSelection
= this.statusBar
? this.statusBar
.getSelection() : null;
2825 if (statusBarSelection
&& statusBarSelection
.textContent
== range
.toString()) {
2826 fullNodeSelected
= true;
2827 node
= statusBarSelection
;
2831 return fullNodeSelected
? node
: null;
2833 /***************************************************
2834 * Category: EVENT HANDLERS
2835 ***************************************************/
2838 * Intercept some native execCommand commands
2840 HTMLArea
.Editor
.prototype.execCommand = function(cmdID
, UI
, param
) {
2845 this.document
.execCommand(cmdID
, UI
, param
);
2847 this.appendToLog('HTMLArea.Editor', 'execCommand', e
+ ' by execCommand(' + cmdID
+ ')', 'error');
2850 this.toolbar
.update();
2854 HTMLArea
.Editor
.prototype.scrollToCaret = function() {
2856 var e
= this.getParentElement(),
2857 w
= this._iframe
.contentWindow
? this._iframe
.contentWindow
: window
,
2858 h
= w
.innerHeight
|| w
.height
,
2860 t
= d
.documentElement
.scrollTop
|| d
.body
.scrollTop
;
2861 if (e
.offsetTop
> h
+t
|| e
.offsetTop
< t
) {
2862 this.getParentElement().scrollIntoView();
2866 /***************************************************
2868 ***************************************************/
2871 * Check if the client agent is supported
2873 HTMLArea
.checkSupportedBrowser = function() {
2874 return Ext
.isGecko
|| Ext
.isWebKit
|| Ext
.isOpera
|| Ext
.isIE
;
2877 * Remove a class name from the class attribute of an element
2879 * @param object el: the element
2880 * @param string className: the class name to remove
2881 * @param boolean substring: if true, remove the first class name starting with the given string
2883 ***********************************************
2884 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2885 ***********************************************
2887 HTMLArea
._removeClass = function(el
, className
, substring
) {
2888 HTMLArea
.DOM
.removeClass(el
, className
, substring
);
2891 * Add a class name to the class attribute
2892 ***********************************************
2893 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2894 ***********************************************
2896 HTMLArea
._addClass = function(el
, className
) {
2897 HTMLArea
.DOM
.addClass(el
, className
);
2900 * Check if a class name is in the class attribute of an element
2902 * @param object el: the element
2903 * @param string className: the class name to look for
2904 * @param boolean substring: if true, look for a class name starting with the given string
2905 * @return boolean true if the class name was found
2906 ***********************************************
2907 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2908 ***********************************************
2910 HTMLArea
._hasClass = function(el
, className
, substring
) {
2911 return HTMLArea
.DOM
.hasClass(el
, className
, substring
);
2914 HTMLArea
.isBlockElement = function(el
) { return el
&& el
.nodeType
== 1 && HTMLArea
.RE_blockTags
.test(el
.nodeName
.toLowerCase()); };
2915 HTMLArea
.needsClosingTag = function(el
) { return el
&& el
.nodeType
== 1 && !HTMLArea
.RE_noClosingTag
.test(el
.tagName
.toLowerCase()); };
2918 * Perform HTML encoding of some given string
2919 * Borrowed in part from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2921 HTMLArea
.htmlDecode = function(str
) {
2922 str
= str
.replace(/</g, "<").replace(/>/g, ">");
2923 str
= str
.replace(/ /g, "\xA0"); // Decimal 160, non-breaking-space
2924 str
= str
.replace(/"/g, "\x22");
2925 str
= str
.replace(/'/g, "'") ;
2926 str
= str
.replace(/&/g, "&");
2929 HTMLArea
.htmlEncode = function(str
) {
2930 if (typeof(str
) != 'string') str
= str
.toString(); // we don't need regexp for that, but.. so be it for now.
2931 str
= str
.replace(/&/g
, "&");
2932 str
= str
.replace(/</g, "<").replace(/>/g
, ">");
2933 str
= str
.replace(/\xA0/g, " "); // Decimal 160, non-breaking-space
2934 str
= str
.replace(/\x22/g, """); // \x22 means '"'
2938 * Retrieve the HTML code from the given node.
2939 * This is a replacement for getting innerHTML, using standard DOM calls.
2940 * Wrapper catches a Mozilla-Exception with non well-formed html source code.
2941 ***********************************************
2942 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2943 ***********************************************
2945 HTMLArea
.getHTML = function(root
, outputRoot
, editor
){
2947 return editor
.iframe
.htmlRenderer
.render(root
, outputRoot
);
2949 editor
.appendToLog('HTMLArea', 'getHTML', 'The HTML document is not well-formed.', 'warn');
2950 TYPO3
.Dialog
.ErrorDialog({
2951 title
: 'htmlArea RTE',
2952 msg
: HTMLArea
.localize('HTML-document-not-well-formed')
2954 return editor
.document
.body
.innerHTML
;
2957 HTMLArea
.getPrevNode = function(node
) {
2958 if(!node
) return null;
2959 if(node
.previousSibling
) return node
.previousSibling
;
2960 if(node
.parentNode
) return node
.parentNode
;
2964 HTMLArea
.getNextNode = function(node
) {
2965 if(!node
) return null;
2966 if(node
.nextSibling
) return node
.nextSibling
;
2967 if(node
.parentNode
) return node
.parentNode
;
2971 HTMLArea
.removeFromParent = function(el
) {
2972 if(!el
.parentNode
) return;
2973 var pN
= el
.parentNode
;
2977 /*****************************************************************
2978 * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
2979 *****************************************************************/
2980 HTMLArea
.DOM = function () {
2982 /***************************************************
2983 * DOM-RELATED CONSTANTS
2984 ***************************************************/
2989 CDATA_SECTION_NODE
: 4,
2990 ENTITY_REFERENCE_NODE
: 5,
2992 PROCESSING_INSTRUCTION_NODE
: 7,
2995 DOCUMENT_TYPE_NODE
: 10,
2996 DOCUMENT_FRAGMENT_NODE
: 11,
2999 * Gets the class names assigned to a node, reserved classes removed
3001 * @param object node: the node
3002 * @return array array of class names on the node, reserved classes removed
3004 getClassNames: function (node
) {
3005 var classNames
= [];
3007 if (node
.className
&& /\S/.test(node
.className
)) {
3008 classNames
= node
.className
.trim().split(' ');
3010 if (HTMLArea
.reservedClassNames
.test(node
.className
)) {
3011 var cleanClassNames
= [];
3013 for (var i
= 0; i
< classNames
.length
; ++i
) {
3014 if (!HTMLArea
.reservedClassNames
.test(classNames
[i
])) {
3015 cleanClassNames
[++j
] = classNames
[i
];
3018 classNames
= cleanClassNames
;
3024 * Check if a class name is in the class attribute of a node
3026 * @param object node: the node
3027 * @param string className: the class name to look for
3028 * @param boolean substring: if true, look for a class name starting with the given string
3029 * @return boolean true if the class name was found, false otherwise
3031 hasClass: function (node
, className
, substring
) {
3033 if (node
&& node
.className
) {
3034 var classes
= node
.className
.trim().split(' ');
3035 for (var i
= classes
.length
; --i
>= 0;) {
3036 found
= ((classes
[i
] == className
) || (substring
&& classes
[i
].indexOf(className
) == 0));
3045 * Add a class name to the class attribute of a node
3047 * @param object node: the node
3048 * @param string className: the name of the class to be added
3051 addClass: function (node
, className
) {
3053 HTMLArea
.DOM
.removeClass(node
, className
);
3054 // Remove classes configured to be incompatible with the class to be added
3055 if (node
.className
&& HTMLArea
.classesXOR
&& HTMLArea
.classesXOR
[className
] && Ext
.isFunction(HTMLArea
.classesXOR
[className
].test
)) {
3056 var classNames
= node
.className
.trim().split(' ');
3057 for (var i
= classNames
.length
; --i
>= 0;) {
3058 if (HTMLArea
.classesXOR
[className
].test(classNames
[i
])) {
3059 HTMLArea
.DOM
.removeClass(node
, classNames
[i
]);
3063 if (node
.className
) {
3064 node
.className
+= ' ' + className
;
3066 node
.className
= className
;
3071 * Remove a class name from the class attribute of a node
3073 * @param object node: the node
3074 * @param string className: the class name to removed
3075 * @param boolean substring: if true, remove the class names starting with the given string