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() {
* 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') {
}
}).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);
}
}
}
$wasInWysiwygMode = true;
}
- value = this.wutil.addNewlines(value);
+ //value = this.wutil.addNewlines(value);
this.$textarea.val(value);
if ($wasInWysiwygMode) {
* - pasting lists/list-items in lists can yield empty <li></li>
*/
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 = $('<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 = $('<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];
{ 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) },
}
},
+ _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;
},
_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 + ')'
+ '(?:='
+ '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)'
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);
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();
},
_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;
});
-define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) {
+define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Language, StringUtil, BBCodeParser) {
"use strict";
var _bbcodes = null;
// 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;
}
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 '';
stack[item.pair] = '</span>';
- return '<span style="color: ' + item.attributes[0] + '">';
+ return '<span style="color: ' + StringUtil.escapeHTML(item.attributes[0]) + '">';
},
- _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++) {
return '<ul>';
},
- _replaceUrl: function(stack, item, pair) {
+ _replaceQuote: function(stack, item, index) {
+ var author = '', link = '';
+ if (item.attributes.length > 1) {
+ author = item.attributes[0];
+ link = item.attributes[1];
+ }
+ else if (item.attributes.length === 1) {
+ author = item.attributes[0];
+ }
+
+ stack[item.pair] = '</div></blockquote>';
+
+ // get rid of the trailing newline for quote content
+ for (var i = item.pair - 1; i > index; i--) {
+ if (typeof stack[i] === 'string') {
+ stack[i] = stack[i].replace(/\n$/, '');
+ break;
+ }
+ }
+
+ var header = '';
+ if (author) {
+ if (link) header = '<a href="' + StringUtil.escapeHTML(link) + '" tabindex="-1">';
+ header += Language.get('wcf.bbcode.quote.title.javascript', { quoteAuthor: author });
+ if (link) header += '</a>';
+ }
+ else {
+ header = '<small>' + Language.get('wcf.bbcode.quote.title.clickToSet') + '</small>';
+ }
+
+ return '<blockquote class="quoteBox container containerPadding quoteBoxSimple" cite="' + StringUtil.escapeHTML(link) + '" data-author="' + StringUtil.escapeHTML(author) + '">'
+ + '<header contenteditable="false">'
+ + '<h3>'
+ + header
+ + '</h3>'
+ + '<a class="redactorQuoteEdit"></a>'
+ + '</header>'
+ + '<div>\u200b';
+ },
+
+ _replaceUrl: function(stack, item, index) {
// ignore url bbcode without arguments
- if (item.attributes === undefined || !item.attributes.length) {
+ if (!item.attributes.length) {
stack[item.pair] = '';
return '';
stack[item.pair] = '</a>';
- return '<a href="' + item.attributes[0] + '">';
+ return '<a href="' + StringUtil.escapeHTML(item.attributes[0]) + '">';
}
};