From c79ab122b068630f0a0ba234a83167f6eafeda1b Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 1 Sep 2016 16:08:15 +0200 Subject: [PATCH] Reworked mention detection --- .../WoltLabSuite/Core/Ui/Redactor/Mention.js | 299 +++++++++--------- 1 file changed, 150 insertions(+), 149 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js index 5a5adab66d..bd127d3040 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js @@ -7,7 +7,6 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U UiRedactorMention.prototype = { init: function(redactor) { this._active = false; - this._caret = null; this._dropdownActive = false; this._dropdownMenu = null; this._itemIndex = 0; @@ -47,6 +46,7 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U break; default: + this._hideDropdown(); return; break; } @@ -111,103 +111,186 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U } }, - _setUsername: function(event, item) { - if (event) { - event.preventDefault(); - item = event.currentTarget; + _getTextLineInFrontOfCaret: function() { + /** @var Range range */ + var range = window.getSelection().getRangeAt(0); + if (!range.collapsed) { + return ''; } - /*if (this._timer !== null) { - this._timer.stop(); - this._timer = null; + // in Firefox, blurring and refocusing the browser creates separate text nodes + if (Environment.browser() === 'firefox' && range.startContainer.nodeType === Node.TEXT_NODE) { + range.startContainer.parentNode.normalize(); } - this._proxy.abortPrevious();*/ - var selection = window.getSelection(); + var text = range.startContainer.textContent.substr(0, range.startOffset); - // restore caret position - selection.removeAllRanges(); - selection.addRange(this._caret); + return text.replace(/\u200B/g, '').replace(/\u00A0/g, ''); + }, + + _getDropdownMenuPosition: function() { + var data = this._selectMention(); + if (data === null) { + return null; + } - var orgRange = selection.getRangeAt(0).cloneRange(); + this._redactor.selection.save(); - // allow redactor to undo this - this._redactor.buffer.set(); + data.selection.removeAllRanges(); + data.selection.addRange(data.newRange); - var startContainer = orgRange.startContainer; - var startOffset = orgRange.startOffset - (this._mentionStart.length + 1); + // get the offsets of the bounding box of current text selection + var rect = data.selection.getRangeAt(0).getBoundingClientRect(); + var offsets = { + top: Math.round(rect.bottom) + window.scrollY, + left: Math.round(rect.left) + document.body.scrollLeft + }; - // navigating with the keyboard before hitting enter will cause the text node to be split - if (startOffset < 0) { - startContainer = startContainer.previousSibling; - startOffset = startContainer.length - (this._mentionStart.length + 1) - (orgRange.startOffset - 1); + if (this._lineHeight === null) { + this._lineHeight = Math.round(rect.bottom - rect.top - window.scrollY); } - var newRange = document.createRange(); - newRange.setStart(startContainer, startOffset); - newRange.setEnd(orgRange.startContainer, orgRange.startOffset); + // restore caret position + this._redactor.selection.restore(); - selection.removeAllRanges(); - selection.addRange(newRange); + return offsets; + }, + + _setUsername: function(event, item) { + if (event) { + event.preventDefault(); + item = event.currentTarget; + } + + var data = this._selectMention(); + if (data === null) { + this._hideDropdown(); + + return; + } + + // allow redactor to undo this + this._redactor.buffer.set(); + + data.selection.removeAllRanges(); + data.selection.addRange(data.newRange); var range = getSelection().getRangeAt(0); range.deleteContents(); range.collapse(true); - var text = document.createTextNode('@' + elData(item, 'username') + '\u00A0'); + var text = document.createTextNode('@' + elData(item, 'username') + ' '); range.insertNode(text); - newRange = document.createRange(); - newRange.selectNode(text); - newRange.collapse(false); - - selection.removeAllRanges(); - selection.addRange(newRange); + range = document.createRange(); + range.selectNode(text); + range.collapse(false); - this._redactor.selection.save(); + data.selection.removeAllRanges(); + data.selection.addRange(range); this._hideDropdown(); }, - _getTextLineInFrontOfCaret: function() { - /** @var Range range */ - var range = window.getSelection().getRangeAt(0); - if (!range.collapsed) { - return ''; + _selectMention: function () { + var selection = window.getSelection(); + if (!selection.rangeCount || !selection.isCollapsed) { + return null; } - // in Firefox, blurring and refocusing the browser creates separate text nodes - if (Environment.browser() === 'firefox' && range.startContainer.nodeType === Node.TEXT_NODE) { - range.startContainer.parentNode.normalize(); - } + var originalRange = selection.getRangeAt(0).cloneRange(); - var text = range.startContainer.textContent.substr(0, range.startOffset); + // mark the entire text, starting from the '@' to the current cursor position + var newRange = document.createRange(); - // remove unicode zero-width space and non-breaking space - var textBackup = text; - text = ''; - var hadSpace = false; - for (var i = 0; i < textBackup.length; i++) { - var byte = textBackup.charCodeAt(i).toString(16); - if (byte !== '200b' && (!/\s/.test(textBackup[i]) || ((byte === 'a0' || byte === '20') && !hadSpace))) { - if (byte === 'a0' || byte === '20') { - hadSpace = true; - } - - if (textBackup[i] === '@' && i && /\s/.test(textBackup[i - 1])) { - hadSpace = false; - text = ''; - } - - text += textBackup[i]; - } - else { - hadSpace = false; - text = ''; + var startContainer = originalRange.startContainer; + var startOffset = originalRange.startOffset - (this._mentionStart.length + 1); + + if (startContainer.nodeType === Node.ELEMENT_NODE) { + startContainer = startContainer.childNodes[originalRange.startOffset]; + startOffset = startContainer.length - (this._mentionStart.length + 1); + } + + // navigating with the keyboard before hitting enter will cause the text node to be split + if (startOffset < 0) { + startContainer = startContainer.previousSibling; + if (!startContainer || startContainer.nodeType !== Node.TEXT_NODE) { + // selection is no longer where it used to be + return null; } + + startOffset = startContainer.length - (this._mentionStart.length + 1) - (originalRange.startOffset - 1); } - return text; + try { + newRange.setStart(startContainer, startOffset); + newRange.setEnd(originalRange.startContainer, originalRange.startOffset); + } + catch (e) { + console.debug(e); + return null; + } + + // check if new range includes the mention text + var div = elCreate('div'); + div.appendChild(newRange.cloneContents()); + if (div.textContent.replace(/\u200B/g, '').replace(/\u00A0/g, '').trim().replace(/^@/, '') !== this._mentionStart) { + // string mismatch + return null; + } + + return { + newRange: newRange, + originalRange: originalRange, + selection: selection + }; + }, + + _updateDropdownPosition: function() { + var offset = this._getDropdownMenuPosition(); + if (offset === null) { + this._hideDropdown(); + + return; + } + offset.top += 7; // add a little vertical gap + + this._dropdownMenu.style.setProperty('left', offset.left + 'px', ''); + this._dropdownMenu.style.setProperty('top', offset.top + 'px', ''); + + this._selectItem(0); + + if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + window.scrollY) { + this._dropdownMenu.classList.add('dropdownArrowBottom'); + + this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', ''); + } + else { + this._dropdownMenu.classList.remove('dropdownArrowBottom'); + } + }, + + _selectItem: function(step) { + // find currently active item + var item = elBySel('.active', this._dropdownMenu); + if (item !== null) { + item.classList.remove('active'); + } + + this._itemIndex += step; + if (this._itemIndex === -1) { + this._itemIndex = this._dropdownMenu.childElementCount - 1; + } + else if (this._itemIndex === this._dropdownMenu.childElementCount) { + this._itemIndex = 0; + } + + this._dropdownMenu.children[this._itemIndex].classList.add('active'); + }, + + _hideDropdown: function() { + if (this._dropdownMenu !== null) this._dropdownMenu.classList.remove('dropdownOpen'); + this._dropdownActive = false; }, _ajaxSetup: function() { @@ -267,88 +350,6 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U this._dropdownActive = true; this._updateDropdownPosition(); - }, - - _getDropdownMenuPosition: function() { - this._redactor.selection.save(); - - var selection = window.getSelection(); - var orgRange = selection.getRangeAt(0).cloneRange(); - - // mark the entire text, starting from the '@' to the current cursor position - var newRange = document.createRange(); - newRange.setStart(orgRange.startContainer, orgRange.startOffset - (this._mentionStart.length + 1)); - newRange.setEnd(orgRange.startContainer, orgRange.startOffset); - - selection.removeAllRanges(); - selection.addRange(newRange); - - // get the offsets of the bounding box of current text selection - var rect = selection.getRangeAt(0).getBoundingClientRect(); - var offsets = { - top: Math.round(rect.bottom) + window.scrollY, - left: Math.round(rect.left) + document.body.scrollLeft - }; - - if (this._lineHeight === null) { - this._lineHeight = Math.round(rect.bottom - rect.top - window.scrollY); - } - - // restore caret position - this._redactor.selection.restore(); - - this._caret = orgRange; - - return offsets; - }, - - _updateDropdownPosition: function() { - try { - var offset = this._getDropdownMenuPosition(); - offset.top += 7; // add a little vertical gap - - this._dropdownMenu.style.setProperty('left', offset.left + 'px', ''); - this._dropdownMenu.style.setProperty('top', offset.top + 'px', ''); - - this._selectItem(0); - - if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + window.scrollY) { - this._dropdownMenu.classList.add('dropdownArrowBottom'); - - this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', ''); - } - else { - this._dropdownMenu.classList.remove('dropdownArrowBottom'); - } - } - catch (e) { - console.debug(e); - // ignore errors that are caused by pressing enter to - // often in a short period of time - } - }, - - _selectItem: function(step) { - // find currently active item - var item = elBySel('.active', this._dropdownMenu); - if (item !== null) { - item.classList.remove('active'); - } - - this._itemIndex += step; - if (this._itemIndex === -1) { - this._itemIndex = this._dropdownMenu.childElementCount - 1; - } - else if (this._itemIndex === this._dropdownMenu.childElementCount) { - this._itemIndex = 0; - } - - this._dropdownMenu.children[this._itemIndex].classList.add('active'); - }, - - _hideDropdown: function() { - if (this._dropdownMenu !== null) this._dropdownMenu.classList.remove('dropdownOpen'); - this._dropdownActive = false; } }; -- 2.20.1