From c7cf00943285aec75deb65194174a1dadb2a54d7 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 17 Sep 2014 02:15:45 +0200 Subject: [PATCH] Improved delete/backspace handling --- .../js/3rdParty/redactor/plugins/wbbcode.js | 111 ++++++++++++++++-- .../3rdParty/redactor/plugins/wmonkeypatch.js | 17 +++ .../js/3rdParty/redactor/plugins/wutil.js | 108 +++++++++++++++++ wcfsetup/install/files/js/WCF.Message.js | 3 +- 4 files changed, 229 insertions(+), 10 deletions(-) diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js index 42b006b2ae..49fc415d24 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js @@ -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(/ /gi, " "); // [quote] - html = html.replace(/
\n?]+>\n?
[\s\S]*?<\/header>/gi, function(match, link, author, innerContent) { + html = html.replace(/
\n?]+>\n?]*?)>[\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 = '
'; } + else if ($line.match(/^@@([0-9\-]+)@@$/)) { + if (WCF.inArray(RegExp.$1, $knownQuotes)) { + // prevent quote being nested inside a

block + data += $line; + continue; + } + } data += '

' + $line + '

'; } @@ -713,7 +721,7 @@ RedactorPlugins.wbbcode = { var $quote = '
' + '
' - + '
' + + '
' + '

' + self._buildQuoteHeader($author, $link) + '

' @@ -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
+ this.$editor.find('blockquote').each(function(index, blockquote) { + var $blockquote = $(blockquote); + if (!$blockquote.find('> div > header').length) { + $blockquote.remove(); + } + }); + }, + /** * Initializes source editing for quotes. */ diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js index 5e09578834..1295a51fc5 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js @@ -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')); diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js index 726a90fb9a..0ab965bee3 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js @@ -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. * diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js index ee9f03c7de..37899c054a 100644 --- a/wcfsetup/install/files/js/WCF.Message.js +++ b/wcfsetup/install/files/js/WCF.Message.js @@ -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); -- 2.20.1