Fixed calculation of positions in IE11
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Ui / Redactor / Mention.js
index d65b67089dda637868f6b08cc21ad6bddb1467f0..ae8c06301d9d889c7bb739e54531981cf7d886b1 100644 (file)
@@ -76,7 +76,7 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U
                        }
                        
                        var text = this._getTextLineInFrontOfCaret();
-                       if (text.length) {
+                       if (text.length > 0 && text.length < 25) {
                                var match = text.match(/@([^,]{3,})$/);
                                if (match) {
                                        // if mentioning is at text begin or there's a whitespace character
@@ -112,20 +112,12 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U
                },
                
                _getTextLineInFrontOfCaret: function() {
-                       /** @var Range range */
-                       var range = window.getSelection().getRangeAt(0);
-                       if (!range.collapsed) {
-                               return '';
+                       var data = this._selectMention(false);
+                       if (data !== null) {
+                               return data.range.cloneContents().textContent.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim();
                        }
                        
-                       // 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 text = range.startContainer.textContent.substr(0, range.startOffset);
-                       
-                       return text.replace(/\u200B/g, '').replace(/\u00A0/g, '');
+                       return '';
                },
                
                _getDropdownMenuPosition: function() {
@@ -137,17 +129,17 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U
                        this._redactor.selection.save();
                        
                        data.selection.removeAllRanges();
-                       data.selection.addRange(data.newRange);
+                       data.selection.addRange(data.range);
                        
                        // 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,
+                               top: Math.round(rect.bottom) + (window.scrollY || window.pageYOffset),
                                left: Math.round(rect.left) + document.body.scrollLeft
                        };
                        
                        if (this._lineHeight === null) {
-                               this._lineHeight = Math.round(rect.bottom - rect.top - window.scrollY);
+                               this._lineHeight = Math.round(rect.bottom - rect.top - (window.scrollY || window.pageYOffset));
                        }
                        
                        // restore caret position
@@ -173,13 +165,13 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U
                        this._redactor.buffer.set();
                        
                        data.selection.removeAllRanges();
-                       data.selection.addRange(data.newRange);
+                       data.selection.addRange(data.range);
                        
                        var range = getSelection().getRangeAt(0);
                        range.deleteContents();
                        range.collapse(true);
                        
-                       var text = document.createTextNode('@' + elData(item, 'username') + ' ');
+                       var text = document.createTextNode('@' + elData(item, 'username') + '\u00A0');
                        range.insertNode(text);
                        
                        range = document.createRange();
@@ -192,56 +184,115 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U
                        this._hideDropdown();
                },
                
-               _selectMention: function () {
+               _selectMention: function (skipCheck) {
                        var selection = window.getSelection();
                        if (!selection.rangeCount || !selection.isCollapsed) {
                                return null;
                        }
                        
-                       var originalRange = selection.getRangeAt(0).cloneRange();
+                       // check if there is an '@' within the current range
+                       if (selection.anchorNode.textContent.indexOf('@') === -1) {
+                               return null;
+                       }
                        
-                       // mark the entire text, starting from the '@' to the current cursor position
-                       var newRange = document.createRange();
+                       // check if we're inside code or quote blocks
+                       var container = selection.anchorNode, editor = this._redactor.core.editor()[0];
+                       while (container && container !== editor) {
+                               if (['PRE', 'WOLTLAB-QUOTE'].indexOf(container.nodeName) !== -1) {
+                                       return null;
+                               }
+                               
+                               container = container.parentNode;
+                       }
                        
-                       var startContainer = originalRange.startContainer;
-                       var startOffset = originalRange.startOffset - (this._mentionStart.length + 1);
+                       var range = selection.getRangeAt(0);
+                       var endContainer = range.startContainer;
+                       var endOffset = range.startOffset;
                        
-                       if (startContainer.nodeType === Node.ELEMENT_NODE) {
-                               startContainer = startContainer.childNodes[originalRange.startOffset];
-                               startOffset = startContainer.length - (this._mentionStart.length + 1);
+                       // find the appropriate end location
+                       while (endContainer.nodeType === Node.ELEMENT_NODE) {
+                               if (endOffset === 0 && endContainer.childNodes.length === 0) {
+                                       // invalid start location
+                                       return null;
+                               }
+                               
+                               // startOffset for elements will always be after a node index
+                               // or at the very start, which means if there is only text node
+                               // and the caret is after it, startOffset will equal `1`
+                               endContainer = endContainer.childNodes[(endOffset ? endOffset - 1 : 0)];
+                               if (endOffset > 0) {
+                                       if (endContainer.nodeType === Node.TEXT_NODE) {
+                                               endOffset = endContainer.textContent.length;
+                                       }
+                                       else {
+                                               endOffset = endContainer.childNodes.length;
+                                       }
+                               }
                        }
                        
-                       // 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
+                       var startContainer = endContainer;
+                       var startOffset = -1;
+                       while (startContainer !== null) {
+                               if (startContainer.nodeType !== Node.TEXT_NODE) {
                                        return null;
                                }
                                
-                               startOffset = startContainer.length - (this._mentionStart.length + 1) - (originalRange.startOffset - 1);
+                               if (startContainer.textContent.indexOf('@') !== -1) {
+                                       startOffset = startContainer.textContent.lastIndexOf('@');
+                                       
+                                       break;
+                               }
+                               
+                               startContainer = startContainer.previousSibling;
+                       }
+                       
+                       if (startOffset === -1) {
+                               // there was a non-text node that was in our way
+                               return null;
                        }
                        
                        try {
-                               newRange.setStart(startContainer, startOffset);
-                               newRange.setEnd(originalRange.startContainer, originalRange.startOffset);
+                               // mark the entire text, starting from the '@' to the current cursor position
+                               range = document.createRange();
+                               range.setStart(startContainer, startOffset);
+                               range.setEnd(endContainer, endOffset);
                        }
                        catch (e) {
-                               console.debug(e);
+                               window.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;
+                       if (skipCheck === false) {
+                               // check if the `@` occurs at the very start of the container
+                               // or at least has a whitespace in front of it
+                               var text = '';
+                               if (startOffset) {
+                                       text = startContainer.textContent.substr(0, startOffset);
+                               }
+                               
+                               while (startContainer = startContainer.previousSibling) {
+                                       if (startContainer.nodeType === Node.TEXT_NODE) {
+                                               text = startContainer.textContent + text;
+                                       }
+                                       else {
+                                               break;
+                                       }
+                               }
+                               
+                               if (text.replace(/\u200B/g, '').match(/\S$/)) {
+                                       return null;
+                               }
+                       }
+                       else {
+                               // check if new range includes the mention text
+                               if (range.cloneContents().textContent.replace(/\u200B/g, '').replace(/\u00A0/g, '').trim().replace(/^@/, '') !== this._mentionStart) {
+                                       // string mismatch
+                                       return null;
+                               }
                        }
                        
                        return {
-                               newRange: newRange,
-                               originalRange: originalRange,
+                               range: range,
                                selection: selection
                        };
                },
@@ -260,14 +311,9 @@ define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, U
                        
                        this._selectItem(0);
                        
-                       if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + window.scrollY) {
-                               this._dropdownMenu.classList.add('dropdownArrowBottom');
-                               
+                       if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + (window.scrollY || window.pageYOffset)) {
                                this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', '');
                        }
-                       else {
-                               this._dropdownMenu.classList.remove('dropdownArrowBottom');
-                       }
                },
                
                _selectItem: function(step) {