Reworked mention detection
authorAlexander Ebert <ebert@woltlab.com>
Thu, 1 Sep 2016 14:08:15 +0000 (16:08 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 1 Sep 2016 14:08:15 +0000 (16:08 +0200)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js

index 5a5adab66d0210d63383e4a31b26aa5cd77e482c..bd127d3040efdf280169f6a6608e04c6ec804639 100644 (file)
@@ -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;
                }
        };