Improved delete/backspace handling
authorAlexander Ebert <ebert@woltlab.com>
Wed, 17 Sep 2014 00:15:45 +0000 (02:15 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 17 Sep 2014 00:15:45 +0000 (02:15 +0200)
wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js
wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js
wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js
wcfsetup/install/files/js/WCF.Message.js

index 42b006b2ae61bc7670b61117fdcf7070a16e6566..49fc415d2482f4971bd515d7b8656355c1f5b672 100644 (file)
@@ -68,6 +68,7 @@ RedactorPlugins.wbbcode = {
                
                // handle keydown
                WCF.System.Event.addListener('com.woltlab.wcf.redactor', 'keydown_' + $identifier, $.proxy(this._wKeydownCallback, this));
+               WCF.System.Event.addListener('com.woltlab.wcf.redactor', 'keyup_' + $identifier, $.proxy(this._wKeyupCallback, this));
        },
        
        /**
@@ -227,11 +228,11 @@ RedactorPlugins.wbbcode = {
                html = html.replace(/&nbsp;/gi, " ");
                
                // [quote]
-               html = html.replace(/<blockquote class="quoteBox" cite="([^"]+)?" data-author="([^"]+)?">\n?<div[^>]+>\n?<header>[\s\S]*?<\/header>/gi, function(match, link, author, innerContent) {
+               html = html.replace(/<blockquote class="quoteBox" cite="([^"]+)?" data-author="([^"]+)?">\n?<div[^>]+>\n?<header(?:[^>]*?)>[\s\S]*?<\/header>/gi, function(match, link, author, innerContent) {
                        var $quote;
                        
-                       author = WCF.String.unescapeHTML(author);
-                       link = WCF.String.unescapeHTML(link);
+                       if (author) author = WCF.String.unescapeHTML(author);
+                       if (link) link = WCF.String.unescapeHTML(link);
                        
                        if (link) {
                                $quote = "[quote='" + author + "','" + link + "']";
@@ -419,7 +420,7 @@ RedactorPlugins.wbbcode = {
                WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'convertFromHtml', { html: html });
                
                // Remove remaining tags.
-               html = html.replace(/<[^>]+>/g, '');
+               html = html.replace(/<[^(<|>)]+>/g, '');
                
                // insert redactor's selection markers
                if ($.getLength($cachedMarkers)) {
@@ -660,6 +661,13 @@ RedactorPlugins.wbbcode = {
                                if (!$line) {
                                        $line = '<br>';
                                }
+                               else if ($line.match(/^@@([0-9\-]+)@@$/)) {
+                                       if (WCF.inArray(RegExp.$1, $knownQuotes)) {
+                                               // prevent quote being nested inside a <p> block
+                                               data += $line;
+                                               continue;
+                                       }
+                               }
                                
                                data += '<p>' + $line + '</p>';
                        }
@@ -713,7 +721,7 @@ RedactorPlugins.wbbcode = {
                                        
                                        var $quote = '<blockquote class="quoteBox" cite="' + $link + '" data-author="' + $author + '">'
                                                + '<div class="container containerPadding">'
-                                                       + '<header>'
+                                                       + '<header contenteditable="false">'
                                                                + '<h3>'
                                                                        + self._buildQuoteHeader($author, $link)
                                                                + '</h3>'
@@ -885,13 +893,22 @@ RedactorPlugins.wbbcode = {
        },
        
        /**
-        * Handles up/down key for quote boxes.
+        * Handles up/down/delete/backspace key for quote boxes.
         * 
         * @param       object          data
         */
        _wKeydownCallback: function(data) {
-               if (data.event.which !== $.ui.keyCode.DOWN && data.event.which !== $.ui.keyCode.UP) {
-                       return;
+               switch (data.event.which) {
+                       case $.ui.keyCode.BACKSPACE:
+                       case $.ui.keyCode.DELETE:
+                       case $.ui.keyCode.DOWN:
+                       case $.ui.keyCode.UP:
+                               // handle keys
+                       break;
+                       
+                       default:
+                               return;
+                       break;
                }
                
                var $current = $(this.getCurrent());
@@ -900,6 +917,65 @@ RedactorPlugins.wbbcode = {
                var $quote = ($parent) ? $parent.closest('blockquote.quoteBox', this.$editor.get()[0]) : { length: 0 };
                
                switch (data.event.which) {
+                       // backspace key
+                       case $.ui.keyCode.BACKSPACE:
+                               if (this.isCaret()) {
+                                       if ($quote.length) {
+                                               // check if quote is empty
+                                               var $isEmpty = true;
+                                               $quote.find('div > div').each(function() {
+                                                       if ($(this).text().replace(/\u200B/, '').length) {
+                                                               $isEmpty = false;
+                                                               return false;
+                                                       }
+                                               });
+                                               
+                                               if ($isEmpty) {
+                                                       // expand selection and prevent delete
+                                                       var $selection = window.getSelection();
+                                                       if ($selection.rangeCount) $selection.removeAllRanges();
+                                                       
+                                                       var $quoteRange = document.createRange();
+                                                       $quoteRange.selectNode($quote[0]);
+                                                       $selection.addRange($quoteRange);
+                                                       
+                                                       data.cancel = true;
+                                               }
+                                       }
+                               }
+                               else {
+                                       // check if selection contains a quote, turn on buffer if true
+                                       var $contents = this.getRange().cloneContents();
+                                       if (this.containsTag($contents, 'BLOCKQUOTE')) {
+                                               this.bufferSet();
+                                       }
+                               }
+                       break;
+                       
+                       // delete key
+                       case $.ui.keyCode.DELETE:
+                               if (this.isCaret()) {
+                                       if (this.isEndOfElement($current[0]) && $current.next('blockquote').length) {
+                                               // expand selection and prevent delete
+                                               var $selection = window.getSelection();
+                                               if ($selection.rangeCount) $selection.removeAllRanges();
+                                               
+                                               var $quoteRange = document.createRange();
+                                               $quoteRange.selectNode($current.next()[0]);
+                                               $selection.addRange($quoteRange);
+                                               
+                                               data.cancel = true;
+                                       }
+                               }
+                               else {
+                                       // check if selection contains a quote, turn on buffer if true
+                                       var $contents = this.getRange().cloneContents();
+                                       if (this.containsTag($contents, 'BLOCKQUOTE')) {
+                                               this.bufferSet();
+                                       }
+                               }
+                       break;
+                       
                        // arrow down
                        case $.ui.keyCode.DOWN:
                                if ($current.next('blockquote.quoteBox').length) {
@@ -973,6 +1049,25 @@ RedactorPlugins.wbbcode = {
                }
        },
        
+       /**
+        * Handles quote deletion.
+        * 
+        * @param       object          data
+        */
+       _wKeyupCallback: function(data) {
+               if (data.event.which !== $.ui.keyCode.BACKSPACE && data.event.which !== $.ui.keyCode.DELETE) {
+                       return;
+               }
+               
+               // check for empty <blockquote>
+               this.$editor.find('blockquote').each(function(index, blockquote) {
+                       var $blockquote = $(blockquote);
+                       if (!$blockquote.find('> div > header').length) {
+                               $blockquote.remove();
+                       }
+               });
+       },
+       
        /**
         * Initializes source editing for quotes.
         */
index 5e095788340268872430d64e201e731824505d7b..1295a51fc567ddcad09d5f99dc54e87c15bac49f 100644 (file)
@@ -46,6 +46,23 @@ RedactorPlugins.wmonkeypatch = {
                        return false;
                };
                
+               // keyup w/ event aborting through callback
+               var $mpBuildEventKeyup = this.buildEventKeyup;
+               this.buildEventKeyup = function(e) {
+                       var $eventData = {
+                               cancel: false,
+                               event: e
+                       };
+                       
+                       WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'keyup_' + $identifier, $eventData);
+                       
+                       if ($eventData.cancel !== true) {
+                               return $mpBuildEventKeyup.call(self, e);
+                       }
+                       
+                       return false;
+               };
+               
                var $mpToggleCode = this.toggleCode;
                this.toggleCode = function(direct) {
                        var $height = self.normalize(self.$editor.css('height'));
index 726a90fb9a7d45eb9c0b72ffc231f99d521d1caa..0ab965bee3a8f30ba4549d7edda234406b591e8d 100644 (file)
@@ -330,6 +330,114 @@ RedactorPlugins.wutil = {
                this.selectionEnd(this.$editor.children(':last')[0]);
        },
        
+       /**
+        * Returns true if current selection is just a caret or false if selection spans content.
+        * 
+        * @param       Range           range
+        * @return      boolean
+        */
+       isCaret: function(range) {
+               var $range = (range) ? range : this.getRange();
+               
+               return $range.collapsed;
+       },
+       
+       /**
+        * Returns true if current selection is just a caret and it is the last possible offset
+        * within the given element.
+        * 
+        * @param       Element         element
+        * @return      boolean
+        */
+       isEndOfElement: function(element) {
+               var $range = this.getRange();
+               
+               // range is not a plain caret
+               if (!this.isCaret($range)) {
+                       console.debug("case#1");
+                       return false;
+               }
+               
+               if ($range.endContainer.nodeType === Element.TEXT_NODE) {
+                       // caret is not at the end
+                       if ($range.endOffset < $range.endContainer.length) {
+                               console.debug("case#2");
+                               return false;
+                       }
+               }
+               
+               // range is not within the provided element
+               if (!this.isNodeWithin($range.endContainer, element)) {
+                       console.debug("case#3");
+                       return false;
+               }
+               
+               var $current = $range.endContainer;
+               while ($current !== element) {
+                       // end of range is not the last element
+                       if ($current.nextSibling) {
+                               console.debug("case#4");
+                               return false;
+                       }
+                       
+                       $current = $current.parentNode;
+               }
+               
+               return true;
+       },
+       
+       /**
+        * Returns true if the provided node is a direct or indirect child of the target element. This
+        * method works similar to jQuery's $.contains() but works recursively.
+        * 
+        * @param       Element         node
+        * @param       Element         element
+        * @return      boolean
+        */
+       isNodeWithin: function(node, element) {
+               var $node = $(node);
+               while ($node[0] !== this.$editor[0]) {
+                       if ($node[0] === element) {
+                               return true;
+                       }
+                       
+                       $node = $node.parent();
+               }
+               
+               return false;
+       },
+       
+       /**
+        * Returns true if the given node equals the provided tagName or contains it.
+        * 
+        * @param       Element         node
+        * @param       string          tagName
+        * @return      boolean
+        */
+       containsTag: function(node, tagName) {
+               switch (node.nodeType) {
+                       case Element.ELEMENT_NODE:
+                               if (node.tagName === tagName) {
+                                       return true;
+                               }
+                               
+                       // fall through
+                       case Element.DOCUMENT_FRAGMENT_NODE:
+                               for (var $i = 0; $i < node.childNodes.length; $i++) {
+                                       if (this.containsTag(node.childNodes[$i], tagName)) {
+                                               return true;
+                                       }
+                               }
+                               
+                               return false;
+                       break;
+                       
+                       default:
+                               return false;
+                       break;
+               }
+       },
+       
        /**
         * Replaces the current content with the provided value.
         * 
index ee9f03c7def7b7384f9edea8f68b9eb2bc6e770d..37899c054a6cafa3fe3ba9b042c3539428580597 100644 (file)
@@ -889,11 +889,10 @@ WCF.Message.QuickReply = Class.extend({
                
                if ($.browser.redactor) {
                        var $html = WCF.String.unescapeHTML(data.returnValues.template);
-                       $html = this._messageField.redactor('transformQuote', $html);
+                       $html = this._messageField.redactor('convertToHtml', $html);
                        
                        this._messageField.redactor('selectionEndOfEditor');
                        this._messageField.redactor('insertDynamic', $html, data.returnValues.template);
-                       this._messageField.redactor('fixQuoteContent');
                }
                else {
                        this._messageField.val(data.returnValues.template);