From 1c2318bbeb61f194349b75be39dc3f37ec6b34cc Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 28 Jun 2015 18:12:46 +0200 Subject: [PATCH] Added support for quotes --- .../js/3rdParty/redactor/plugins/wbbcode.js | 10 +- .../3rdParty/redactor/plugins/wmonkeypatch.js | 207 +++++++++--------- .../js/3rdParty/redactor/plugins/wutil.js | 40 +--- .../files/js/WoltLab/WCF/BBCode/FromHtml.js | 26 +++ .../files/js/WoltLab/WCF/BBCode/Parser.js | 19 +- .../files/js/WoltLab/WCF/BBCode/ToHtml.js | 67 ++++-- 6 files changed, 204 insertions(+), 165 deletions(-) diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js index 003eef9003..0b561df451 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js @@ -106,11 +106,10 @@ RedactorPlugins.wbbcode = function() { var $tooltip = $('.redactor-toolbar-tooltip-html:not(.jsWbbcode)').addClass('jsWbbcode').text(WCF.Language.get('wcf.bbcode.button.toggleBBCode')); var $fixBR = function(editor) { - editor.find('br').each(function(index, br) { - if (br.children.length) { - $(br).empty(); - } - }); + var elements = editor[0].querySelectorAll('br:not(:empty)'); + for (var i = 0, length = elements.length; i < length; i++) { + elements[0].innerHTML = ''; + } }; this.code.toggle = (function() { @@ -1857,6 +1856,7 @@ RedactorPlugins.wbbcode = function() { * Inserting block-level elements (e.g. quotes or code bbcode) can lead to void paragraphs. */ fixBlockLevelElements: function() { + return; var $removeVoidElements = (function(referenceElement, position) { var $sibling = referenceElement[position]; if ($sibling && $sibling.nodeType === Node.ELEMENT_NODE && $sibling.tagName === 'P') { diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js index d0cd2e9aaf..d0c7e23f37 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js @@ -105,151 +105,160 @@ RedactorPlugins.wmonkeypatch = function() { } }).bind(this)); - var $setCaretBeforeOrAfter = (function(element, setBefore) { + var isTargetElement = function(element) { + // [quote] + if (element.nodeName === 'BLOCKQUOTE') { + return true; + } + + // [code] + if (element.nodeName === 'DIV' && element.classList.contains('codeBox')) { + return true; + } + + return false; + }; + + var getOffset = function(element) { + var offsets = element.getBoundingClientRect(); + + return { + left: offsets.left + document.body.scrollLeft, + top: offsets.top + document.body.scrollTop + }; + }; + + function getVerticalBoundaries(element) { + var offset = getOffset(element); + var styles = window.getComputedStyle(element); + + return { + bottom: offset.top + element.offsetHeight + parseInt(styles.marginBottom), + top: offset.top - parseInt(styles.marginTop) + }; + }; + + var setCaretBeforeOrAfter = (function(element, setBefore) { + var ref; if (setBefore) { - if (element.previousElementSibling && (element.previousElementSibling.tagName === 'P' || element.previousElementSibling.tagName === 'DIV')) { - this.caret.setEnd(element.previousElementSibling); + ref = element.previousSibling; + if (ref === null) { + var space = this.utils.createSpaceElement(); + element.parentNode.insertBefore(space, element); + + this.caret.setEnd(space); } else { - this.wutil.setCaretBefore(element); + this.caret[(ref.nodeType === Node.ELEMENT_NODE && ref.nodeName === 'BR') ? 'setBefore' : 'setAfter'](ref); } } else { - if (element.nextElementSibling && (element.nextElementSibling.tagName === 'P' || element.nextElementSibling.tagName === 'DIV')) { - this.caret.setEnd(element.nextElementSibling); + ref = element.nextSibling; + if (ref === null) { + var space = this.utils.createSpaceElement(); + if (element.nextSibling === null) element.parentNode.appendChild(space); + else element.parentNode.insertBefore(space, element.nextSibling); + + this.caret.setEnd(space); } else { - this.wutil.setCaretAfter(element); + this.caret.setBefore(ref); } } }).bind(this); - var $editorPadding = null; + var editor = this.$editor[0]; this.$editor.on('click.wmonkeypatch', (function(event) { - if (event.target === this.$editor[0]) { - var $range = (window.getSelection().rangeCount) ? window.getSelection().getRangeAt(0) : null; - - if ($range && $range.collapsed) { - var $current = $range.startContainer; + var range = (window.getSelection().rangeCount) ? window.getSelection().getRangeAt(0) : null; + + if (event.target === editor) { + var boundaries, element; + for (var i = 0, length = editor.childElementCount; i < length; i++) { + element = editor.children[i]; - // this can occur if click occurs within the editor padding - var $offsets = this.$editor.offset(); - if ($editorPadding === null) { - $editorPadding = { - left: this.$editor.cssAsNumber('padding-left'), - top: this.$editor.cssAsNumber('padding-top') - }; + if (!isTargetElement(element)) { + continue; } - if (event.pageY <= $offsets.top + $editorPadding.top) { - var $firstChild = this.$editor[0].children[0]; - if ($firstChild.tagName !== 'BLOCKQUOTE' && ($firstChild.tagName !== 'DIV' || !/\bcodeBox\b/.test($firstChild.className))) { - return; - } + boundaries = getVerticalBoundaries(element); + + if (event.pageY > boundaries.bottom) { + continue; } - else { - if (event.pageX <= $offsets.left + $editorPadding.left) { - return; - } - else { - if (event.pageX > $offsets.left + this.$editor.width()) { - return; - } - } + else if (event.pageY < boundaries.top) { + break; } - while ($current && $current !== this.$editor[0]) { - if ($current.nodeType === Node.ELEMENT_NODE) { - if ($current.tagName === 'BLOCKQUOTE' || ($current.tagName === 'DIV' && /\bcodeBox\b/.test($current.className))) { - var $offset = $($current).offset(); - if (event.pageY <= $offset.top) { - $setCaretBeforeOrAfter($current, true); - } - else { - $setCaretBeforeOrAfter($current, false); - } - - // stop processing - return false; + if (event.pageY >= boundaries.top && event.pageY <= boundaries.bottom) { + var diffToTop = event.pageY - boundaries.top; + var height = boundaries.bottom - boundaries.top; + var setBefore = (diffToTop <= (height / 2)); + + var ref = element[setBefore ? 'previousSibling' : 'nextSibling']; + while (ref !== null) { + if (ref.nodeType === Node.TEXT_NODE && ref.textContent !== '') { + // non-empty text node, default behavior is okay + return; + } + else if (ref.nodeType === Node.ELEMENT_NODE && !isTargetElement(ref)) { + // a non-blocking element such as a formatted line or something, default behavior is okay + return; } + + ref = ref[setBefore ? 'previousSibling' : 'nextSibling']; } - $current = $current.parentElement; + setCaretBeforeOrAfter(element, setBefore); } } - var $elements = this.$editor.children('blockquote, div.codeBox'); - $elements.each(function(index, element) { - var $element = $(element); - var $offset = $element.offset(); - - if (event.pageY <= $offset.top) { - $setCaretBeforeOrAfter(element, true); - - return false; - } - else { - var $height = $element.outerHeight() + (parseInt($element.css('margin-bottom'), 10) || 0); - if (event.pageY <= $offset.top + $height) { - $setCaretBeforeOrAfter(element, false); - - return false; - } - } - }); - return false; } - else if (event.target.tagName === 'LI') { + else if (event.target.nodeName === 'LI') { // work-around for #1942 - var $range = (window.getSelection().rangeCount) ? window.getSelection().getRangeAt(0) : null; - var $caretInsideList = false; - if ($range !== null) { - if (!$range.collapsed) { - return; - } - - var $current = $range.startContainer; - while ($current !== null && $current !== this.$editor[0]) { - if ($current.tagName === 'LI') { - $caretInsideList = true; + var caretInsideList = false; + if (range !== null && range.collapsed) { + var current = range.startContainer; + while (current !== null && current !== editor) { + if (current.nodeName === 'LI') { + caretInsideList = true; break; } - $current = $current.parentElement; + current = current.parentNode; } } - if (!$caretInsideList || $range === null) { - var $node = document.createTextNode('\u200b'); - var $firstChild = event.target.children[0]; - $firstChild.appendChild($node); + if (!caretInsideList || range === null) { + var node = document.createTextNode('\u200b'); + var firstChild = event.target.children[0]; + firstChild.appendChild(node); - this.caret.setEnd($firstChild); + this.caret.setEnd(firstChild); } } - else if (event.target.tagName === 'BLOCKQUOTE') { - var $range = (window.getSelection().rangeCount) ? window.getSelection().getRangeAt(0) : null; - if ($range !== null && $range.collapsed) { + else if (event.target.nodeName === 'BLOCKQUOTE') { + range = (window.getSelection().rangeCount) ? window.getSelection().getRangeAt(0) : null; + if (range !== null && range.collapsed) { // check if caret is now inside a quote - var $blockquote = null; - var $current = ($range.startContainer.nodeType === Node.TEXT_NODE) ? $range.startContainer.parentElement : $range.startContainer; - while ($current !== null && $current !== this.$editor[0]) { - if ($current.tagName === 'BLOCKQUOTE') { - $blockquote = $current; + var blockquote = null; + var current = range.startContainer; + while (current !== null && current !== editor) { + if (current.nodeName === 'BLOCKQUOTE') { + blockquote = current; break; } - $current = $current.parentElement; + current = current.parentNode; } - if ($blockquote !== null && $blockquote !== event.target) { + if (blockquote !== null && blockquote !== event.target) { // click occured within inner quote margin, check if click happened before inner quote - if (event.pageY <= $($blockquote).offset().top) { - $setCaretBeforeOrAfter($blockquote, true); + if (event.pageY <= getOffset(blockquote).top) { + setCaretBeforeOrAfter(blockquote, true); } else { - $setCaretBeforeOrAfter($blockquote, false); + setCaretBeforeOrAfter(blockquote, false); } } } diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js index d41694bb41..ceb0244e44 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wutil.js @@ -894,7 +894,7 @@ RedactorPlugins.wutil = function() { $wasInWysiwygMode = true; } - value = this.wutil.addNewlines(value); + //value = this.wutil.addNewlines(value); this.$textarea.val(value); if ($wasInWysiwygMode) { @@ -951,44 +951,6 @@ RedactorPlugins.wutil = function() { * - pasting lists/list-items in lists can yield empty
  • */ fixDOM: function() { - var $current = this.$editor[0].childNodes[0]; - var $nextSibling = $current; - var $p = null; - - while ($nextSibling) { - $current = $nextSibling; - $nextSibling = $current.nextSibling; - - if ($current.nodeType === Element.ELEMENT_NODE) { - if (this.reIsBlock.test($current.tagName)) { - $p = null; - } - else { - if ($p === null) { - $p = $('

    ').insertBefore($current); - } - - $p.append($current); - } - } - else if ($current.nodeType === Element.TEXT_NODE) { - if ($p === null) { - // check for ghost paragraphs next - if ($nextSibling) { - if ($nextSibling.nodeType === Element.ELEMENT_NODE && $nextSibling.tagName === 'P' && $nextSibling.innerHTML === '\u200B') { - var $afterNextSibling = $nextSibling.nextSibling; - this.$editor[0].removeChild($nextSibling); - $nextSibling = $afterNextSibling; - } - } - - $p = $('

    ').insertBefore($current); - } - - $p.append($current); - } - } - var $listItems = this.$editor[0].getElementsByTagName('li'); for (var $i = 0, $length = $listItems.length; $i < $length; $i++) { var $listItem = $listItems[$i]; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js index 583a9d0acc..8612a5d1a0 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js @@ -55,6 +55,7 @@ define(['DOM/Traverse'], function(DOMTraverse) { { tagName: 'OL', callback: this._convertList.bind(this) }, { tagName: 'TABLE', callback: this._convertTable.bind(this) }, { tagName: 'UL', callback: this._convertList.bind(this) }, + { tagName: 'BLOCKQUOTE', callback: this._convertBlockquote.bind(this) }, // convert these last { tagName: 'SPAN', callback: this._convertSpan.bind(this) }, @@ -92,6 +93,31 @@ define(['DOM/Traverse'], function(DOMTraverse) { } }, + _convertBlockquote: function(element) { + var author = element.getAttribute('data-author') || ''; + var link = element.getAttribute('cite') || ''; + + var open = '[quote]'; + if (author) { + if (link) { + open = "[quote='" + author + "','" + link + "']"; + } + else { + open = "[quote='" + author + "']"; + } + } + + var header = DOMTraverse.childByTag(element, 'HEADER'); + if (header !== null) element.removeChild(header); + + var divs = DOMTraverse.childrenByTag(element, 'DIV'); + for (var i = 0, length = divs.length; i < length; i++) { + divs[i].outerHTML = divs[i].innerHTML + '\n'; + } + + element.outerHTML = open + element.innerHTML.replace(/^\n*/, '').replace(/\n*$/, '') + '[/quote]\n'; + }, + _convertList: function(element) { var open; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js index 2b8999f5a3..13bc070889 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js @@ -10,7 +10,8 @@ define([], function() { }, _splitTags: function(message) { - var validTags = 'attach|b|color|i|list|url|table|td|tr'; + // TODO: `validTags` should be dynamic similar to the PHP implementation + var validTags = 'attach|b|color|i|list|url|table|td|tr|quote'; var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')' + '(?:=' + '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)' @@ -26,7 +27,7 @@ define([], function() { continue; } else if (part.match(isBBCode)) { - tag = { name: '', closing: false, source: part }; + tag = { name: '', closing: false, attributes: [], source: part }; if (part[1] === '/') { tag.name = part.substring(2, part.length - 1); @@ -106,13 +107,13 @@ define([], function() { break; } - tag = { name: item.name, closing: true, source: '[/' + item.name + ']' }; + tag = { name: item.name, closing: true, attributes: item.attributes.slice(), source: '[/' + item.name + ']' }; item.pair = stack.length; stack.push(tag); openTags.pop(); - reopenTags.push({ name: item.name, closing: false, source: item.source }); + reopenTags.push({ name: item.name, closing: false, attributes: item.attributes.slice(), source: item.source }); } return reopenTags.reverse(); @@ -129,25 +130,25 @@ define([], function() { }, _parseAttributes: function(attrString) { - var tmp = attrString.match(/(?:^|,)('[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^,]*)/g); + var tmp = attrString.split(/(?:^|,)('[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^,]*)/g); var attribute, attributes = []; for (var i = 0, length = tmp.length; i < length; i++) { attribute = tmp[i]; if (attribute !== '') { - if (attribute[0] === "'" && attribute.substr(-1) === "'") { - attributes.push(attribute.substring(1, attribute.length - 1)); + if (attribute.charAt(0) === "'" && attribute.substr(-1) === "'") { + attributes.push(attribute.substring(1, attribute.length - 1).trim()); } else { - attributes.push(attribute); + attributes.push(attribute.trim()); } } } return attributes; } - } + }; return BBCodeParser; }); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js index b5c02461bd..86ac7ad04d 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js @@ -1,4 +1,4 @@ -define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) { +define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Language, StringUtil, BBCodeParser) { "use strict"; var _bbcodes = null; @@ -59,19 +59,20 @@ define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) { // callback replacement color: this._replaceColor.bind(this), list: this._replaceList.bind(this), + quote: this._replaceQuote.bind(this), url: this._replaceUrl.bind(this) }; - _removeNewlineAfter = ['table', 'td', 'tr']; + _removeNewlineAfter = ['quote', 'table', 'td', 'tr']; _removeNewlineBefore = ['table', 'td', 'tr']; }, _replace: function(stack, item, index) { - var pair = stack[item.pair], replace = _bbcodes[item.name], tmp; + var replace = _bbcodes[item.name], tmp; if (replace === undefined) { // treat as plain text - stack[item.pair] = pair.source; + stack[item.pair] = stack[item.pair].source; return item.source; } @@ -110,12 +111,12 @@ define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) { return '<' + replace + '>'; } else { - return replace(stack, item, pair, index); + return replace(stack, item, index); } }, - _replaceColor: function(stack, item, pair) { - if (item.attributes === undefined || !item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) { + _replaceColor: function(stack, item, index) { + if (!item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) { stack[item.pair] = ''; return ''; @@ -123,11 +124,11 @@ define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) { stack[item.pair] = ''; - return ''; + return ''; }, - _replaceList: function(stack, item, pair, index) { - var type = (item.attributes === undefined || !items.attributes.length) ? '' : item.attributes[0].trim(); + _replaceList: function(stack, item, index) { + var type = (items.attributes.length) ? item.attributes[0] : ''; // replace list items for (var i = index + 1; i < item.pair; i++) { @@ -150,9 +151,49 @@ define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) { return '