Fixed bug #14066: htmlArea RTE: Erratic handling of oncut and onpaste events
authorStanislas Rolland <typo3@sjbr.ca>
Mon, 12 Apr 2010 04:06:33 +0000 (04:06 +0000)
committerStanislas Rolland <typo3@sjbr.ca>
Mon, 12 Apr 2010 04:06:33 +0000 (04:06 +0000)
git-svn-id: https://svn.typo3.org/TYPO3v4/Core/trunk@7299 709f56b5-9817-0410-a4d7-c38de5d9e867

ChangeLog
typo3/sysext/rtehtmlarea/ChangeLog
typo3/sysext/rtehtmlarea/htmlarea/htmlarea.js
typo3/sysext/rtehtmlarea/htmlarea/plugins/CopyPaste/copy-paste.js
typo3/sysext/rtehtmlarea/htmlarea/plugins/TYPO3HtmlParser/typo3html-parser.js

index 5934e23..e6d3045 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,6 +2,7 @@
 
        * Fixed bug #14062: Text Content element crashes (thanks to Jigal van Hemert)
        * Fixed bug #10629: class and title parameters of typolinks get broken
+       * Fixed bug #14066: htmlArea RTE: Erratic handling of oncut and onpaste events
 
 2010-04-11  Francois Suter  <francois@typo3.org>
 
index dfaec1c..f2d303c 100644 (file)
@@ -1,6 +1,7 @@
 2010-04-11  Stanislas Rolland  <typo3@sjbr.ca>
 
-       * Fixed bug #14062: Text Content element crashes (thanks to Jigal van Hemert)
+       * Fixed bug #14062: Text Content element crashes (thanks to Jigal van Hemert
+       * Fixed bug #14066: htmlArea RTE: Erratic handling of oncut and onpaste events
 
 2010-04-10  Stanislas Rolland  <typo3@sjbr.ca>
 
