-define(['Ajax', 'Environment', 'EventHandler', 'Ui/Alignment'], function(Ajax, Environment, EventHandler, UiAlignment) {
+define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax, Environment, UiCloseOverlay) {
"use strict";
var _dropdownContainer = null;
UiRedactorMention.prototype = {
init: function(redactor) {
this._active = false;
- this._caret = null;
this._dropdownActive = false;
this._dropdownMenu = null;
this._itemIndex = 0;
redactor.WoltLabEvent.register('keydown', this._keyDown.bind(this));
redactor.WoltLabEvent.register('keyup', this._keyUp.bind(this));
+
+ UiCloseOverlay.add('UiRedactorMention-' + redactor.core.element()[0].id, this._hideDropdown.bind(this));
},
_keyDown: function(data) {
break;
default:
+ this._hideDropdown();
return;
break;
}
return;
}
+ if (this._dropdownActive) {
+ data.cancel = true;
+
+ // ignore arrow up/down
+ if (event.which === 38 || event.which === 40) {
+ return;
+ }
+ }
+
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
}
},
- _setUsername: function(event, item) {
- if (event) {
- event.preventDefault();
- item = event.currentTarget;
+ _getTextLineInFrontOfCaret: function() {
+ var data = this._selectMention(false);
+ if (data !== null) {
+ return data.range.cloneContents().textContent.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim();
}
- /*if (this._timer !== null) {
- this._timer.stop();
- this._timer = null;
+ return '';
+ },
+
+ _getDropdownMenuPosition: function() {
+ var data = this._selectMention();
+ if (data === null) {
+ return null;
}
- this._proxy.abortPrevious();*/
- var selection = window.getSelection();
+ this._redactor.selection.save();
- // restore caret position
- selection.removeAllRanges();
- selection.addRange(this._caret);
+ data.selection.removeAllRanges();
+ data.selection.addRange(data.range);
- var orgRange = selection.getRangeAt(0).cloneRange();
+ // 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 || window.pageYOffset),
+ left: Math.round(rect.left) + document.body.scrollLeft
+ };
- // allow redactor to undo this
- this._redactor.buffer.set();
+ if (this._lineHeight === null) {
+ this._lineHeight = Math.round(rect.bottom - rect.top - (window.scrollY || window.pageYOffset));
+ }
- var startContainer = orgRange.startContainer;
- var startOffset = orgRange.startOffset - (this._mentionStart.length + 1);
+ // restore caret position
+ this._redactor.selection.restore();
- // 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);
+ return offsets;
+ },
+
+ _setUsername: function(event, item) {
+ if (event) {
+ event.preventDefault();
+ item = event.currentTarget;
}
- var newRange = document.createRange();
- newRange.setStart(startContainer, startOffset);
- newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
+ var data = this._selectMention();
+ if (data === null) {
+ this._hideDropdown();
+
+ return;
+ }
- selection.removeAllRanges();
- selection.addRange(newRange);
+ // allow redactor to undo this
+ this._redactor.buffer.set();
+
+ data.selection.removeAllRanges();
+ data.selection.addRange(data.range);
var range = getSelection().getRangeAt(0);
range.deleteContents();
var text = document.createTextNode('@' + elData(item, 'username') + '\u00A0');
range.insertNode(text);
- newRange = document.createRange();
- newRange.selectNode(text);
- newRange.collapse(false);
+ range = document.createRange();
+ range.selectNode(text);
+ range.collapse(false);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- 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 (skipCheck) {
+ var selection = window.getSelection();
+ if (!selection.rangeCount || !selection.isCollapsed) {
+ return null;
+ }
+
+ // check if there is an '@' within the current range
+ if (selection.anchorNode.textContent.indexOf('@') === -1) {
+ 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();
+ // 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 text = range.startContainer.textContent.substr(0, range.startOffset);
-
- // 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;
+ var range = selection.getRangeAt(0);
+ var endContainer = range.startContainer;
+ var endOffset = range.startOffset;
+
+ // 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;
}
-
- if (textBackup[i] === '@' && i && /\s/.test(textBackup[i - 1])) {
- hadSpace = false;
- text = '';
+ else {
+ endOffset = endContainer.childNodes.length;
}
+ }
+ }
+
+ var startContainer = endContainer;
+ var startOffset = -1;
+ while (startContainer !== null) {
+ if (startContainer.nodeType !== Node.TEXT_NODE) {
+ return null;
+ }
+
+ if (startContainer.textContent.indexOf('@') !== -1) {
+ startOffset = startContainer.textContent.lastIndexOf('@');
- text += textBackup[i];
+ break;
}
- else {
- hadSpace = false;
- text = '';
+
+ startContainer = startContainer.previousSibling;
+ }
+
+ if (startOffset === -1) {
+ // there was a non-text node that was in our way
+ return null;
+ }
+
+ try {
+ // 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) {
+ window.console.debug(e);
+ 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 text;
+ return {
+ range: range,
+ 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 || window.pageYOffset)) {
+ this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', '');
+ }
+ },
+
+ _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() {
listItem = elCreate('li');
link = elCreate('a');
- link.addEventListener(WCF_CLICK_EVENT, callbackClick);
+ link.addEventListener('mousedown', callbackClick);
link.className = 'box16';
link.innerHTML = '<span>' + user.icon + '</span> <span>' + user.label + '</span>';
elData(link, 'user-id', user.objectID);
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;
}
};