[TASK] RTE: Move JavaScript files to Resources directory 51/33851/2
authorStanislas Rolland <typo3@sjbr.ca>
Thu, 6 Nov 2014 16:22:09 +0000 (11:22 -0500)
committerStanislas Rolland <typo3@sjbr.ca>
Thu, 6 Nov 2014 16:24:00 +0000 (17:24 +0100)
Releases: master
Resolves: #62733
Change-Id: I2387e316f110312488c32045d0f3e92e0f9cb7a5
Reviewed-on: http://review.typo3.org/33851
Reviewed-by: Stanislas Rolland <typo3@sjbr.ca>
Tested-by: Stanislas Rolland <typo3@sjbr.ca>
128 files changed:
typo3/sysext/core/Documentation/Changelog/master/Breaking-62733-RTEJavaScriptFilesMoved.rst [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Classes/RteHtmlAreaBase.php
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Ajax/HTMLArea.Ajax.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/CSS/HTMLArea.CSS.Parser.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Configuration/HTMLArea.Config.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.BookMark.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Node.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Selection.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Walker.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Editor.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Framework.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Iframe.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.StatusBar.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Toolbar.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Extjs/Ext.ColorPalette.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Extjs/ux/Ext.ux.HTMLAreaButton.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Extjs/ux/Ext.ux.Toolbar.HTMLAreaToolbarText.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Extjs/ux/Ext.ux.form.ColorPaletteField.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Extjs/ux/Ext.ux.form.HTMLAreaCombo.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Extjs/ux/Ext.ux.menu.HTMLAreaColorMenu.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/HTMLArea.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/LoremIpsum.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/NameSpace/NameSpace.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Plugin/Plugin.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/UserAgent/UserAgent.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Util/HTMLArea.util.Color.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Util/HTMLArea.util.TYPO3.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Util/HTMLArea.util.Tips.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Util/HTMLArea.util.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Util/Wrap.close.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Util/Wrap.open.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/about-editor.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/acronym.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/block-elements.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/block-style.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/character-map.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/context-menu.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/copy-paste.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/default-clean.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/default-image.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/default-inline.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/default-link.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/definition-list.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/edit-element.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/editor-mode.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/find-replace.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/inline-elements.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/insert-smiley.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/language.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/microdata-schema.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/plain-text.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/quick-tag.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/remove-format.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/select-font.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/spell-checker.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/table-operations.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/text-indicator.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/text-style.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/typo3color.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/typo3html-parser.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/typo3image.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/typo3link.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/undo-redo.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/Plugins/user-elements.js [new file with mode: 0644]
typo3/sysext/rtehtmlarea/htmlarea/Ajax/HTMLArea.Ajax.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/CSS/HTMLArea.CSS.Parser.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Configuration/HTMLArea.Config.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/DOM/HTMLArea.DOM.BookMark.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/DOM/HTMLArea.DOM.Node.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/DOM/HTMLArea.DOM.Selection.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/DOM/HTMLArea.DOM.Walker.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/DOM/HTMLArea.DOM.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Editor/HTMLArea.Editor.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Editor/HTMLArea.Framework.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Editor/HTMLArea.Iframe.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Editor/HTMLArea.StatusBar.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Editor/HTMLArea.Toolbar.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Extjs/Ext.ColorPalette.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Extjs/ux/Ext.ux.HTMLAreaButton.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Extjs/ux/Ext.ux.Toolbar.HTMLAreaToolbarText.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Extjs/ux/Ext.ux.form.ColorPaletteField.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Extjs/ux/Ext.ux.form.HTMLAreaCombo.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Extjs/ux/Ext.ux.menu.HTMLAreaColorMenu.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/HTMLArea.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/LoremIpsum.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/NameSpace/NameSpace.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/UserAgent/UserAgent.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Util/HTMLArea.util.Color.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Util/HTMLArea.util.TYPO3.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Util/HTMLArea.util.Tips.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Util/HTMLArea.util.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Util/Wrap.close.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/Util/Wrap.open.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/AboutEditor/about-editor.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/Acronym/acronym.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/BlockElements/block-elements.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/BlockStyle/block-style.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/CharacterMap/character-map.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/ContextMenu/context-menu.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/CopyPaste/copy-paste.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/DefaultClean/default-clean.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/DefaultImage/default-image.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/DefaultInline/default-inline.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/DefaultLink/default-link.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/DefinitionList/definition-list.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/EditElement/edit-element.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/EditorMode/editor-mode.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/FindReplace/find-replace.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/InlineElements/inline-elements.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/InsertSmiley/insert-smiley.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/Language/language.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/MicrodataSchema/microdata-schema.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/PlainText/plain-text.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/Plugin.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/QuickTag/quick-tag.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/RemoveFormat/remove-format.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/SelectFont/select-font.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/SpellChecker/spell-checker.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TYPO3Color/typo3color.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TYPO3HtmlParser/typo3html-parser.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TYPO3Image/typo3image.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TYPO3Link/typo3link.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TableOperations/table-operations.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TextIndicator/text-indicator.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/TextStyle/text-style.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/UndoRedo/undo-redo.js [deleted file]
typo3/sysext/rtehtmlarea/htmlarea/plugins/UserElements/user-elements.js [deleted file]

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-62733-RTEJavaScriptFilesMoved.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-62733-RTEJavaScriptFilesMoved.rst
new file mode 100644 (file)
index 0000000..1f1db77
--- /dev/null
@@ -0,0 +1,27 @@
+=============================================================
+Breaking: #62733 - RTE Javascript Files Moved
+=============================================================
+
+Description
+===========
+
+Javascript files of the rtehtmlarea extension were moved from EXT:rtehtmlarea/htmlarea/ to
+EXT:rtehtmlarea/Resources/Public/JavaScript/
+
+
+Impact
+======
+
+Javascript or file not found errors.
+
+
+Affected installations
+======================
+
+An installation is affected if a 3rd party extension loads any JavaScript file from EXT:rtehtmlarea/htmlarea/
+
+
+Migration
+=========
+
+Any affected 3rd party extension must be modified to load any JavaScript file from EXT:rtehtmlarea/Resources/Public/JavaScript/ instead.
\ No newline at end of file
index 7fa8d51..623e5ad 100644 (file)
@@ -829,16 +829,28 @@ class RteHtmlAreaBase extends \TYPO3\CMS\Backend\Rte\AbstractRte {
                        'Extjs/ux/Ext.ux.menu.HTMLAreaColorMenu',
                        'Extjs/ux/Ext.ux.form.ColorPaletteField',
                        'LoremIpsum',
-                       'plugins/Plugin'
+                       'Plugin/Plugin'
                );
                foreach ($components as $component) {
-                       $this->pageRenderer->addJsFile($this->getFullFileName('EXT:' . $this->ID . '/htmlarea/' . $component . '.js'));
+                       $this->pageRenderer->addJsFile($this->getFullFileName('EXT:' . $this->ID . '/Resources/Public/JavaScript/HTMLArea/' . $component . '.js'));
                }
                foreach ($this->pluginEnabledCumulativeArray[$RTEcounter] as $pluginId) {
                        $extensionKey = is_object($this->registeredPlugins[$pluginId]) ? $this->registeredPlugins[$pluginId]->getExtensionKey() : $this->ID;
-                       $this->pageRenderer->addJsFile($this->getFullFileName('EXT:' . $extensionKey . '/htmlarea/plugins/' . $pluginId . '/' . strtolower(preg_replace('/([a-z])([A-Z])([a-z])/', '$1-$2$3', $pluginId)) . '.js'));
+                       $pluginName = strtolower(preg_replace('/([a-z])([A-Z])([a-z])/', '$1-$2$3', $pluginId));
+                       $fileName = 'EXT:' . $extensionKey . '/Resources/Public/JavaScript/Plugins/' . $pluginName . '.js';
+                       $absolutePath = GeneralUtility::getFileAbsFileName($fileName);
+                       if (file_exists($absolutePath)) {
+                               $this->pageRenderer->addJsFile($this->getFullFileName($fileName));
+                       } else {
+                               // Backward compatibility
+                               $fileName = 'EXT:' . $extensionKey . '/htmlarea/plugins/' . $pluginId . '/' . $pluginName . '.js';
+                               $absolutePath = GeneralUtility::getFileAbsFileName($fileName);
+                               if (file_exists($absolutePath)) {
+                                       $this->pageRenderer->addJsFile($this->getFullFileName($fileName));
+                               }
+                       }
                }
-               $this->pageRenderer->addJsFile($this->getFullFileName('EXT:' . $this->ID . '/htmlarea/Util/Wrap.close.js'));
+               $this->pageRenderer->addJsFile($this->getFullFileName('EXT:' . $this->ID . '/Resources/Public/JavaScript/HTMLArea/Util/Wrap.close.js'));
        }
 
        /**
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Ajax/HTMLArea.Ajax.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Ajax/HTMLArea.Ajax.js
new file mode 100644 (file)
index 0000000..f39ec6f
--- /dev/null
@@ -0,0 +1,73 @@
+HTMLArea.Ajax = function (config) {
+       Ext.apply(this, config);
+};
+HTMLArea.Ajax = Ext.extend(HTMLArea.Ajax, {
+       /*
+        * Load a Javascript file asynchronously
+        *
+        * @param       string          url: url of the file to load
+        * @param       function        callBack: the callBack function
+        * @param       object          scope: scope of the callbacks
+        *
+        * @return      boolean         true on success of the request submission
+        */
+       getJavascriptFile: function (url, callback, scope) {
+               var success = false;
+               var self = this;
+               Ext.Ajax.request({
+                       method: 'GET',
+                       url: url,
+                       callback: callback,
+                       success: function (response) {
+                               success = true;
+                       },
+                       failure: function (response) {
+                               self.editor.inhibitKeyboardInput = false;
+                               self.editor.appendToLog('HTMLArea.Ajax', 'getJavascriptFile', 'Unable to get ' + url + ' . Server reported ' + response.status, 'error');
+                       },
+                       scope: scope
+               });
+               return success;
+       },
+       /*
+        * Post data to the server
+        *
+        * @param       string          url: url to post data to
+        * @param       object          data: data to be posted
+        * @param       function        callback: function that will handle the response returned by the server
+        * @param       object          scope: scope of the callbacks
+        *
+        * @return      boolean         true on success
+        */
+       postData: function (url, data, callback, scope) {
+               var success = false;
+               var self = this;
+               data.charset = this.editor.config.typo3ContentCharset ? this.editor.config.typo3ContentCharset : 'utf-8';
+               var params = '';
+               for (var parameter in data) {
+                       params += (params.length ? '&' : '') + parameter + '=' + encodeURIComponent(data[parameter]);
+               }
+               params += this.editor.config.RTEtsConfigParams;
+               Ext.Ajax.request({
+                       method: 'POST',
+                       headers: {
+                               'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+                       },
+                       url: url,
+                       params: params,
+                       callback: typeof callback === 'function' ? callback : function (options, success, response) {
+                               if (!success) {
+                                       self.editor.appendToLog('HTMLArea.Ajax', 'postData', 'Post request to ' + url + ' failed. Server reported ' + response.status, 'error');
+                               }
+                       },
+                       success: function (response) {
+                               success = true;
+                       },
+                       failure: function (response) {
+                               self.editor.appendToLog('HTMLArea.Ajax', 'postData', 'Unable to post ' + url + ' . Server reported ' + response.status, 'error');
+                       },
+                       scope: scope
+               });
+               return success;
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/CSS/HTMLArea.CSS.Parser.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/CSS/HTMLArea.CSS.Parser.js
new file mode 100644 (file)
index 0000000..da3ee27
--- /dev/null
@@ -0,0 +1,355 @@
+/***************************************************
+ *  HTMLArea.CSS.Parser: CSS Parser
+ ***************************************************/
+HTMLArea.CSS.Parser = Ext.extend(Ext.util.Observable, {
+       /*
+        * HTMLArea.CSS.Parser constructor
+        */
+       constructor: function (config) {
+               HTMLArea.CSS.Parser.superclass.constructor.call(this, {});
+               var configDefaults = {
+                       parseAttemptsMaximumNumber: 20,
+                       prefixLabelWithClassName: false,
+                       postfixLabelWithClassName: false,
+                       showTagFreeClasses: false,
+                       tags: null,
+                       editor: null
+               };
+               Ext.apply(this, config, configDefaults);
+               if (this.editor.config.styleSheetsMaximumAttempts) {
+                       this.parseAttemptsMaximumNumber = this.editor.config.styleSheetsMaximumAttempts;
+               }
+               this.addEvents(
+                       /*
+                        * @event HTMLAreaEventCssParsingComplete
+                        * Fires when parsing of the stylesheets of the iframe is complete
+                        */
+                       'HTMLAreaEventCssParsingComplete'
+               );
+       },
+       /*
+        * The parsed classes
+        */
+       parsedClasses: {},
+       /*
+        * Boolean indicating whether are not parsing is complete
+        */
+       isReady: false,
+       /*
+        * Boolean indicating whether or not the stylesheets were accessible
+        */
+       cssLoaded: false,
+       /*
+        * Counter of the number of attempts at parsing the stylesheets
+        */
+       parseAttemptsCounter: 0,
+       /*
+        * Parsing attempt timeout id
+        */
+       attemptTimeout: null,
+       /*
+        * The error that occurred on the last attempt at parsing the stylesheets
+        */
+       error: null,
+       /*
+        * This function gets the parsed css classes
+        *
+        * @return      object  this.parsedClasses
+        */
+       getClasses: function() {
+               return this.parsedClasses;
+       },
+       /*
+        * This function initiates parsing of the stylesheets
+        *
+        * @return      void
+        */
+       initiateParsing: function () {
+               if (this.editor.config.classesUrl && typeof HTMLArea.classesLabels === 'undefined') {
+                       this.editor.ajax.getJavascriptFile(this.editor.config.classesUrl, function (options, success, response) {
+                               if (success) {
+                                       try {
+                                               if (typeof HTMLArea.classesLabels === 'undefined') {
+                                                       eval(response.responseText);
+                                               }
+                                       } catch(e) {
+                                               this.editor.appendToLog('HTMLArea.CSS.Parser', 'initiateParsing', 'Error evaluating contents of Javascript file: ' + this.editor.config.classesUrl, 'error');
+                                       }
+                               }
+                               this.parse();
+                       }, this);
+               } else {
+                       this.parse();
+               }
+       },
+       /*
+        * This function parses the stylesheets of the iframe set in config
+        *
+        * @return      void    parsed css classes are accumulated in this.parsedClasses
+        */
+       parse: function() {
+               if (this.editor.document) {
+                       this.parseStyleSheets();
+                       if (!this.cssLoaded) {
+                               if (/Security/i.test(this.error)) {
+                                       this.editor.appendToLog('HTMLArea.CSS.Parser', 'parse', 'A security error occurred. Make sure all stylesheets are accessed from the same domain/subdomain and using the same protocol as the current script.', 'error');
+                                       this.fireEvent('HTMLAreaEventCssParsingComplete');
+                               } else if (this.parseAttemptsCounter < this.parseAttemptsMaximumNumber) {
+                                       this.parseAttemptsCounter++;
+                                       this.attemptTimeout = this.parse.defer(200, this);
+                               } else {
+                                       this.editor.appendToLog('HTMLArea.CSS.Parser', 'parse', 'The stylesheets could not be parsed. Reported error: ' + this.error, 'error');
+                                       this.fireEvent('HTMLAreaEventCssParsingComplete');
+                               }
+                       } else {
+                               this.attemptTimeout = null;
+                               this.isReady = true;
+                               this.filterAllowedClasses();
+                               this.sort();
+                               this.fireEvent('HTMLAreaEventCssParsingComplete');
+                       }
+               }
+       },
+       /*
+        * This function parses the stylesheets of an iframe
+        *
+        * @return      void    parsed css classes are accumulated in this.parsedClasses
+        */
+       parseStyleSheets: function () {
+               this.cssLoaded = true;
+               this.error = null;
+                       // Test if the styleSheets array is at all accessible
+               if (HTMLArea.UserAgent.isOpera) {
+                       if (this.editor.document.readyState !== 'complete') {
+                               this.cssLoaded = false;
+                               this.error = 'Document.readyState not complete';
+                       }
+               } else {
+                       if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                               try {
+                                       var rules = this.editor.document.styleSheets[0].rules;
+                                       var imports = this.editor.document.styleSheets[0].imports;
+                                       if (!rules.length && !imports.length) {
+                                               this.cssLoaded = false;
+                                               this.error = 'Empty rules and imports arrays';
+                                       }
+                               } catch(e) {
+                                       this.cssLoaded = false;
+                                       this.error = e;
+                               }
+                       } else {
+                               try {
+                                       this.editor.document.styleSheets && this.editor.document.styleSheets[0] && this.editor.document.styleSheets[0].rules;
+                               } catch(e) {
+                                       this.cssLoaded = false;
+                                       this.error = e;
+                               }
+                       }
+               }
+               if (this.cssLoaded) {
+                               // Expecting at least 2 stylesheets...
+                       if (this.editor.document.styleSheets.length > 1) {
+                               var styleSheets = this.editor.document.styleSheets;
+                               for (var index = 0, n = styleSheets.length; index < n; index++) {
+                                       try {
+                                               var styleSheet = styleSheets[index];
+                                               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                                                       var rules = styleSheet.rules;
+                                                       var imports = styleSheet.imports;
+                                                       if (!rules.length && !imports.length) {
+                                                               this.cssLoaded = false;
+                                                               this.error = 'Empty rules and imports arrays of styleSheets[' + index + ']';
+                                                               break;
+                                                       }
+                                                       if (styleSheet.imports) {
+                                                               this.parseIeRules(styleSheet.imports);
+                                                       }
+                                                       if (styleSheet.rules) {
+                                                               this.parseRules(styleSheet.rules);
+                                                       }
+                                               } else {
+                                                       this.parseRules(styleSheet.cssRules);
+                                               }
+                                       } catch (e) {
+                                               this.error = e;
+                                               this.cssLoaded = false;
+                                               this.parsedClasses = {};
+                                               break;
+                                       }
+                               }
+                       } else {
+                               this.cssLoaded = false;
+                               this.error = 'Empty stylesheets array or missing linked stylesheets';
+                       }
+               }
+       },
+       /*
+        * This function parses the set of rules from a standard stylesheet
+        *
+        * @param       array           cssRules: the array of rules of a stylesheet
+        * @return      void
+        */
+       parseRules: function (cssRules) {
+               for (var rule = 0, n = cssRules.length; rule < n; rule++) {
+                               // Style rule
+                       if (cssRules[rule].selectorText) {
+                               this.parseSelectorText(cssRules[rule].selectorText);
+                       } else {
+                                       // Import rule
+                               try {
+                                       if (cssRules[rule].styleSheet && cssRules[rule].styleSheet.cssRules) {
+                                                       this.parseRules(cssRules[rule].styleSheet.cssRules);
+                                       }
+                               } catch (e) {
+                                       if (/Security/i.test(e)) {
+                                               // If this is a security error, silently log the error and continue parsing
+                                               this.editor.appendToLog('HTMLArea.CSS.Parser', 'parseRules', 'A security error occurred. Make sure all stylesheets are accessed from the same domain/subdomain and using the same protocol as the current script.', 'error');
+                                       } else {
+                                               throw e;
+                                       }
+                               }
+                                       // Media rule
+                               if (cssRules[rule].cssRules) {
+                                       this.parseRules(cssRules[rule].cssRules);
+                               }
+                       }
+               }
+       },
+       /*
+        * This function parses the set of rules from an IE stylesheet
+        *
+        * @param       array           cssRules: the array of rules of a stylesheet
+        * @return      void
+        */
+       parseIeRules: function (cssRules) {
+               for (var rule = 0, n = cssRules.length; rule < n; rule++) {
+                               // Import rule
+                       if (cssRules[rule].imports) {
+                               this.parseIeRules(cssRules[rule].imports);
+                       }
+                               // Style rule
+                       if (cssRules[rule].rules) {
+                               this.parseRules(cssRules[rule].rules);
+                       }
+               }
+       },
+       /*
+        * This function parses a selector rule
+        *
+        * @param       string          selectorText: the text of the rule to parsed
+        * @return      void
+        */
+       parseSelectorText: function (selectorText) {
+               var cssElements = [],
+                       cssElement = [],
+                       nodeName, className,
+                       pattern = /(\S*)\.(\S+)/;
+               if (selectorText.search(/:+/) == -1) {
+                               // Split equal styles
+                       cssElements = selectorText.split(',');
+                       for (var k = 0, n = cssElements.length; k < n; k++) {
+                                       // Match all classes (<element name (optional)>.<class name>) in selector rule
+                               var s = cssElements[k], index;
+                               while ((index = s.search(pattern)) > -1) {
+                                       var match = pattern.exec(s.substring(index));
+                                       s = s.substring(index+match[0].length);
+                                       nodeName = (match[1] && (match[1] != '*')) ? match[1].toLowerCase().trim() : 'all';
+                                       className = match[2];
+                                       if (className && !HTMLArea.reservedClassNames.test(className)) {
+                                               if (((nodeName != 'all') && (!this.tags || !this.tags[nodeName]))
+                                                       || ((nodeName == 'all') && (!this.tags || !this.tags[nodeName]) && this.showTagFreeClasses)
+                                                       || (this.tags && this.tags[nodeName] && this.tags[nodeName].allowedClasses && this.tags[nodeName].allowedClasses.test(className))) {
+                                                       if (!this.parsedClasses[nodeName]) {
+                                                               this.parsedClasses[nodeName] = {};
+                                                       }
+                                                       cssName = className;
+                                                       if (HTMLArea.classesLabels && HTMLArea.classesLabels[className]) {
+                                                               cssName = this.prefixLabelWithClassName ? (className + ' - ' + HTMLArea.classesLabels[className]) : HTMLArea.classesLabels[className];
+                                                               cssName = this.postfixLabelWithClassName ? (cssName + ' - ' + className) : cssName;
+                                                       }
+                                                       this.parsedClasses[nodeName][className] = cssName;
+                                               }
+                                       }
+                               }
+                       }
+               }
+       },
+       /*
+        * This function filters the class selectors allowed for each nodeName
+        *
+        * @return      void
+        */
+       filterAllowedClasses: function() {
+               var nodeName, cssClass;
+               for (nodeName in this.tags) {
+                       var allowedClasses = {};
+                       // Get classes allowed for all tags
+                       if (nodeName !== 'all' && typeof this.parsedClasses['all'] !== 'undefined') {
+                               if (this.tags && this.tags[nodeName] && this.tags[nodeName].allowedClasses) {
+                                       var allowed = this.tags[nodeName].allowedClasses;
+                                       for (cssClass in this.parsedClasses['all']) {
+                                               if (allowed.test(cssClass)) {
+                                                       allowedClasses[cssClass] = this.parsedClasses['all'][cssClass];
+                                               }
+                                       }
+                               } else {
+                                       allowedClasses = this.parsedClasses['all'];
+                               }
+                       }
+                       // Merge classes allowed for nodeName
+                       if (typeof this.parsedClasses[nodeName] !== 'undefined') {
+                               if (this.tags && this.tags[nodeName] && this.tags[nodeName].allowedClasses) {
+                                       var allowed = this.tags[nodeName].allowedClasses;
+                                       for (cssClass in this.parsedClasses[nodeName]) {
+                                               if (allowed.test(cssClass)) {
+                                                       allowedClasses[cssClass] = this.parsedClasses[nodeName][cssClass];
+                                               }
+                                       }
+                               } else {
+                                       for (cssClass in this.parsedClasses[nodeName]) {
+                                               allowedClasses[cssClass] = this.parsedClasses[nodeName][cssClass];
+                                       }
+                               }
+                       }
+                       this.parsedClasses[nodeName] = allowedClasses;
+               }
+               // If showTagFreeClasses is set and there is no allowedClasses clause on a tag, merge classes allowed for all tags
+               if (this.showTagFreeClasses && typeof this.parsedClasses['all'] !== 'undefined') {
+                       for (nodeName in this.parsedClasses) {
+                               if (nodeName !== 'all' && !this.tags[nodeName]) {
+                                       for (cssClass in this.parsedClasses['all']) {
+                                               this.parsedClasses[nodeName][cssClass] = this.parsedClasses['all'][cssClass];
+                                       }
+                               }
+                       }
+               }
+       },
+       /*
+        * This function sorts the class selectors for each nodeName
+        *
+        * @return      void
+        */
+       sort: function() {
+               var nodeName, cssClass, i, n;
+               for (nodeName in this.parsedClasses) {
+                       var value = this.parsedClasses[nodeName];
+                       var classes = [];
+                       var sortedClasses= {};
+                       // Collect keys
+                       for (cssClass in value) {
+                               classes.push(cssClass);
+                       }
+                       function compare(a, b) {
+                               x = value[a];
+                               y = value[b];
+                               return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+                       }
+                       // Sort keys by comparing texts
+                       classes = classes.sort(compare);
+                       for (i = 0, n = classes.length; i < n; ++i) {
+                               sortedClasses[classes[i]] = value[classes[i]];
+                       }
+                       this.parsedClasses[nodeName] = sortedClasses;
+               }
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Configuration/HTMLArea.Config.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Configuration/HTMLArea.Config.js
new file mode 100644 (file)
index 0000000..907e7ff
--- /dev/null
@@ -0,0 +1,173 @@
+/***************************************************
+ *  EDITOR CONFIGURATION
+ ***************************************************/
+HTMLArea.Config = function (editorId) {
+       this.editorId = editorId;
+               // if the site is secure, create a secure iframe
+       this.useHTTPS = false;
+               // for Mozilla
+       this.useCSS = false;
+       this.enableMozillaExtension = true;
+       this.disableEnterParagraphs = false;
+       this.disableObjectResizing = false;
+       this.removeTrailingBR = true;
+               // style included in the iframe document
+       this.editedContentStyle = HTMLArea.editedContentCSS;
+               // Array of content styles
+       this.pageStyle = [];
+               // Maximum attempts at accessing the stylesheets
+       this.styleSheetsMaximumAttempts = 20;
+               // Remove tags (must be a regular expression)
+       this.htmlRemoveTags = /none/i;
+               // Remove tags and their contents (must be a regular expression)
+       this.htmlRemoveTagsAndContents = /none/i;
+               // Remove comments
+       this.htmlRemoveComments = false;
+               // Array of custom tags
+       this.customTags = [];
+               // BaseURL to be included in the iframe document
+       this.baseURL = document.baseURI;
+               // IE does not support document.baseURI
+               // Since document.URL is incorrect when using realurl, get first base tag and extract href
+       if (!this.baseURL) {
+               var baseTags = document.getElementsByTagName ('base');
+               if (baseTags.length > 0) {
+                       this.baseURL = baseTags[0].href;
+               } else {
+                       this.baseURL = document.URL;
+               }
+       }
+       if (this.baseURL && this.baseURL.match(/(.*\:\/\/.*\/)[^\/]*/)) {
+               this.baseURL = RegExp.$1;
+       }
+               // URL-s
+       this.popupURL = "popups/";
+               // DocumentType
+       this.documentType = '<!DOCTYPE html\r'
+                       + '    PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\r'
+                       + '    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r';
+       this.blankDocument = '<html><head></head><body></body></html>';
+               // Hold the configuration of buttons and hot keys registered by plugins
+       this.buttonsConfig = {};
+       this.hotKeyList = {};
+               // Default configurations for toolbar items
+       this.configDefaults = {
+               all: {
+                       xtype: 'htmlareabutton',
+                       disabledClass: 'buttonDisabled',
+                       textMode: false,
+                       selection: false,
+                       dialog: false,
+                       hidden: false,
+                       hideMode: 'display'
+               },
+               htmlareabutton: {
+                       cls: 'button',
+                       overCls: 'buttonHover',
+                               // Erratic behaviour of click event in WebKit and IE browsers
+                       clickEvent: (HTMLArea.UserAgent.isWebKit || HTMLArea.UserAgent.isIE) ? 'mousedown' : 'click'
+               },
+               htmlareacombo: {
+                       cls: 'select',
+                       typeAhead: true,
+                       lastQuery: '',
+                       triggerAction: 'all',
+                       editable: !HTMLArea.UserAgent.isIE,
+                       selectOnFocus: !HTMLArea.UserAgent.isIE,
+                       validationEvent: false,
+                       validateOnBlur: false,
+                       submitValue: false,
+                       forceSelection: true,
+                       mode: 'local',
+                       storeRoot: 'options',
+                       storeFields: [ { name: 'text'}, { name: 'value'}],
+                       valueField: 'value',
+                       displayField: 'text',
+                       labelSeparator: '',
+                       hideLabel: true,
+                       tpl: '<tpl for="."><div ext:qtip="{value}" style="text-align:left;font-size:11px;" class="x-combo-list-item">{text}</div></tpl>'
+               }
+       };
+};
+HTMLArea.Config = Ext.extend(HTMLArea.Config, {
+       /**
+        * Registers a button for inclusion in the toolbar, adding some standard configuration properties for the ExtJS widgets
+        *
+        * @param       object          buttonConfiguration: the configuration object of the button:
+        *                                      id              : unique id for the button
+        *                                      tooltip         : tooltip for the button
+        *                                      textMode        : enable in text mode
+        *                                      context         : disable if not inside one of listed elements
+        *                                      hidden          : hide in menu and show only in context menu
+        *                                      selection       : disable if there is no selection
+        *                                      hotkey          : hotkey character
+        *                                      dialog          : if true, the button opens a dialogue
+        *                                      dimensions      : the opening dimensions object of the dialogue window: { width: nn, height: mm }
+        *                                      and potentially other ExtJS config properties (will be forwarded)
+        *
+        * @return      boolean         true if the button was successfully registered
+        */
+       registerButton: function (config) {
+               config.itemId = config.id;
+               if (Ext.type(this.buttonsConfig[config.id])) {
+                       HTMLArea.appendToLog('', 'HTMLArea.Config', 'registerButton', 'A toolbar item with the same Id: ' + config.id + ' already exists and will be overidden.', 'warn');
+               }
+                       // Apply defaults
+               config = Ext.applyIf(config, this.configDefaults['all']);
+               config = Ext.applyIf(config, this.configDefaults[config.xtype]);
+                       // Set some additional properties
+               switch (config.xtype) {
+                       case 'htmlareacombo':
+                               if (config.options) {
+                                               // Create combo array store
+                                       config.store = new Ext.data.ArrayStore({
+                                               autoDestroy:  true,
+                                               fields: config.storeFields,
+                                               data: config.options
+                                       });
+                               } else if (config.storeUrl) {
+                                               // Create combo json store
+                                       config.store = new Ext.data.JsonStore({
+                                               autoDestroy:  true,
+                                               autoLoad: true,
+                                               root: config.storeRoot,
+                                               fields: config.storeFields,
+                                               url: config.storeUrl
+                                       });
+                               }
+                               config.hideLabel = typeof config.fieldLabel !== 'string' || !config.fieldLabel.length || HTMLArea.UserAgent.isIE6;
+                               config.helpTitle = config.tooltip;
+                               break;
+                       default:
+                               if (!config.iconCls) {
+                                       config.iconCls = config.id;
+                               }
+                               break;
+               }
+               config.cmd = config.id;
+               config.tooltip = { title: config.tooltip };
+               this.buttonsConfig[config.id] = config;
+               return true;
+       },
+       /*
+        * Register a hotkey with the editor configuration.
+        */
+       registerHotKey: function (hotKeyConfiguration) {
+               if (typeof this.hotKeyList[hotKeyConfiguration.id] !== 'undefined') {
+                       HTMLArea.appendToLog('', 'HTMLArea.Config', 'registerHotKey', 'A hotkey with the same key ' + hotKeyConfiguration.id + ' already exists and will be overidden.', 'warn');
+               }
+               if (typeof hotKeyConfiguration.cmd === 'string' && hotKeyConfiguration.cmd.length > 0 && typeof this.buttonsConfig[hotKeyConfiguration.cmd] !== 'undefined') {
+                       this.hotKeyList[hotKeyConfiguration.id] = hotKeyConfiguration;
+                       return true;
+               } else {
+                       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');
+                       return false;
+               }
+       },
+       /*
+        * Get the configured document type for dialogue windows
+        */
+       getDocumentType: function () {
+               return this.documentType;
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.BookMark.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.BookMark.js
new file mode 100644 (file)
index 0000000..78ae093
--- /dev/null
@@ -0,0 +1,307 @@
+/***************************************************
+ *  HTMLArea.DOM.BookMark: BookMark object
+ ***************************************************/
+HTMLArea.DOM.BookMark = function (config) {
+};
+HTMLArea.DOM.BookMark = Ext.extend(HTMLArea.DOM.BookMark, {
+       /*
+        * Reference to the editor MUST be set in config
+        */
+       editor: null,
+       /*
+        * Reference to the editor document
+        */
+       document: null,
+       /*
+        * Reference to the editor selection object
+        */
+       selection: null,
+       /*
+        * HTMLArea.DOM.Selection constructor
+        */
+       constructor: function (config) {
+                       // Apply config
+               Ext.apply(this, config);
+                       // Initialize references
+               this.document = this.editor.document;
+               this.selection = this.editor.getSelection();
+       },
+       /*
+        * Get a bookMark
+        *
+        * @param       object          range: the range to bookMark
+        * @param       boolean         nonIntrusive: if true, a non-intrusive bookmark is requested
+        *
+        * @return      object          the bookMark
+        */
+       get: function (range, nonIntrusive) {
+               var bookMark;
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       // Bookmarking will not work on control ranges
+                       try {
+                               bookMark = range.getBookmark();
+                       } catch (e) {
+                               bookMark = null;
+                       }
+               } else {
+                       if (nonIntrusive) {
+                               bookMark = this.getNonIntrusiveBookMark(range, true);
+                       } else {
+                               bookMark = this.getIntrusiveBookMark(range);
+                       }
+               }
+               return bookMark;
+       },
+       /*
+        * Get an intrusive bookMark
+        * Adapted from FCKeditor
+        * This is an "intrusive" way to create a bookMark. It includes <span> tags
+        * in the range boundaries. The advantage of it is that it is possible to
+        * handle DOM mutations when moving back to the bookMark.
+        *
+        * @param       object          range: the range to bookMark
+        *
+        * @return      object          the bookMark
+        */
+       getIntrusiveBookMark: function (range) {
+               // Create the bookmark info (random IDs).
+               var bookMark = {
+                       nonIntrusive: false,
+                       startId: (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'S',
+                       endId: (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'E'
+               };
+               var startSpan;
+               var endSpan;
+               var rangeClone = range.cloneRange();
+               // For collapsed ranges, add just the start marker
+               if (!range.collapsed ) {
+                       endSpan = this.document.createElement('span');
+                       endSpan.style.display = 'none';
+                       endSpan.id = bookMark.endId;
+                       endSpan.setAttribute('data-htmlarea-bookmark', true);
+                       endSpan.innerHTML = '&nbsp;';
+                       rangeClone.collapse(false);
+                       rangeClone.insertNode(endSpan);
+               }
+               startSpan = this.document.createElement('span');
+               startSpan.style.display = 'none';
+               startSpan.id = bookMark.startId;
+               startSpan.setAttribute('data-htmlarea-bookmark', true);
+               startSpan.innerHTML = '&nbsp;';
+               var rangeClone = range.cloneRange();
+               rangeClone.collapse(true);
+               rangeClone.insertNode(startSpan);
+               bookMark.startNode = startSpan;
+               bookMark.endNode = endSpan;
+               // Update the range position.
+               if (endSpan) {
+                       range.setEndBefore(endSpan);
+                       range.setStartAfter(startSpan);
+               } else {
+                       range.setEndAfter(startSpan);
+                       range.collapse(false);
+               }
+               return bookMark;
+       },
+       /*
+        * Get a non-intrusive bookMark
+        * Adapted from FCKeditor
+        *
+        * @param       object          range: the range to bookMark
+        * @param       boolean         normalized: if true, normalized enpoints are calculated
+        *
+        * @return      object          the bookMark
+        */
+       getNonIntrusiveBookMark: function (range, normalized) {
+               var startContainer = range.startContainer,
+                       endContainer = range.endContainer,
+                       startOffset = range.startOffset,
+                       endOffset = range.endOffset,
+                       collapsed = range.collapsed,
+                       child,
+                       previous,
+                       bookMark = {};
+               if (!startContainer || !endContainer) {
+                       bookMark = {
+                               nonIntrusive: true,
+                               start: 0,
+                               end: 0
+                       };
+               } else {
+                       if (normalized) {
+                               // Find out if the start is pointing to a text node that might be normalized
+                               if (startContainer.nodeType == HTMLArea.DOM.NODE_ELEMENT) {
+                                       child = startContainer.childNodes[startOffset];
+                                       // In this case, move the start to that text node
+                                       if (
+                                               child
+                                               && child.nodeType == HTMLArea.DOM.NODE_TEXT
+                                               && startOffset > 0
+                                               && child.previousSibling.nodeType == HTMLArea.DOM.NODE_TEXT
+                                       ) {
+                                               startContainer = child;
+                                               startOffset = 0;
+                                       }
+                                       // Get the normalized offset
+                                       if (child && child.nodeType == HTMLArea.DOM.NODE_ELEMENT) {
+                                               startOffset = HTMLArea.DOM.getPositionWithinParent(child, true);
+                                       }
+                               }
+                               // Normalize the start
+                               while (
+                                       startContainer.nodeType == HTMLArea.DOM.NODE_TEXT
+                                       && (previous = startContainer.previousSibling)
+                                       && previous.nodeType == HTMLArea.DOM.NODE_TEXT
+                               ) {
+                                       startContainer = previous;
+                                       startOffset += previous.nodeValue.length;
+                               }
+                               // Process the end only if not collapsed
+                               if (!collapsed) {
+                                       // Find out if the start is pointing to a text node that will be normalized
+                                       if (endContainer.nodeType == HTMLArea.DOM.NODE_ELEMENT) {
+                                               child = endContainer.childNodes[endOffset];
+                                               // In this case, move the end to that text node
+                                               if (
+                                                       child
+                                                       && child.nodeType == HTMLArea.DOM.NODE_TEXT
+                                                       && endOffset > 0
+                                                       && child.previousSibling.nodeType == HTMLArea.DOM.NODE_TEXT
+                                               ) {
+                                                       endContainer = child;
+                                                       endOffset = 0;
+                                               }
+                                               // Get the normalized offset
+                                               if (child && child.nodeType == HTMLArea.DOM.NODE_ELEMENT) {
+                                                       endOffset = HTMLArea.DOM.getPositionWithinParent(child, true);
+                                               }
+                                       }
+                                       // Normalize the end
+                                       while (
+                                               endContainer.nodeType == HTMLArea.DOM.NODE_TEXT
+                                               && (previous = endContainer.previousSibling)
+                                               && previous.nodeType == HTMLArea.DOM.NODE_TEXT
+                                       ) {
+                                               endContainer = previous;
+                                               endOffset += previous.nodeValue.length;
+                                       }
+                               }
+                       }
+                       bookMark = {
+                               start: this.editor.domNode.getPositionWithinTree(startContainer, normalized),
+                               end: collapsed ? null : getPositionWithinTree(endContainer, normalized),
+                               startOffset: startOffset,
+                               endOffset: endOffset,
+                               normalized: normalized,
+                               collapsed: collapsed,
+                               nonIntrusive: true
+                       };
+               }
+               return bookMark;
+       },
+       /*
+        * Get the end point of the bookMark
+        * Adapted from FCKeditor
+        *
+        * @param       object          bookMark: the bookMark
+        * @param       boolean         endPoint: true, for startPoint, false for endPoint
+        *
+        * @return      object          the endPoint node
+        */
+       getEndPoint: function (bookMark, endPoint) {
+               if (endPoint) {
+                       return this.document.getElementById(bookMark.startId);
+               } else {
+                       return this.document.getElementById(bookMark.endId);
+               }
+       },
+       /*
+        * Get a range and move it to the bookMark
+        *
+        * @param       object          bookMark: the bookmark to move to
+        *
+        * @return      object          the range that was bookmarked
+        */
+       moveTo: function (bookMark) {
+               var range = this.selection.createRange();
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       if (bookMark) {
+                               range.moveToBookmark(bookMark);
+                       }
+               } else {
+                       if (bookMark.nonIntrusive) {
+                               range = this.moveToNonIntrusiveBookMark(range, bookMark);
+                       } else {
+                               range = this.moveToIntrusiveBookMark(range, bookMark);
+                       }
+               }
+               return range;
+       },
+       /*
+        * Move the range to the intrusive bookMark
+        * Adapted from FCKeditor
+        *
+        * @param       object          range: the range to be moved
+        * @param       object          bookMark: the bookmark to move to
+        *
+        * @return      object          the range that was bookmarked
+        */
+       moveToIntrusiveBookMark: function (range, bookMark) {
+               var startSpan = this.getEndPoint(bookMark, true),
+                       endSpan = this.getEndPoint(bookMark, false),
+                       parent;
+               if (startSpan) {
+                       // If the previous sibling is a text node, let the anchorNode have it as parent
+                       if (startSpan.previousSibling && startSpan.previousSibling.nodeType === HTMLArea.DOM.TEXT_NODE) {
+                               range.setStart(startSpan.previousSibling, startSpan.previousSibling.data.length);
+                       } else {
+                               range.setStartBefore(startSpan);
+                       }
+                       HTMLArea.DOM.removeFromParent(startSpan);
+               } else {
+                       // For some reason, the startSpan was removed or its id attribute was removed so that it cannot be retrieved
+                       range.setStart(this.document.body, 0);
+               }
+               // If the bookmarked range was collapsed, the end span will not be available
+               if (endSpan) {
+                       // If the next sibling is a text node, let the focusNode have it as parent
+                       if (endSpan.nextSibling && endSpan.nextSibling.nodeType === HTMLArea.DOM.TEXT_NODE) {
+                               range.setEnd(endSpan.nextSibling, 0);
+                       } else {
+                               range.setEndBefore(endSpan);
+                       }
+                       HTMLArea.DOM.removeFromParent(endSpan);
+               } else {
+                       range.collapse(true);
+               }
+               return range;
+       },
+       /*
+        * Move the range to the non-intrusive bookMark
+        * Adapted from FCKeditor
+        *
+        * @param       object          range: the range to be moved
+        * @param       object          bookMark: the bookMark to move to
+        *
+        * @return      object          the range that was bookmarked
+        */
+       moveToNonIntrusiveBookMark: function (range, bookMark) {
+               if (bookMark.start) {
+                       // Get the start information
+                       var startContainer = this.editor.getNodeByPosition(bookMark.start, bookMark.normalized),
+                               startOffset = bookMark.startOffset;
+                       // Set the start boundary
+                       range.setStart(startContainer, startOffset);
+                       // Get the end information
+                       var endContainer = bookMark.end && this.editor.getNodeByPosition(bookMark.end, bookMark.normalized),
+                               endOffset = bookMark.endOffset;
+                       // Set the end boundary. If not available, collapse the range
+                       if (endContainer) {
+                               range.setEnd(endContainer, endOffset);
+                       } else {
+                               range.collapse(true);
+                       }
+               }
+               return range;
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Node.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Node.js
new file mode 100644 (file)
index 0000000..44fdf66
--- /dev/null
@@ -0,0 +1,198 @@
+/***************************************************
+ *  HTMLArea.DOM.Node: Node object
+ ***************************************************/
+HTMLArea.DOM.Node = function (config) {
+};
+HTMLArea.DOM.Node = Ext.extend(HTMLArea.DOM.Node, {
+       /*
+        * Reference to the editor MUST be set in config
+        */
+       editor: null,
+       /*
+        * Reference to the editor document
+        */
+       document: null,
+       /*
+        * Reference to the editor selection object
+        */
+       selection: null,
+       /*
+        * Reference to the editor bookmark object
+        */
+       bookMark: null,
+       /*
+        * HTMLArea.DOM.Selection constructor
+        */
+       constructor: function (config) {
+                       // Apply config
+               Ext.apply(this, config);
+                       // Initialize references
+               this.document = this.editor.document;
+               this.selection = this.editor.getSelection();
+               this.bookMark = this.editor.getBookMark();
+       },
+       /*
+        * Remove the given element
+        *
+        * @param       object          element: the element to be removed, content and selection being preserved
+        *
+        * @return      void
+        */
+       removeMarkup: function (element) {
+               var bookMark = this.bookMark.get(this.selection.createRange());
+               var parent = element.parentNode;
+               while (element.firstChild) {
+                       parent.insertBefore(element.firstChild, element);
+               }
+               parent.removeChild(element);
+               this.selection.selectRange(this.bookMark.moveTo(bookMark));
+       },
+       /*
+        * Wrap the range with an inline element
+        *
+        * @param       string  element: the node that will wrap the range
+        * @param       object  range: the range to be wrapped
+        *
+        * @return      void
+        */
+       wrapWithInlineElement: function (element, range) {
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       var nodeName = element.nodeName;
+                       var bookMark = this.bookMark.get(range);
+                       if (range.parentElement) {
+                               var parent = range.parentElement();
+                               var rangeStart = range.duplicate();
+                               rangeStart.collapse(true);
+                               var parentStart = rangeStart.parentElement();
+                               var rangeEnd = range.duplicate();
+                               rangeEnd.collapse(true);
+                               var newRange = this.selection.createRange();
+
+                               var parentEnd = rangeEnd.parentElement();
+                               var upperParentStart = parentStart;
+                               if (parentStart !== parent) {
+                                       while (upperParentStart.parentNode !== parent) {
+                                               upperParentStart = upperParentStart.parentNode;
+                                       }
+                               }
+
+                               element.innerHTML = range.htmlText;
+                                       // IE eats spaces on the start boundary
+                               if (range.htmlText.charAt(0) === '\x20') {
+                                       element.innerHTML = '&nbsp;' + element.innerHTML;
+                               }
+                               var elementClone = element.cloneNode(true);
+                               range.pasteHTML(element.outerHTML);
+                                       // IE inserts the element as the last child of the start container
+                               if (parentStart !== parent
+                                               && parentStart.lastChild
+                                               && parentStart.lastChild.nodeType === HTMLArea.DOM.ELEMENT_NODE
+                                               && parentStart.lastChild.nodeName.toLowerCase() === nodeName) {
+                                       parent.insertBefore(elementClone, upperParentStart.nextSibling);
+                                       parentStart.removeChild(parentStart.lastChild);
+                                               // Sometimes an empty previous sibling was created
+                                       if (elementClone.previousSibling
+                                                       && elementClone.previousSibling.nodeType === HTMLArea.DOM.ELEMENT_NODE
+                                                       && !elementClone.previousSibling.innerText) {
+                                               parent.removeChild(elementClone.previousSibling);
+                                       }
+                                               // The bookmark will not work anymore
+                                       newRange.moveToElementText(elementClone);
+                                       newRange.collapse(false);
+                                       newRange.select();
+                               } else {
+                                               // Working around IE boookmark bug
+                                       if (parentStart != parentEnd) {
+                                               var newRange = this.selection.createRange();
+                                               if (newRange.moveToBookmark(bookMark)) {
+                                                       newRange.collapse(false);
+                                                       newRange.select();
+                                               }
+                                       } else {
+                                               range.collapse(false);
+                                       }
+                               }
+                               parent.normalize();
+                       } else {
+                               var parent = range.item(0);
+                               element = parent.parentNode.insertBefore(element, parent);
+                               element.appendChild(parent);
+                               this.bookMark.moveTo(bookMark);
+                       }
+               } else {
+                       element.appendChild(range.extractContents());
+                       range.insertNode(element);
+                       element.normalize();
+                               // Sometimes Firefox inserts empty elements just outside the boundaries of the range
+                       var neighbour = element.previousSibling;
+                       if (neighbour && (neighbour.nodeType !== HTMLArea.DOM.TEXT_NODE) && !/\S/.test(neighbour.textContent)) {
+                               HTMLArea.DOM.removeFromParent(neighbour);
+                       }
+                       neighbour = element.nextSibling;
+                       if (neighbour && (neighbour.nodeType !== HTMLArea.DOM.TEXT_NODE) && !/\S/.test(neighbour.textContent)) {
+                               HTMLArea.DOM.removeFromParent(neighbour);
+                       }
+                       this.selection.selectNodeContents(element, false);
+               }
+       },
+       /*
+        * Get the position of the node within the document tree.
+        * The tree address returned is an array of integers, with each integer
+        * indicating a child index of a DOM node, starting from
+        * document.documentElement.
+        * The position cannot be used for finding back the DOM tree node once
+        * the DOM tree structure has been modified.
+        * Adapted from FCKeditor
+        *
+        * @param       object          node: the DOM node
+        * @param       boolean         normalized: if true, a normalized position is calculated
+        *
+        * @return      array           the position of the node
+        */
+       getPositionWithinTree: function (node, normalized) {
+               var documentElement = this.document.documentElement,
+                       current = node,
+                       position = [];
+               while (current && current != documentElement) {
+                       var parentNode = current.parentNode;
+                       if (parentNode) {
+                               // Get the current node position
+                               position.unshift(HTMLArea.DOM.getPositionWithinParent(current, normalized));
+                       }
+                       current = parentNode;
+               }
+               return position;
+       },
+       /**
+        * Clean Apple wrapping span and font elements under the specified node
+        *
+        * @param       object          node: the node in the subtree of which cleaning is performed
+        *
+        * @return      void
+        */
+       cleanAppleStyleSpans: function (node) {
+               if (HTMLArea.UserAgent.isWebKit || HTMLArea.UserAgent.isOpera) {
+                       if (node.getElementsByClassName) {
+                               var spans = node.getElementsByClassName('Apple-style-span');
+                               for (var i = spans.length; --i >= 0;) {
+                                       this.removeMarkup(spans[i]);
+                               }
+                       }
+                       var spans = node.getElementsByTagName('span');
+                       for (var i = spans.length; --i >= 0;) {
+                               if (HTMLArea.DOM.hasClass(spans[i], 'Apple-style-span')) {
+                                       this.removeMarkup(spans[i]);
+                               }
+                               if (/^(li)$/i.test(spans[i].parentNode.nodeName) && (spans[i].style.cssText.indexOf('line-height') !== -1 || spans[i].style.cssText.indexOf('font-family') !== -1 || spans[i].style.cssText.indexOf('font-size') !== -1)) {
+                                       this.removeMarkup(spans[i]);
+                               }
+                       }
+                       var fonts = node.getElementsByTagName('font');
+                       for (i = fonts.length; --i >= 0;) {
+                               if (HTMLArea.DOM.hasClass(fonts[i], 'Apple-style-span')) {
+                                       this.removeMarkup(fonts[i]);
+                               }
+                       }
+               }
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Selection.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Selection.js
new file mode 100644 (file)
index 0000000..5b8cd9e
--- /dev/null
@@ -0,0 +1,1037 @@
+/***************************************************
+ *  HTMLArea.DOM.Selection: Selection object
+ ***************************************************/
+HTMLArea.DOM.Selection = function (config) {
+};
+HTMLArea.DOM.Selection = Ext.extend(HTMLArea.DOM.Selection, {
+       /*
+        * Reference to the editor MUST be set in config
+        */
+       editor: null,
+       /*
+        * Reference to the editor document
+        */
+       document: null,
+       /*
+        * Reference to the editor iframe window
+        */
+       window: null,
+       /*
+        * The current selection
+        */
+       selection: null,
+       /*
+        * HTMLArea.DOM.Selection constructor
+        */
+       constructor: function (config) {
+                       // Apply config
+               Ext.apply(this, config);
+                       // Initialize references
+               this.document = this.editor.document;
+               this.window = this.editor.iframe.getEl().dom.contentWindow;
+                       // Set current selection
+               this.get();
+       },
+       /*
+        * Get the current selection object
+        *
+        * @return      object          this
+        */
+       get: function () {
+               this.editor.focus();
+               this.selection = this.window.getSelection ? this.window.getSelection() : this.document.selection;
+               return this;
+       },
+       /*
+        * Get the type of the current selection
+        *
+        * @return      string          the type of selection ("None", "Text" or "Control")
+        */
+       getType: function() {
+                       // By default set the type to "Text"
+               var type = 'Text';
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (typeof this.selection.getRangeAt === 'function') {
+                                       // Check if the current selection is a Control
+                               if (this.selection && this.selection.rangeCount == 1) {
+                                       var range = this.selection.getRangeAt(0);
+                                       if (range.startContainer.nodeType === HTMLArea.DOM.ELEMENT_NODE) {
+                                               if (
+                                                               // Gecko
+                                                       (range.startContainer == range.endContainer && (range.endOffset - range.startOffset) == 1) ||
+                                                               // Opera and WebKit
+                                                       (range.endContainer.nodeType === HTMLArea.DOM.TEXT_NODE && range.endOffset == 0 && range.startContainer.childNodes[range.startOffset].nextSibling == range.endContainer)
+                                               ) {
+                                                       if (/^(img|hr|li|table|tr|td|embed|object|ol|ul|dl)$/i.test(range.startContainer.childNodes[range.startOffset].nodeName)) {
+                                                               type = 'Control';
+                                                       }
+                                               }
+                                       }
+                               }
+                       } else {
+                                       // IE8 or IE7
+                               type = this.selection.type;
+                       }
+               }
+               return type;
+       },
+       /*
+        * Empty the current selection
+        *
+        * @return      object          this
+        */
+       empty: function () {
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (typeof this.selection.removeAllRanges === 'function') {
+                               this.selection.removeAllRanges();
+                       } else {
+                                       // IE8, IE7 or old version of WebKit
+                               this.selection.empty();
+                       }
+                       if (HTMLArea.UserAgent.isOpera) {
+                               this.editor.focus();
+                       }
+               }
+               return this;
+       },
+       /*
+        * Determine whether the current selection is empty or not
+        *
+        * @return      boolean         true, if the selection is empty
+        */
+       isEmpty: function () {
+               var isEmpty = true;
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                               switch (this.selection.type) {
+                                       case 'None':
+                                               isEmpty = true;
+                                               break;
+                                       case 'Text':
+                                               isEmpty = !this.createRange().text;
+                                               break;
+                                       default:
+                                               isEmpty = !this.createRange().htmlText;
+                                               break;
+                               }
+                       } else {
+                               isEmpty = this.selection.isCollapsed;
+                       }
+               }
+               return isEmpty;
+       },
+       /*
+        * Get a range corresponding to the current selection
+        *
+        * @return      object          the range of the selection
+        */
+       createRange: function () {
+               var range;
+               this.get();
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       range = this.selection.createRange();
+               } else {
+                       if (typeof this.selection !== 'object' || this.selection === null) {
+                               range = this.document.createRange();
+                       } else {
+                                       // Older versions of WebKit did not support getRangeAt
+                               if (HTMLArea.UserAgent.isWebKit && typeof this.selection.getRangeAt !== 'function') {
+                                       range = this.document.createRange();
+                                       if (this.selection.baseNode == null) {
+                                               range.setStart(this.document.body, 0);
+                                               range.setEnd(this.document.body, 0);
+                                       } else {
+                                               range.setStart(this.selection.baseNode, this.selection.baseOffset);
+                                               range.setEnd(this.selection.extentNode, this.selection.extentOffset);
+                                               if (range.collapsed != this.selection.isCollapsed) {
+                                                       range.setStart(this.selection.extentNode, this.selection.extentOffset);
+                                                       range.setEnd(this.selection.baseNode, this.selection.baseOffset);
+                                               }
+                                       }
+                               } else {
+                                       try {
+                                               range = this.selection.getRangeAt(0);
+                                       } catch (e) {
+                                               range = this.document.createRange();
+                                       }
+                               }
+                       }
+               }
+               return range;
+       },
+       /*
+        * Return the ranges of the selection
+        *
+        * @return      array           array of ranges
+        */
+       getRanges: function () {
+               this.get();
+               var ranges = [];
+                       // Older versions of WebKit, IE7 and IE8 did not support getRangeAt
+               if (typeof this.selection === 'object' && this.selection !== null && typeof this.selection.getRangeAt === 'function') {
+                       for (var i = this.selection.rangeCount; --i >= 0;) {
+                               ranges.push(this.selection.getRangeAt(i));
+                       }
+               } else {
+                       ranges.push(this.createRange());
+               }
+               return ranges;
+       },
+       /*
+        * Add a range to the selection
+        *
+        * @param       object          range: the range to be added to the selection
+        *
+        * @return      object          this
+        */
+       addRange: function (range) {
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (typeof this.selection.addRange === 'function') {
+                               this.selection.addRange(range);
+                       } else if (HTMLArea.UserAgent.isWebKit) {
+                               this.selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+                       }
+               }
+               return this;
+       },
+       /*
+        * Set the ranges of the selection
+        *
+        * @param       array           ranges: array of range to be added to the selection
+        *
+        * @return      object          this
+        */
+       setRanges: function (ranges) {
+               this.get();
+               this.empty();
+               for (var i = ranges.length; --i >= 0;) {
+                       this.addRange(ranges[i]);
+               }
+               return this;
+       },
+       /*
+        * Set the selection to a given range
+        *
+        * @param       object          range: the range to be selected
+        *
+        * @return      object          this
+        */
+       selectRange: function (range) {
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (typeof this.selection.getRangeAt === 'function') {
+                               this.empty().addRange(range);
+                       } else {
+                                       // IE8 or IE7
+                               range.select();
+                       }
+               }
+               return this;
+       },
+       /*
+        * Set the selection to a given node
+        *
+        * @param       object          node: the node to be selected
+        * @param       boolean         endPoint: collapse the selection at the start point (true) or end point (false) of the node
+        *
+        * @return      object          this
+        */
+       selectNode: function (node, endPoint) {
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                                       // IE8/7/6 cannot set this type of selection
+                               this.selectNodeContents(node, endPoint);
+                       } else if (HTMLArea.UserAgent.isWebKit && /^(img)$/i.test(node.nodeName)) {
+                               this.selection.setBaseAndExtent(node, 0, node, 1);
+                       } else {
+                               var range = this.document.createRange();
+                               if (node.nodeType === HTMLArea.DOM.ELEMENT_NODE && /^(body)$/i.test(node.nodeName)) {
+                                       if (HTMLArea.UserAgent.isWebKit) {
+                                               range.setStart(node, 0);
+                                               range.setEnd(node, node.childNodes.length);
+                                       } else {
+                                               range.selectNodeContents(node);
+                                       }
+                               } else {
+                                       range.selectNode(node);
+                               }
+                               if (typeof endPoint !== 'undefined') {
+                                       range.collapse(endPoint);
+                               }
+                               this.selectRange(range);
+                       }
+               }
+               return this;
+       },
+       /*
+        * Set the selection to the inner contents of a given node
+        *
+        * @param       object          node: the node of which the contents are to be selected
+        * @param       boolean         endPoint: collapse the selection at the start point (true) or end point (false)
+        *
+        * @return      object          this
+        */
+       selectNodeContents: function (node, endPoint) {
+               var range;
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null) {
+                       if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                               range = this.document.body.createTextRange();
+                               range.moveToElementText(node);
+                       } else {
+                               range = this.document.createRange();
+                               if (HTMLArea.UserAgent.isWebKit) {
+                                       range.setStart(node, 0);
+                                       if (node.nodeType === HTMLArea.DOM.TEXT_NODE || node.nodeType === HTMLArea.DOM.COMMENT_NODE || node.nodeType === HTMLArea.DOM.CDATA_SECTION_NODE) {
+                                               range.setEnd(node, node.textContent.length);
+                                       } else {
+                                               range.setEnd(node, node.childNodes.length);
+                                       }
+                               } else {
+                                       range.selectNodeContents(node);
+                               }
+                       }
+                       if (typeof endPoint !== 'undefined') {
+                               range.collapse(endPoint);
+                       }
+                       this.selectRange(range);
+               }
+               return this;
+       },
+       /*
+        * Get the deepest node that contains both endpoints of the current selection.
+        *
+        * @return      object          the deepest node that contains both endpoints of the current selection.
+        */
+       getParentElement: function () {
+               var parentElement,
+                       range;
+               this.get();
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       range = this.createRange();
+                       switch (this.selection.type) {
+                               case 'Text':
+                               case 'None':
+                                       parentElement = range.parentElement();
+                                       if (/^(form)$/i.test(parentElement.nodeName)) {
+                                               parentElement = this.document.body;
+                                       } else if (/^(li)$/i.test(parentElement.nodeName) && range.htmlText.replace(/\s/g, '') == parentElement.parentNode.outerHTML.replace(/\s/g, '')) {
+                                               parentElement = parentElement.parentNode;
+                                       }
+                                       break;
+                               case 'Control':
+                                       parentElement = range.item(0);
+                                       break;
+                               default:
+                                       parentElement = this.document.body;
+                                       break;
+                       }
+               } else {
+                       if (this.getType() === 'Control') {
+                               parentElement = this.getElement();
+                       } else {
+                               range = this.createRange();
+                               parentElement = range.commonAncestorContainer;
+                                       // Firefox 3 may report the document as commonAncestorContainer
+                               if (parentElement.nodeType === HTMLArea.DOM.DOCUMENT_NODE) {
+                                       parentElement = this.document.body;
+                               } else {
+                                       while (parentElement && parentElement.nodeType === HTMLArea.DOM.TEXT_NODE) {
+                                               parentElement = parentElement.parentNode;
+                                       }
+                               }
+                       }
+               }
+               return parentElement;
+       },
+       /*
+        * Get the selected element (if any), in the case that a single element (object like and image or a table) is selected
+        * In IE language, we have a range of type 'Control'
+        *
+        * @return      object          the selected node
+        */
+       getElement: function () {
+               var element = null;
+               this.get();
+               if (typeof this.selection === 'object' && this.selection !== null && this.selection.anchorNode && this.selection.anchorNode.nodeType === HTMLArea.DOM.ELEMENT_NODE && this.getType() == 'Control') {
+                       element = this.selection.anchorNode.childNodes[this.selection.anchorOffset];
+                               // For Safari, the anchor node for a control selection is the control itself
+                       if (!element) {
+                               element = this.selection.anchorNode;
+                       } else if (element.nodeType !== HTMLArea.DOM.ELEMENT_NODE) {
+                               element = null;
+                       }
+               }
+               return element;
+       },
+       /*
+        * Get the deepest element ancestor of the selection that is of one of the specified types
+        *
+        * @param       array           types: an array of nodeNames
+        *
+        * @return      object          the found ancestor of one of the given types or null
+        */
+       getFirstAncestorOfType: function (types) {
+               var node = this.getParentElement();
+               return HTMLArea.DOM.getFirstAncestorOfType(node, types);
+       },
+       /*
+        * Get an array with all the ancestor nodes of the current selection
+        *
+        * @return      array           the ancestor nodes
+        */
+       getAllAncestors: function () {
+               var parent = this.getParentElement(),
+                       ancestors = [];
+               while (parent && parent.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
+                       ancestors.push(parent);
+                       parent = parent.parentNode;
+               }
+               ancestors.push(this.document.body);
+               return ancestors;
+       },
+       /*
+        * Get an array with the parent elements of a multiple selection
+        *
+        * @return      array           the selected elements
+        */
+       getElements: function () {
+               var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null,
+                       elements = [];
+               if (statusBarSelection) {
+                       elements.push(statusBarSelection);
+               } else {
+                       var ranges = this.getRanges();
+                               parent;
+                       if (ranges.length > 1) {
+                               for (var i = ranges.length; --i >= 0;) {
+                                       parent = range[i].commonAncestorContainer;
+                                               // Firefox 3 may report the document as commonAncestorContainer
+                                       if (parent.nodeType === HTMLArea.DOM.DOCUMENT_NODE) {
+                                               parent = this.document.body;
+                                       } else {
+                                               while (parent && parent.nodeType === HTMLArea.DOM.TEXT_NODE) {
+                                                       parent = parent.parentNode;
+                                               }
+                                       }
+                                       elements.push(parent);
+                               }
+                       } else {
+                               elements.push(this.getParentElement());
+                       }
+               }
+               return elements;
+       },
+       /*
+        * Get the node whose contents are currently fully selected
+        *
+        * @return      object          the fully selected node, if any, null otherwise
+        */
+       getFullySelectedNode: function () {
+               var node = null,
+                       isFullySelected = false;
+               this.get();
+               if (!this.isEmpty()) {
+                       var type = this.getType();
+                       var range = this.createRange();
+                       var ancestors = this.getAllAncestors();
+                       for (var i = 0, n = ancestors.length; i < n; i++) {
+                               var ancestor = ancestors[i];
+                               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                                       isFullySelected = (type !== 'Control' && ancestor.innerText == range.text) || (type === 'Control' && ancestor.innerText == range.item(0).text);
+                               } else {
+                                       isFullySelected = (ancestor.textContent == range.toString());
+                               }
+                               if (isFullySelected) {
+                                       node = ancestor;
+                                       break;
+                               }
+                       }
+                               // Working around bug with WebKit selection
+                       if (HTMLArea.UserAgent.isWebKit && !isFullySelected) {
+                               var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null;
+                               if (statusBarSelection && statusBarSelection.textContent == range.toString()) {
+                                       isFullySelected = true;
+                                       node = statusBarSelection;
+                               }
+                       }
+               }
+               return node;
+       },
+       /*
+        * Get the block elements containing the start and the end points of the selection
+        *
+        * @return      object          object with properties start and end set to the end blocks of the selection
+        */
+       getEndBlocks: function () {
+               var range = this.createRange(),
+                       parentStart,
+                       parentEnd;
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       if (this.getType() === 'Control') {
+                               parentStart = range.item(0);
+                               parentEnd = parentStart;
+                       } else {
+                               var rangeEnd = range.duplicate();
+                               range.collapse(true);
+                               parentStart = range.parentElement();
+                               rangeEnd.collapse(false);
+                               parentEnd = rangeEnd.parentElement();
+                       }
+               } else {
+                       parentStart = range.startContainer;
+                       if (/^(body)$/i.test(parentStart.nodeName)) {
+                               parentStart = parentStart.firstChild;
+                       }
+                       parentEnd = range.endContainer;
+                       if (/^(body)$/i.test(parentEnd.nodeName)) {
+                               parentEnd = parentEnd.lastChild;
+                       }
+               }
+               while (parentStart && !HTMLArea.DOM.isBlockElement(parentStart)) {
+                       parentStart = parentStart.parentNode;
+               }
+               while (parentEnd && !HTMLArea.DOM.isBlockElement(parentEnd)) {
+                       parentEnd = parentEnd.parentNode;
+               }
+               return {
+                       start: parentStart,
+                       end: parentEnd
+               };
+       },
+       /*
+        * Determine whether the end poins of the current selection are within the same block
+        *
+        * @return      boolean         true if the end points of the current selection are in the same block
+        */
+       endPointsInSameBlock: function() {
+               var endPointsInSameBlock = true;
+               this.get();
+               if (!this.isEmpty()) {
+                       var parent = this.getParentElement();
+                       var endBlocks = this.getEndBlocks();
+                       endPointsInSameBlock = (endBlocks.start === endBlocks.end && !/^(table|thead|tbody|tfoot|tr)$/i.test(parent.nodeName));
+               }
+               return endPointsInSameBlock;
+       },
+       /*
+        * Retrieve the HTML contents of the current selection
+        *
+        * @return      string          HTML text of the current selection
+        */
+       getHtml: function () {
+               var range = this.createRange(),
+                       html = '';
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       if (this.getType() === 'Control') {
+                                       // We have a controlRange collection
+                               var bodyRange = this.document.body.createTextRange();
+                               bodyRange.moveToElementText(range(0));
+                               html = bodyRange.htmlText;
+                       } else {
+                               html = range.htmlText;
+                       }
+               } else if (!range.collapsed) {
+                       var cloneContents = range.cloneContents();
+                       if (!cloneContents) {
+                               cloneContents = this.document.createDocumentFragment();
+                       }
+                       html = this.editor.iframe.htmlRenderer.render(cloneContents, false);
+               }
+               return html;
+       },
+        /*
+        * Insert a node at the current position
+        * Delete the current selection, if any.
+        * Split the text node, if needed.
+        *
+        * @param       object          toBeInserted: the node to be inserted
+        *
+        * @return      object          this
+        */
+       insertNode: function (toBeInserted) {
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       this.insertHtml(toBeInserted.outerHTML);
+               } else {
+                       var range = this.createRange();
+                       range.deleteContents();
+                       toBeSelected = (toBeInserted.nodeType === HTMLArea.DOM.DOCUMENT_FRAGMENT_NODE) ? toBeInserted.lastChild : toBeInserted;
+                       range.insertNode(toBeInserted);
+                       this.selectNodeContents(toBeSelected, false);
+               }
+               return this;
+       },
+       /*
+        * Insert HTML source code at the current position
+        * Delete the current selection, if any.
+        *
+        * @param       string          html: the HTML source code
+        *
+        * @return      object          this
+        */
+       insertHtml: function (html) {
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       this.get();
+                       if (this.getType() === 'Control') {
+                               this.selection.clear();
+                               this.get();
+                       }
+                       var range = this.createRange();
+                       range.pasteHTML(html);
+               } else {
+                       this.editor.focus();
+                       var fragment = this.document.createDocumentFragment();
+                       var div = this.document.createElement('div');
+                       div.innerHTML = html;
+                       while (div.firstChild) {
+                               fragment.appendChild(div.firstChild);
+                       }
+                       this.insertNode(fragment);
+               }
+               return this;
+       },
+       /*
+        * Surround the selection with an element specified by its start and end tags
+        * Delete the selection, if any.
+        *
+        * @param       string          startTag: the start tag
+        * @param       string          endTag: the end tag
+        *
+        * @return      void
+        */
+       surroundHtml: function (startTag, endTag) {
+               this.insertHtml(startTag + this.getHtml().replace(HTMLArea.DOM.RE_bodyTag, '') + endTag);
+       },
+       /*
+        * Execute some native execCommand command on the current selection
+        *
+        * @param       string          cmdID: the command name or id
+        * @param       object          UI:
+        * @param       object          param:
+        *
+        * @return      boolean         false
+        */
+       execCommand: function (cmdID, UI, param) {
+               var success = true;
+               this.editor.focus();
+               try {
+                       this.document.execCommand(cmdID, UI, param);
+               } catch (e) {
+                       success = false;
+                       this.editor.appendToLog('HTMLArea.DOM.Selection', 'execCommand', e + ' by execCommand(' + cmdID + ')', 'error');
+               }
+               this.editor.updateToolbar();
+               return success;
+       },
+       /*
+        * Handle backspace event on the current selection
+        *
+        * @return      boolean         true to stop the event and cancel the default action
+        */
+       handleBackSpace: function () {
+               var range = this.createRange();
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       if (this.getType() === 'Control') {
+                                       // Deleting or backspacing on a control selection : delete the element
+                               var element = this.getParentElement();
+                               var parent = element.parentNode;
+                               parent.removeChild(el);
+                               return true;
+                       } else if (this.isEmpty()) {
+                                       // Check if deleting an empty block with a table as next sibling
+                               var element = this.getParentElement();
+                               if (!element.innerHTML && HTMLArea.DOM.isBlockElement(element) && element.nextSibling && /^table$/i.test(element.nextSibling.nodeName)) {
+                                       var previous = element.previousSibling;
+                                       if (!previous) {
+                                               this.selectNodeContents(element.nextSibling.rows[0].cells[0], true);
+                                       } else if (/^table$/i.test(previous.nodeName)) {
+                                               this.selectNodeContents(previous.rows[previous.rows.length-1].cells[previous.rows[previous.rows.length-1].cells.length-1], false);
+                                       } else {
+                                               range.moveStart('character', -1);
+                                               range.collapse(true);
+                                               range.select();
+                                       }
+                                       el.parentNode.removeChild(element);
+                                       return true;
+                               }
+                       } else {
+                                       // Backspacing into a link
+                               var range2 = range.duplicate();
+                               range2.moveStart('character', -1);
+                               var a = range2.parentElement();
+                               if (a != range.parentElement() && /^a$/i.test(a.nodeName)) {
+                                       range2.collapse(true);
+                                       range2.moveEnd('character', 1);
+                                       range2.pasteHTML('');
+                                       range2.select();
+                                       return true;
+                               }
+                               return false;
+                       }
+               } else {
+                       var self = this;
+                       window.setTimeout(function() {
+                               var range = self.createRange();
+                               var startContainer = range.startContainer;
+                               var startOffset = range.startOffset;
+                                       // If the selection is collapsed...
+                               if (self.isEmpty()) {
+                                               // ... and the cursor lies in a direct child of body...
+                                       if (/^(body)$/i.test(startContainer.nodeName)) {
+                                               var node = startContainer.childNodes[startOffset-1];
+                                       } else if (/^(body)$/i.test(startContainer.parentNode.nodeName)) {
+                                               var node = startContainer;
+                                       } else {
+                                               return false;
+                                       }
+                                               // ... which is a br or text node containing no non-whitespace character...
+                                       node.normalize();
+                                       if (/^(br|#text)$/i.test(node.nodeName) && !/\S/.test(node.textContent)) {
+                                                       // Get a meaningful previous sibling in which to reposition de cursor
+                                               var previousSibling = node.previousSibling;
+                                               while (previousSibling && /^(br|#text)$/i.test(previousSibling.nodeName) && !/\S/.test(previousSibling.textContent)) {
+                                                       previousSibling = previousSibling.previousSibling;
+                                               }
+                                                       // If there is no meaningful previous sibling, the cursor is at the start of body or the start of a direct child of body
+                                               if (previousSibling) {
+                                                               // Remove the node
+                                                       HTMLArea.DOM.removeFromParent(node);
+                                                               // Position the cursor
+                                                       if (/^(ol|ul|dl)$/i.test(previousSibling.nodeName)) {
+                                                               self.selectNodeContents(previousSibling.lastChild, false);
+                                                       } else if (/^(table)$/i.test(previousSibling.nodeName)) {
+                                                               self.selectNodeContents(previousSibling.rows[previousSibling.rows.length-1].cells[previousSibling.rows[previousSibling.rows.length-1].cells.length-1], false);
+                                                       } else if (!/\S/.test(previousSibling.textContent) && previousSibling.firstChild) {
+                                                               self.selectNode(previousSibling.firstChild, true);
+                                                       } else {
+                                                               self.selectNodeContents(previousSibling, false);
+                                                       }
+                                               }
+                                               // ... or the only child of body and having no child (IE) or only a br child (FF)
+                                       } else if (
+                                                       /^(body)$/i.test(node.parentNode.nodeName)
+                                                       && !/\S/.test(node.parentNode.textContent)
+                                                       && (node.childNodes.length === 0 || (node.childNodes.length === 1 && /^(br)$/i.test(node.firstChild.nodeName)))
+                                               ) {
+                                               var parentNode = node.parentNode;
+                                               HTMLArea.DOM.removeFromParent(node);
+                                               parentNode.innerHTML = '<br />';
+                                               self.selectNodeContents(parentNode, true);
+                                       }
+                               }
+                       }, 10);
+                       return false;
+               }
+       },
+       /*
+        * Detect emails and urls as they are typed in non-IE browsers
+        * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
+        *
+        * @param       object          event: the ExtJS key event
+        *
+        * @return      void
+        */
+       detectURL: function (event) {
+               var ev = event.browserEvent;
+               var editor = this.editor;
+               var selection = this.get().selection;
+               if (!/^(a)$/i.test(this.getParentElement().nodeName)) {
+                       var autoWrap = function (textNode, tag) {
+                               var rightText = textNode.nextSibling;
+                               if (typeof tag === 'string') {
+                                       tag = editor.document.createElement(tag);
+                               }
+                               var a = textNode.parentNode.insertBefore(tag, rightText);
+                               HTMLArea.DOM.removeFromParent(textNode);
+                               a.appendChild(textNode);
+                               selection.collapse(rightText, 0);
+                               rightText.parentNode.normalize();
+
+                               editor.unLink = function() {
+                                       var t = a.firstChild;
+                                       a.removeChild(t);
+                                       a.parentNode.insertBefore(t, a);
+                                       HTMLArea.DOM.removeFromParent(a);
+                                       t.parentNode.normalize();
+                                       editor.unLink = null;
+                                       editor.unlinkOnUndo = false;
+                               };
+
+                               editor.unlinkOnUndo = true;
+                               return a;
+                       };
+                       switch (ev.which) {
+                                       // Space or Enter or >, see if the text just typed looks like a URL, or email address and link it accordingly
+                               case 13:
+                               case 32:
+                                       if (selection && selection.isCollapsed && selection.anchorNode.nodeType === HTMLArea.DOM.TEXT_NODE && selection.anchorNode.data.length > 3 && selection.anchorNode.data.indexOf('.') >= 0) {
+                                               var midStart = selection.anchorNode.data.substring(0,selection.anchorOffset).search(/[a-zA-Z0-9]+\S{3,}$/);
+                                               if (midStart == -1) {
+                                                       break;
+                                               }
+                                               if (this.getFirstAncestorOfType('a')) {
+                                                               // already in an anchor
+                                                       break;
+                                               }
+                                               var matchData = selection.anchorNode.data.substring(0,selection.anchorOffset).replace(/^.*?(\S*)$/, '$1');
+                                               if (matchData.indexOf('@') != -1) {
+                                                       var m = matchData.match(HTMLArea.RE_email);
+                                                       if (m) {
+                                                               var leftText  = selection.anchorNode;
+                                                               var rightText = leftText.splitText(selection.anchorOffset);
+                                                               var midText   = leftText.splitText(midStart);
+                                                               var midEnd = midText.data.search(/[^a-zA-Z0-9\.@_\-]/);
+                                                               if (midEnd != -1) {
+                                                                       var endText = midText.splitText(midEnd);
+                                                               }
+                                                               autoWrap(midText, 'a').href = 'mailto:' + m[0];
+                                                               break;
+                                                       }
+                                               }
+                                               var m = matchData.match(HTMLArea.RE_url);
+                                               if (m) {
+                                                       var leftText  = selection.anchorNode;
+                                                       var rightText = leftText.splitText(selection.anchorOffset);
+                                                       var midText   = leftText.splitText(midStart);
+                                                       var midEnd = midText.data.search(/[^a-zA-Z0-9\._\-\/\&\?=:@]/);
+                                                       if (midEnd != -1) {
+                                                               var endText = midText.splitText(midEnd);
+                                                       }
+                                                       autoWrap(midText, 'a').href = (m[1] ? m[1] : 'http://') + m[3];
+                                                       break;
+                                               }
+                                       }
+                                       break;
+                               default:
+                                       if (ev.keyCode == 27 || (editor.unlinkOnUndo && ev.ctrlKey && ev.which == 122)) {
+                                               if (editor.unLink) {
+                                                       editor.unLink();
+                                                       event.stopEvent();
+                                               }
+                                               break;
+                                       } else if (ev.which || ev.keyCode == 8 || ev.keyCode == 46) {
+                                               editor.unlinkOnUndo = false;
+                                               if (selection.anchorNode && selection.anchorNode.nodeType === HTMLArea.DOM.TEXT_NODE) {
+                                                               // See if we might be changing a link
+                                                       var a = this.getFirstAncestorOfType('a');
+                                                       if (!a) {
+                                                               break;
+                                                       }
+                                                       if (!a.updateAnchorTimeout) {
+                                                               if (selection.anchorNode.data.match(HTMLArea.RE_email) && (a.href.match('mailto:' + selection.anchorNode.data.trim()))) {
+                                                                       var textNode = selection.anchorNode;
+                                                                       var fn = function() {
+                                                                               a.href = 'mailto:' + textNode.data.trim();
+                                                                               a.updateAnchorTimeout = setTimeout(fn, 250);
+                                                                       };
+                                                                       a.updateAnchorTimeout = setTimeout(fn, 250);
+                                                                       break;
+                                                               }
+                                                               var m = selection.anchorNode.data.match(HTMLArea.RE_url);
+                                                               if (m && a.href.match(selection.anchorNode.data.trim())) {
+                                                                       var textNode = selection.anchorNode;
+                                                                       var fn = function() {
+                                                                               var m = textNode.data.match(HTMLArea.RE_url);
+                                                                               a.href = (m[1] ? m[1] : 'http://') + m[3];
+                                                                               a.updateAnchorTimeout = setTimeout(fn, 250);
+                                                                       }
+                                                                       a.updateAnchorTimeout = setTimeout(fn, 250);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                                       break;
+                       }
+               }
+       },
+       /*
+        * Enter event handler
+        *
+        * @return      boolean         true to stop the event and cancel the default action
+        */
+       checkInsertParagraph: function() {
+               var editor = this.editor;
+               var left, right, rangeClone,
+                       sel     = this.get().selection,
+                       range   = this.createRange(),
+                       p       = this.getAllAncestors(),
+                       block   = null,
+                       a       = null,
+                       doc     = this.document;
+               for (var i = 0, n = p.length; i < n; ++i) {
+                       if (HTMLArea.DOM.isBlockElement(p[i]) && !/^(html|body|table|tbody|thead|tfoot|tr|dl)$/i.test(p[i].nodeName)) {
+                               block = p[i];
+                               break;
+                       }
+               }
+               if (block && /^(td|th|tr|tbody|thead|tfoot|table)$/i.test(block.nodeName) && this.editor.config.buttons.table && this.editor.config.buttons.table.disableEnterParagraphs) {
+                       return false;
+               }
+               if (!range.collapsed) {
+                       range.deleteContents();
+               }
+               this.empty();
+               if (!block || /^(td|div|article|aside|footer|header|nav|section)$/i.test(block.nodeName)) {
+                       if (!block) {
+                               block = doc.body;
+                       }
+                       if (block.hasChildNodes()) {
+                               rangeClone = range.cloneRange();
+                               if (range.startContainer == block) {
+                                               // Selection is directly under the block
+                                       var blockOnLeft = null;
+                                       var leftSibling = null;
+                                               // Looking for the farthest node on the left that is not a block
+                                       for (var i = range.startOffset; --i >= 0;) {
+                                               if (HTMLArea.DOM.isBlockElement(block.childNodes[i])) {
+                                                       blockOnLeft = block.childNodes[i];
+                                                       break;
+                                               } else {
+                                                       rangeClone.setStartBefore(block.childNodes[i]);
+                                               }
+                                       }
+                               } else {
+                                               // Looking for inline or text container immediate child of block
+                                       var inlineContainer = range.startContainer;
+                                       while (inlineContainer.parentNode != block) {
+                                               inlineContainer = inlineContainer.parentNode;
+                                       }
+                                               // Looking for the farthest node on the left that is not a block
+                                       var leftSibling = inlineContainer;
+                                       while (leftSibling.previousSibling && !HTMLArea.DOM.isBlockElement(leftSibling.previousSibling)) {
+                                               leftSibling = leftSibling.previousSibling;
+                                       }
+                                       rangeClone.setStartBefore(leftSibling);
+                                       var blockOnLeft = leftSibling.previousSibling;
+                               }
+                                       // Avoiding surroundContents buggy in Opera and Safari
+                               left = doc.createElement('p');
+                               left.appendChild(rangeClone.extractContents());
+                               if (!left.textContent && !left.getElementsByTagName('img').length && !left.getElementsByTagName('table').length) {
+                                       left.innerHTML = '<br />';
+                               }
+                               if (block.hasChildNodes()) {
+                                       if (blockOnLeft) {
+                                               left = block.insertBefore(left, blockOnLeft.nextSibling);
+                                       } else {
+                                               left = block.insertBefore(left, block.firstChild);
+                                       }
+                               } else {
+                                       left = block.appendChild(left);
+                               }
+                               block.normalize();
+                                       // Looking for the farthest node on the right that is not a block
+                               var rightSibling = left;
+                               while (rightSibling.nextSibling && !HTMLArea.DOM.isBlockElement(rightSibling.nextSibling)) {
+                                       rightSibling = rightSibling.nextSibling;
+                               }
+                               var blockOnRight = rightSibling.nextSibling;
+                               range.setEndAfter(rightSibling);
+                               range.setStartAfter(left);
+                                       // Avoiding surroundContents buggy in Opera and Safari
+                               right = doc.createElement('p');
+                               right.appendChild(range.extractContents());
+                               if (!right.textContent && !right.getElementsByTagName('img').length && !right.getElementsByTagName('table').length) {
+                                       right.innerHTML = '<br />';
+                               }
+                               if (!(left.childNodes.length == 1 && right.childNodes.length == 1 && left.firstChild.nodeName.toLowerCase() == 'br' && right.firstChild.nodeName.toLowerCase() == 'br')) {
+                                       if (blockOnRight) {
+                                               right = block.insertBefore(right, blockOnRight);
+                                       } else {
+                                               right = block.appendChild(right);
+                                       }
+                                       this.selectNodeContents(right, true);
+                               } else {
+                                       this.selectNodeContents(left, true);
+                               }
+                               block.normalize();
+                       } else {
+                               var first = block.firstChild;
+                               if (first) {
+                                       block.removeChild(first);
+                               }
+                               right = doc.createElement('p');
+                               if (HTMLArea.UserAgent.isWebKit || HTMLArea.UserAgent.isOpera) {
+                                       right.innerHTML = '<br />';
+                               }
+                               right = block.appendChild(right);
+                               this.selectNodeContents(right, true);
+                       }
+               } else {
+                       range.setEndAfter(block);
+                       var df = range.extractContents(), left_empty = false;
+                       if (!/\S/.test(block.innerHTML) || (!/\S/.test(block.textContent) && !/<(img|hr|table)/i.test(block.innerHTML))) {
+                               if (!HTMLArea.UserAgent.isOpera) {
+                                       block.innerHTML = '<br />';
+                               }
+                               left_empty = true;
+                       }
+                       p = df.firstChild;
+                       if (p) {
+                               if (!/\S/.test(p.innerHTML) || (!/\S/.test(p.textContent) && !/<(img|hr|table)/i.test(p.innerHTML))) {
+                                       if (/^h[1-6]$/i.test(p.nodeName)) {
+                                               p = HTMLArea.DOM.convertNode(p, 'p');
+                                       }
+                                       if (/^(dt|dd)$/i.test(p.nodeName)) {
+                                                p = HTMLArea.DOM.convertNode(p, /^(dt)$/i.test(p.nodeName) ? 'dd' : 'dt');
+                                       }
+                                       if (!HTMLArea.UserAgent.isOpera) {
+                                               p.innerHTML = '<br />';
+                                       }
+                                       if (/^li$/i.test(p.nodeName) && left_empty && (!block.nextSibling || !/^li$/i.test(block.nextSibling.nodeName))) {
+                                               left = block.parentNode;
+                                               left.removeChild(block);
+                                               range.setEndAfter(left);
+                                               range.collapse(false);
+                                               p = HTMLArea.DOM.convertNode(p, /^(li|dd|td|th|p|h[1-6])$/i.test(left.parentNode.nodeName) ? 'br' : 'p');
+                                       }
+                               }
+                               range.insertNode(df);
+                                       // Remove any anchor created empty on both sides of the selection
+                               if (p.previousSibling) {
+                                       var a = p.previousSibling.lastChild;
+                                       if (a && /^a$/i.test(a.nodeName) && !/\S/.test(a.innerHTML)) {
+                                               HTMLArea.DOM.convertNode(a, 'br');
+                                       }
+                               }
+                               var a = p.lastChild;
+                               if (a && /^a$/i.test(a.nodeName) && !/\S/.test(a.innerHTML)) {
+                                       HTMLArea.DOM.convertNode(a, 'br');
+                               }
+                                       // Walk inside the deepest child element (presumably inline element)
+                               while (p.firstChild && p.firstChild.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(br|img|hr|table)$/i.test(p.firstChild.nodeName)) {
+                                       p = p.firstChild;
+                               }
+                               if (/^br$/i.test(p.nodeName)) {
+                                       p = p.parentNode.insertBefore(doc.createTextNode('\x20'), p);
+                               } else if (!/\S/.test(p.innerHTML)) {
+                                               // Need some element inside the deepest element
+                                       p.appendChild(doc.createElement('br'));
+                               }
+                               this.selectNodeContents(p, true);
+                       } else {
+                               if (/^(li|dt|dd)$/i.test(block.nodeName)) {
+                                       p = doc.createElement(block.nodeName);
+                               } else {
+                                       p = doc.createElement('p');
+                               }
+                               if (!HTMLArea.UserAgent.isOpera) {
+                                       p.innerHTML = '<br />';
+                               }
+                               if (block.nextSibling) {
+                                       p = block.parentNode.insertBefore(p, block.nextSibling);
+                               } else {
+                                       p = block.parentNode.appendChild(p);
+                               }
+                               this.selectNodeContents(p, true);
+                       }
+               }
+               this.editor.scrollToCaret();
+               return true;
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Walker.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.Walker.js
new file mode 100644 (file)
index 0000000..99dca95
--- /dev/null
@@ -0,0 +1,224 @@
+/***************************************************
+ *  HTMLArea.DOM.Walker: DOM tree walk
+ ***************************************************/
+HTMLArea.DOM.Walker = function (config) {
+       var configDefaults = {
+               keepComments: false,
+               keepCDATASections: false,
+               removeTags: /none/i,
+               removeTagsAndContents: /none/i,
+               keepTags: /.*/i,
+               removeAttributes: /none/i,
+               removeTrailingBR: true,
+               baseUrl: ''
+       };
+       Ext.apply(this, config, configDefaults);
+};
+HTMLArea.DOM.Walker = Ext.extend(HTMLArea.DOM.Walker, {
+       /*
+        * Walk the DOM tree
+        *
+        * @param       object          node: the root node of the tree
+        * @param       boolean         includeNode: if set, apply callback to the node
+        * @param       string          startCallback: a function call to be evaluated on each node, before walking the children
+        * @param       string          endCallback: a function call to be evaluated on each node, after walking the children
+        * @param       array           args: array of arguments
+        * @return      void
+        */
+       walk: function (node, includeNode, startCallback, endCallback, args) {
+               if (!this.removeTagsAndContents.test(node.nodeName)) {
+                       if (includeNode) {
+                               eval(startCallback);
+                       }
+                               // Walk the children
+                       var child = node.firstChild;
+                       while (child) {
+                               this.walk(child, true, startCallback, endCallback, args);
+                               child = child.nextSibling;
+                       }
+                       if (includeNode) {
+                               eval(endCallback);
+                       }
+               }
+       },
+       /*
+        * Generate html string from DOM tree
+        *
+        * @param       object          node: the root node of the tree
+        * @param       boolean         includeNode: if set, apply callback to root element
+        * @return      string          rendered html code
+        */
+       render: function (node, includeNode) {
+               this.html = '';
+               this.walk(node, includeNode, 'args[0].renderNodeStart(node)', 'args[0].renderNodeEnd(node)', [this]);
+               return this.html;
+       },
+       /*
+        * Generate html string for the start of a node
+        *
+        * @param       object          node: the root node of the tree
+        * @return      string          rendered html code (accumulated in this.html)
+        */
+       renderNodeStart: function (node) {
+               var html = '';
+               switch (node.nodeType) {
+                       case HTMLArea.DOM.ELEMENT_NODE:
+                               if (this.keepTags.test(node.nodeName) && !this.removeTags.test(node.nodeName)) {
+                                       html += this.setOpeningTag(node);
+                               }
+                               break;
+                       case HTMLArea.DOM.TEXT_NODE:
+                               html += /^(script|style)$/i.test(node.parentNode.nodeName) ? node.data : HTMLArea.util.htmlEncode(node.data);
+                               break;
+                       case HTMLArea.DOM.ENTITY_NODE:
+                               html += node.nodeValue;
+                               break;
+                       case HTMLArea.DOM.ENTITY_REFERENCE_NODE:
+                               html += '&' + node.nodeValue + ';';
+                               break;
+                       case HTMLArea.DOM.COMMENT_NODE:
+                               if (this.keepComments) {
+                                       html += '<!--' + node.data + '-->';
+                               }
+                               break;
+                       case HTMLArea.DOM.CDATA_SECTION_NODE:
+                               if (this.keepCDATASections) {
+                                       html += '<![CDATA[' + node.data + ']]>';
+                               }
+                               break;
+                       default:
+                                       // Ignore all other node types
+                               break;
+               }
+               this.html += html;
+       },
+       /*
+        * Generate html string for the end of a node
+        *
+        * @param       object          node: the root node of the tree
+        * @return      string          rendered html code (accumulated in this.html)
+        */
+       renderNodeEnd: function (node) {
+               var html = '';
+               if (node.nodeType === HTMLArea.DOM.ELEMENT_NODE) {
+                       if (this.keepTags.test(node.nodeName) && !this.removeTags.test(node.nodeName)) {
+                               html += this.setClosingTag(node);
+                       }
+               }
+               this.html += html;
+       },
+       /*
+        * Get the attributes of the node, filtered and cleaned-up
+        *
+        * @param       object          node: the node
+        * @return      object          an object with attribute name as key and attribute value as value
+        */
+       getAttributes: function (node) {
+               var attributes = node.attributes;
+               var filterededAttributes = [];
+               var attribute, attributeName, attributeValue;
+               for (var i = attributes.length; --i >= 0;) {
+                       attribute = attributes.item(i);
+                       attributeName = attribute.nodeName.toLowerCase();
+                       attributeValue = attribute.nodeValue;
+                               // Ignore some attributes and those configured to be removed
+                       if (/_moz|contenteditable|complete/.test(attributeName) || this.removeAttributes.test(attributeName)) {
+                               continue;
+                       }
+                               // Ignore default values except for the value attribute
+                       if (!attribute.specified && attributeName !== 'value') {
+                               continue;
+                       }
+                       if (HTMLArea.UserAgent.isIE) {
+                                       // IE before I9 fails to put style in attributes list.
+                               if (attributeName === 'style') {
+                                       if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                                               attributeValue = node.style.cssText;
+                                       }
+                                       // May need to strip the base url
+                               } else if (attributeName === 'href' || attributeName === 'src') {
+                                       attributeValue = this.stripBaseURL(attributeValue);
+                                       // Ignore value="0" reported by IE on all li elements
+                               } else if (attributeName === 'value' && /^li$/i.test(node.nodeName) && attributeValue == 0) {
+                                       continue;
+                               }
+                       } else if (HTMLArea.UserAgent.isGecko) {
+                                       // Ignore special values reported by Mozilla
+                               if (/(_moz|^$)/.test(attributeValue)) {
+                                       continue;
+                                       // Pasted internal url's are made relative by Mozilla: https://bugzilla.mozilla.org/show_bug.cgi?id=613517
+                               } else if (attributeName === 'href' || attributeName === 'src') {
+                                       attributeValue = HTMLArea.DOM.addBaseUrl(attributeValue, this.baseUrl);
+                               }
+                       }
+                               // Ignore id attributes generated by ExtJS
+                       if (attributeName === 'id' && /^ext-gen/.test(attributeValue)) {
+                               continue;
+                       }
+                       filterededAttributes.push({
+                               attributeName: attributeName,
+                               attributeValue: attributeValue
+                       });
+               }
+               return (HTMLArea.UserAgent.isWebKit || HTMLArea.UserAgent.isOpera) ? filterededAttributes.reverse() : filterededAttributes;
+       },
+       /*
+        * Set opening tag for a node
+        *
+        * @param       object          node: the node
+        * @return      object          opening tag
+        */
+       setOpeningTag: function (node) {
+               var html = '';
+                       // Handle br oddities
+               if (/^br$/i.test(node.nodeName)) {
+                               // Remove Mozilla special br node
+                       if (HTMLArea.UserAgent.isGecko && node.hasAttribute('_moz_editor_bogus_node')) {
+                               return html;
+                               // In Gecko, whenever some text is entered in an empty block, a trailing br tag is added by the browser.
+                               // If the br element is a trailing br in a block element with no other content or with content other than a br, it may be configured to be removed
+                       } else if (this.removeTrailingBR && !node.nextSibling && HTMLArea.DOM.isBlockElement(node.parentNode) && (!node.previousSibling || !/^br$/i.test(node.previousSibling.nodeName))) {
+                                               // If an empty paragraph with a class attribute, insert a non-breaking space so that RTE transform does not clean it away
+                                       if (!node.previousSibling && node.parentNode && /^p$/i.test(node.parentNode.nodeName) && node.parentNode.className) {
+                                               html += "&nbsp;";
+                                       }
+                               return html;
+                       }
+               }
+                       // Normal node
+               var attributes = this.getAttributes(node);
+               for (var i = 0, n = attributes.length; i < n; i++) {
+                       html +=  ' ' + attributes[i]['attributeName'] + '="' + HTMLArea.util.htmlEncode(attributes[i]['attributeValue']) + '"';
+               }
+               html = '<' + node.nodeName.toLowerCase() + html + (HTMLArea.DOM.RE_noClosingTag.test(node.nodeName) ? ' />' : '>');
+                       // Fix orphan list elements
+               if (/^li$/i.test(node.nodeName) && !/^[ou]l$/i.test(node.parentNode.nodeName)) {
+                       html = '<ul>' + html;
+               }
+               return html;
+       },
+       /*
+        * Set closing tag for a node
+        *
+        * @param       object          node: the node
+        * @return      object          closing tag, if required
+        */
+       setClosingTag: function (node) {
+               var html = HTMLArea.DOM.RE_noClosingTag.test(node.nodeName) ? '' : '</' + node.nodeName.toLowerCase() + '>';
+                       // Fix orphan list elements
+               if (/^li$/i.test(node.nodeName) && !/^[ou]l$/i.test(node.parentNode.nodeName)) {
+                       html += '</ul>';
+               }
+               return html;
+       },
+       /*
+        * Strip base url
+        * May be overridden by link handling plugin
+        *
+        * @param       string          value: value of a href or src attribute
+        * @return      tring           stripped value
+        */
+       stripBaseURL: function (value) {
+               return value;
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/DOM/HTMLArea.DOM.js
new file mode 100644 (file)
index 0000000..2152c97
--- /dev/null
@@ -0,0 +1,463 @@
+/*****************************************************************
+ * HTMLArea.DOM: Utility functions for dealing with the DOM tree *
+ *****************************************************************/
+HTMLArea.DOM = function () {
+       return {
+               /***************************************************
+               *  DOM-RELATED CONSTANTS
+               ***************************************************/
+                       // DOM node types
+               ELEMENT_NODE: 1,
+               ATTRIBUTE_NODE: 2,
+               TEXT_NODE: 3,
+               CDATA_SECTION_NODE: 4,
+               ENTITY_REFERENCE_NODE: 5,
+               ENTITY_NODE: 6,
+               PROCESSING_INSTRUCTION_NODE: 7,
+               COMMENT_NODE: 8,
+               DOCUMENT_NODE: 9,
+               DOCUMENT_TYPE_NODE: 10,
+               DOCUMENT_FRAGMENT_NODE: 11,
+               NOTATION_NODE: 12,
+               /***************************************************
+               *  DOM-RELATED REGULAR EXPRESSIONS
+               ***************************************************/
+               RE_blockTags: /^(address|article|aside|body|blockquote|caption|dd|div|dl|dt|fieldset|footer|form|header|hr|h1|h2|h3|h4|h5|h6|iframe|li|ol|p|pre|nav|noscript|section|table|tbody|td|tfoot|th|thead|tr|ul)$/i,
+               RE_noClosingTag: /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i,
+               RE_bodyTag: new RegExp('<\/?(body)[^>]*>', 'gi'),
+               /***************************************************
+               *  STATIC METHODS ON DOM NODE
+               ***************************************************/
+               /*
+                * Determine whether an element node is a block element
+                *
+                * @param       object          element: the element node
+                *
+                * @return      boolean         true, if the element node is a block element
+                */
+               isBlockElement: function (element) {
+                       return element && element.nodeType === HTMLArea.DOM.ELEMENT_NODE && HTMLArea.DOM.RE_blockTags.test(element.nodeName);
+               },
+               /*
+                * Determine whether an element node needs a closing tag
+                *
+                * @param       object          element: the element node
+                *
+                * @return      boolean         true, if the element node needs a closing tag
+                */
+               needsClosingTag: function (element) {
+                       return element && element.nodeType === HTMLArea.DOM.ELEMENT_NODE && !HTMLArea.DOM.RE_noClosingTag.test(element.nodeName);
+               },
+               /*
+                * Gets the class names assigned to a node, reserved classes removed
+                *
+                * @param       object          node: the node
+                * @return      array           array of class names on the node, reserved classes removed
+                */
+               getClassNames: function (node) {
+                       var classNames = [];
+                       if (node) {
+                               if (node.className && /\S/.test(node.className)) {
+                                       classNames = node.className.trim().split(' ');
+                               }
+                               if (HTMLArea.reservedClassNames.test(node.className)) {
+                                       var cleanClassNames = [];
+                                       var j = -1;
+                                       for (var i = 0, n = classNames.length; i < n; ++i) {
+                                               if (!HTMLArea.reservedClassNames.test(classNames[i])) {
+                                                       cleanClassNames[++j] = classNames[i];
+                                               }
+                                       }
+                                       classNames = cleanClassNames;
+                               }
+                       }
+                       return classNames;
+               },
+               /*
+                * Check if a class name is in the class attribute of a node
+                *
+                * @param       object          node: the node
+                * @param       string          className: the class name to look for
+                * @param       boolean         substring: if true, look for a class name starting with the given string
+                * @return      boolean         true if the class name was found, false otherwise
+                */
+               hasClass: function (node, className, substring) {
+                       var found = false;
+                       if (node && node.className) {
+                               var classes = node.className.trim().split(' ');
+                               for (var i = classes.length; --i >= 0;) {
+                                       found = ((classes[i] == className) || (substring && classes[i].indexOf(className) == 0));
+                                       if (found) {
+                                               break;
+                                       }
+                               }
+                       }
+                       return found;
+               },
+               /**
+                * Add a class name to the class attribute of a node
+                *
+                * @param object node: the node
+                * @param string className: the name of the class to be added
+                * @param integer recursionLevel: recursion level of current call
+                * @return void
+                */
+               addClass: function (node, className, recursionLevel) {
+                       if (node) {
+                               var classNames = HTMLArea.DOM.getClassNames(node);
+                               if (classNames.indexOf(className) === -1) {
+                                       // Remove classes configured to be incompatible with the class to be added
+                                       if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && Ext.isFunction(HTMLArea.classesXOR[className].test)) {
+                                               for (var i = classNames.length; --i >= 0;) {
+                                                       if (HTMLArea.classesXOR[className].test(classNames[i])) {
+                                                               HTMLArea.DOM.removeClass(node, classNames[i]);
+                                                       }
+                                               }
+                                       }
+                                       // Check dependencies to add required classes recursively
+                                       if (typeof HTMLArea.classesRequires !== 'undefined' && typeof HTMLArea.classesRequires[className] !== 'undefined') {
+                                               if (typeof recursionLevel === 'undefined') {
+                                                       var recursionLevel = 1;
+                                               } else {
+                                                       recursionLevel++;
+                                               }
+                                               if (recursionLevel < 20) {
+                                                       for (var i = 0, n = HTMLArea.classesRequires[className].length; i < n; i++) { 
+                                                               var classNames = HTMLArea.DOM.getClassNames(node);
+                                                               if (classNames.indexOf(HTMLArea.classesRequires[className][i]) === -1) {
+                                                                       HTMLArea.DOM.addClass(node, HTMLArea.classesRequires[className][i], recursionLevel);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                                       if (node.className) {
+                                               node.className += ' ' + className;
+                                       } else {
+                                               node.className = className;
+                                       }
+                               }
+                       }
+               },
+
+               /**
+                * Remove a class name from the class attribute of a node
+                *
+                * @param       object          node: the node
+                * @param       string          className: the class name to removed
+                * @param       boolean         substring: if true, remove the class names starting with the given string
+                * @return      void
+                */
+               removeClass: function (node, className, substring) {
+                       if (node && node.className) {
+                               var classes = node.className.trim().split(' ');
+                               var newClasses = [];
+                               for (var i = classes.length; --i >= 0;) {
+                                       if ((!substring && classes[i] != className) || (substring && classes[i].indexOf(className) != 0)) {
+                                               newClasses[newClasses.length] = classes[i];
+                                       }
+                               }
+                               if (newClasses.length) {
+                                       node.className = newClasses.join(' ');
+                               } else {
+                                       if (!Ext.isOpera) {
+                                               node.removeAttribute('class');
+                                               if (HTMLArea.isIEBeforeIE9) {
+                                                       node.removeAttribute('className');
+                                               }
+                                       } else {
+                                               node.className = '';
+                                       }
+                               }
+                               // Remove the first unselectable class that is no more required, the following ones being removed by recursive calls
+                               if (node.className && typeof HTMLArea.classesSelectable !== 'undefined') {
+                                       classes = HTMLArea.DOM.getClassNames(node);
+                                       for (var i = classes.length; --i >= 0;) {
+                                               if (typeof HTMLArea.classesSelectable[classes[i]] !== 'undefined' && !HTMLArea.classesSelectable[classes[i]] && !HTMLArea.DOM.isRequiredClass(node, classes[i])) {
+                                                       HTMLArea.DOM.removeClass(node, classes[i]);
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+               },
+
+               /**
+                * Check if the class is required by another class assigned to the node
+                * 
+                * @param object node: the node
+                * @param string className: the class name to check
+                * @return boolean 
+                */
+               isRequiredClass: function (node, className) {
+                       if (typeof HTMLArea.classesRequiredBy !== 'undefined') {
+                               var classes = HTMLArea.DOM.getClassNames(node);
+                               for (var i = classes.length; --i >= 0;) {
+                                       if (typeof HTMLArea.classesRequiredBy[classes[i]] !== 'undefined' && HTMLArea.classesRequiredBy[classes[i]].indexOf(className) !== -1) {
+                                               return true;
+                                       }
+                               }
+                       }
+                       return false;
+               },
+
+               /**
+                * Get the innerText of a given node
+                *
+                * @param       object          node: the node
+                *
+                * @return      string          the text inside the node
+                */
+               getInnerText: function (node) {
+                       return HTMLArea.isIEBeforeIE9 ? node.innerText : node.textContent;;
+               },
+               /*
+                * Get the block ancestors of a node within a given block
+                *
+                * @param       object          node: the given node
+                * @param       object          withinBlock: the containing node
+                *
+                * @return      array           array of block ancestors
+                */
+               getBlockAncestors: function (node, withinBlock) {
+                       var ancestors = [];
+                       var ancestor = node;
+                       while (ancestor && (ancestor.nodeType === HTMLArea.DOM.ELEMENT_NODE) && !/^(body)$/i.test(ancestor.nodeName) && ancestor != withinBlock) {
+                               if (HTMLArea.DOM.isBlockElement(ancestor)) {
+                                       ancestors.unshift(ancestor);
+                               }
+                               ancestor = ancestor.parentNode;
+                       }
+                       ancestors.unshift(ancestor);
+                       return ancestors;
+               },
+               /*
+                * Get the deepest element ancestor of a given node that is of one of the specified types
+                *
+                * @param       object          node: the given node
+                * @param       array           types: an array of nodeNames
+                *
+                * @return      object          the found ancestor of one of the given types or null
+                */
+               getFirstAncestorOfType: function (node, types) {
+                       var ancestor = null,
+                               parent = node;
+                       if (!Ext.isEmpty(types)) {
+                               if (Ext.isString(types)) {
+                                       var types = [types];
+                               }
+                               types = new RegExp( '^(' + types.join('|') + ')$', 'i');
+                               while (parent && parent.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
+                                       if (types.test(parent.nodeName)) {
+                                               ancestor = parent;
+                                               break;
+                                       }
+                                       parent = parent.parentNode;
+                               }
+                       }
+                       return ancestor;
+               },
+               /*
+                * Get the position of the node within the children of its parent
+                * Adapted from FCKeditor
+                *
+                * @param       object          node: the DOM node
+                * @param       boolean         normalized: if true, a normalized position is calculated
+                *
+                * @return      integer         the position of the node
+                */
+               getPositionWithinParent: function (node, normalized) {
+                       var current = node,
+                               position = 0;
+                       while (current = current.previousSibling) {
+                               // For a normalized position, do not count any empty text node or any text node following another one
+                               if (
+                                       normalized
+                                       && current.nodeType == HTMLArea.DOM.TEXT_NODE
+                                       && (!current.nodeValue.length || (current.previousSibling && current.previousSibling.nodeType == HTMLArea.DOM.TEXT_NODE))
+                               ) {
+                                       continue;
+                               }
+                               position++;
+                       }
+                       return position;
+               },
+               /*
+                * Determine whether a given node has any allowed attributes
+                *
+                * @param       object          node: the DOM node
+                * @param       array           allowedAttributes: array of allowed attribute names
+                *
+                * @return      boolean         true if the node has one of the allowed attributes
+                */
+                hasAllowedAttributes: function (node, allowedAttributes) {
+                       var value,
+                               hasAllowedAttributes = false;
+                       if (Ext.isString(allowedAttributes)) {
+                               allowedAttributes = [allowedAttributes];
+                       }
+                       allowedAttributes = allowedAttributes || [];
+                       for (var i = allowedAttributes.length; --i >= 0;) {
+                               value = node.getAttribute(allowedAttributes[i]);
+                               if (value) {
+                                       if (allowedAttributes[i] === 'style') {
+                                               if (node.style.cssText) {
+                                                       hasAllowedAttributes = true;
+                                                       break;
+                                               }
+                                       } else {
+                                               hasAllowedAttributes = true;
+                                               break;
+                                       }
+                               }
+                       }
+                       return hasAllowedAttributes;
+               },
+               /*
+                * Remove the given node from its parent
+                *
+                * @param       object          node: the DOM node
+                *
+                * @return      void
+                */
+               removeFromParent: function (node) {
+                       var parent = node.parentNode;
+                       if (parent) {
+                               parent.removeChild(node);
+                       }
+               },
+               /*
+                * Change the nodeName of an element node
+                *
+                * @param       object          node: the node to convert (must belong to a document)
+                * @param       string          nodeName: the nodeName of the converted node
+                *
+                * @retrun      object          the converted node or the input node
+                */
+               convertNode: function (node, nodeName) {
+                       var convertedNode = node,
+                               ownerDocument = node.ownerDocument;
+                       if (ownerDocument && node.nodeType === HTMLArea.DOM.ELEMENT_NODE) {
+                               var convertedNode = ownerDocument.createElement(nodeName),
+                                       parent = node.parentNode;
+                               while (node.firstChild) {
+                                       convertedNode.appendChild(node.firstChild);
+                               }
+                               parent.insertBefore(convertedNode, node);
+                               parent.removeChild(node);
+                       }
+                       return convertedNode;
+               },
+               /*
+                * Determine whether a given range intersects a given node
+                *
+                * @param       object          range: the range
+                * @param       object          node: the DOM node (must belong to a document)
+                *
+                * @return      boolean         true if the range intersects the node
+                */
+               rangeIntersectsNode: function (range, node) {
+                       var rangeIntersectsNode = false,
+                               ownerDocument = node.ownerDocument;
+                       if (ownerDocument) {
+                               if (HTMLArea.isIEBeforeIE9) {
+                                       var nodeRange = ownerDocument.body.createTextRange();
+                                       nodeRange.moveToElementText(node);
+                                       rangeIntersectsNode = (range.compareEndPoints('EndToStart', nodeRange) == -1 && range.compareEndPoints('StartToEnd', nodeRange) == 1) ||
+                                               (range.compareEndPoints('EndToStart', nodeRange) == 1 && range.compareEndPoints('StartToEnd', nodeRange) == -1);
+                               } else {
+                                       var nodeRange = ownerDocument.createRange();
+                                       try {
+                                               nodeRange.selectNode(node);
+                                       } catch (e) {
+                                               if (Ext.isWebKit) {
+                                                       nodeRange.setStart(node, 0);
+                                                       if (node.nodeType === HTMLArea.DOM.TEXT_NODE || node.nodeType === HTMLArea.DOM.COMMENT_NODE || node.nodeType === HTMLArea.DOM.CDATA_SECTION_NODE) {
+                                                               nodeRange.setEnd(node, node.textContent.length);
+                                                       } else {
+                                                               nodeRange.setEnd(node, node.childNodes.length);
+                                                       }
+                                               } else {
+                                                       nodeRange.selectNodeContents(node);
+                                               }
+                                       }
+                                               // Note: sometimes WebKit inverts the end points
+                                       rangeIntersectsNode = (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == -1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == 1) ||
+                                               (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == 1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == -1);
+                               }
+                       }
+                       return rangeIntersectsNode;
+               },
+               /*
+                * Make url's absolute in the DOM tree under the root node
+                *
+                * @param       object          root: the root node
+                * @param       string          baseUrl: base url to use
+                * @param       string          walker: a HLMLArea.DOM.Walker object
+                * @return      void
+                */
+               makeUrlsAbsolute: function (node, baseUrl, walker) {
+                       walker.walk(node, true, 'HTMLArea.DOM.makeImageSourceAbsolute(node, args[0]) || HTMLArea.DOM.makeLinkHrefAbsolute(node, args[0])', 'Ext.emptyFn', [baseUrl]);
+               },
+               /*
+                * Make the src attribute of an image node absolute
+                *
+                * @param       object          node: the image node
+                * @param       string          baseUrl: base url to use
+                * @return      void
+                */
+               makeImageSourceAbsolute: function (node, baseUrl) {
+                       if (/^img$/i.test(node.nodeName)) {
+                               var src = node.getAttribute('src');
+                               if (src) {
+                                       node.setAttribute('src', HTMLArea.DOM.addBaseUrl(src, baseUrl));
+                               }
+                               return true;
+                       }
+                       return false;
+               },
+               /*
+                * Make the href attribute of an a node absolute
+                *
+                * @param       object          node: the image node
+                * @param       string          baseUrl: base url to use
+                * @return      void
+                */
+               makeLinkHrefAbsolute: function (node, baseUrl) {
+                       if (/^a$/i.test(node.nodeName)) {
+                               var href = node.getAttribute('href');
+                               if (href) {
+                                       node.setAttribute('href', HTMLArea.DOM.addBaseUrl(href, baseUrl));
+                               }
+                               return true;
+                       }
+                       return false;
+               },
+               /*
+                * Add base url
+                *
+                * @param       string          url: value of a href or src attribute
+                * @param       string          baseUrl: base url to add
+                * @return      string          absolute url
+                */
+               addBaseUrl: function (url, baseUrl) {
+                       var absoluteUrl = url;
+                               // If the url has no scheme...
+                       if (!/^[a-z0-9_]{2,}\:/i.test(absoluteUrl)) {
+                               var base = baseUrl;
+                               while (absoluteUrl.match(/^\.\.\/(.*)/)) {
+                                               // Remove leading ../ from url
+                                       absoluteUrl = RegExp.$1;
+                                       base.match(/(.*\:\/\/.*\/)[^\/]+\/$/);
+                                               // Remove lowest directory level from base
+                                       base = RegExp.$1;
+                                       absoluteUrl = base + absoluteUrl;
+                               }
+                                       // If the url is still not absolute...
+                               if (!/^.*\:\/\//.test(absoluteUrl)) {
+                                       absoluteUrl = baseUrl + absoluteUrl;
+                               }
+                       }
+                       return absoluteUrl;
+               }
+       };
+}();
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Editor.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Editor.js
new file mode 100644 (file)
index 0000000..ba96cc4
--- /dev/null
@@ -0,0 +1,524 @@
+/***************************************************
+ *  HTMLArea.Editor extends Ext.util.Observable
+ ***************************************************/
+HTMLArea.Editor = Ext.extend(Ext.util.Observable, {
+       /*
+        * HTMLArea.Editor constructor
+        */
+       constructor: function (config) {
+               HTMLArea.Editor.superclass.constructor.call(this, {});
+                       // Save the config
+               this.config = config;
+                       // Establish references to this editor
+               this.editorId = this.config.editorId;
+               RTEarea[this.editorId].editor = this;
+                       // Get textarea size and wizard context
+               this.textArea = Ext.get(this.config.id);
+               this.textAreaInitialSize = {
+                       width: this.config.RTEWidthOverride ? this.config.RTEWidthOverride : this.textArea.getStyle('width'),
+                       height: this.config.fullScreen ? HTMLArea.util.TYPO3.getWindowSize().height - 20 : this.textArea.getStyle('height'),
+                       wizardsWidth: 0
+               };
+                       // TYPO3 Inline elements and tabs
+               this.nestedParentElements = {
+                       all: this.config.tceformsNested,
+                       sorted: HTMLArea.util.TYPO3.simplifyNested(this.config.tceformsNested)
+               };
+               this.isNested = this.nestedParentElements.sorted.length > 0;
+                       // If in BE, get width of wizards
+               if (Ext.get('typo3-docheader')) {
+                       this.wizards = this.textArea.parent().parent().next();
+                       if (this.wizards) {
+                               if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                                       this.textAreaInitialSize.wizardsWidth = this.wizards.getWidth();
+                               } else {
+                                               // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
+                                       var parentElements = [].concat(this.nestedParentElements.sorted);
+                                               // Walk through all nested tabs and inline levels to get correct size
+                                       this.textAreaInitialSize.wizardsWidth = HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].getWidth()', [this.wizards]);
+                               }
+                                       // Hide the wizards so that they do not move around while the editor framework is being sized
+                               this.wizards.hide();
+                       }
+               }
+               // Plugins register
+               this.plugins = {};
+               // Register the plugins included in the configuration
+               for (var plugin in this.config.plugin) {
+                       if (this.config.plugin[plugin]) {
+                               this.registerPlugin(plugin);
+                       }
+               }
+                       // Create Ajax object
+               this.ajax = new HTMLArea.Ajax({
+                       editor: this
+               });
+                       // Initialize keyboard input inhibit flag
+               this.inhibitKeyboardInput = false;
+               this.addEvents(
+                       /*
+                        * @event HTMLAreaEventEditorReady
+                        * Fires when initialization of the editor is complete
+                        */
+                       'HTMLAreaEventEditorReady',
+                       /*
+                        * @event HTMLAreaEventModeChange
+                        * Fires when the editor changes mode
+                        */
+                       'HTMLAreaEventModeChange'
+               );
+       },
+       /*
+        * Flag set to true when the editor initialization has completed
+        */
+       ready: false,
+       /*
+        * The current mode of the editor: 'wysiwyg' or 'textmode'
+        */
+       mode: 'textmode',
+       /*
+        * Determine whether the editor document is currently contentEditable
+        *
+        * @return      boolean         true, if the document is contentEditable
+        */
+       isEditable: function () {
+               return HTMLArea.UserAgent.isIE ? this.document.body.contentEditable : (this.document.designMode === 'on');
+       },
+       /*
+        * The selection object
+        */
+       selection: null,
+       getSelection: function () {
+               if (!this.selection) {
+                       this.selection = new HTMLArea.DOM.Selection({
+                               editor: this
+                       });
+               }
+               return this.selection;
+       },
+       /*
+        * The bookmark object
+        */
+       bookMark: null,
+       getBookMark: function () {
+               if (!this.bookMark) {
+                       this.bookMark = new HTMLArea.DOM.BookMark({
+                               editor: this
+                       });
+               }
+               return this.bookMark;
+       },
+       /*
+        * The DOM node object
+        */
+       domNode: null,
+       getDomNode: function () {
+               if (!this.domNode) {
+                       this.domNode = new HTMLArea.DOM.Node({
+                               editor: this
+                       });
+               }
+               return this.domNode;
+       },
+       /*
+        * Create the htmlArea framework
+        */
+       generate: function () {
+                       // Create the editor framework
+               this.htmlArea = new HTMLArea.Framework({
+                       id: this.editorId + '-htmlArea',
+                       layout: 'anchor',
+                       baseCls: 'htmlarea',
+                       editorId: this.editorId,
+                       textArea: this.textArea,
+                       textAreaInitialSize: this.textAreaInitialSize,
+                       fullScreen: this.config.fullScreen,
+                       resizable: this.config.resizable,
+                       maxHeight: this.config.maxHeight,
+                       isNested: this.isNested,
+                       nestedParentElements: this.nestedParentElements,
+                               // The toolbar
+                       tbar: {
+                               xtype: 'htmlareatoolbar',
+                               id: this.editorId + '-toolbar',
+                               anchor: '100%',
+                               layout: 'form',
+                               cls: 'toolbar',
+                               editorId: this.editorId
+                       },
+                       items: [{
+                                               // The iframe
+                                       xtype: 'htmlareaiframe',
+                                       itemId: 'iframe',
+                                       anchor: '100%',
+                                       width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
+                                       height: parseInt(this.textAreaInitialSize.height),
+                                       autoEl: {
+                                               id: this.editorId + '-iframe',
+                                               tag: 'iframe',
+                                               cls: 'editorIframe',
+                                               src: HTMLArea.UserAgent.isGecko ? 'javascript:void(0);' : (HTMLArea.UserAgent.isWebKit ? 'javascript: \'' + HTMLArea.util.htmlEncode(this.config.documentType + this.config.blankDocument) + '\'' : HTMLArea.editorUrl + 'popups/blank.html')
+                                       },
+                                       isNested: this.isNested,
+                                       nestedParentElements: this.nestedParentElements,
+                                       editorId: this.editorId
+                               },{
+                                               // Box container for the textarea
+                                       xtype: 'box',
+                                       itemId: 'textAreaContainer',
+                                       anchor: '100%',
+                                       width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
+                                               // Let the framework swallow the textarea and throw it back
+                                       listeners: {
+                                               afterrender: {
+                                                       fn: function (textAreaContainer) {
+                                                               this.originalParent = this.textArea.parent().dom;
+                                                               textAreaContainer.getEl().appendChild(this.textArea);
+                                                       },
+                                                       single: true,
+                                                       scope: this
+                                               },
+                                               beforedestroy: {
+                                                       fn: function (textAreaContainer) {
+                                                               this.originalParent.appendChild(this.textArea.dom);
+                                                               return true;
+                                                       },
+                                                       single: true,
+                                                       scope: this
+                                               }
+                                       }
+                               }
+                       ],
+                               // The status bar
+                       bbar: {
+                               xtype: 'htmlareastatusbar',
+                               anchor: '100%',
+                               cls: 'statusBar',
+                               editorId: this.editorId
+                       }
+               });
+                       // Set some references
+               this.toolbar = this.htmlArea.getTopToolbar();
+               this.statusBar = this.htmlArea.getBottomToolbar();
+               this.iframe = this.htmlArea.getComponent('iframe');
+               this.textAreaContainer = this.htmlArea.getComponent('textAreaContainer');
+                       // Get triggered when the framework becomes ready
+               this.relayEvents(this.htmlArea, ['HTMLAreaEventFrameworkReady']);
+               this.on('HTMLAreaEventFrameworkReady', this.onFrameworkReady, this, {single: true});
+       },
+       /*
+        * Initialize the editor
+        */
+       onFrameworkReady: function () {
+                       // Initialize editor mode
+               this.setMode('wysiwyg');
+                       // Create the selection object
+               this.getSelection();
+                       // Create the bookmark object
+               this.getBookMark();
+                       // Create the DOM node object
+               this.getDomNode();
+                       // Initiate events listening
+               this.initEventsListening();
+                       // Generate plugins
+               this.generatePlugins();
+                       // Make the editor visible
+               this.show();
+                       // Make the wizards visible again
+               if (this.wizards) {
+                       this.wizards.show();
+               }
+               // Focus on the first editor that is not hidden
+               for (var editorId in RTEarea) {
+                       var RTE = RTEarea[editorId];
+                       if (typeof RTE.editor !== 'object' || RTE.editor === null || (RTE.editor.isNested && !HTMLArea.util.TYPO3.allElementsAreDisplayed(RTE.editor.nestedParentElements.sorted))) {
+                               continue;
+                       } else {
+                               RTE.editor.focus();
+                               break;
+                       }
+               }
+               this.ready = true;
+               this.fireEvent('HTMLAreaEventEditorReady');
+               this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
+       },
+       /*
+        * Set editor mode
+        *
+        * @param       string          mode: 'textmode' or 'wysiwyg'
+        *
+        * @return      void
+        */
+       setMode: function (mode) {
+               switch (mode) {
+                       case 'textmode':
+                               this.textArea.set({ value: this.getHTML() }, false);
+                               this.iframe.setDesignMode(false);
+                               this.iframe.hide();
+                               this.textAreaContainer.show();
+                               this.mode = mode;
+                               break;
+                       case 'wysiwyg':
+                               try {
+                                       this.document.body.innerHTML = this.getHTML();
+                               } catch(e) {
+                                       this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
+                                       TYPO3.Dialog.ErrorDialog({
+                                               title: 'htmlArea RTE',
+                                               msg: HTMLArea.localize('HTML-document-not-well-formed')
+                                       });
+                                       break;
+                               }
+                               this.textAreaContainer.hide();
+                               this.iframe.show();
+                               this.iframe.setDesignMode(true);
+                               this.mode = mode;
+                               break;
+               }
+               this.fireEvent('HTMLAreaEventModeChange', this.mode);
+               this.focus();
+               for (var pluginId in this.plugins) {
+                       this.getPlugin(pluginId).onMode(this.mode);
+               }
+       },
+       /*
+        * Get current editor mode
+        */
+       getMode: function () {
+               return this.mode;
+       },
+       /*
+        * Retrieve the HTML
+        * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
+        *
+        * @return      string          the textual html content from the current editing mode
+        */
+       getHTML: function () {
+               switch (this.mode) {
+                       case 'wysiwyg':
+                               return this.iframe.getHTML();
+                       case 'textmode':
+                                       // Collapse repeated spaces non-editable in wysiwyg
+                                       // Replace leading and trailing spaces non-editable in wysiwyg
+                               return this.textArea.getValue().
+                                       replace(/[\x20]+/g, '\x20').
+                                       replace(/^\x20/g, '&nbsp;').
+                                       replace(/\x20$/g, '&nbsp;');
+                       default:
+                               return '';
+               }
+       },
+       /*
+        * Retrieve raw HTML
+        *
+        * @return      string  the textual html content from the current editing mode
+        */
+       getInnerHTML: function () {
+               switch (this.mode) {
+                       case 'wysiwyg':
+                               return this.document.body.innerHTML;
+                       case 'textmode':
+                               return this.textArea.getValue();
+                       default:
+                               return '';
+               }
+       },
+       /*
+        * Replace the html content
+        *
+        * @param       string          html: the textual html
+        *
+        * @return      void
+        */
+       setHTML: function (html) {
+               switch (this.mode) {
+                       case 'wysiwyg':
+                               this.document.body.innerHTML = html;
+                               break;
+                       case 'textmode':
+                               this.textArea.set({ value: html }, false);;
+                               break;
+               }
+       },
+       /*
+        * Get the node given its position in the document tree.
+        * Adapted from FCKeditor
+        * See HTMLArea.DOM.Node::getPositionWithinTree
+        *
+        * @param       array           position: the position of the node in the document tree
+        * @param       boolean         normalized: if true, a normalized position is given
+        *
+        * @return      objet           the node
+        */
+       getNodeByPosition: function (position, normalized) {
+               var current = this.document.documentElement;
+               var i, j, n, m;
+               for (i = 0, n = position.length; current && i < n; i++) {
+                       var target = position[i];
+                       if (normalized) {
+                               var currentIndex = -1;
+                               for (j = 0, m = current.childNodes.length; j < m; j++) {
+                                       var candidate = current.childNodes[j];
+                                       if (
+                                               candidate.nodeType == HTMLArea.DOM.TEXT_NODE
+                                               && candidate.previousSibling
+                                               && candidate.previousSibling.nodeType == HTMLArea.DOM.TEXT_NODE
+                                       ) {
+                                               continue;
+                                       }
+                                       currentIndex++;
+                                       if (currentIndex == target) {
+                                               current = candidate;
+                                               break;
+                                       }
+                               }
+                       } else {
+                               current = current.childNodes[target];
+                       }
+               }
+               return current ? current : null;
+       },
+       /*
+        * Instantiate the specified plugin and register it with the editor
+        *
+        * @param       string          plugin: the name of the plugin
+        *
+        * @return      boolean         true if the plugin was successfully registered
+        */
+       registerPlugin: function (pluginName) {
+               var plugin = HTMLArea[pluginName],
+                       isRegistered = false;
+               if (typeof plugin === 'function') {
+                       var pluginInstance = new plugin(this, pluginName);
+                       if (pluginInstance) {
+                               var pluginInformation = pluginInstance.getPluginInformation();
+                               pluginInformation.instance = pluginInstance;
+                               this.plugins[pluginName] = pluginInformation;
+                               isRegistered = true;
+                       }
+               }
+               if (!isRegistered) {
+                       this.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName + '.', 'warn');
+               }
+               return isRegistered;
+       },
+       /*
+        * Generate registered plugins
+        */
+       generatePlugins: function () {
+               for (var pluginId in this.plugins) {
+                       var plugin = this.getPlugin(pluginId);
+                       plugin.onGenerate();
+               }
+       },
+       /*
+        * Get the instance of the specified plugin, if it exists
+        *
+        * @param       string          pluginName: the name of the plugin
+        * @return      object          the plugin instance or null
+        */
+       getPlugin: function(pluginName) {
+               return (this.plugins[pluginName] ? this.plugins[pluginName].instance : null);
+       },
+       /*
+        * Unregister the instance of the specified plugin
+        *
+        * @param       string          pluginName: the name of the plugin
+        * @return      void
+        */
+       unRegisterPlugin: function(pluginName) {
+               delete this.plugins[pluginName].instance;
+               delete this.plugins[pluginName];
+       },
+       /*
+        * Update the edito toolbar
+        */
+       updateToolbar: function (noStatus) {
+               this.toolbar.update(noStatus);
+       },
+       /*
+        * Focus on the editor
+        */
+       focus: function () {
+               switch (this.getMode()) {
+                       case 'wysiwyg':
+                               this.iframe.focus();
+                               break;
+                       case 'textmode':
+                               this.textArea.focus();
+                               break;
+               }
+       },
+       /*
+        * Scroll the editor window to the current caret position
+        */
+       scrollToCaret: function () {
+               if (!HTMLArea.UserAgent.isIE) {
+                       var e = this.getSelection().getParentElement(),
+                               w = this.iframe.getEl().dom.contentWindow ? this.iframe.getEl().dom.contentWindow : window,
+                               h = w.innerHeight || w.height,
+                               d = this.document,
+                               t = d.documentElement.scrollTop || d.body.scrollTop;
+                       if (e.offsetTop > h+t || e.offsetTop < t) {
+                               this.getSelection().getParentElement().scrollIntoView();
+                       }
+               }
+       },
+       /*
+        * Add listeners
+        */
+       initEventsListening: function () {
+               if (HTMLArea.UserAgent.isOpera) {
+                       this.iframe.startListening();
+               }
+                       // Add unload handler
+               var iframe = this.iframe.getEl().dom;
+               Ext.EventManager.on(iframe.contentWindow ? iframe.contentWindow : iframe.contentDocument, 'unload', this.onUnload, this, {single: true});
+       },
+       /*
+        * Make the editor framework visible
+        */
+       show: function () {
+               document.getElementById('pleasewait' + this.editorId).style.display = 'none';
+               document.getElementById('editorWrap' + this.editorId).style.visibility = 'visible';
+       },
+       /*
+        * Append an entry at the end of the troubleshooting log
+        *
+        * @param       string          functionName: the name of the editor function writing to the log
+        * @param       string          text: the text of the message
+        * @param       string          type: the type of message
+        *
+        * @return      void
+        */
+       appendToLog: function (objectName, functionName, text, type) {
+               HTMLArea.appendToLog(this.editorId, objectName, functionName, text, type);
+       },
+       /*
+        * Iframe unload handler: Update the textarea for submission and cleanup
+        */
+       onUnload: function (event) {
+                       // Save the HTML content into the original textarea for submit, back/forward, etc.
+               if (this.ready) {
+                       this.textArea.set({
+                               value: this.getHTML()
+                       }, false);
+               }
+               // Cleanup
+               Ext.TaskMgr.stopAll();
+               for (var pluginId in this.plugins) {
+                       this.unRegisterPlugin(pluginId);
+               }
+               this.purgeListeners();
+                       // Cleaning references to DOM in order to avoid IE memory leaks
+               if (this.wizards) {
+                       this.wizards.dom = null;
+                       this.textArea.parent().parent().dom = null;
+                       this.textArea.parent().dom = null;
+               }
+               this.textArea.dom = null;
+               RTEarea[this.editorId].editor = null;
+               // ExtJS is not releasing any resources when the iframe is unloaded
+               this.htmlArea.destroy();
+       }
+});
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Framework.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Framework.js
new file mode 100644 (file)
index 0000000..6195674
--- /dev/null
@@ -0,0 +1,291 @@
+/*
+ * HTMLArea.Framework extends Ext.Panel
+ */
+HTMLArea.Framework = Ext.extend(Ext.Panel, {
+       /*
+        * Constructor
+        */
+       initComponent: function () {
+               HTMLArea.Framework.superclass.initComponent.call(this);
+                       // Set some references
+               this.toolbar = this.getTopToolbar();
+               this.statusBar = this.getBottomToolbar();
+               this.iframe = this.getComponent('iframe');
+               this.textAreaContainer = this.getComponent('textAreaContainer');
+               this.addEvents(
+                       /*
+                        * @event HTMLAreaEventFrameworkReady
+                        * Fires when the iframe is ready and all components are rendered
+                        */
+                       'HTMLAreaEventFrameworkReady'
+               );
+               this.addListener({
+                       beforedestroy: {
+                               fn: this.onBeforeDestroy,
+                               single: true
+                       }
+               });
+                       // Monitor iframe becoming ready
+               this.mon(this.iframe, 'HTMLAreaEventIframeReady', this.onIframeReady, this, {single: true});
+                       // Let the framefork render itself, but it will fail to do so if inside a hidden tab or inline element
+               if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                       this.render(this.textArea.parent(), this.textArea.id);
+               } else {
+                               // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
+                       var parentElements = [].concat(this.nestedParentElements.sorted);
+                               // Walk through all nested tabs and inline levels to get correct sizes
+                       HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].render(args[0].textArea.parent(), args[0].textArea.id)', [this]);
+               }
+       },
+       /*
+        * Initiate events monitoring
+        */
+       initEventListeners: function () {
+                       // Make the framework resizable, if configured by the user
+               this.makeResizable();
+                       // Monitor textArea container becoming shown or hidden as it may change the height of the status bar
+               this.mon(this.textAreaContainer, 'show', this.resizable ? this.onTextAreaShow : this.onWindowResize, this);
+                       // Monitor iframe becoming shown or hidden as it may change the height of the status bar
+               this.mon(this.iframe, 'show', this.resizable ? this.onIframeShow : this.onWindowResize, this);
+                       // Monitor window resizing
+               Ext.EventManager.onWindowResize(this.onWindowResize, this);
+                       // If the textarea is inside a form, on reset, re-initialize the HTMLArea content and update the toolbar
+               var form = this.textArea.dom.form;
+               if (form) {
+                       if (typeof form.onreset === 'function') {
+                               if (typeof form.htmlAreaPreviousOnReset === 'undefined') {
+                                       form.htmlAreaPreviousOnReset = [];
+                               }
+                               form.htmlAreaPreviousOnReset.push(form.onreset);
+                       }
+                       this.mon(Ext.get(form), 'reset', this.onReset, this);
+               }
+               this.addListener({
+                       resize: {
+                               fn: this.onFrameworkResize
+                       }
+               });
+       },
+       /*
+        * editorId should be set in config
+        */
+       editorId: null,
+       /*
+        * Get a reference to the editor
+        */
+       getEditor: function() {
+               return RTEarea[this.editorId].editor;
+       },
+       /*
+        * Flag indicating whether the framework is inside a tab or inline element that may be hidden
+        * Should be set in config
+        */
+       isNested: false,
+       /*
+        * All nested tabs and inline levels in the sorting order they were applied
+        * Should be set in config
+        */
+       nestedParentElements: {},
+       /*
+        * Flag set to true when the framework is ready
+        */
+       ready: false,
+       /*
+        * All nested tabs and inline levels in the sorting order they were applied
+        * Should be set in config
+        */
+       nestedParentElements: {},
+       /*
+        * Whether the framework should be made resizable
+        * May be set in config
+        */
+       resizable: false,
+       /*
+        * Maximum height to which the framework may resized (in pixels)
+        * May be set in config
+        */
+       maxHeight: 2000,
+       /*
+        * Initial textArea dimensions
+        * Should be set in config
+        */
+       textAreaInitialSize: {
+               width: 0,
+               contextWidth: 0,
+               height: 0
+       },
+       /*
+        * doLayout will fail if inside a hidden tab or inline element
+        */
+       doLayout: function () {
+               if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                       HTMLArea.Framework.superclass.doLayout.call(this);
+               } else {
+                               // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
+                       var parentElements = [].concat(this.nestedParentElements.sorted);
+                               // Walk through all nested tabs and inline levels to get correct sizes
+                       HTMLArea.util.TYPO3.accessParentElements(parentElements, 'HTMLArea.Framework.superclass.doLayout.call(args[0])', [this]);
+               }
+       },
+       /*
+        * onLayout will fail if inside a hidden tab or inline element
+        */
+       onLayout: function () {
+               if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                       HTMLArea.Framework.superclass.onLayout.call(this);
+               } else {
+                               // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
+                       var parentElements = [].concat(this.nestedParentElements.sorted);
+                               // Walk through all nested tabs and inline levels to get correct sizes
+                               HTMLArea.util.TYPO3.accessParentElements(parentElements, 'HTMLArea.Framework.superclass.onLayout.call(args[0])', [this]);
+               }
+       },
+       /*
+        * Make the framework resizable, if configured
+        */
+       makeResizable: function () {
+               if (this.resizable) {
+                       this.addClass('resizable');
+                       this.resizer = new Ext.Resizable(this.getEl(), {
+                               minWidth: 300,
+                               maxHeight: this.maxHeight,
+                               dynamic: false
+                       });
+                       this.resizer.on('resize', this.onHtmlAreaResize, this);
+               }
+       },
+       /*
+        * Resize the framework when the resizer handles are used
+        */
+       onHtmlAreaResize: function (resizer, width, height, event) {
+                       // Set width first as it may change the height of the toolbar and of the statusBar
+               this.setWidth(width);
+                       // Set height of iframe and textarea
+               this.iframe.setHeight(this.getInnerHeight());
+               this.textArea.setSize(this.getInnerWidth(), this.getInnerHeight());
+       },
+       /*
+        * Size the iframe according to initial textarea size as set by Page and User TSConfig
+        */
+       onWindowResize: function (width, height) {
+               if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                       this.resizeFramework(width, height);
+               } else {
+                               // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
+                       var parentElements = [].concat(this.nestedParentElements.sorted);
+                               // Walk through all nested tabs and inline levels to get correct sizes
+                       HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].resizeFramework(args[1], args[2])', [this, width, height]);
+               }
+       },
+       /*
+        * Resize the framework to its initial size
+        */
+       resizeFramework: function (width, height) {
+               var frameworkHeight = parseInt(this.textAreaInitialSize.height);
+               if (this.textAreaInitialSize.width.indexOf('%') === -1) {
+                               // Width is specified in pixels
+                       var frameworkWidth = parseInt(this.textAreaInitialSize.width) - this.getFrameWidth();
+               } else {
+                               // Width is specified in %
+                       if (Ext.isNumber(width)) {
+                                       // Framework sizing on actual window resize
+                               var frameworkWidth = parseInt(((width - this.textAreaInitialSize.wizardsWidth - (this.fullScreen ? 10 : Ext.getScrollBarWidth()) - this.getBox().x - 15) * parseInt(this.textAreaInitialSize.width))/100);
+                       } else {
+                                       // Initial framework sizing
+                               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);
+                       }
+               }
+               if (this.resizable) {
+                       this.resizer.resizeTo(frameworkWidth, frameworkHeight);
+               } else {
+                       this.setSize(frameworkWidth, frameworkHeight);
+                       this.doLayout();
+               }
+       },
+       /*
+        * Resize the framework components
+        */
+       onFrameworkResize: function () {
+               this.iframe.setSize(this.getInnerWidth(), this.getInnerHeight());
+               this.textArea.setSize(this.getInnerWidth(), this.getInnerHeight());
+       },
+       /*
+        * Adjust the height to the changing size of the statusbar when the textarea is shown
+        */
+       onTextAreaShow: function () {
+               this.iframe.setHeight(this.getInnerHeight());
+               this.textArea.setHeight(this.getInnerHeight());
+       },
+       /*
+        * Adjust the height to the changing size of the statusbar when the iframe is shown
+        */
+       onIframeShow: function () {
+               if (this.getInnerHeight() <= 0) {
+                       this.onWindowResize();
+               } else {
+                       this.iframe.setHeight(this.getInnerHeight());
+                       this.textArea.setHeight(this.getInnerHeight());
+               }
+       },
+       /*
+        * Calculate the height available for the editing iframe
+        */
+       getInnerHeight: function () {
+               return this.getSize().height - this.toolbar.getHeight() - this.statusBar.getHeight() -  5;
+       },
+       /*
+        * Fire the editor when all components of the framework are rendered and ready
+        */
+       onIframeReady: function () {
+               this.ready = this.rendered && this.toolbar.rendered && this.statusBar.rendered && this.textAreaContainer.rendered;
+               if (this.ready) {
+                       this.initEventListeners();
+                       this.textAreaContainer.show();
+                       if (!this.getEditor().config.showStatusBar) {
+                               this.statusBar.hide();
+                       }
+                               // Set the initial size of the framework
+                       this.onWindowResize();
+                       this.fireEvent('HTMLAreaEventFrameworkReady');
+               } else {
+                       this.onIframeReady.defer(50, this);
+               }
+       },
+       /**
+        * Handler invoked if we are inside a form and the form is reset
+        * On reset, re-initialize the HTMLArea content and update the toolbar
+        */
+       onReset: function (event) {
+               this.getEditor().setHTML(this.textArea.getValue());
+               this.toolbar.update();
+               // Invoke previous reset handlers, if any
+               var htmlAreaPreviousOnReset = event.getTarget().dom.htmlAreaPreviousOnReset;
+               if (typeof htmlAreaPreviousOnReset !== 'undefined') {
+                       for (var i = 0, n = htmlAreaPreviousOnReset.length; i < n; i++) {
+                               htmlAreaPreviousOnReset[i]();
+                       }
+               }
+       },
+       /*
+        * Cleanup on framework destruction
+        */
+       onBeforeDestroy: function () {
+               Ext.EventManager.removeResizeListener(this.onWindowResize, this);
+                       // Cleaning references to DOM in order to avoid IE memory leaks
+               var form = this.textArea.dom.form;
+               if (form) {
+                       form.htmlAreaPreviousOnReset = null;
+                       Ext.get(form).dom = null;
+               }
+               Ext.getBody().dom = null;
+                       // ExtJS is not releasing any resources when the iframe is unloaded
+               this.toolbar.destroy();
+               this.statusBar.destroy();
+               this.removeAll(true);
+               if (this.resizer) {
+                       this.resizer.destroy();
+               }
+               return true;
+       }
+});
+Ext.reg('htmlareaframework', HTMLArea.Framework);
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Iframe.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.Iframe.js
new file mode 100644 (file)
index 0000000..b8cd5f0
--- /dev/null
@@ -0,0 +1,652 @@
+/*
+ * HTMLArea.Iframe extends Ext.BoxComponent
+ */
+HTMLArea.Iframe = Ext.extend(Ext.BoxComponent, {
+       /*
+        * Constructor
+        */
+       initComponent: function () {
+               HTMLArea.Iframe.superclass.initComponent.call(this);
+               this.addEvents(
+                       /*
+                        * @event HTMLAreaEventIframeReady
+                        * Fires when the iframe style sheets become accessible
+                        */
+                       'HTMLAreaEventIframeReady',
+                       /*
+                        * @event HTMLAreaEventWordCountChange
+                        * Fires when the word count may have changed
+                        */
+                       'HTMLAreaEventWordCountChange'
+               );
+               this.addListener({
+                       afterrender: {
+                               fn: this.initEventListeners,
+                               single: true
+                       },
+                       beforedestroy: {
+                               fn: this.onBeforeDestroy,
+                               single: true
+                       }
+               });
+               this.config = this.getEditor().config;
+               this.htmlRenderer = new HTMLArea.DOM.Walker({
+                       keepComments: !this.config.htmlRemoveComments,
+                       removeTags: this.config.htmlRemoveTags,
+                       removeTagsAndContents: this.config.htmlRemoveTagsAndContents,
+                       baseUrl: this.config.baseURL
+               });
+               if (!this.config.showStatusBar) {
+                       this.addClass('noStatusBar');
+               }
+       },
+       /*
+        * Initialize event listeners and the document after the iframe has rendered
+        */
+       initEventListeners: function () {
+               this.initStyleChangeEventListener();
+               if (HTMLArea.UserAgent.isOpera) {
+                       this.mon(this.getEl(), 'load', this.initializeIframe , this, {single: true});
+               } else {
+                       this.initializeIframe();
+               }
+       },
+       /*
+        * The editor iframe may become hidden with style.display = "none" on some parent div
+        * 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"
+        * In all browsers, it breaks the evaluation of the framework dimensions
+        */
+       initStyleChangeEventListener: function () {
+               if (this.isNested && HTMLArea.UserAgent.isGecko) {
+                       var options = {
+                               stopEvent: true,
+                               delay: 50
+                       };
+                       for (var i = this.nestedParentElements.sorted.length; --i >= 0;) {
+                               var nestedElement = Ext.get(this.nestedParentElements.sorted[i]);
+                               this.mon(
+                                       nestedElement,
+                                       HTMLArea.UserAgent.isIE ? 'propertychange' : 'DOMAttrModified',
+                                       this.onNestedShow,
+                                       this,
+                                       options
+                               );
+                               this.mon(
+                                       nestedElement.parent(),
+                                       HTMLArea.UserAgent.isIE ? 'propertychange' : 'DOMAttrModified',
+                                       this.onNestedShow,
+                                       this,
+                                       options
+                               );
+                       }
+               }
+       },
+       /*
+        * editorId should be set in config
+        */
+       editorId: null,
+       /*
+        * Get a reference to the editor
+        */
+       getEditor: function() {
+               return RTEarea[this.editorId].editor;
+       },
+       /*
+        * Get a reference to the toolbar
+        */
+       getToolbar: function () {
+               return this.ownerCt.getTopToolbar();
+       },
+       /*
+        * Get a reference to the statusBar
+        */
+       getStatusBar: function () {
+               return this.ownerCt.getBottomToolbar();
+       },
+       /*
+        * Get a reference to a button
+        */
+       getButton: function (buttonId) {
+               return this.getToolbar().getButton(buttonId);
+       },
+       /*
+        * Flag set to true when the iframe becomes usable for editing
+        */
+       ready: false,
+       /*
+        * Create the iframe element at rendering time
+        */
+       onRender: function (ct, position){
+                       // from Ext.Component
+               if (!this.el && this.autoEl) {
+                       if (typeof this.autoEl === 'string' && this.autoEl.length > 0) {
+                               this.el = document.createElement(this.autoEl);
+                       } else {
+                                       // ExtJS Default method will not work with iframe element
+                               this.el = Ext.DomHelper.append(ct, this.autoEl, true);
+                       }
+                       if (!this.el.id) {
+                               this.el.id = this.getId();
+                       }
+               }
+                       // from Ext.BoxComponent
+               if (this.resizeEl){
+                       this.resizeEl = Ext.get(this.resizeEl);
+               }
+               if (this.positionEl){
+                       this.positionEl = Ext.get(this.positionEl);
+               }
+       },
+       /*
+        * Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
+        */
+       initializeIframe: function () {
+               var iframe = this.getEl().dom;
+                       // All browsers
+               if (!iframe || (!iframe.contentWindow && !iframe.contentDocument)) {
+                       this.initializeIframe.defer(50, this);
+                       // All except WebKit
+               } else if (iframe.contentWindow && !HTMLArea.UserAgent.isWebKit && (!iframe.contentWindow.document || !iframe.contentWindow.document.documentElement)) {
+                       this.initializeIframe.defer(50, this);
+                       // WebKit
+               } else if (HTMLArea.UserAgent.isWebKit && (!iframe.contentDocument.documentElement || !iframe.contentDocument.body)) {
+                       this.initializeIframe.defer(50, this);
+               } else {
+                       this.document = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
+                       this.getEditor().document = this.document;
+                       this.initializeCustomTags();
+                       this.createHead();
+                               // Style the document body
+                       Ext.get(this.document.body).addClass('htmlarea-content-body');
+                               // Start listening to things happening in the iframe
+                               // For some unknown reason, this is too early for Opera
+                       if (!HTMLArea.UserAgent.isOpera) {
+                               this.startListening();
+                       }
+                               // Hide the iframe
+                       this.hide();
+                               // Set iframe ready
+                       this.ready = true;
+                       this.fireEvent('HTMLAreaEventIframeReady');
+               }
+       },
+       /*
+        * Create one of each of the configured custom tags so they are properly parsed by the walker when using IE
+        * See: http://en.wikipedia.org/wiki/HTML5_Shiv
+        *
+        * @return      void
+        */
+       initializeCustomTags: function () {
+               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                       for (var i = this.config.customTags.length; --i >= 0;) {
+                               this.document.createElement(this.config.customTags[i]);
+                       }
+               }
+       },
+       /*
+        * Build the iframe document head
+        */
+       createHead: function () {
+               var head = this.document.getElementsByTagName('head')[0];
+               if (!head) {
+                       head = this.document.createElement('head');
+                       this.document.documentElement.appendChild(head);
+               }
+               if (this.config.baseURL) {
+                       var base = this.document.getElementsByTagName('base')[0];
+                       if (!base) {
+                               base = this.document.createElement('base');
+                               base.href = this.config.baseURL;
+                               head.appendChild(base);
+                       }
+                       this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Iframe baseURL set to: ' + base.href, 'info');
+               }
+               var link0 = this.document.getElementsByTagName('link')[0];
+               if (!link0) {
+                       link0 = this.document.createElement('link');
+                       link0.rel = 'stylesheet';
+                       link0.type = 'text/css';
+                               // 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.
+                               // Therefore, for versions before 3.6.8, we prepend the url with the base, if the url is not absolute
+                       link0.href = ((HTMLArea.UserAgent.isGecko && navigator.productSub < 2010072200 && !/^http(s?):\/{2}/.test(this.config.editedContentStyle)) ? this.config.baseURL : '') + this.config.editedContentStyle;
+                       head.appendChild(link0);
+                       this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Skin CSS set to: ' + link0.href, 'info');
+               }
+               var pageStyle;
+               for (var i = 0, n = this.config.pageStyle.length; i < n; i++) {
+                       pageStyle = this.config.pageStyle[i];
+                       var link = this.document.createElement('link');
+                       link.rel = 'stylesheet';
+                       link.type = 'text/css';
+                       link.href = ((HTMLArea.UserAgent.isGecko && navigator.productSub < 2010072200 && !/^https?:\/{2}/.test(pageStyle)) ? this.config.baseURL : '') + pageStyle;
+                       head.appendChild(link);
+                       this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link.href, 'info');
+               }
+       },
+       /*
+        * Focus on the iframe
+        */
+       focus: function () {
+               try {
+                       if (HTMLArea.UserAgent.isWebKit) {
+                               this.getEl().dom.focus();
+                       } else {
+                               this.getEl().dom.contentWindow.focus();
+                       }
+               } catch(e) { }
+       },
+       /*
+        * Flag indicating whether the framework is inside a tab or inline element that may be hidden
+        * Should be set in config
+        */
+       isNested: false,
+       /*
+        * All nested tabs and inline levels in the sorting order they were applied
+        * Should be set in config
+        */
+       nestedParentElements: {},
+       /*
+        * Set designMode
+        *
+        * @param       boolean         on: if true set designMode to on, otherwise set to off
+        *
+        * @rturn       void
+        */
+       setDesignMode: function (on) {
+               if (on) {
+                       if (!HTMLArea.UserAgent.isIE) {
+                               if (HTMLArea.UserAgent.isGecko) {
+                                               // In Firefox, we can't set designMode when we are in a hidden TYPO3 tab or inline element
+                                       if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                                               this.document.designMode = 'on';
+                                               this.setOptions();
+                                       }
+                               } else {
+                                       this.document.designMode = 'on';
+                                       this.setOptions();
+                               }
+                       }
+                       if (HTMLArea.UserAgent.isIE || HTMLArea.UserAgent.isWebKit) {
+                               this.document.body.contentEditable = true;
+                       }
+               } else {
+                       if (!HTMLArea.UserAgent.isIE) {
+                               this.document.designMode = 'off';
+                       }
+                       if (HTMLArea.UserAgent.isIE || HTMLArea.UserAgent.isWebKit) {
+                               this.document.body.contentEditable = false;
+                       }
+               }
+       },
+       /*
+        * Set editing mode options (if we can... raises exception in Firefox 3)
+        *
+        * @return      void
+        */
+       setOptions: function () {
+               if (!HTMLArea.UserAgent.isIE) {
+                       try {
+                               if (this.document.queryCommandEnabled('insertBrOnReturn')) {
+                                       this.document.execCommand('insertBrOnReturn', false, this.config.disableEnterParagraphs);
+                               }
+                               if (this.document.queryCommandEnabled('styleWithCSS')) {
+                                       this.document.execCommand('styleWithCSS', false, this.config.useCSS);
+                               } else if (HTMLArea.UserAgent.isGecko && this.document.queryCommandEnabled('useCSS')) {
+                                       this.document.execCommand('useCSS', false, !this.config.useCSS);
+                               }
+                               if (HTMLArea.UserAgent.isGecko) {
+                                       if (this.document.queryCommandEnabled('enableObjectResizing')) {
+                                               this.document.execCommand('enableObjectResizing', false, !this.config.disableObjectResizing);
+                                       }
+                                       if (this.document.queryCommandEnabled('enableInlineTableEditing')) {
+                                               this.document.execCommand('enableInlineTableEditing', false, (this.config.buttons.table && this.config.buttons.table.enableHandles) ? true : false);
+                                       }
+                               }
+                       } catch(e) {}
+               }
+       },
+       /*
+        * Handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
+        */
+       onNestedShow: function (event, target) {
+               var styleEvent = true;
+                       // In older versions of Gecko attrName is not set and refering to it causes a non-catchable crash
+               if ((HTMLArea.UserAgent.isGecko && navigator.productSub > 2007112700) || HTMLArea.UserAgent.isOpera) {
+                       styleEvent = (event.browserEvent.attrName == 'style') || (event.browserEvent.attrName == 'className');
+               } else if (HTMLArea.UserAgent.isIE) {
+                       styleEvent = (event.browserEvent.propertyName == 'style.display');
+               }
+               if (styleEvent && (this.nestedParentElements.sorted.indexOf(target.id) != -1 || this.nestedParentElements.sorted.indexOf(target.id.replace('_div', '_fields')) != -1)) {
+                               // Check if all container nested elements are displayed
+                       if (HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
+                               if (this.getEditor().getMode() === 'wysiwyg') {
+                                       if (HTMLArea.UserAgent.isGecko) {
+                                               this.setDesignMode(true);
+                                       }
+                                       this.fireEvent('show');
+                               } else {
+                                       this.ownerCt.textAreaContainer.fireEvent('show');
+                               }
+                               this.getToolbar().update();
+                               return false;
+                       }
+               }
+       },
+       /*
+        * Instance of DOM walker
+        */
+       htmlRenderer: {},
+       /*
+        * Get the HTML content of the iframe
+        */
+       getHTML: function () {
+               return this.htmlRenderer.render(this.document.body, false);
+       },
+       /*
+        * Start listening to things happening in the iframe
+        */
+       startListening: function () {
+                       // Create keyMap so that plugins may bind key handlers
+               this.keyMap = new Ext.KeyMap(Ext.get(this.document.documentElement), [], (HTMLArea.UserAgent.isIE || HTMLArea.UserAgent.isWebKit) ? 'keydown' : 'keypress');
+                       // Special keys map
+               this.keyMap.addBinding([
+                       {
+                               key: [Ext.EventObject.DOWN, Ext.EventObject.UP, Ext.EventObject.LEFT, Ext.EventObject.RIGHT],
+                               alt: false,
+                               handler: this.onArrow,
+                               scope: this
+                       },
+                       {
+                               key: Ext.EventObject.TAB,
+                               ctrl: false,
+                               alt: false,
+                               handler: this.onTab,
+                               scope: this
+                       },
+                       {
+                               key: Ext.EventObject.SPACE,
+                               ctrl: true,
+                               shift: false,
+                               alt: false,
+                               handler: this.onCtrlSpace,
+                               scope: this
+                       }
+               ]);
+               if (HTMLArea.UserAgent.isGecko || HTMLArea.UserAgent.isIE || HTMLArea.UserAgent.isWebKit) {
+                       this.keyMap.addBinding(
+                       {
+                               key: [Ext.EventObject.BACKSPACE, Ext.EventObject.DELETE],
+                               alt: false,
+                               handler: this.onBackSpace,
+                               scope: this
+                       });
+               }
+               if (!HTMLArea.UserAgent.isIE && !this.config.disableEnterParagraphs) {
+                       this.keyMap.addBinding(
+                       {
+                               key: Ext.EventObject.ENTER,
+                               shift: false,
+                               handler: this.onEnter,
+                               scope: this
+                       });
+               }
+               if (HTMLArea.UserAgent.isWebKit) {
+                       this.keyMap.addBinding(
+                       {
+                               key: Ext.EventObject.ENTER,
+                               alt: false,
+                               handler: this.onWebKitEnter,
+                               scope: this
+                       });
+               }
+               // Hot key map (on keydown for all browsers)
+               var hotKeys = '';
+               for (var key in this.config.hotKeyList) {
+                       if (key.length == 1) {
+                               hotKeys += key.toUpperCase();
+                       }
+               }
+                       // Make hot key map available, even if empty, so that plugins may add bindings
+               this.hotKeyMap = new Ext.KeyMap(Ext.get(this.document.documentElement));
+               if (hotKeys.length > 0) {
+                       this.hotKeyMap.addBinding({
+                               key: hotKeys,
+                               ctrl: true,
+                               shift: false,
+                               alt: false,
+                               handler: this.onHotKey,
+                               scope: this
+                       });
+               }
+               this.mon(Ext.get(this.document.documentElement), (HTMLArea.UserAgent.isIE || HTMLArea.UserAgent.isWebKit) ? 'keydown' : 'keypress', this.onAnyKey, this);
+               this.mon(Ext.get(this.document.documentElement), 'mouseup', this.onMouse, this);
+               this.mon(Ext.get(this.document.documentElement), 'click', this.onMouse, this);
+               if (HTMLArea.UserAgent.isGecko) {
+                       this.mon(Ext.get(this.document.documentElement), 'paste', this.onPaste, this);
+               }
+               this.mon(Ext.get(this.document.documentElement), 'drop', this.onDrop, this);
+               if (HTMLArea.UserAgent.isWebKit) {
+                       this.mon(Ext.get(this.document.body), 'dragend', this.onDrop, this);
+               }
+       },
+       /*
+        * Handler for other key events
+        */
+       onAnyKey: function(event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               this.fireEvent('HTMLAreaEventWordCountChange', 100);
+               if (!event.altKey && !event.ctrlKey) {
+                               // Detect URL in non-IE browsers
+                       if (!HTMLArea.UserAgent.isIE && (event.getKey() != Ext.EventObject.ENTER || (event.shiftKey && !HTMLArea.UserAgent.isWebKit))) {
+                               this.getEditor().getSelection().detectURL(event);
+                       }
+                               // Handle option+SPACE for Mac users
+                       if (HTMLArea.UserAgent.isMac && event.browserEvent.charCode == 160) {
+                               return this.onOptionSpace(event.browserEvent.charCode, event);
+                       }
+               }
+               return true;
+       },
+       /*
+        * On any key input event, check if input is currently inhibited
+        */
+       inhibitKeyboardInput: function (event) {
+                       // Inhibit key events while server-based cleaning is being processed
+               if (this.getEditor().inhibitKeyboardInput) {
+                       event.stopEvent();
+                       return true;
+               } else {
+                       return false;
+               }
+       },
+       /*
+        * Handler for mouse events
+        */
+       onMouse: function (event, target) {
+                       // In WebKit, select the image when it is clicked
+               if (HTMLArea.UserAgent.isWebKit && /^(img)$/i.test(target.nodeName) && event.browserEvent.type == 'click') {
+                       this.getEditor().getSelection().selectNode(target);
+               }
+               this.getToolbar().updateLater.delay(100);
+               return true;
+       },
+       /*
+        * Handler for paste operations in Gecko
+        */
+       onPaste: function (event) {
+                       // Make src and href urls absolute
+               if (HTMLArea.UserAgent.isGecko) {
+                       HTMLArea.DOM.makeUrlsAbsolute.defer(50, this, [this.getEditor().document.body, this.config.baseURL, this.htmlRenderer]);
+               }
+       },
+       /*
+        * Handler for drag and drop operations
+        */
+       onDrop: function (event, target) {
+                       // Clean up span elements added by WebKit
+               if (HTMLArea.UserAgent.isWebKit) {
+                       this.getEditor().getDomNode().cleanAppleStyleSpans.defer(50, this.getEditor(), [this.getEditor().document.body]);
+               }
+                       // Make src url absolute in Firefox
+               if (HTMLArea.UserAgent.isGecko) {
+                       HTMLArea.DOM.makeUrlsAbsolute.defer(50, this, [target, this.config.baseURL, this.htmlRenderer]);
+               }
+               this.getToolbar().updateLater.delay(100);
+       },
+       /*
+        * Handler for UP, DOWN, LEFT and RIGHT keys
+        */
+       onArrow: function () {
+               this.getToolbar().updateLater.delay(100);
+               return true;
+       },
+       /*
+        * Handler for TAB and SHIFT-TAB keys
+        *
+        * If available, BlockElements plugin will handle the TAB key
+        */
+       onTab: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               var keyName = (event.shiftKey ? 'SHIFT-' : '') + 'TAB';
+               if (this.config.hotKeyList[keyName] && this.config.hotKeyList[keyName].cmd) {
+                       var button = this.getButton(this.config.hotKeyList[keyName].cmd);
+                       if (button) {
+                               event.stopEvent();
+                               button.fireEvent('HTMLAreaEventHotkey', keyName, event);
+                               return false;
+                       }
+               }
+               event.stopEvent();
+               return false;
+       },
+       /*
+        * Handler for BACKSPACE and DELETE keys
+        */
+       onBackSpace: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               if ((!HTMLArea.UserAgent.isIE && !event.shiftKey) || HTMLArea.UserAgent.isIE) {
+                       if (this.getEditor().getSelection().handleBackSpace()) {
+                               event.stopEvent();
+                       }
+               }
+                       // Update the toolbar state after some time
+               this.getToolbar().updateLater.delay(200);
+               return false;
+       },
+       /*
+        * Handler for ENTER key in non-IE browsers
+        */
+       onEnter: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               this.getEditor().getSelection().detectURL(event);
+               if (this.getEditor().getSelection().checkInsertParagraph()) {
+                       event.stopEvent();
+               }
+                       // Update the toolbar state after some time
+               this.getToolbar().updateLater.delay(200);
+               return false;
+       },
+       /*
+        * Handler for ENTER key in WebKit browsers
+        */
+       onWebKitEnter: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               if (event.shiftKey || this.config.disableEnterParagraphs) {
+                       var editor = this.getEditor();
+                       editor.getSelection().detectURL(event);
+                       if (HTMLArea.UserAgent.isSafari) {
+                               var brNode = editor.document.createElement('br');
+                               editor.getSelection().insertNode(brNode);
+                               brNode.parentNode.normalize();
+                                       // Selection issue when an URL was detected
+                               if (editor._unlinkOnUndo) {
+                                       brNode = brNode.parentNode.parentNode.insertBefore(brNode, brNode.parentNode.nextSibling);
+                               }
+                               if (!brNode.nextSibling || !/\S+/i.test(brNode.nextSibling.textContent)) {
+                                       var secondBrNode = editor.document.createElement('br');
+                                       secondBrNode = brNode.parentNode.appendChild(secondBrNode);
+                               }
+                               editor.getSelection().selectNode(brNode, false);
+                               event.stopEvent();
+                       }
+               }
+                       // Update the toolbar state after some time
+               this.getToolbar().updateLater.delay(200);
+               return false;
+       },
+       /*
+        * Handler for CTRL-SPACE keys
+        */
+       onCtrlSpace: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               this.getEditor().getSelection().insertHtml('&nbsp;');
+               event.stopEvent();
+               return false;
+       },
+       /*
+        * Handler for OPTION-SPACE keys on Mac
+        */
+       onOptionSpace: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               this.getEditor().getSelection().insertHtml('&nbsp;');
+               event.stopEvent();
+               return false;
+       },
+       /*
+        * Handler for configured hotkeys
+        */
+       onHotKey: function (key, event) {
+               if (this.inhibitKeyboardInput(event)) {
+                       return false;
+               }
+               var hotKey = String.fromCharCode(key).toLowerCase();
+               this.getButton(this.config.hotKeyList[hotKey].cmd).fireEvent('HTMLAreaEventHotkey', hotKey, event);
+               return false;
+       },
+       /**
+        * Cleanup
+        */
+       onBeforeDestroy: function () {
+               // ExtJS KeyMap object makes IE leak memory
+               // Nullify EXTJS private handlers
+               for (var index = this.keyMap.bindings.length; --index >= 0;) {
+                       this.keyMap.bindings[index] = null;
+               }
+               this.keyMap.handleKeyDown = null;
+               for (var index = this.hotKeyMap.bindings.length; --index >= 0;) {
+                       this.hotKeyMap.bindings[index] = null;
+               }
+               this.hotKeyMap.handleKeyDown = null;
+               this.keyMap.disable();
+               this.hotKeyMap.disable();
+                       // Cleaning references to DOM in order to avoid IE memory leaks
+               Ext.get(this.document.body).purgeAllListeners();
+               Ext.get(this.document.body).dom = null;
+               Ext.get(this.document.documentElement).purgeAllListeners();
+               Ext.get(this.document.documentElement).dom = null;
+               this.document = null;
+               this.getEditor().document = null;
+               for (var index = this.nestedParentElements.sorted.length; --index >= 0;) {
+                       var nested = this.nestedParentElements.sorted[index];
+                       Ext.get(nested).purgeAllListeners();
+                       Ext.get(nested).dom = null;
+               }
+               Ext.destroy(this.autoEl, this.el, this.resizeEl, this.positionEl);
+               return true;
+       }
+});
+Ext.reg('htmlareaiframe', HTMLArea.Iframe);
diff --git a/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.StatusBar.js b/typo3/sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/Editor/HTMLArea.StatusBar.js
new file mode 100644 (file)
index 0000000..c33da65
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * HTMLArea.StatusBar extends Ext.Container
+ */
+HTMLArea.StatusBar = Ext.extend(Ext.Container, {
+       /*
+        * Constructor
+        */
+       initComponent: function () {
+               HTMLArea.StatusBar.superclass.initComponent.call(this);
+                       // Build the deferred word count update task
+               this.updateWordCountLater = new Ext.util.DelayedTask(this.updateWordCount, this);
+               this.addListener({
+                       render: {
+                               fn: this.addComponents,
+                               single: true
+                       },
+                       afterrender: {
+                               fn: this.initEventListeners,
+                               single: true
+                       }
+               });
+       },
+       /*
+        * Initialize listeners
+        */
+       initEventListeners: function () {
+               this.addListener({
+                       beforedestroy: {
+                               fn: this.onBeforeDestroy,
+                               single: true
+                       }
+               });
+                       // Monitor toolbar updates in order to refresh the contents of the statusbar
+                       // The toolbar must have been rendered
+               this.mon(this.ownerCt.toolbar, 'HTMLAreaEventToolbarUpdate', this.onUpdateToolbar, this);
+                       // Monitor editor changing mode
+               this.mon(this.getEditor(), 'HTMLAreaEventModeChange', this.onModeChange, this);
+                       // Monitor word count change
+               this.mon(this.ownerCt.iframe, 'HTMLAreaEventWordCountChange', this.onWordCountChange, this);
+       },
+       /*
+        * editorId should be set in config
+        */
+       editorId: null,
+       /*
+        * Get a reference to the editor
+        */
+       getEditor: function() {
+               return RTEarea[this.editorId].editor;
+       },
+       /*
+        * Create span elements to display when the status bar tree or a message when the editor is in text mode
+        */
+       addComponents: function () {
+               this.statusBarWordCount = Ext.DomHelper.append(this.getEl(), {
+                       id: this.editorId + '-statusBarWordCount',
+                       tag: 'span',
+                       cls: 'statusBarWordCount',
+                       html: '&nbsp;'
+               }, true);
+               this.statusBarTree = Ext.DomHelper.append(this.getEl(), {
+                       id: this.editorId + '-statusBarTree',
+                       tag: 'span',
+                       cls: 'statusBarTree',
+                       html: HTMLArea.localize('Path') + ': '
+               }, true).setVisibilityMode(Ext.Element.DISPLAY).setVisible(true);
+               this.statusBarTextMode = Ext.DomHelper.append(this.getEl(), {
+                       id: this.editorId + '-statusBarTextMode',
+                       tag: 'span',
+                       cls: 'statusBarTextMode',
+                       html: HTMLArea.localize('TEXT_MODE')
+               }, true).setVisibilityMode(Ext.Element.DISPLAY).setVisible(false);
+       },
+       /*
+        * Clear the status bar tree
+        */
+       clear: function () {
+               this.statusBarTree.removeAllListeners();
+               var statusBarNodes = this.statusBarTree.query('a');
+               for (var i = statusBarNodes.length; --i >= 0;) {
+                       var node = statusBarNodes[i];
+                       Ext.QuickTips.unregister(node);
+                       Ext.get(node).dom.ancestor = null;
+                       Ext.destroy(node);
+               }
+               this.statusBarTree.update('');
+               this.setSelection(null);
+       },
+       /*
+        * Flag indicating that the status bar should not be updated on this toolbar update
+        */
+       noUpdate: false,
+       /*
+        * Update the status bar
+        */
+       onUpdateToolbar: function (mode, selectionEmpty, ancestors, endPointsInSameBlock) {
+               if (mode === 'wysiwyg' && !this.noUpdate) {
+                       var text,
+                               language,
+                               languageObject = this.getEditor().getPlugin('Language'),
+                               classes = new Array(),
+                               classText;
+                       this.clear();
+                       var path = Ext.DomHelper.append(this.statusBarTree, {
+                               tag: 'span',
+                               html: HTMLArea.localize('Path') + ': '
+                       },true);
+                       var index, n, j, m;
+                       for (index = 0, n = ancestors.length; index < n; index++) {
+                               var ancestor = ancestors[index];
+                               if (!ancestor) {
+                                       continue;
+                               }
+                               text = ancestor.nodeName.toLowerCase();
+                                       // Do not show any id generated by ExtJS
+                               if (ancestor.id && text !== 'body' && ancestor.id.substr(0, 7) !== 'ext-gen') {
+                                       text += '#' + ancestor.id;
+                               }
+                               if (languageObject && languageObject.getLanguageAttribute) {
+                                       language = languageObject.getLanguageAttribute(ancestor);
+                                       if (language != 'none') {
+                                               text += '[' + language + ']';
+                                       }
+                               }
+                               if (ancestor.className) {
+                                       classText = '';
+                                       classes = ancestor.className.trim().split(' ');
+                                       for (j = 0, m = classes.length; j < m; ++j) {
+                                               if (!HTMLArea.reservedClassNames.test(classes[j])) {
+                                                       classText += '.' + classes[j];
+                                               }
+                                       }
+                                       text += classText;
+                               }
+                               var element = Ext.DomHelper.insertAfter(path, {
+                                       tag: 'a',
+                                       href: '#',
+     &nbs