index 5326f55..723079b 100644 (file)
@@ -87,8 +87,6 @@ HTMLArea.init = function() {
        if (!Ext.isString(HTMLArea.editedContentCSS)) {
                HTMLArea.editedContentCSS = HTMLArea.editorSkin + 'htmlarea-edited-content.css';
        }
-               // Initialize pending request flag for Opera
-       HTMLArea.pendingSynchronousXMLHttpRequest = false;
                // Localization of core script
        HTMLArea.I18N = HTMLArea_langArray;
        HTMLArea.isReady = true;
@@ -1171,8 +1169,8 @@ HTMLArea.Iframe = Ext.extend(Ext.BoxComponent, {
         * Handler for other key events
         */
        onAnyKey: function(event) {
-                       // In Opera, inhibit key events while synchronous XMLHttpRequest is being processed
-               if (Ext.isOpera && HTMLArea.pendingSynchronousXMLHttpRequest) {
+                       // Inhibit key events while server-based cleaning is being processed
+               if (this.getEditor().inhibitKeyboardInput) {
                        event.stopEvent();
                        return false;
                }
@@ -2288,6 +2286,8 @@ HTMLArea.Editor = Ext.extend(Ext.util.Observable, {
                                this.registerPlugin(plugin);
                        }
                }, this);
+                       // Initialize keyboard input inhibit flag
+               this.inhibitKeyboardInput = false;
                this.addEvents(
                        /*
                         * @event editorready
@@ -4160,6 +4160,7 @@ HTMLArea.Plugin = HTMLArea.Base.extend({
                                success = true;
                        },
                        failure: function (response) {
+                               this.editor.inhibitKeyboardInput = false;
                                this.appendToLog('getJavascriptFile', 'Unable to get ' + url + ' . Server reported ' + response.status);
                        },
                        scope: this
index 23c61cc..2983c03 100644 (file)
  * TYPO3 SVN ID: $Id$
  */
 CopyPaste = HTMLArea.Plugin.extend({
-               
-       constructor : function(editor, pluginName) {
+       constructor: function(editor, pluginName) {
                this.base(editor, pluginName);
        },
-       
        /*
         * This function gets called by the class constructor
         */
-       configurePlugin : function (editor) {
-               
+       configurePlugin: function (editor) {
                /*
                 * Setting up some properties from PageTSConfig
                 */
                this.buttonsConfiguration = this.editorConfiguration.buttons;
-               
                /*
                 * Registering plugin "About" information
                 */
                var pluginInformation = {
-                       version         : "2.0",
-                       developer       : "Stanislas Rolland",
-                       developerUrl    : "http://www.sjbr.ca/",
-                       copyrightOwner  : "Stanislas Rolland",
-                       sponsor         : this.localize("Technische Universitat Ilmenau"),
-                       sponsorUrl      : "http://www.tu-ilmenau.de/",
-                       license         : "GPL"
+                       version         : '2.0',
+                       developer       : 'Stanislas Rolland',
+                       developerUrl    : 'http://www.sjbr.ca/',
+                       copyrightOwner  : 'Stanislas Rolland',
+                       sponsor         : this.localize('Technische Universitat Ilmenau'),
+                       sponsorUrl      : 'http://www.tu-ilmenau.de/',
+                       license         : 'GPL'
                };
                this.registerPluginInformation(pluginInformation);
                /*
                 * Registering the buttons
                 */
-               for (var buttonId in this.buttonList) {
-                       if (this.buttonList.hasOwnProperty(buttonId)) {
-                               var button = this.buttonList[buttonId];
-                               var buttonConfiguration = {
-                                       id              : buttonId,
-                                       tooltip         : this.localize(buttonId.toLowerCase()),
-                                       iconCls         : 'htmlarea-action-' + button[2],
-                                       action          : 'onButtonPress',
-                                       context         : button[0],
-                                       selection       : button[3],
-                                       hotKey          : (this.buttonsConfiguration[button[2]] ? this.buttonsConfiguration[button[2]].hotKey : (button[1] ? button[1] : null))
-                               };
-                               this.registerButton(buttonConfiguration);
-                               if (!this.isButtonInToolbar(buttonId)) {
-                                       var hotKeyConfiguration = {
-                                               id      : buttonConfiguration.hotKey,
-                                               cmd     : buttonConfiguration.id
-                                       };
-                                       this.registerHotKey(hotKeyConfiguration);
-                               }
-                       }
-               }
+               Ext.iterate(this.buttonList, function (buttonId, button) {
+                       var buttonConfiguration = {
+                               id              : buttonId,
+                               tooltip         : this.localize(buttonId.toLowerCase()),
+                               iconCls         : 'htmlarea-action-' + button[2],
+                               action          : 'onButtonPress',
+                               context         : button[0],
+                               selection       : button[3],
+                               hotKey          : button[1]
+                       };
+                       this.registerButton(buttonConfiguration);
+               }, this);
                return true;
        },
        /*
@@ -94,75 +80,134 @@ CopyPaste = HTMLArea.Plugin.extend({
                Paste   : [null, 'v', 'paste', false]
        },
        /*
+        * This function gets called when the editor is generated
+        */
+       onGenerate: function () {
+               this.editor.iframe.mon(Ext.get(Ext.isIE ? this.editor.document.body : this.editor.document.documentElement), 'cut', this.cutHandler, this);
+                       // Add hot key handling if the button is not enabled in the toolbar
+               Ext.iterate(this.buttonList, function (buttonId, button) {
+                       if (!this.isButtonInToolbar(buttonId)) {
+                               this.editor.iframe.hotKeyMap.addBinding({
+                                       key: button[1].toUpperCase(),
+                                       ctrl: true,
+                                       shift: false,
+                                       alt: false,
+                                       handler: this.onHotKey,
+                                       scope: this
+                               });
+                                       // Ensure the hot key can be translated
+                               this.editorConfiguration.hotKeyList[button[1]] = {
+                                       id      : button[1],
+                                       cmd     : buttonId
+                               };
+                       }
+               }, this);
+       },
+       /*
         * This function gets called when a button or a hotkey was pressed.
         *
         * @param       object          editor: the editor instance
         * @param       string          id: the button id or the key
-        * @param       object          target: the target element of the contextmenu event, when invoked from the context menu
         *
         * @return      boolean         false if action is completed
         */
-       onButtonPress : function (editor, id, target) {
+       onButtonPress: function (editor, id) {
                        // Could be a button or its hotkey
                var buttonId = this.translateHotKey(id);
                buttonId = buttonId ? buttonId : id;
                this.editor.focus();
-               if (!this.applyToTable(buttonId, target)) {
+               if (!this.applyToTable(buttonId)) {
                                // If we are not handling table cells
                        switch (buttonId) {
-                               case "Copy":
-                               case "Cut" :
+                               case 'Copy':
                                        if (buttonId == id) {
                                                        // If we are handling a button, not a hotkey
                                                this.applyBrowserCommand(buttonId);
-                                       } else if (buttonId == "Cut") {
-                                                       // If we are handling the cut hotkey
-                                               this.removeEmptyLinkLater.defer(50, this);
                                        }
                                        break;
-                               case "Paste":
+                               case 'Cut' :
+                                       if (buttonId == id) {
+                                                       // If we are handling a button, not a hotkey
+                                               this.applyBrowserCommand(buttonId);
+                                       }
+                                               // Opera will not trigger the onCut event
+                                       if (Ext.isOpera) {
+                                               this.cutHandler();
+                                       }
+                                       break;
+                               case 'Paste':
                                        if (buttonId == id) {
                                                        // If we are handling a button, not a hotkey
                                                this.applyBrowserCommand(buttonId);
                                        }
                                                // In FF3, the paste operation will indeed trigger the onPaste event not in FF2; nor in Opera
-                                       if (Ext.isOpera || Ext.isGecko2 || Ext.isWebKit) {
-                                               var cleaner = this.getPluginInstance('DefaultClean');
-                                               if (!cleaner) {
-                                                       cleaner = this.getPluginInstance('TYPO3HtmlParser');
-                                               }
+                                       if (Ext.isOpera || Ext.isGecko2) {
+                                               var cleaner = this.getButton('CleanWord');
                                                if (cleaner) {
-                                                       cleaner.clean.defer(50, cleaner);
+                                                       cleaner.fireEvent.defer(250, cleaner, ['click', cleaner]);
                                                }
                                        }
                                        break;
                                default:
                                        break;
                        }
+                               // Stop the event if a button was handled
                        return (buttonId != id);
                } else {
-                               // We handled the table case
+                               // The table case was handled, let the event be stopped.
+                               // No cleaning required as the pasted cells are copied from the editor.
+                               // However paste by Opera cannot be stopped.
+                               // Revert Opera's operation as it produces invalid html anyways
+                       if (Ext.isOpera) {
+                               this.editor.inhibitKeyboardInput = true;
+                               var bookmark = this.editor.getBookmark(this.editor._createRange(this.editor._getSelection()));
+                               var html = this.editor.getInnerHTML();
+                               this.revertPaste.defer(200, this, [html, bookmark]);
+                       }
                        return false;
                }
        },
-       
-       applyBrowserCommand : function (buttonId) {
+       /*
+        * This funcion reverts the paste operation (performed by Opera)
+        */
+       revertPaste: function (html, bookmark) {
+               this.editor.setHTML(html);
+               this.editor.selectRange(this.editor.moveToBookmark(bookmark));
+               this.editor.inhibitKeyboardInput = false;
+       },
+       /*
+        * This function applies the browser command when a button is pressed
+        * In the case of hot key, the browser does it automatically
+        */
+       applyBrowserCommand: function (buttonId) {
                try {
-                       this.editor._doc.execCommand(buttonId, false, null);
+                       this.editor.document.execCommand(buttonId, false, null);
                } catch (e) {
                        if (Ext.isGecko) {
                                this.mozillaClipboardAccessException();
                        }
                }
-               if (buttonId == "Cut") {
-                       this.removeEmptyLink();
+       },
+       /*
+        * Handler for hotkeys configured through the hotKeyMap while button not enabled in toolbar (see onGenerate above)
+        */
+       onHotKey: function (key, event) {
+               var hotKey = String.fromCharCode(key).toLowerCase();
+                       // Stop the event if it was handled here
+               if (!this.onButtonPress(this, hotKey)) {
+                       event.stopEvent();
                }
        },
-       
+       /*
+        * This function removes any link left over by the cut operation
+        */
+       cutHandler: function (event) {
+               this.removeEmptyLink.defer(50, this);
+       },
        /*
         * This function unlinks any empty link left over by the cut operation
         */
-       removeEmptyLink : function() {
+       removeEmptyLink: function() {
                var selection = this.editor._getSelection();
                var range = this.editor._createRange(selection);
                var parent = this.editor.getParentElement(selection, range);
@@ -177,7 +222,7 @@ CopyPaste = HTMLArea.Plugin.extend({
                                        this.editor.removeMarkup(parent);
                                                // Opera does not render empty list items
                                        if (Ext.isOpera && /^(li)$/i.test(container.nodeName) && !container.firstChild) {
-                                               container.innerHTML = "<br />";
+                                               container.innerHTML = '<br />';
                                                this.editor.selectNodeContents(container, true);
                                        }
                                } else {
@@ -187,49 +232,45 @@ CopyPaste = HTMLArea.Plugin.extend({
                }
                if (Ext.isWebKit) {
                                // Remove Apple's span and font tags
-                       this.editor.cleanAppleStyleSpans(this.editor._doc.body);
+                       this.editor.cleanAppleStyleSpans(this.editor.document.body);
                                // Reset Safari selection in order to prevent insertion of span and/or font tags on next text input
                        var bookmark = this.editor.getBookmark(this.editor._createRange(this.editor._getSelection()));
                        this.editor.selectRange(this.editor.moveToBookmark(bookmark));
                }
-       },
-       
-       /*
-        * This function removes any link left over by the cut operation triggered by hotkey
-        */
-       removeEmptyLinkLater : function() {
-               this.removeEmptyLink();
                this.editor.updateToolbar();
        },
-       
        /*
-        * This function gets called by the main editor when a copy/cut/paste operation is to be performed
+        * This function gets called when a copy/cut/paste operation is to be performed
+        * This feature allows to paste a region of table cells
         */
-       applyToTable : function (buttonId, target) {
+       applyToTable: function (buttonId) {
                var selection = this.editor._getSelection();
                var range = this.editor._createRange(selection);
                var parent = this.editor.getParentElement(selection, range);
                var endBlocks = this.editor.getEndBlocks(selection);
                switch (buttonId) {
-                       case "Copy":
-                       case "Cut" :
+                       case 'Copy':
+                       case 'Cut' :
                                HTMLArea.copiedCells = null;
                                var endBlocks = this.editor.getEndBlocks(selection);
                                if ((/^(tr)$/i.test(parent.nodeName) && !Ext.isIE) || (/^(td|th)$/i.test(endBlocks.start.nodeName) && /^(td|th)$/i.test(endBlocks.end.nodeName) && !Ext.isGecko && endBlocks.start != endBlocks.end)) {
                                        HTMLArea.copiedCells = this.collectCells(buttonId, selection, endBlocks);
-                                       if (buttonId === "Cut") return true;
                                }
                                break;
-                       case "Paste":
+                       case 'Paste':
                                if (/^(tr|td|th)$/i.test(parent.nodeName) && HTMLArea.copiedCells) {
                                        return this.pasteCells(selection, endBlocks);
                                }
                                break;
+                       default:
+                               break;
                }
                return false;
        },
-       
-       pasteCells : function (selection, endBlocks) {
+       /*
+        * This function handles pasting of a collection of table cells
+        */
+       pasteCells: function (selection, endBlocks) {
                var cell = null;
                if (Ext.isGecko) {
                        range = selection.getRangeAt(0);
@@ -241,8 +282,11 @@ CopyPaste = HTMLArea.Plugin.extend({
                if (!cell && /^(td|th)$/i.test(endBlocks.start.nodeName)) {
                        cell = endBlocks.start;
                }
-               if (!cell) return false;
-               var tableParts = ["thead", "tbody", "tfoot"];
+               if (!cell) {
+                               // Let the browser do it
+                       return false;
+               }
+               var tableParts = ['thead', 'tbody', 'tfoot'];
                var tablePartsIndex = { thead : 0, tbody : 1, tfoot : 2 };
                var tablePart = cell.parentNode.parentNode;
                var tablePartIndex = tablePartsIndex[tablePart.nodeName.toLowerCase()]
@@ -274,12 +318,11 @@ CopyPaste = HTMLArea.Plugin.extend({
                }
                return true;
        },
-       
        /*
         * This function collects the selected table cells for copy/cut operations
         */
-       collectCells : function (operation, selection, endBlocks) {
-               var tableParts = ["thead", "tbody", "tfoot"];
+       collectCells: function (operation, selection, endBlocks) {
+               var tableParts = ['thead', 'tbody', 'tfoot'];
                var tablePartsIndex = { thead : 0, tbody : 1, tfoot : 2 };
                var selection = this.editor._getSelection();
                var range, i = 0, cell, cells = null;
@@ -295,10 +338,10 @@ CopyPaste = HTMLArea.Plugin.extend({
                                for (var i = 0, n = endBlocks.start.cells.length; i < n; ++i) {
                                        cell = endBlocks.start.cells[i];
                                        cells.push(cell.innerHTML);
-                                       if (operation === "Cut") {
-                                               cell.innerHTML = "<br />";
+                                       if (operation === 'Cut') {
+                                               cell.innerHTML = '<br />';
                                        }
-                                       if (operation === "Cut") {
+                                       if (operation === 'Cut') {
                                                cutRows.push(endBlocks.start);
                                        }
                                }
@@ -311,15 +354,15 @@ CopyPaste = HTMLArea.Plugin.extend({
                                                cell = range.startContainer.childNodes[range.startOffset];
                                                if (cell.parentNode != row) {
                                                        (cells) && rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells);
-                                                       if (operation === "Cut" && firstCellOfRow && lastCellOfRow) cutRows.push(row);
+                                                       if (operation === 'Cut' && firstCellOfRow && lastCellOfRow) cutRows.push(row);
                                                        row = cell.parentNode;
                                                        cells = [];
                                                        firstCellOfRow = false;
                                                        lastCellOfRow = false;
                                                }
                                                cells.push(cell.innerHTML);
-                                               if (operation === "Cut") {
-                                                       cell.innerHTML = "<br />";
+                                               if (operation === 'Cut') {
+                                                       cell.innerHTML = '<br />';
                                                }
                                                if (!cell.previousSibling) firstCellOfRow = true;
                                                if (!cell.nextSibling) lastCellOfRow = true;
@@ -328,7 +371,7 @@ CopyPaste = HTMLArea.Plugin.extend({
                                        /* finished walking through selection */
                                }
                                try { rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells); } catch(e) { }
-                               if (row && operation === "Cut" && firstCellOfRow && lastCellOfRow) {
+                               if (row && operation === 'Cut' && firstCellOfRow && lastCellOfRow) {
                                        cutRows.push(row);
                                }
                        }
@@ -342,8 +385,8 @@ CopyPaste = HTMLArea.Plugin.extend({
                                cell = endBlocks.start;
                                while (cell) {
                                        cells.push(cell.innerHTML);
-                                       if (operation === "Cut") {
-                                               cell.innerHTML = "";
+                                       if (operation === 'Cut') {
+                                               cell.innerHTML = '';
                                        }
                                        if (!cell.previousSibling) firstCellOfRow = true;
                                        if (!cell.nextSibling) lastCellOfRow = true;
@@ -351,19 +394,19 @@ CopyPaste = HTMLArea.Plugin.extend({
                                        cell = cell.nextSibling;
                                }
                                rows[tablePartsIndex[firstRow.parentNode.nodeName.toLowerCase()]].push(cells);
-                               if (operation === "Cut" && firstCellOfRow && lastCellOfRow) cutRows.push(firstRow);
+                               if (operation === 'Cut' && firstCellOfRow && lastCellOfRow) cutRows.push(firstRow);
                        } else { // Collect all cells on selected rows
                                row = firstRow;
                                while (row) {
                                        cells = [];
                                        for (var i = 0, n = row.cells.length; i < n ; ++i) {
                                                cells.push(row.cells[i].innerHTML);
-                                               if (operation === "Cut") {
-                                                       row.cells[i].innerHTML = "";
+                                               if (operation === 'Cut') {
+                                                       row.cells[i].innerHTML = '';
                                                }
                                        }
                                        rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells);
-                                       if (operation === "Cut") cutRows.push(row);
+                                       if (operation === 'Cut') cutRows.push(row);
                                        if (row == lastRow) break;
                                        row = row.nextSibling;
                                }
@@ -385,52 +428,48 @@ CopyPaste = HTMLArea.Plugin.extend({
                }
                return rows;
        },
-       
        /*
         * This function gets called when the toolbar is updated
         */
        onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
                if (mode === 'wysiwyg' && this.editor.isEditable() && button.itemId === 'Paste') {
                        try {
-                               button.setDisabled(!this.editor._doc.queryCommandEnabled(button.itemId));
+                               button.setDisabled(!this.editor.document.queryCommandEnabled(button.itemId));
                        } catch(e) {
                                button.setDisabled(true);
                        }
                }
        },
-       
        /*
         * Mozilla clipboard access exception handler
         */
-       mozillaClipboardAccessException : function () {
+       mozillaClipboardAccessException: function () {
                if (this.buttonsConfiguration.paste && this.buttonsConfiguration.paste.mozillaAllowClipboardURL) {
-                       if (confirm(this.localize("Allow-Clipboard-Helper-Extension"))) {
+                       if (confirm(this.localize('Allow-Clipboard-Helper-Extension'))) {
                                if (InstallTrigger.enabled()) {
                                        var mozillaXpi = new Object();
-                                       mozillaXpi["AllowClipboard Helper"] = this.buttonsConfiguration.paste.mozillaAllowClipboardURL;
+                                       mozillaXpi['AllowClipboard Helper'] = this.buttonsConfiguration.paste.mozillaAllowClipboardURL;
                                        InstallTrigger.install(mozillaXpi, this.mozillaInstallCallback);
                                } else {
-                                       alert(this.localize("Mozilla-Org-Install-Not-Enabled"));
-                                       this.appendToLog("mozillaClipboardAccessException", "Mozilla install was not enabled.");
+                                       alert(this.localize('Mozilla-Org-Install-Not-Enabled'));
+                                       this.appendToLog('mozillaClipboardAccessException', 'Mozilla install was not enabled.');
                                        return;
                                }
                        }
-               } else if (confirm(this.localize("Moz-Clipboard"))) {
-                       window.open("http://mozilla.org/editor/midasdemo/securityprefs.html");
+               } else if (confirm(this.localize('Moz-Clipboard'))) {
+                       window.open('http://mozilla.org/editor/midasdemo/securityprefs.html');
                }
        },
-       
        /*
         * Mozilla Add-on installer call back
         */
-       mozillaInstallCallback : function (url, returnCode) {
+       mozillaInstallCallback: function (url, returnCode) {
                if (returnCode == 0) {
-                       alert(this.localize("Allow-Clipboard-Helper-Extension-Success"));
+                       alert(this.localize('Allow-Clipboard-Helper-Extension-Success'));
                } else {
-                       alert(this.localize("Moz-Extension-Failure"));
-                       this.appendToLog("mozillaInstallCallback", "Mozilla install return code was: " + returnCode + ".");
+                       alert(this.localize('Moz-Extension-Failure'));
+                       this.appendToLog('mozillaInstallCallback', 'Mozilla install return code was: ' + returnCode + '.');
                }
                return;
        }
 });
-
index d847c3e..c9bf2d3 100644 (file)
@@ -86,6 +86,7 @@ TYPO3HtmlParser = HTMLArea.Plugin.extend({
                this.editor.iframe.mon(Ext.get(Ext.isIE ? this.editor.document.body : this.editor.document.documentElement), 'paste', this.wordCleanHandler, this);
        },
        clean: function() {
+               this.editor.inhibitKeyboardInput = true;
                var editor = this.editor;
                if (Ext.isWebKit) {
                        editor.cleanAppleStyleSpans(editor._doc.body);
@@ -107,6 +108,7 @@ TYPO3HtmlParser = HTMLArea.Plugin.extend({
                                        } else {
                                                this.appendToLog('clean', 'Post request to ' + url + ' failed. Server reported ' + response.status);
                                        }
+                                       this.editor.inhibitKeyboardInput = false;
                                }
                );
        },
@@ -114,6 +116,6 @@ TYPO3HtmlParser = HTMLArea.Plugin.extend({
         * Handler for paste, dragdrop and drop events
         */
        wordCleanHandler: function (event) {
-               this.clean.defer(250, this);
+               this.clean.defer(50, this);
        }
 });