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
815 if (!this.config
.showStatusBar
) {
816 this.addClass('noStatusBar');
820 * Initialize event listeners and the document after the iframe has rendered
822 initEventListeners: function () {
823 this.initStyleChangeEventListener();
825 this.mon(this.getEl(), 'load', this.initializeIframe
, this, {single
: true});
827 this.initializeIframe();
831 * The editor iframe may become hidden with style.display = "none" on some parent div
832 * This breaks the editor in Firefox: the designMode attribute needs to be reset after the style.display of the container div is reset to "block"
833 * In all browsers, it breaks the evaluation of the framework dimensions
835 initStyleChangeEventListener: function () {
836 if (this.isNested
&& !Ext
.isWebKit
) {
843 Ext
.each(this.nestedParentElements
.sorted
, function (nested
) {
845 options
.target
= Ext
.get(nested
);
849 Ext
.isIE
? 'propertychange' : 'DOMAttrModified',
858 * editorId should be set in config
862 * Get a reference to the editor
864 getEditor: function() {
865 return RTEarea
[this.editorId
].editor
;
868 * Get a reference to the toolbar
870 getToolbar: function () {
871 return this.ownerCt
.getTopToolbar();
874 * Get a reference to the statusBar
876 getStatusBar: function () {
877 return this.ownerCt
.getBottomToolbar();
880 * Get a reference to a button
882 getButton: function (buttonId
) {
883 return this.getToolbar().getButton(buttonId
);
886 * Flag set to true when the iframe becomes usable for editing
890 * Create the iframe element at rendering time
892 onRender: function (ct
, position
){
893 // from Ext.Component
894 if (!this.el
&& this.autoEl
) {
895 if (Ext
.isString(this.autoEl
)) {
896 this.el
= document
.createElement(this.autoEl
);
898 // ExtJS Default method will not work with iframe element
899 this.el
= Ext
.DomHelper
.append(ct
, this.autoEl
, true);
902 this.el
.id
= this.getId();
905 // from Ext.BoxComponent
907 this.resizeEl
= Ext
.get(this.resizeEl
);
909 if (this.positionEl
){
910 this.positionEl
= Ext
.get(this.positionEl
);
914 * Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
916 initializeIframe: function () {
917 var iframe
= this.getEl().dom
;
919 if (!iframe
|| (!iframe
.contentWindow
&& !iframe
.contentDocument
)) {
920 this.initializeIframe
.defer(50, this);
922 } else if (iframe
.contentWindow
&& !Ext
.isWebKit
&& (!iframe
.contentWindow
.document
|| !iframe
.contentWindow
.document
.documentElement
)) {
923 this.initializeIframe
.defer(50, this);
925 } else if (Ext
.isWebKit
&& (!iframe
.contentDocument
.documentElement
|| !iframe
.contentDocument
.body
)) {
926 this.initializeIframe
.defer(50, this);
928 this.document
= iframe
.contentWindow
? iframe
.contentWindow
.document
: iframe
.contentDocument
;
929 this.getEditor().document
= this.document
;
930 this.getEditor()._doc
= this.document
;
931 this.getEditor()._iframe
= iframe
;
933 // Style the document body
934 Ext
.get(this.document
.body
).addClass('htmlarea-content-body');
935 // Start listening to things happening in the iframe
936 // For some unknown reason, this is too early for Opera
938 this.startListening();
944 this.fireEvent('HTMLAreaEventIframeReady');
948 * Build the iframe document head
950 createHead: function () {
951 var head
= this.document
.getElementsByTagName('head')[0];
953 head
= this.document
.createElement('head');
954 this.document
.documentElement
.appendChild(head
);
956 if (this.config
.baseURL
) {
957 var base
= this.document
.getElementsByTagName('base')[0];
959 base
= this.document
.createElement('base');
960 base
.href
= this.config
.baseURL
;
961 head
.appendChild(base
);
963 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Iframe baseURL set to: ' + base
.href
, 'info');
965 var link0
= this.document
.getElementsByTagName('link')[0];
967 link0
= this.document
.createElement('link');
968 link0
.rel
= 'stylesheet';
969 // Firefox 3.0.1 does not apply the base URL while Firefox 3.6.8 does so. Do not know in what version this was fixed.
970 // Therefore, for versions before 3.6.8, we prepend the url with the base, if the url is not absolute
971 link0
.href
= ((Ext
.isGecko
&& navigator
.productSub
< 2010072200 && !/^http(s?):\/{2}/.test(this.config
.editedContentStyle
)) ? this.config
.baseURL
: '') + this.config
.editedContentStyle
;
972 head
.appendChild(link0
);
973 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Skin CSS set to: ' + link0
.href
, 'info');
975 if (this.config
.defaultPageStyle
) {
976 var link
= this.document
.getElementsByTagName('link')[1];
978 link
= this.document
.createElement('link');
979 link
.rel
= 'stylesheet';
980 link
.href
= ((Ext
.isGecko
&& navigator
.productSub
< 2010072200 && !/^https?:\/{2}/.test(this.config
.defaultPageStyle
)) ? this.config
.baseURL
: '') + this.config
.defaultPageStyle
;
981 head
.appendChild(link
);
983 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Override CSS set to: ' + link
.href
, 'info');
985 if (this.config
.pageStyle
) {
986 var link
= this.document
.getElementsByTagName('link')[2];
988 link
= this.document
.createElement('link');
989 link
.rel
= 'stylesheet';
990 link
.href
= ((Ext
.isGecko
&& navigator
.productSub
< 2010072200 && !/^https?:\/{2}/.test(this.config
.pageStyle
)) ? this.config
.baseURL
: '') + this.config
.pageStyle
;
991 head
.appendChild(link
);
993 this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link
.href
, 'info');
997 * Focus on the iframe
1002 this.getEl().dom
.focus();
1004 this.getEl().dom
.contentWindow
.focus();
1009 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1010 * Should be set in config
1014 * All nested tabs and inline levels in the sorting order they were applied
1015 * Should be set in config
1017 nestedParentElements
: {},
1021 * @param boolean on: if true set designMode to on, otherwise set to off
1025 setDesignMode: function (on
) {
1029 // In Firefox, we can't set designMode when we are in a hidden TYPO3 tab or inline element
1030 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1031 this.document
.designMode
= 'on';
1035 this.document
.designMode
= 'on';
1039 if (Ext
.isIE
|| Ext
.isWebKit
) {
1040 this.document
.body
.contentEditable
= true;
1044 this.document
.designMode
= 'off';
1046 if (Ext
.isIE
|| Ext
.isWebKit
) {
1047 this.document
.body
.contentEditable
= false;
1052 * Set editing mode options (if we can... raises exception in Firefox 3)
1056 setOptions: function () {
1059 if (this.document
.queryCommandEnabled('insertBrOnReturn')) {
1060 this.document
.execCommand('insertBrOnReturn', false, this.config
.disableEnterParagraphs
);
1062 if (this.document
.queryCommandEnabled('styleWithCSS')) {
1063 this.document
.execCommand('styleWithCSS', false, this.config
.useCSS
);
1064 } else if (Ext
.isGecko
&& this.document
.queryCommandEnabled('useCSS')) {
1065 this.document
.execCommand('useCSS', false, !this.config
.useCSS
);
1068 if (this.document
.queryCommandEnabled('enableObjectResizing')) {
1069 this.document
.execCommand('enableObjectResizing', false, !this.config
.disableObjectResizing
);
1071 if (this.document
.queryCommandEnabled('enableInlineTableEditing')) {
1072 this.document
.execCommand('enableInlineTableEditing', false, (this.config
.buttons
.table
&& this.config
.buttons
.table
.enableHandles
) ? true : false);
1079 * Handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
1081 onNestedShow: function (event
, target
) {
1082 var styleEvent
= true;
1083 // In older versions of Gecko attrName is not set and refering to it causes a non-catchable crash
1084 if ((Ext
.isGecko
&& navigator
.productSub
> 2007112700) || Ext
.isOpera
) {
1085 styleEvent
= (event
.browserEvent
.attrName
== 'style');
1086 } else if (Ext
.isIE
) {
1087 styleEvent
= (event
.browserEvent
.propertyName
== 'style.display');
1089 if (styleEvent
&& this.nestedParentElements
.sorted
.indexOf(target
.id
) != -1 && (target
.style
.display
== '' || target
.style
.display
== 'block')) {
1090 // Check if all container nested elements are displayed
1091 if (HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1092 if (this.getEditor().getMode() === 'wysiwyg') {
1094 this.setDesignMode(true);
1096 this.fireEvent('show');
1098 this.ownerCt
.textAreaContainer
.fireEvent('show');
1100 this.getToolbar().update();
1106 * Instance of DOM walker
1110 * Get the HTML content of the iframe
1112 getHTML: function () {
1113 return this.htmlRenderer
.render(this.document
.body
, false);
1116 * Start listening to things happening in the iframe
1118 startListening: function () {
1119 // Create keyMap so that plugins may bind key handlers
1120 this.keyMap
= new Ext
.KeyMap(Ext
.get(this.document
.documentElement
), [], (Ext
.isIE
|| Ext
.isWebKit
) ? 'keydown' : 'keypress');
1122 this.keyMap
.addBinding([
1124 key
: [Ext
.EventObject
.DOWN
, Ext
.EventObject
.UP
, Ext
.EventObject
.LEFT
, Ext
.EventObject
.RIGHT
],
1126 handler
: this.onArrow
,
1130 key
: Ext
.EventObject
.TAB
,
1133 handler
: this.onTab
,
1137 key
: Ext
.EventObject
.SPACE
,
1141 handler
: this.onCtrlSpace
,
1145 if (Ext
.isGecko
|| Ext
.isIE
) {
1146 this.keyMap
.addBinding(
1148 key
: [Ext
.EventObject
.BACKSPACE
, Ext
.EventObject
.DELETE
],
1150 handler
: this.onBackSpace
,
1154 if (!Ext
.isIE
&& !this.config
.disableEnterParagraphs
) {
1155 this.keyMap
.addBinding(
1157 key
: Ext
.EventObject
.ENTER
,
1159 handler
: this.onEnter
,
1164 this.keyMap
.addBinding(
1166 key
: Ext
.EventObject
.ENTER
,
1168 handler
: this.onWebKitEnter
,
1172 // Hot key map (on keydown for all browsers)
1174 Ext
.iterate(this.config
.hotKeyList
, function (key
) {
1175 if (key
.length
== 1) {
1176 hotKeys
+= key
.toUpperCase();
1179 // Make hot key map available, even if empty, so that plugins may add bindings
1180 this.hotKeyMap
= new Ext
.KeyMap(Ext
.get(this.document
.documentElement
));
1181 if (!Ext
.isEmpty(hotKeys
)) {
1182 this.hotKeyMap
.addBinding({
1187 handler
: this.onHotKey
,
1191 this.mon(Ext
.get(this.document
.documentElement
), (Ext
.isIE
|| Ext
.isWebKit
) ? 'keydown' : 'keypress', this.onAnyKey
, this);
1192 this.mon(Ext
.get(this.document
.documentElement
), 'mouseup', this.onMouse
, this);
1193 this.mon(Ext
.get(this.document
.documentElement
), 'click', this.onMouse
, this);
1194 this.mon(Ext
.get(this.document
.documentElement
), 'drop', this.onDrop
, this);
1196 this.mon(Ext
.get(this.document
.body
), 'dragend', this.onDrop
, this);
1200 * Handler for other key events
1202 onAnyKey: function(event
) {
1203 if (this.inhibitKeyboardInput(event
)) {
1206 this.fireEvent('HTMLAreaEventWordCountChange', 100);
1207 if (!event
.altKey
&& !event
.ctrlKey
) {
1208 // Detect URL in non-IE browsers
1209 if (!Ext
.isIE
&& (event
.getKey() != Ext
.EventObject
.ENTER
|| (event
.shiftKey
&& !Ext
.isWebKit
))) {
1210 this.getEditor()._detectURL(event
);
1212 // Handle option+SPACE for Mac users
1213 if (Ext
.isMac
&& event
.browserEvent
.charCode
== 160) {
1214 return this.onOptionSpace(event
.browserEvent
.charCode
, event
);
1220 * On any key input event, check if input is currently inhibited
1222 inhibitKeyboardInput: function (event
) {
1223 // Inhibit key events while server-based cleaning is being processed
1224 if (this.getEditor().inhibitKeyboardInput
) {
1232 * Handler for mouse events
1234 onMouse: function (event
, target
) {
1235 // In WebKit, select the image when it is clicked
1236 if (Ext
.isWebKit
&& /^(img)$/i.test(target
.nodeName
) && event
.browserEvent
.type
== 'click') {
1237 this.getEditor().selectNode(target
);
1239 this.getToolbar().updateLater
.delay(100);
1243 * Handlers for drag and drop operations
1245 onDrop: function (event
) {
1247 this.getEditor().cleanAppleStyleSpans
.defer(50, this.getEditor(), [this.getEditor().document
.body
]);
1249 this.getToolbar().updateLater
.delay(100);
1252 * Handler for UP, DOWN, LEFT and RIGHT keys
1254 onArrow: function () {
1255 this.getToolbar().updateLater
.delay(100);
1259 * Handler for TAB and SHIFT-TAB keys
1261 * If available, BlockElements plugin will handle the TAB key
1263 onTab: function (key
, event
) {
1264 if (this.inhibitKeyboardInput(event
)) {
1267 var keyName
= (event
.shiftKey
? 'SHIFT-' : '') + 'TAB';
1268 if (this.config
.hotKeyList
[keyName
] && this.config
.hotKeyList
[keyName
].cmd
) {
1269 var button
= this.getButton(this.config
.hotKeyList
[keyName
].cmd
);
1272 button
.fireEvent('HTMLAreaEventHotkey', keyName
, event
);
1279 * Handler for BACKSPACE and DELETE keys
1281 onBackSpace: function (key
, event
) {
1282 if (this.inhibitKeyboardInput(event
)) {
1285 if ((!Ext
.isIE
&& !event
.shiftKey
) || Ext
.isIE
) {
1286 if (this.getEditor()._checkBackspace()) {
1290 // Update the toolbar state after some time
1291 this.getToolbar().updateLater
.delay(200);
1295 * Handler for ENTER key in non-IE browsers
1297 onEnter: function (key
, event
) {
1298 if (this.inhibitKeyboardInput(event
)) {
1301 this.getEditor()._detectURL(event
);
1302 if (this.getEditor()._checkInsertP()) {
1305 // Update the toolbar state after some time
1306 this.getToolbar().updateLater
.delay(200);
1310 * Handler for ENTER key in WebKit browsers
1312 onWebKitEnter: function (key
, event
) {
1313 if (this.inhibitKeyboardInput(event
)) {
1316 if (event
.shiftKey
|| this.config
.disableEnterParagraphs
) {
1317 var editor
= this.getEditor();
1318 editor
._detectURL(event
);
1320 var brNode
= editor
.document
.createElement('br');
1321 editor
.insertNodeAtSelection(brNode
);
1322 brNode
.parentNode
.normalize();
1323 // Selection issue when an URL was detected
1324 if (editor
._unlinkOnUndo
) {
1325 brNode
= brNode
.parentNode
.parentNode
.insertBefore(brNode
, brNode
.parentNode
.nextSibling
);
1327 if (!brNode
.nextSibling
|| !/\S+/i.test(brNode
.nextSibling
.textContent
)) {
1328 var secondBrNode
= editor
.document
.createElement('br');
1329 secondBrNode
= brNode
.parentNode
.appendChild(secondBrNode
);
1331 editor
.selectNode(brNode
, false);
1335 // Update the toolbar state after some time
1336 this.getToolbar().updateLater
.delay(200);
1340 * Handler for CTRL-SPACE keys
1342 onCtrlSpace: function (key
, event
) {
1343 if (this.inhibitKeyboardInput(event
)) {
1346 this.getEditor().insertHTML(' ');
1351 * Handler for OPTION-SPACE keys on Mac
1353 onOptionSpace: function (key
, event
) {
1354 if (this.inhibitKeyboardInput(event
)) {
1357 this.getEditor().insertHTML(' ');
1362 * Handler for configured hotkeys
1364 onHotKey: function (key
, event
) {
1365 if (this.inhibitKeyboardInput(event
)) {
1368 var hotKey
= String
.fromCharCode(key
).toLowerCase();
1369 this.getButton(this.config
.hotKeyList
[hotKey
].cmd
).fireEvent('HTMLAreaEventHotkey', hotKey
, event
);
1375 onBeforeDestroy: function () {
1376 // ExtJS KeyMap object makes IE leak memory
1377 // Nullify EXTJS private handlers
1378 Ext
.each(this.keyMap
.bindings
, function (binding
, index
) {
1379 this.keyMap
.bindings
[index
] = null;
1381 this.keyMap
.handleKeyDown
= null;
1382 Ext
.each(this.hotKeyMap
.bindings
, function (binding
, index
) {
1383 this.hotKeyMap
.bindings
[index
] = null;
1385 this.hotKeyMap
.handleKeyDown
= null;
1386 this.keyMap
.disable();
1387 this.hotKeyMap
.disable();
1388 // Cleaning references to DOM in order to avoid IE memory leaks
1389 Ext
.get(this.document
.body
).purgeAllListeners();
1390 Ext
.get(this.document
.body
).dom
= null;
1391 Ext
.get(this.document
.documentElement
).purgeAllListeners();
1392 Ext
.get(this.document
.documentElement
).dom
= null;
1393 this.document
= null;
1394 this.getEditor().document
= null;
1395 this.getEditor()._doc
= null;
1396 this.getEditor()._iframe
= null;
1397 Ext
.each(this.nestedParentElements
.sorted
, function (nested
) {
1398 Ext
.get(nested
).purgeAllListeners();
1399 Ext
.get(nested
).dom
= null;
1401 Ext
.destroy(this.autoEl
, this.el
, this.resizeEl
, this.positionEl
);
1405 Ext
.reg('htmlareaiframe', HTMLArea
.Iframe
);
1407 * HTMLArea.StatusBar extends Ext.Container
1409 HTMLArea
.StatusBar
= Ext
.extend(Ext
.Container
, {
1413 initComponent: function () {
1414 HTMLArea
.StatusBar
.superclass
.initComponent
.call(this);
1415 // Build the deferred word count update task
1416 this.updateWordCountLater
= new Ext
.util
.DelayedTask(this.updateWordCount
, this);
1419 fn
: this.addComponents
,
1423 fn
: this.initEventListeners
,
1429 * Initialize listeners
1431 initEventListeners: function () {
1434 fn
: this.onBeforeDestroy
,
1438 // Monitor toolbar updates in order to refresh the contents of the statusbar
1439 // The toolbar must have been rendered
1440 this.mon(this.ownerCt
.toolbar
, 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar
, this);
1441 // Monitor editor changing mode
1442 this.mon(this.getEditor(), 'HTMLAreaEventModeChange', this.onModeChange
, this);
1443 // Monitor word count change
1444 this.mon(this.ownerCt
.iframe
, 'HTMLAreaEventWordCountChange', this.onWordCountChange
, this);
1447 * editorId should be set in config
1451 * Get a reference to the editor
1453 getEditor: function() {
1454 return RTEarea
[this.editorId
].editor
;
1457 * Create span elements to display when the status bar tree or a message when the editor is in text mode
1459 addComponents: function () {
1460 this.statusBarWordCount
= Ext
.DomHelper
.append(this.getEl(), {
1461 id
: this.editorId
+ '-statusBarWordCount',
1463 cls
: 'statusBarWordCount',
1466 this.statusBarTree
= Ext
.DomHelper
.append(this.getEl(), {
1467 id
: this.editorId
+ '-statusBarTree',
1469 cls
: 'statusBarTree',
1470 html
: HTMLArea
.localize('Path') + ': '
1471 }, true).setVisibilityMode(Ext
.Element
.DISPLAY
).setVisible(true);
1472 this.statusBarTextMode
= Ext
.DomHelper
.append(this.getEl(), {
1473 id
: this.editorId
+ '-statusBarTextMode',
1475 cls
: 'statusBarTextMode',
1476 html
: HTMLArea
.localize('TEXT_MODE')
1477 }, true).setVisibilityMode(Ext
.Element
.DISPLAY
).setVisible(false);
1480 * Clear the status bar tree
1482 clear: function () {
1483 this.statusBarTree
.removeAllListeners();
1484 Ext
.each(this.statusBarTree
.query('a'), function (node
) {
1485 Ext
.QuickTips
.unregister(node
);
1486 Ext
.get(node
).dom
.ancestor
= null;
1489 this.statusBarTree
.update('');
1490 this.setSelection(null);
1493 * Flag indicating that the status bar should not be updated on this toolbar update
1497 * Update the status bar
1499 onUpdateToolbar: function (mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
) {
1500 if (mode
=== 'wysiwyg' && !this.noUpdate
) {
1503 languageObject
= this.getEditor().getPlugin('Language'),
1504 classes
= new Array(),
1507 var path
= Ext
.DomHelper
.append(this.statusBarTree
, {
1509 html
: HTMLArea
.localize('Path') + ': '
1511 Ext
.each(ancestors
, function (ancestor
, index
) {
1515 text
= ancestor
.nodeName
.toLowerCase();
1516 // Do not show any id generated by ExtJS
1517 if (ancestor
.id
&& text
!== 'body' && ancestor
.id
.substr(0, 7) !== 'ext-gen') {
1518 text
+= '#' + ancestor
.id
;
1520 if (languageObject
&& languageObject
.getLanguageAttribute
) {
1521 language
= languageObject
.getLanguageAttribute(ancestor
);
1522 if (language
!= 'none') {
1523 text
+= '[' + language
+ ']';
1526 if (ancestor
.className
) {
1528 classes
= ancestor
.className
.trim().split(' ');
1529 for (var j
= 0, n
= classes
.length
; j
< n
; ++j
) {
1530 if (!HTMLArea
.reservedClassNames
.test(classes
[j
])) {
1531 classText
+= '.' + classes
[j
];
1536 var element
= Ext
.DomHelper
.insertAfter(path
, {
1539 'ext:qtitle': HTMLArea
.localize('statusBarStyle'),
1540 'ext:qtip': ancestor
.style
.cssText
.split(';').join('<br />'),
1543 // Ext.DomHelper does not honour the custom attribute
1544 element
.dom
.ancestor
= ancestor
;
1545 element
.on('click', this.onClick
, this);
1546 element
.on('mousedown', this.onClick
, this);
1548 element
.on('contextmenu', this.onContextMenu
, this);
1551 Ext
.DomHelper
.insertAfter(element
, {
1553 html
: String
.fromCharCode(0xbb)
1558 this.updateWordCount();
1559 this.noUpdate
= false;
1562 * Handler when the word count may have changed
1564 onWordCountChange: function(delay
) {
1565 this.updateWordCountLater
.delay(delay
? delay
: 0);
1568 * Update the word count
1570 updateWordCount: function() {
1572 if (this.getEditor().getMode() == 'wysiwyg') {
1573 // Get the html content
1574 var text
= this.getEditor().getHTML();
1575 if (!Ext
.isEmpty(text
)) {
1576 // Replace html tags with spaces
1577 text
= text
.replace(HTMLArea
.RE_htmlTag
, ' ');
1578 // Replace html space entities
1579 text
= text
.replace(/ | /gi, ' ');
1580 // Remove numbers and punctuation
1581 text
= text
.replace(HTMLArea
.RE_numberOrPunctuation
, '');
1582 // Get the number of word
1583 wordCount
= text
.split(/\S\s+/g).length
- 1;
1586 // Update the word count of the status bar
1587 this.statusBarWordCount
.dom
.innerHTML
= wordCount
? ( wordCount
+ ' ' + HTMLArea
.localize((wordCount
== 1) ? 'word' : 'words')) : ' ';
1590 * Adapt status bar to current editor mode
1592 * @param string mode: the mode to which the editor got switched to
1594 onModeChange: function (mode
) {
1597 this.statusBarTextMode
.setVisible(false);
1598 this.statusBarTree
.setVisible(true);
1602 this.statusBarTree
.setVisible(false);
1603 this.statusBarTextMode
.setVisible(true);
1608 * Refrence to the element last selected on the status bar
1612 * Get the status bar selection
1614 getSelection: function() {
1615 return this.selected
;
1618 * Set the status bar selection
1620 * @param object element: set the status bar selection to the given element
1622 setSelection: function(element
) {
1623 this.selected
= element
? element
: null;
1626 * Select the element that was clicked in the status bar and set the status bar selection
1628 selectElement: function (element
) {
1629 var editor
= this.getEditor();
1632 if (/^(img)$/i.test(element
.ancestor
.nodeName
)) {
1633 editor
.selectNode(element
.ancestor
);
1635 editor
.selectNodeContents(element
.ancestor
);
1638 if (/^(img|table)$/i.test(element
.ancestor
.nodeName
)) {
1639 var range
= editor
.document
.body
.createControlRange();
1640 range
.addElement(element
.ancestor
);
1643 editor
.selectNode(element
.ancestor
);
1646 this.setSelection(element
.ancestor
);
1647 this.noUpdate
= true;
1648 editor
.toolbar
.update();
1653 onClick: function (event
, element
) {
1654 this.selectElement(element
);
1659 * ContextMenu handler
1661 onContextMenu: function (event
, target
) {
1662 this.selectElement(target
);
1663 return this.getEditor().getPlugin('ContextMenu') ? this.getEditor().getPlugin('ContextMenu').show(event
, target
.ancestor
) : false;
1668 onBeforeDestroy: function() {
1670 this.removeAll(true);
1671 Ext
.destroy(this.statusBarTree
, this.statusBarTextMode
);
1675 Ext
.reg('htmlareastatusbar', HTMLArea
.StatusBar
);
1677 * HTMLArea.Framework extends Ext.Panel
1679 HTMLArea
.Framework
= Ext
.extend(Ext
.Panel
, {
1683 initComponent: function () {
1684 HTMLArea
.Framework
.superclass
.initComponent
.call(this);
1685 // Set some references
1686 this.toolbar
= this.getTopToolbar();
1687 this.statusBar
= this.getBottomToolbar();
1688 this.iframe
= this.getComponent('iframe');
1689 this.textAreaContainer
= this.getComponent('textAreaContainer');
1692 * @event HTMLAreaEventFrameworkReady
1693 * Fires when the iframe is ready and all components are rendered
1695 'HTMLAreaEventFrameworkReady'
1699 fn
: this.onBeforeDestroy
,
1703 // Monitor iframe becoming ready
1704 this.mon(this.iframe
, 'HTMLAreaEventIframeReady', this.onIframeReady
, this, {single
: true});
1705 // Let the framefork render itself, but it will fail to do so if inside a hidden tab or inline element
1706 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1707 this.render(this.textArea
.parent(), this.textArea
.id
);
1709 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1710 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
1711 // Walk through all nested tabs and inline levels to get correct sizes
1712 HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'args[0].render(args[0].textArea.parent(), args[0].textArea.id)', [this]);
1716 * Initiate events monitoring
1718 initEventListeners: function () {
1719 // Make the framework resizable, if configured by the user
1720 this.makeResizable();
1721 // Monitor textArea container becoming shown or hidden as it may change the height of the status bar
1722 this.mon(this.textAreaContainer
, 'show', this.resizable
? this.onTextAreaShow
: this.onWindowResize
, this);
1723 // Monitor iframe becoming shown or hidden as it may change the height of the status bar
1724 this.mon(this.iframe
, 'show', this.resizable
? this.onIframeShow
: this.onWindowResize
, this);
1725 // Monitor window resizing
1726 Ext
.EventManager
.onWindowResize(this.onWindowResize
, this);
1727 // If the textarea is inside a form, on reset, re-initialize the HTMLArea content and update the toolbar
1728 var form
= this.textArea
.dom
.form
;
1730 if (Ext
.isFunction(form
.onreset
)) {
1731 if (typeof(form
.htmlAreaPreviousOnReset
) == 'undefined') {
1732 form
.htmlAreaPreviousOnReset
= [];
1734 form
.htmlAreaPreviousOnReset
.push(form
.onreset
);
1736 this.mon(Ext
.get(form
), 'reset', this.onReset
, this);
1740 fn
: this.onFrameworkResize
1745 * editorId should be set in config
1749 * Get a reference to the editor
1751 getEditor: function() {
1752 return RTEarea
[this.editorId
].editor
;
1755 * Flag indicating whether the framework is inside a tab or inline element that may be hidden
1756 * Should be set in config
1760 * All nested tabs and inline levels in the sorting order they were applied
1761 * Should be set in config
1763 nestedParentElements
: {},
1765 * Flag set to true when the framework is ready
1769 * All nested tabs and inline levels in the sorting order they were applied
1770 * Should be set in config
1772 nestedParentElements
: {},
1774 * Whether the framework should be made resizable
1775 * May be set in config
1779 * Maximum height to which the framework may resized (in pixels)
1780 * May be set in config
1784 * Initial textArea dimensions
1785 * Should be set in config
1787 textAreaInitialSize
: {
1793 * doLayout will fail if inside a hidden tab or inline element
1795 doLayout: function () {
1796 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1797 HTMLArea
.Framework
.superclass
.doLayout
.call(this);
1799 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1800 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
1801 // Walk through all nested tabs and inline levels to get correct sizes
1802 HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'HTMLArea.Framework.superclass.doLayout.call(args[0])', [this]);
1806 * Make the framework resizable, if configured
1808 makeResizable: function () {
1809 if (this.resizable
) {
1810 this.addClass('resizable');
1811 this.resizer
= new Ext
.Resizable(this.getEl(), {
1813 maxHeight
: this.maxHeight
,
1816 this.resizer
.on('resize', this.onHtmlAreaResize
, this);
1820 * Resize the framework when the resizer handles are used
1822 onHtmlAreaResize: function (resizer
, width
, height
, event
) {
1823 // Set width first as it may change the height of the toolbar and of the statusBar
1824 this.setWidth(width
);
1825 // Set height of iframe and textarea
1826 this.iframe
.setHeight(this.getInnerHeight());
1827 this.textArea
.setSize(this.getInnerWidth(), this.getInnerHeight());
1830 * Size the iframe according to initial textarea size as set by Page and User TSConfig
1832 onWindowResize: function (width
, height
) {
1833 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1834 this.resizeFramework(width
, height
);
1836 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1837 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
1838 // Walk through all nested tabs and inline levels to get correct sizes
1839 HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'args[0].resizeFramework(args[1], args[2])', [this, width
, height
]);
1843 * Resize the framework to its initial size
1845 resizeFramework: function (width
, height
) {
1846 var frameworkHeight
= parseInt(this.textAreaInitialSize
.height
);
1847 if (this.textAreaInitialSize
.width
.indexOf('%') === -1) {
1848 // Width is specified in pixels
1849 var frameworkWidth
= parseInt(this.textAreaInitialSize
.width
) - this.getFrameWidth();
1851 // Width is specified in %
1852 if (Ext
.isNumber(width
)) {
1853 // Framework sizing on actual window resize
1854 var frameworkWidth
= parseInt(((width
- this.textAreaInitialSize
.wizardsWidth
- (this.fullScreen
? 10 : Ext
.getScrollBarWidth()) - this.getBox().x
- 15) * parseInt(this.textAreaInitialSize
.width
))/100);
1856 // Initial framework sizing
1857 var frameworkWidth
= parseInt(((HTMLArea
.util
.TYPO3
.getWindowSize().width
- this.textAreaInitialSize
.wizardsWidth
- (this.fullScreen
? 10 : Ext
.getScrollBarWidth()) - this.getBox().x
- 15) * parseInt(this.textAreaInitialSize
.width
))/100);
1860 if (this.resizable
) {
1861 this.resizer
.resizeTo(frameworkWidth
, frameworkHeight
);
1863 this.setSize(frameworkWidth
, frameworkHeight
);
1867 * Resize the framework components
1869 onFrameworkResize: function () {
1870 // For unknown reason, in Chrome 7, this following is the only way to set the height of the iframe
1872 this.iframe
.getResizeEl().dom
.setAttribute('style', 'width:' + this.getInnerWidth() + 'px; height:' + this.getInnerHeight() + 'px;');
1874 this.iframe
.setSize(this.getInnerWidth(), this.getInnerHeight());
1876 this.textArea
.setSize(this.getInnerWidth(), this.getInnerHeight());
1879 * Adjust the height to the changing size of the statusbar when the textarea is shown
1881 onTextAreaShow: function () {
1882 this.iframe
.setHeight(this.getInnerHeight());
1883 this.textArea
.setHeight(this.getInnerHeight());
1886 * Adjust the height to the changing size of the statusbar when the iframe is shown
1888 onIframeShow: function () {
1889 if (this.getInnerHeight() <= 0) {
1890 this.onWindowResize();
1892 // For unknown reason, in Chrome 7, this following is the only way to set the height of the iframe
1894 this.iframe
.getResizeEl().dom
.setAttribute('style', 'width:' + this.getInnerWidth() + 'px; height:' + this.getInnerHeight() + 'px;');
1896 this.iframe
.setHeight(this.getInnerHeight());
1898 this.textArea
.setHeight(this.getInnerHeight());
1902 * Calculate the height available for the editing iframe
1904 getInnerHeight: function () {
1905 return this.getSize().height
- this.toolbar
.getHeight() - this.statusBar
.getHeight() - 5;
1908 * Fire the editor when all components of the framework are rendered and ready
1910 onIframeReady: function () {
1911 this.ready
= this.rendered
&& this.toolbar
.rendered
&& this.statusBar
.rendered
&& this.textAreaContainer
.rendered
;
1913 this.initEventListeners();
1914 this.textAreaContainer
.show();
1915 if (!this.getEditor().config
.showStatusBar
) {
1916 this.statusBar
.hide();
1918 // Set the initial size of the framework
1919 this.onWindowResize();
1920 this.fireEvent('HTMLAreaEventFrameworkReady');
1922 this.onIframeReady
.defer(50, this);
1926 * Handler invoked if we are inside a form and the form is reset
1927 * On reset, re-initialize the HTMLArea content and update the toolbar
1929 onReset: function (event
) {
1930 this.getEditor().setHTML(this.textArea
.getValue());
1931 this.toolbar
.update();
1932 // Invoke previous reset handlers, if any
1933 var htmlAreaPreviousOnReset
= event
.getTarget().dom
.htmlAreaPreviousOnReset
;
1934 if (typeof(htmlAreaPreviousOnReset
) != 'undefined') {
1935 Ext
.each(htmlAreaPreviousOnReset
, function (onReset
) {
1942 * Cleanup on framework destruction
1944 onBeforeDestroy: function () {
1945 Ext
.EventManager
.removeResizeListener(this.onWindowResize
, this);
1946 // Cleaning references to DOM in order to avoid IE memory leaks
1947 var form
= this.textArea
.dom
.form
;
1949 form
.htmlAreaPreviousOnReset
= null;
1950 Ext
.get(form
).dom
= null;
1952 Ext
.getBody().dom
= null;
1953 // ExtJS is not releasing any resources when the iframe is unloaded
1954 this.toolbar
.destroy();
1955 this.statusBar
.destroy();
1956 this.removeAll(true);
1958 this.resizer
.destroy();
1963 Ext
.reg('htmlareaframework', HTMLArea
.Framework
);
1964 /***************************************************
1965 * HTMLArea.Editor extends Ext.util.Observable
1966 ***************************************************/
1967 HTMLArea
.Editor
= Ext
.extend(Ext
.util
.Observable
, {
1969 * HTMLArea.Editor constructor
1971 constructor: function (config
) {
1972 HTMLArea
.Editor
.superclass
.constructor.call(this, {});
1974 this.config
= config
;
1975 // Establish references to this editor
1976 this.editorId
= this.config
.editorId
;
1977 RTEarea
[this.editorId
].editor
= this;
1978 // Get textarea size and wizard context
1979 this.textArea
= Ext
.get(this.config
.id
);
1980 this.textAreaInitialSize
= {
1981 width
: this.config
.RTEWidthOverride
? this.config
.RTEWidthOverride
: this.textArea
.getStyle('width'),
1982 height
: this.config
.fullScreen
? HTMLArea
.util
.TYPO3
.getWindowSize().height
- 20 : this.textArea
.getStyle('height'),
1985 // TYPO3 Inline elements and tabs
1986 this.nestedParentElements
= {
1987 all
: this.config
.tceformsNested
,
1988 sorted
: HTMLArea
.util
.TYPO3
.simplifyNested(this.config
.tceformsNested
)
1990 this.isNested
= !Ext
.isEmpty(this.nestedParentElements
.sorted
);
1991 // If in BE, get width of wizards
1992 if (Ext
.get('typo3-docheader')) {
1993 this.wizards
= this.textArea
.parent().parent().next();
1995 if (!this.isNested
|| HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(this.nestedParentElements
.sorted
)) {
1996 this.textAreaInitialSize
.wizardsWidth
= this.wizards
.getWidth();
1998 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
1999 var parentElements
= [].concat(this.nestedParentElements
.sorted
);
2000 // Walk through all nested tabs and inline levels to get correct size
2001 this.textAreaInitialSize
.wizardsWidth
= HTMLArea
.util
.TYPO3
.accessParentElements(parentElements
, 'args[0].getWidth()', [this.wizards
]);
2003 // Hide the wizards so that they do not move around while the editor framework is being sized
2004 this.wizards
.hide();
2009 // Register the plugins included in the configuration
2010 Ext
.iterate(this.config
.plugin
, function (plugin
) {
2011 if (this.config
.plugin
[plugin
]) {
2012 this.registerPlugin(plugin
);
2015 // Create Ajax object
2016 this.ajax
= new HTMLArea
.Ajax({
2019 // Initialize keyboard input inhibit flag
2020 this.inhibitKeyboardInput
= false;
2023 * @event HTMLAreaEventEditorReady
2024 * Fires when initialization of the editor is complete
2026 'HTMLAreaEventEditorReady',
2028 * @event HTMLAreaEventModeChange
2029 * Fires when the editor changes mode
2031 'HTMLAreaEventModeChange'
2035 * Flag set to true when the editor initialization has completed
2039 * The current mode of the editor: 'wysiwyg' or 'textmode'
2043 * Create the htmlArea framework
2045 generate: function () {
2046 // Create the editor framework
2047 this.htmlArea
= new HTMLArea
.Framework({
2048 id
: this.editorId
+ '-htmlArea',
2050 baseCls
: 'htmlarea',
2051 editorId
: this.editorId
,
2052 textArea
: this.textArea
,
2053 textAreaInitialSize
: this.textAreaInitialSize
,
2054 fullScreen
: this.config
.fullScreen
,
2055 resizable
: this.config
.resizable
,
2056 maxHeight
: this.config
.maxHeight
,
2057 isNested
: this.isNested
,
2058 nestedParentElements
: this.nestedParentElements
,
2061 xtype
: 'htmlareatoolbar',
2062 id
: this.editorId
+ '-toolbar',
2066 editorId
: this.editorId
2070 xtype
: 'htmlareaiframe',
2073 width
: (this.textAreaInitialSize
.width
.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize
.width
) : 300,
2074 height
: parseInt(this.textAreaInitialSize
.height
),
2076 id
: this.editorId
+ '-iframe',
2078 cls
: 'editorIframe',
2079 src
: (Ext
.isGecko
|| Ext
.isChrome
) ? 'javascript:void(0);' : HTMLArea
.editorUrl
+ 'popups/blank.html'
2081 isNested
: this.isNested
,
2082 nestedParentElements
: this.nestedParentElements
,
2083 editorId
: this.editorId
2085 // Box container for the textarea
2087 itemId
: 'textAreaContainer',
2089 width
: (this.textAreaInitialSize
.width
.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize
.width
) : 300,
2090 // Let the framework swallow the textarea and throw it back
2093 fn: function (textAreaContainer
) {
2094 this.originalParent
= this.textArea
.parent().dom
;
2095 textAreaContainer
.getEl().appendChild(this.textArea
);
2101 fn: function (textAreaContainer
) {
2102 this.originalParent
.appendChild(this.textArea
.dom
);
2113 xtype
: 'htmlareastatusbar',
2116 editorId
: this.editorId
2119 // Set some references
2120 this.toolbar
= this.htmlArea
.getTopToolbar();
2121 this.statusBar
= this.htmlArea
.getBottomToolbar();
2122 this.iframe
= this.htmlArea
.getComponent('iframe');
2123 this.textAreaContainer
= this.htmlArea
.getComponent('textAreaContainer');
2124 // Get triggered when the framework becomes ready
2125 this.relayEvents(this.htmlArea
, ['HTMLAreaEventFrameworkReady']);
2126 this.on('HTMLAreaEventFrameworkReady', this.onFrameworkReady
, this, {single
: true});
2129 * Initialize the editor
2131 onFrameworkReady: function () {
2132 // Initialize editor mode
2133 this.setMode('wysiwyg');
2134 // Initiate events listening
2135 this.initEventsListening();
2137 this.generatePlugins();
2138 // Make the editor visible
2140 // Make the wizards visible again
2142 this.wizards
.show();
2144 // Focus on the first editor that is not hidden
2145 Ext
.iterate(RTEarea
, function (editorId
, RTE
) {
2146 if (!Ext
.isDefined(RTE
.editor
) || (RTE
.editor
.isNested
&& !HTMLArea
.util
.TYPO3
.allElementsAreDisplayed(RTE
.editor
.nestedParentElements
.sorted
))) {
2154 this.fireEvent('HTMLAreaEventEditorReady');
2155 this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
2160 * @param string mode: 'textmode' or 'wysiwyg'
2164 setMode: function (mode
) {
2167 this.textArea
.set({ value
: this.getHTML() }, false);
2168 this.iframe
.setDesignMode(false);
2170 this.textAreaContainer
.show();
2175 this.document
.body
.innerHTML
= this.getHTML();
2177 this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
2178 TYPO3
.Dialog
.ErrorDialog({
2179 title
: 'htmlArea RTE',
2180 msg
: HTMLArea
.localize('HTML-document-not-well-formed')
2184 this.textAreaContainer
.hide();
2186 this.iframe
.setDesignMode(true);
2190 this.fireEvent('HTMLAreaEventModeChange', this.mode
);
2192 Ext
.iterate(this.plugins
, function(pluginId
) {
2193 this.getPlugin(pluginId
).onMode(this.mode
);
2197 * Get current editor mode
2199 getMode: function () {
2204 * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
2206 * @return string the textual html content from the current editing mode
2208 getHTML: function () {
2209 switch (this.mode
) {
2211 return this.iframe
.getHTML();
2213 return this.textArea
.getValue();
2221 * @return string the textual html content from the current editing mode
2223 getInnerHTML: function () {
2224 switch (this.mode
) {
2226 return this.document
.body
.innerHTML
;
2228 return this.textArea
.getValue();
2234 * Replace the html content
2236 * @param string html: the textual html
2240 setHTML: function (html
) {
2241 switch (this.mode
) {
2243 this.document
.body
.innerHTML
= html
;
2246 this.textArea
.set({ value
: html
}, false);;
2251 * Instantiate the specified plugin and register it with the editor
2253 * @param string plugin: the name of the plugin
2255 * @return boolean true if the plugin was successfully registered
2257 registerPlugin: function (pluginName
) {
2258 var plugin
= HTMLArea
[pluginName
],
2259 isRegistered
= false;
2260 if (typeof(plugin
) !== 'undefined' && Ext
.isFunction(plugin
)) {
2261 var pluginInstance
= new plugin(this, pluginName
);
2262 if (pluginInstance
) {
2263 var pluginInformation
= pluginInstance
.getPluginInformation();
2264 pluginInformation
.instance
= pluginInstance
;
2265 this.plugins
[pluginName
] = pluginInformation
;
2266 isRegistered
= true;
2269 if (!isRegistered
) {
2270 this.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName
+ '.', 'warn');
2272 return isRegistered
;
2275 * Generate registered plugins
2277 generatePlugins: function () {
2278 Ext
.iterate(this.plugins
, function (pluginId
) {
2279 var plugin
= this.getPlugin(pluginId
);
2280 plugin
.onGenerate();
2284 * Get the instance of the specified plugin, if it exists
2286 * @param string pluginName: the name of the plugin
2287 * @return object the plugin instance or null
2289 getPlugin: function(pluginName
) {
2290 return (this.plugins
[pluginName
] ? this.plugins
[pluginName
].instance
: null);
2293 * Unregister the instance of the specified plugin
2295 * @param string pluginName: the name of the plugin
2298 unRegisterPlugin: function(pluginName
) {
2299 delete this.plugins
[pluginName
].instance
;
2300 delete this.plugins
[pluginName
];
2303 * Focus on the editor
2305 focus: function () {
2306 switch (this.getMode()) {
2308 this.iframe
.focus();
2311 this.textArea
.focus();
2318 initEventsListening: function () {
2320 this.iframe
.startListening();
2322 // Add unload handler
2323 var iframe
= this.iframe
.getEl().dom
;
2324 Ext
.EventManager
.on(iframe
.contentWindow
? iframe
.contentWindow
: iframe
.contentDocument
, 'unload', this.onUnload
, this, {single
: true});
2327 * Make the editor framework visible
2330 document
.getElementById('pleasewait' + this.editorId
).style
.display
= 'none';
2331 document
.getElementById('editorWrap' + this.editorId
).style
.visibility
= 'visible';
2334 * Append an entry at the end of the troubleshooting log
2336 * @param string functionName: the name of the editor function writing to the log
2337 * @param string text: the text of the message
2338 * @param string type: the type of message
2342 appendToLog: function (objectName
, functionName
, text
, type
) {
2343 HTMLArea
.appendToLog(this.editorId
, objectName
, functionName
, text
, type
);
2346 * Iframe unload handler: Update the textarea for submission and cleanup
2348 onUnload: function (event
) {
2349 // Save the HTML content into the original textarea for submit, back/forward, etc.
2352 value
: this.getHTML()
2356 Ext
.TaskMgr
.stopAll();
2357 // ExtJS is not releasing any resources when the iframe is unloaded
2358 this.htmlArea
.destroy();
2359 Ext
.iterate(this.plugins
, function (pluginId
) {
2360 this.unRegisterPlugin(pluginId
);
2362 this.purgeListeners();
2363 // Cleaning references to DOM in order to avoid IE memory leaks
2365 this.wizards
.dom
= null;
2366 this.textArea
.parent().parent().dom
= null;
2367 this.textArea
.parent().dom
= null;
2369 this.textArea
.dom
= null;
2370 RTEarea
[this.editorId
].editor
= null;
2373 HTMLArea
.Ajax = function (config
) {
2374 Ext
.apply(this, config
);
2376 HTMLArea
.Ajax
= Ext
.extend(HTMLArea
.Ajax
, {
2378 * Load a Javascript file asynchronously
2380 * @param string url: url of the file to load
2381 * @param function callBack: the callBack function
2382 * @param object scope: scope of the callbacks
2384 * @return boolean true on success of the request submission
2386 getJavascriptFile: function (url
, callback
, scope
) {
2387 var success
= false;
2393 success: function (response
) {
2396 failure: function (response
) {
2397 self
.editor
.inhibitKeyboardInput
= false;
2398 self
.editor
.appendToLog('HTMLArea.Ajax', 'getJavascriptFile', 'Unable to get ' + url
+ ' . Server reported ' + response
.status
, 'error');
2405 * Post data to the server
2407 * @param string url: url to post data to
2408 * @param object data: data to be posted
2409 * @param function callback: function that will handle the response returned by the server
2410 * @param object scope: scope of the callbacks
2412 * @return boolean true on success
2414 postData: function (url
, data
, callback
, scope
) {
2415 var success
= false;
2417 data
.charset
= this.editor
.config
.typo3ContentCharset
? this.editor
.config
.typo3ContentCharset
: 'utf-8';
2419 Ext
.iterate(data
, function (parameter
, value
) {
2420 params
+= (params
.length
? '&' : '') + parameter
+ '=' + encodeURIComponent(value
);
2422 params
+= this.editor
.config
.RTEtsConfigParams
;
2426 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
2430 callback
: Ext
.isFunction(callback
) ? callback: function (options
, success
, response
) {
2432 self
.editor
.appendToLog('HTMLArea.Ajax', 'postData', 'Post request to ' + url
+ ' failed. Server reported ' + response
.status
, 'error');
2435 success: function (response
) {
2438 failure: function (response
) {
2439 self
.editor
.appendToLog('HTMLArea.Ajax', 'postData', 'Unable to post ' + url
+ ' . Server reported ' + response
.status
, 'error');
2446 /***************************************************
2447 * HTMLArea.util.TYPO3: Utility functions for dealing with tabs and inline elements in TYPO3 forms
2448 ***************************************************/
2449 HTMLArea
.util
.TYPO3 = function () {
2452 * Simplify the array of nested levels. Create an indexed array with the correct names of the elements.
2454 * @param object nested: The array with the nested levels
2455 * @return object The simplified array
2456 * @author Oliver Hader <oh@inpublica.de>
2458 simplifyNested: function(nested
) {
2459 var i
, type
, level
, elementId
, max
, simplifiedNested
=[],
2465 if (nested
&& nested
.length
) {
2466 if (nested
[0][0]=='inline') {
2467 nested
= inline
.findContinuedNestedLevel(nested
, nested
[0][1]);
2469 for (i
=0, max
=nested
.length
; i
<max
; i
++) {
2470 type
= nested
[i
][0];
2471 level
= nested
[i
][1];
2472 elementId
= level
+ elementIdSuffix
[type
];
2473 if (Ext
.get(elementId
)) {
2474 simplifiedNested
.push(elementId
);
2478 return simplifiedNested
;
2481 * Access an inline relational element or tab menu and make it "accessible".
2482 * If a parent or ancestor object has the style "display: none", offsetWidth & offsetHeight are '0'.
2484 * @params arry parentElements: array of parent elements id's; note that this input array will be modified
2485 * @params object callbackFunc: A function to be called, when the embedded objects are "accessible".
2486 * @params array args: array of arguments
2487 * @return object An object returned by the callbackFunc.
2488 * @author Oliver Hader <oh@inpublica.de>
2490 accessParentElements: function (parentElements
, callbackFunc
, args
) {
2492 if (parentElements
.length
) {
2493 var currentElement
= parentElements
.pop();
2494 currentElement
= Ext
.get(currentElement
);
2495 var actionRequired
= (currentElement
.getStyle('display') == 'none');
2496 if (actionRequired
) {
2497 var originalStyles
= currentElement
.getStyles('visibility', 'position', 'top', 'display');
2498 currentElement
.setStyle({
2499 visibility
: 'hidden',
2500 position
: 'absolute',
2505 result
= this.accessParentElements(parentElements
, callbackFunc
, args
);
2506 if (actionRequired
) {
2507 currentElement
.setStyle(originalStyles
);
2510 result
= eval(callbackFunc
);
2515 * Check if all elements in input array are currently displayed
2517 * @param array elements: array of element id's
2518 * @return boolean true if all elements are displayed
2520 allElementsAreDisplayed: function(elements
) {
2521 var allDisplayed
= true;
2522 Ext
.each(elements
, function (element
) {
2523 allDisplayed
= Ext
.get(element
).getStyle('display') != 'none';
2524 return allDisplayed
;
2526 return allDisplayed
;
2529 * Get current size of window
2531 * @return object width and height of window
2533 getWindowSize: function () {
2535 var size
= Ext
.getBody().getSize();
2538 width
: window
.innerWidth
,
2539 height
: window
.innerHeight
2542 // Subtract the docheader height from the calculated window height
2543 var docHeader
= Ext
.get('typo3-docheader');
2545 size
.height
-= docHeader
.getHeight();
2546 docHeader
.dom
= null;
2552 /***************************************************
2554 ***************************************************/
2555 HTMLArea
.getInnerText = function(el
) {
2558 for(i
=el
.firstChild
;i
;i
=i
.nextSibling
) {
2559 if(i
.nodeType
== 3) txt
+= i
.data
;
2560 else if(i
.nodeType
== 1) txt
+= HTMLArea
.getInnerText(i
);
2563 if(el
.nodeType
== 3) txt
= el
.data
;
2568 HTMLArea
.Editor
.prototype.forceRedraw = function() {
2569 this.htmlArea
.doLayout();
2572 HTMLArea
.Editor
.prototype.updateToolbar = function(noStatus
) {
2573 this.toolbar
.update(noStatus
);
2575 /***************************************************
2576 * DOM TREE MANIPULATION
2577 ***************************************************/
2580 * Surround the currently selected HTML source code with the given tags.
2581 * Delete the selection, if any.
2583 HTMLArea
.Editor
.prototype.surroundHTML = function(startTag
,endTag
) {
2584 this.insertHTML(startTag
+ this.getSelectedHTML().replace(HTMLArea
.Reg_body
, "") + endTag
);
2588 * Change the tag name of a node.
2590 HTMLArea
.Editor
.prototype.convertNode = function(el
,newTagName
) {
2591 var newel
= this.document
.createElement(newTagName
), p
= el
.parentNode
;
2592 while (el
.firstChild
) newel
.appendChild(el
.firstChild
);
2593 p
.insertBefore(newel
, el
);
2599 * Find a parent of an element with a specified tag
2601 HTMLArea
.getElementObject = function(el
,tagName
) {
2603 while (oEl
!= null && oEl
.nodeName
.toLowerCase() != tagName
) oEl
= oEl
.parentNode
;
2608 * This function removes the given markup element
2610 * @param object element: the inline element to be removed, content being preserved
2614 HTMLArea
.Editor
.prototype.removeMarkup = function(element
) {
2615 var bookmark
= this.getBookmark(this._createRange(this._getSelection()));
2616 var parent
= element
.parentNode
;
2617 while (element
.firstChild
) {
2618 parent
.insertBefore(element
.firstChild
, element
);
2620 parent
.removeChild(element
);
2621 this.selectRange(this.moveToBookmark(bookmark
));
2625 * This function verifies if the element has any allowed attributes
2627 * @param object element: the DOM element
2628 * @param array allowedAttributes: array of allowed attribute names
2630 * @return boolean true if the element has one of the allowed attributes
2632 HTMLArea
.hasAllowedAttributes = function(element
,allowedAttributes
) {
2634 for (var i
= allowedAttributes
.length
; --i
>= 0;) {
2635 value
= element
.getAttribute(allowedAttributes
[i
]);
2637 if (allowedAttributes
[i
] == "style" && element
.style
.cssText
) {
2647 /***************************************************
2648 * SELECTIONS AND RANGES
2649 ***************************************************/
2652 * Return true if we have some selected content
2654 HTMLArea
.Editor
.prototype.hasSelectedText = function() {
2655 return this.getSelectedHTML() != "";
2659 * Get an array with all the ancestor nodes of the selection.
2661 HTMLArea
.Editor
.prototype.getAllAncestors = function() {
2662 var p
= this.getParentElement();
2664 while (p
&& (p
.nodeType
=== 1) && (p
.nodeName
.toLowerCase() !== "body")) {
2668 a
.push(this.document
.body
);
2673 * Get the block ancestors of an element within a given block
2675 HTMLArea
.Editor
.prototype.getBlockAncestors = function(element
, withinBlock
) {
2676 var ancestors
= new Array();
2677 var ancestor
= element
;
2678 while (ancestor
&& (ancestor
.nodeType
=== 1) && !/^(body)$/i.test(ancestor
.nodeName
) && ancestor
!= withinBlock
) {
2679 if (HTMLArea
.isBlockElement(ancestor
)) {
2680 ancestors
.unshift(ancestor
);
2682 ancestor
= ancestor
.parentNode
;
2684 ancestors
.unshift(ancestor
);
2689 * Get the block elements containing the start and the end points of the selection
2691 HTMLArea
.Editor
.prototype.getEndBlocks = function(selection
) {
2692 var range
= this._createRange(selection
);
2694 var parentStart
= range
.startContainer
;
2695 if (/^(body)$/i.test(parentStart
.nodeName
)) {
2696 parentStart
= parentStart
.firstChild
;
2698 var parentEnd
= range
.endContainer
;
2699 if (/^(body)$/i.test(parentEnd
.nodeName
)) {
2700 parentEnd
= parentEnd
.lastChild
;
2703 if (selection
.type
!== "Control" ) {
2704 var rangeEnd
= range
.duplicate();
2705 range
.collapse(true);
2706 var parentStart
= range
.parentElement();
2707 rangeEnd
.collapse(false);
2708 var parentEnd
= rangeEnd
.parentElement();
2710 var parentStart
= range
.item(0);
2711 var parentEnd
= parentStart
;
2714 while (parentStart
&& !HTMLArea
.isBlockElement(parentStart
)) {
2715 parentStart
= parentStart
.parentNode
;
2717 while (parentEnd
&& !HTMLArea
.isBlockElement(parentEnd
)) {
2718 parentEnd
= parentEnd
.parentNode
;
2720 return { start
: parentStart
,
2726 * This function determines if the end poins of the current selection are within the same block
2728 * @return boolean true if the end points of the current selection are inside the same block element
2730 HTMLArea
.Editor
.prototype.endPointsInSameBlock = function() {
2731 var selection
= this._getSelection();
2732 if (this._selectionEmpty(selection
)) {
2735 var parent
= this.getParentElement(selection
);
2736 var endBlocks
= this.getEndBlocks(selection
);
2737 return (endBlocks
.start
=== endBlocks
.end
&& !/^(table|thead|tbody|tfoot|tr)$/i.test(parent
.nodeName
));
2742 * Get the deepest ancestor of the selection that is of the specified type
2743 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2745 HTMLArea
.Editor
.prototype._getFirstAncestor = function(sel
,types
) {
2746 var prnt
= this._activeElement(sel
);
2749 prnt
= (Ext
.isIE
? this._createRange(sel
).parentElement() : this._createRange(sel
).commonAncestorContainer
);
2754 if (typeof(types
) == 'string') types
= [types
];
2757 if (prnt
.nodeType
== 1) {
2758 if (types
== null) return prnt
;
2759 for (var i
= 0; i
< types
.length
; i
++) {
2760 if(prnt
.tagName
.toLowerCase() == types
[i
]) return prnt
;
2762 if(prnt
.tagName
.toLowerCase() == 'body') break;
2763 if(prnt
.tagName
.toLowerCase() == 'table') break;
2765 prnt
= prnt
.parentNode
;
2770 * Get the node whose contents are currently fully selected
2772 * @param array selection: the current selection
2773 * @param array range: the range of the current selection
2774 * @param array ancestors: the array of ancestors node of the current selection
2776 * @return object the fully selected node, if any, null otherwise
2778 HTMLArea
.Editor
.prototype.getFullySelectedNode = function (selection
, range
, ancestors
) {
2779 var node
, fullNodeSelected
= false;
2781 var selection
= this._getSelection();
2783 if (!this._selectionEmpty(selection
)) {
2785 var range
= this._createRange(selection
);
2788 var ancestors
= this.getAllAncestors();
2790 Ext
.each(ancestors
, function (ancestor
) {
2792 fullNodeSelected
= (selection
.type
!== 'Control' && ancestor
.innerText
== range
.text
) || (selection
.type
=== 'Control' && ancestor
.innerText
== range
.item(0).text
);
2794 fullNodeSelected
= (ancestor
.textContent
== range
.toString());
2796 if (fullNodeSelected
) {
2801 // Working around bug with WebKit selection
2802 if (Ext
.isWebKit
&& !fullNodeSelected
) {
2803 var statusBarSelection
= this.statusBar
? this.statusBar
.getSelection() : null;
2804 if (statusBarSelection
&& statusBarSelection
.textContent
== range
.toString()) {
2805 fullNodeSelected
= true;
2806 node
= statusBarSelection
;
2810 return fullNodeSelected
? node
: null;
2812 /***************************************************
2813 * Category: EVENT HANDLERS
2814 ***************************************************/
2817 * Intercept some native execCommand commands
2819 HTMLArea
.Editor
.prototype.execCommand = function(cmdID
, UI
, param
) {
2824 this.document
.execCommand(cmdID
, UI
, param
);
2826 this.appendToLog('HTMLArea.Editor', 'execCommand', e
+ ' by execCommand(' + cmdID
+ ')', 'error');
2829 this.toolbar
.update();
2833 HTMLArea
.Editor
.prototype.scrollToCaret = function() {
2835 var e
= this.getParentElement(),
2836 w
= this._iframe
.contentWindow
? this._iframe
.contentWindow
: window
,
2837 h
= w
.innerHeight
|| w
.height
,
2839 t
= d
.documentElement
.scrollTop
|| d
.body
.scrollTop
;
2840 if (e
.offsetTop
> h
+t
|| e
.offsetTop
< t
) {
2841 this.getParentElement().scrollIntoView();
2845 /***************************************************
2847 ***************************************************/
2850 * Check if the client agent is supported
2852 HTMLArea
.checkSupportedBrowser = function() {
2853 return Ext
.isGecko
|| Ext
.isWebKit
|| Ext
.isOpera
|| Ext
.isIE
;
2856 * Remove a class name from the class attribute of an element
2858 * @param object el: the element
2859 * @param string className: the class name to remove
2860 * @param boolean substring: if true, remove the first class name starting with the given string
2862 ***********************************************
2863 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2864 ***********************************************
2866 HTMLArea
._removeClass = function(el
, className
, substring
) {
2867 HTMLArea
.DOM
.removeClass(el
, className
, substring
);
2870 * Add a class name to the class attribute
2871 ***********************************************
2872 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2873 ***********************************************
2875 HTMLArea
._addClass = function(el
, className
) {
2876 HTMLArea
.DOM
.addClass(el
, className
);
2879 * Check if a class name is in the class attribute of an element
2881 * @param object el: the element
2882 * @param string className: the class name to look for
2883 * @param boolean substring: if true, look for a class name starting with the given string
2884 * @return boolean true if the class name was found
2885 ***********************************************
2886 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2887 ***********************************************
2889 HTMLArea
._hasClass = function(el
, className
, substring
) {
2890 return HTMLArea
.DOM
.hasClass(el
, className
, substring
);
2893 HTMLArea
.isBlockElement = function(el
) { return el
&& el
.nodeType
== 1 && HTMLArea
.RE_blockTags
.test(el
.nodeName
.toLowerCase()); };
2894 HTMLArea
.needsClosingTag = function(el
) { return el
&& el
.nodeType
== 1 && !HTMLArea
.RE_noClosingTag
.test(el
.tagName
.toLowerCase()); };
2897 * Perform HTML encoding of some given string
2898 * Borrowed in part from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
2900 HTMLArea
.htmlDecode = function(str
) {
2901 str
= str
.replace(/</g, "<").replace(/>/g, ">");
2902 str
= str
.replace(/ /g, "\xA0"); // Decimal 160, non-breaking-space
2903 str
= str
.replace(/"/g, "\x22");
2904 str
= str
.replace(/'/g, "'") ;
2905 str
= str
.replace(/&/g, "&");
2908 HTMLArea
.htmlEncode = function(str
) {
2909 if (typeof(str
) != 'string') str
= str
.toString(); // we don't need regexp for that, but.. so be it for now.
2910 str
= str
.replace(/&/g
, "&");
2911 str
= str
.replace(/</g, "<").replace(/>/g
, ">");
2912 str
= str
.replace(/\xA0/g, " "); // Decimal 160, non-breaking-space
2913 str
= str
.replace(/\x22/g, """); // \x22 means '"'
2917 * Retrieve the HTML code from the given node.
2918 * This is a replacement for getting innerHTML, using standard DOM calls.
2919 * Wrapper catches a Mozilla-Exception with non well-formed html source code.
2920 ***********************************************
2921 * THIS FUNCTION IS DEPRECATED AS OF TYPO3 4.5 *
2922 ***********************************************
2924 HTMLArea
.getHTML = function(root
, outputRoot
, editor
){
2926 return editor
.iframe
.htmlRenderer
.render(root
, outputRoot
);
2928 editor
.appendToLog('HTMLArea', 'getHTML', 'The HTML document is not well-formed.', 'warn');
2929 TYPO3
.Dialog
.ErrorDialog({
2930 title
: 'htmlArea RTE',
2931 msg
: HTMLArea
.localize('HTML-document-not-well-formed')
2933 return editor
.document
.body
.innerHTML
;
2936 HTMLArea
.getPrevNode = function(node
) {
2937 if(!node
) return null;
2938 if(node
.previousSibling
) return node
.previousSibling
;
2939 if(node
.parentNode
) return node
.parentNode
;
2943 HTMLArea
.getNextNode = function(node
) {
2944 if(!node
) return null;
2945 if(node
.nextSibling
) return node
.nextSibling
;
2946 if(node
.parentNode
) return node
.parentNode
;
2950 HTMLArea
.removeFromParent = function(el
) {
2951 if(!el
.parentNode
) return;
2952 var pN
= el
.parentNode
;
2956 /*****************************************************************
2957 * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
2958 *****************************************************************/
2959 HTMLArea
.DOM = function () {
2961 /***************************************************
2962 * DOM-RELATED CONSTANTS
2963 ***************************************************/
2968 CDATA_SECTION_NODE
: 4,
2969 ENTITY_REFERENCE_NODE
: 5,
2971 PROCESSING_INSTRUCTION_NODE
: 7,
2974 DOCUMENT_TYPE_NODE
: 10,
2975 DOCUMENT_FRAGMENT_NODE
: 11,
2978 * Gets the class names assigned to a node, reserved classes removed
2980 * @param object node: the node
2981 * @return array array of class names on the node, reserved classes removed
2983 getClassNames: function (node
) {
2984 var classNames
= [];
2986 if (node
.className
&& /\S/.test(node
.className
)) {
2987 classNames
= node
.className
.trim().split(' ');
2989 if (HTMLArea
.reservedClassNames
.test(node
.className
)) {
2990 var cleanClassNames
= [];
2992 for (var i
= 0; i
< classNames
.length
; ++i
) {
2993 if (!HTMLArea
.reservedClassNames
.test(classNames
[i
])) {
2994 cleanClassNames
[++j
] = classNames
[i
];
2997 classNames
= cleanClassNames
;
3003 * Check if a class name is in the class attribute of a node
3005 * @param object node: the node
3006 * @param string className: the class name to look for
3007 * @param boolean substring: if true, look for a class name starting with the given string
3008 * @return boolean true if the class name was found, false otherwise
3010 hasClass: function (node
, className
, substring
) {
3012 if (node
&& node
.className
) {
3013 var classes
= node
.className
.trim().split(' ');
3014 for (var i
= classes
.length
; --i
>= 0;) {
3015 found
= ((classes
[i
] == className
) || (substring
&& classes
[i
].indexOf(className
) == 0));
3024 * Add a class name to the class attribute of a node
3026 * @param object node: the node
3027 * @param string className: the name of the class to be added
3030 addClass: function (node
, className
) {
3032 HTMLArea
.DOM
.removeClass(node
, className
);
3033 // Remove classes configured to be incompatible with the class to be added
3034 if (node
.className
&& HTMLArea
.classesXOR
&& HTMLArea
.classesXOR
[className
] && Ext
.isFunction(HTMLArea
.classesXOR
[className
].test
)) {
3035 var classNames
= node
.className
.trim().split(' ');
3036 for (var i
= classNames
.length
; --i
>= 0;) {
3037 if (HTMLArea
.classesXOR
[className
].test(classNames
[i
])) {
3038 HTMLArea
.DOM
.removeClass(node
, classNames
[i
]);
3042 if (node
.className
) {
3043 node
.className
+= ' ' + className
;
3045 node
.className
= className
;
3050 * Remove a class name from the class attribute of a node
3052 * @param object node: the node
3053 * @param string className: the class name to removed
3054 * @param boolean substring: if true, remove the class names starting with the given string
3057 removeClass: function (node
, className
, substring
) {
3058 if (node
&& node
.className
) {
3059 var classes
= node
.className
.trim().split(' ');
3060 var newClasses
= [];
3061 for (var i
= classes
.length
; --i
>= 0;) {
3062 if ((!substring
&& classes
[i
] != className
) || (substring
&& classes
[i
].indexOf(className
) != 0)) {
3063 newClasses
[newClasses
.length
] = classes
[i
];
3066 if (newClasses
.length
) {
3067 node
.className
= newClasses
.join(' ');
3070 node
.removeAttribute('class');
3072 node
.removeAttribute('className');
3075 node
.className
= '';