// 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));
},
/**
html = html.replace(/ /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 + "']";
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)) {
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>';
}
var $quote = '<blockquote class="quoteBox" cite="' + $link + '" data-author="' + $author + '">'
+ '<div class="container containerPadding">'
- + '<header>'
+ + '<header contenteditable="false">'
+ '<h3>'
+ self._buildQuoteHeader($author, $link)
+ '</h3>'
},
/**
- * 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());
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) {
}
},
+ /**
+ * 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.
*/
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.
*