-define(['Ajax', 'Environment', 'StringUtil', 'Ui/CloseOverlay'], function (Ajax, Environment, StringUtil, UiCloseOverlay) {
+define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../StringUtil", "../CloseOverlay"], function (require, exports, tslib_1, Ajax, Core, StringUtil, CloseOverlay_1) {
"use strict";
- if (!COMPILER_TARGET_DEFAULT) {
- var Fake = function () { };
- Fake.prototype = {
- init: function () { },
- _keyDown: function () { },
- _keyUp: function () { },
- _getTextLineInFrontOfCaret: function () { },
- _getDropdownMenuPosition: function () { },
- _setUsername: function () { },
- _selectMention: function () { },
- _updateDropdownPosition: function () { },
- _selectItem: function () { },
- _hideDropdown: function () { },
- _ajaxSetup: function () { },
- _ajaxSuccess: function () { }
- };
- return Fake;
- }
- var _dropdownContainer = null;
- function UiRedactorMention(redactor) { this.init(redactor); }
- UiRedactorMention.prototype = {
- init: function (redactor) {
+ Ajax = tslib_1.__importStar(Ajax);
+ Core = tslib_1.__importStar(Core);
+ StringUtil = tslib_1.__importStar(StringUtil);
+ CloseOverlay_1 = tslib_1.__importDefault(CloseOverlay_1);
+ let _dropdownContainer = null;
+ class UiRedactorMention {
+ constructor(redactor) {
this._active = false;
this._dropdownActive = false;
this._dropdownMenu = null;
this._itemIndex = 0;
this._lineHeight = null;
- this._mentionStart = '';
- this._redactor = redactor;
+ this._mentionStart = "";
this._timer = null;
- 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) {
+ this._redactor = redactor;
+ redactor.WoltLabEvent.register("keydown", (data) => this._keyDown(data));
+ redactor.WoltLabEvent.register("keyup", (data) => this._keyUp(data));
+ CloseOverlay_1.default.add(`UiRedactorMention-${redactor.core.element()[0].id}`, () => this._hideDropdown());
+ }
+ _keyDown(data) {
if (!this._dropdownActive) {
return;
}
- /** @var Event event */
- var event = data.event;
- switch (event.which) {
- // enter
- case 13:
+ const event = data.event;
+ switch (event.key) {
+ case "Enter":
this._setUsername(null, this._dropdownMenu.children[this._itemIndex].children[0]);
break;
- // arrow up
- case 38:
+ case "ArrowUp":
this._selectItem(-1);
break;
- // arrow down
- case 40:
+ case "ArrowDown":
this._selectItem(1);
break;
default:
}
event.preventDefault();
data.cancel = true;
- },
- _keyUp: function (data) {
- /** @var Event event */
- var event = data.event;
+ }
+ _keyUp(data) {
+ const event = data.event;
// ignore return key
- if (event.which === 13) {
+ if (event.key === "Enter") {
this._active = false;
return;
}
if (this._dropdownActive) {
data.cancel = true;
// ignore arrow up/down
- if (event.which === 38 || event.which === 40) {
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
return;
}
}
- var text = this._getTextLineInFrontOfCaret();
+ const text = this._getTextLineInFrontOfCaret();
if (text.length > 0 && text.length < 25) {
- var match = text.match(/@([^,]{3,})$/);
+ const match = /@([^,]{3,})$/.exec(text);
if (match) {
// if mentioning is at text begin or there's a whitespace character
// before the '@', everything is fine
- if (!match.index || text[match.index - 1].match(/\s/)) {
+ if (!match.index || /\s/.test(text[match.index - 1])) {
this._mentionStart = match[1];
if (this._timer !== null) {
window.clearTimeout(this._timer);
this._timer = null;
}
- this._timer = window.setTimeout((function () {
+ this._timer = window.setTimeout(() => {
Ajax.api(this, {
parameters: {
data: {
- searchString: this._mentionStart
- }
- }
+ searchString: this._mentionStart,
+ },
+ },
});
this._timer = null;
- }).bind(this), 500);
+ }, 500);
}
}
else {
else {
this._hideDropdown();
}
- },
- _getTextLineInFrontOfCaret: function () {
- var data = this._selectMention(false);
+ }
+ _getTextLineInFrontOfCaret() {
+ const data = this._selectMention(false);
if (data !== null) {
- return data.range.cloneContents().textContent.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim();
+ return data.range
+ .cloneContents()
+ .textContent.replace(/\u200B/g, "")
+ .replace(/\u00A0/g, " ")
+ .trim();
}
- return '';
- },
- _getDropdownMenuPosition: function () {
- var data = this._selectMention();
+ return "";
+ }
+ _getDropdownMenuPosition() {
+ const data = this._selectMention();
if (data === null) {
return null;
}
data.selection.removeAllRanges();
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 = {
+ const rect = data.selection.getRangeAt(0).getBoundingClientRect();
+ const offsets = {
top: Math.round(rect.bottom) + (window.scrollY || window.pageYOffset),
- left: Math.round(rect.left) + document.body.scrollLeft
+ left: Math.round(rect.left) + document.body.scrollLeft,
};
if (this._lineHeight === null) {
this._lineHeight = Math.round(rect.bottom - rect.top);
// restore caret position
this._redactor.selection.restore();
return offsets;
- },
- _setUsername: function (event, item) {
+ }
+ _setUsername(event, item) {
if (event) {
event.preventDefault();
item = event.currentTarget;
}
- var data = this._selectMention();
+ const data = this._selectMention();
if (data === null) {
this._hideDropdown();
return;
this._redactor.buffer.set();
data.selection.removeAllRanges();
data.selection.addRange(data.range);
- var range = getSelection().getRangeAt(0);
+ let range = window.getSelection().getRangeAt(0);
range.deleteContents();
range.collapse(true);
// Mentions only allow for one whitespace per match, putting the username in apostrophes
// will allow an arbitrary number of spaces.
- var username = elData(item, 'username').trim();
+ let username = item.dataset.username.trim();
if (username.split(/\s/g).length > 2) {
username = "'" + username.replace(/'/g, "''") + "'";
}
- var text = document.createTextNode('@' + username + '\u00A0');
+ const text = document.createTextNode("@" + username + "\u00A0");
range.insertNode(text);
range = document.createRange();
range.selectNode(text);
data.selection.removeAllRanges();
data.selection.addRange(range);
this._hideDropdown();
- },
- _selectMention: function (skipCheck) {
- var selection = window.getSelection();
+ }
+ _selectMention(skipCheck) {
+ const selection = window.getSelection();
if (!selection.rangeCount || !selection.isCollapsed) {
return null;
}
- var container = selection.anchorNode;
+ let container = selection.anchorNode;
if (container.nodeType === Node.TEXT_NODE) {
// work-around for Firefox after suggestions have been presented
- container = container.parentNode;
+ container = container.parentElement;
}
// check if there is an '@' within the current range
- if (container.textContent.indexOf('@') === -1) {
+ if (container.textContent.indexOf("@") === -1) {
return null;
}
// check if we're inside code or quote blocks
- var editor = this._redactor.core.editor()[0];
+ const editor = this._redactor.core.editor()[0];
while (container && container !== editor) {
- if (['PRE', 'WOLTLAB-QUOTE'].indexOf(container.nodeName) !== -1) {
+ if (["PRE", "WOLTLAB-QUOTE"].indexOf(container.nodeName) !== -1) {
return null;
}
- container = container.parentNode;
+ container = container.parentElement;
}
- var range = selection.getRangeAt(0);
- var endContainer = range.startContainer;
- var endOffset = range.startOffset;
+ let range = selection.getRangeAt(0);
+ let endContainer = range.startContainer;
+ let endOffset = range.startOffset;
// find the appropriate end location
while (endContainer.nodeType === Node.ELEMENT_NODE) {
if (endOffset === 0 && endContainer.childNodes.length === 0) {
// 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)];
+ endContainer = endContainer.childNodes[endOffset ? endOffset - 1 : 0];
if (endOffset > 0) {
if (endContainer.nodeType === Node.TEXT_NODE) {
endOffset = endContainer.textContent.length;
}
}
}
- var startContainer = endContainer;
- var startOffset = -1;
+ let startContainer = endContainer;
+ let startOffset = -1;
while (startContainer !== null) {
if (startContainer.nodeType !== Node.TEXT_NODE) {
return null;
}
- if (startContainer.textContent.indexOf('@') !== -1) {
- startOffset = startContainer.textContent.lastIndexOf('@');
+ if (startContainer.textContent.indexOf("@") !== -1) {
+ startOffset = startContainer.textContent.lastIndexOf("@");
break;
}
startContainer = startContainer.previousSibling;
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 = '';
+ let text = "";
if (startOffset) {
text = startContainer.textContent.substr(0, startOffset);
}
- while (startContainer = startContainer.previousSibling) {
+ while ((startContainer = startContainer.previousSibling)) {
if (startContainer.nodeType === Node.TEXT_NODE) {
text = startContainer.textContent + text;
}
break;
}
}
- if (text.replace(/\u200B/g, '').match(/\S$/)) {
+ if (/\S$/.test(text.replace(/\u200B/g, ""))) {
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) {
+ if (range
+ .cloneContents()
+ .textContent.replace(/\u200B/g, "")
+ .replace(/\u00A0/g, "")
+ .trim()
+ .replace(/^@/, "") !== this._mentionStart) {
// string mismatch
return null;
}
}
return {
range: range,
- selection: selection
+ selection: selection,
};
- },
- _updateDropdownPosition: function () {
- var offset = this._getDropdownMenuPosition();
+ }
+ _updateDropdownPosition() {
+ const 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', '');
+ const dropdownMenu = this._dropdownMenu;
+ dropdownMenu.style.setProperty("left", `${offset.left}px`, "");
+ 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', '');
+ if (offset.top + dropdownMenu.offsetHeight + 10 > window.innerHeight + (window.scrollY || window.pageYOffset)) {
+ const top = offset.top - dropdownMenu.offsetHeight - 2 * this._lineHeight + 7;
+ dropdownMenu.style.setProperty("top", `${top}px`, "");
}
- },
- _selectItem: function (step) {
+ }
+ _selectItem(step) {
+ const dropdownMenu = this._dropdownMenu;
// find currently active item
- var item = elBySel('.active', this._dropdownMenu);
+ const item = dropdownMenu.querySelector(".active");
if (item !== null) {
- item.classList.remove('active');
+ item.classList.remove("active");
}
this._itemIndex += step;
if (this._itemIndex < 0) {
- this._itemIndex = this._dropdownMenu.childElementCount - 1;
+ this._itemIndex = dropdownMenu.childElementCount - 1;
}
- else if (this._itemIndex >= this._dropdownMenu.childElementCount) {
+ else if (this._itemIndex >= dropdownMenu.childElementCount) {
this._itemIndex = 0;
}
- this._dropdownMenu.children[this._itemIndex].classList.add('active');
- },
- _hideDropdown: function () {
- if (this._dropdownMenu !== null)
- this._dropdownMenu.classList.remove('dropdownOpen');
+ dropdownMenu.children[this._itemIndex].classList.add("active");
+ }
+ _hideDropdown() {
+ if (this._dropdownMenu !== null) {
+ this._dropdownMenu.classList.remove("dropdownOpen");
+ }
this._dropdownActive = false;
this._itemIndex = 0;
- },
- _ajaxSetup: function () {
+ }
+ _ajaxSetup() {
return {
data: {
- actionName: 'getSearchResultList',
- className: 'wcf\\data\\user\\UserAction',
- interfaceName: 'wcf\\data\\ISearchAction',
+ actionName: "getSearchResultList",
+ className: "wcf\\data\\user\\UserAction",
+ interfaceName: "wcf\\data\\ISearchAction",
parameters: {
data: {
includeUserGroups: true,
- scope: 'mention'
- }
- }
+ scope: "mention",
+ },
+ },
},
- silent: true
+ silent: true,
};
- },
- _ajaxSuccess: function (data) {
+ }
+ _ajaxSuccess(data) {
if (!Array.isArray(data.returnValues) || !data.returnValues.length) {
this._hideDropdown();
return;
}
if (this._dropdownMenu === null) {
- this._dropdownMenu = elCreate('ol');
- this._dropdownMenu.className = 'dropdownMenu';
+ this._dropdownMenu = document.createElement("ol");
+ this._dropdownMenu.className = "dropdownMenu";
if (_dropdownContainer === null) {
- _dropdownContainer = elCreate('div');
- _dropdownContainer.className = 'dropdownMenuContainer';
+ _dropdownContainer = document.createElement("div");
+ _dropdownContainer.className = "dropdownMenuContainer";
document.body.appendChild(_dropdownContainer);
}
_dropdownContainer.appendChild(this._dropdownMenu);
}
- this._dropdownMenu.innerHTML = '';
- var callbackClick = this._setUsername.bind(this), link, listItem, user;
- for (var i = 0, length = data.returnValues.length; i < length; i++) {
- user = data.returnValues[i];
- listItem = elCreate('li');
- link = elCreate('a');
- link.addEventListener('mousedown', callbackClick);
- link.className = 'box16';
- link.innerHTML = '<span>' + user.icon + '</span> <span>' + StringUtil.escapeHTML(user.label) + '</span>';
- elData(link, 'user-id', user.objectID);
- elData(link, 'username', user.label);
+ this._dropdownMenu.innerHTML = "";
+ data.returnValues.forEach((item) => {
+ const listItem = document.createElement("li");
+ const link = document.createElement("a");
+ link.addEventListener("mousedown", (ev) => this._setUsername(ev));
+ link.className = "box16";
+ link.innerHTML = `<span>${item.icon}</span> <span>${StringUtil.escapeHTML(item.label)}</span>`;
+ link.dataset.userId = item.objectID.toString();
+ link.dataset.username = item.label;
listItem.appendChild(link);
this._dropdownMenu.appendChild(listItem);
- }
- this._dropdownMenu.classList.add('dropdownOpen');
+ });
+ this._dropdownMenu.classList.add("dropdownOpen");
this._dropdownActive = true;
this._updateDropdownPosition();
}
- };
+ }
+ Core.enableLegacyInheritance(UiRedactorMention);
return UiRedactorMention;
});
utils: {
isEmpty(html: string): boolean;
};
+
+ WoltLabEvent: {
+ register(event: string, callback: (data: WoltLabEventData) => void): void;
+ };
+}
+
+export interface WoltLabEventData {
+ cancel: boolean;
+ event: Event;
+ redactor: RedactorEditor;
}
+++ /dev/null
-define(['Ajax', 'Environment', 'StringUtil', 'Ui/CloseOverlay'], function(Ajax, Environment, StringUtil, UiCloseOverlay) {
- "use strict";
-
- if (!COMPILER_TARGET_DEFAULT) {
- var Fake = function() {};
- Fake.prototype = {
- init: function() {},
- _keyDown: function() {},
- _keyUp: function() {},
- _getTextLineInFrontOfCaret: function() {},
- _getDropdownMenuPosition: function() {},
- _setUsername: function() {},
- _selectMention: function() {},
- _updateDropdownPosition: function() {},
- _selectItem: function() {},
- _hideDropdown: function() {},
- _ajaxSetup: function() {},
- _ajaxSuccess: function() {}
- };
- return Fake;
- }
-
- var _dropdownContainer = null;
-
- function UiRedactorMention(redactor) { this.init(redactor); }
- UiRedactorMention.prototype = {
- init: function(redactor) {
- this._active = false;
- this._dropdownActive = false;
- this._dropdownMenu = null;
- this._itemIndex = 0;
- this._lineHeight = null;
- this._mentionStart = '';
- this._redactor = redactor;
- this._timer = null;
-
- 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) {
- if (!this._dropdownActive) {
- return;
- }
-
- /** @var Event event */
- var event = data.event;
-
- switch (event.which) {
- // enter
- case 13:
- this._setUsername(null, this._dropdownMenu.children[this._itemIndex].children[0]);
- break;
-
- // arrow up
- case 38:
- this._selectItem(-1);
- break;
-
- // arrow down
- case 40:
- this._selectItem(1);
- break;
-
- default:
- this._hideDropdown();
- return;
- }
-
- event.preventDefault();
- data.cancel = true;
- },
-
- _keyUp: function(data) {
- /** @var Event event */
- var event = data.event;
-
- // ignore return key
- if (event.which === 13) {
- this._active = false;
-
- 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 > 0 && text.length < 25) {
- var match = text.match(/@([^,]{3,})$/);
- if (match) {
- // if mentioning is at text begin or there's a whitespace character
- // before the '@', everything is fine
- if (!match.index || text[match.index - 1].match(/\s/)) {
- this._mentionStart = match[1];
-
- if (this._timer !== null) {
- window.clearTimeout(this._timer);
- this._timer = null;
- }
-
- this._timer = window.setTimeout((function() {
- Ajax.api(this, {
- parameters: {
- data: {
- searchString: this._mentionStart
- }
- }
- });
-
- this._timer = null;
- }).bind(this), 500);
- }
- }
- else {
- this._hideDropdown();
- }
- }
- else {
- this._hideDropdown();
- }
- },
-
- _getTextLineInFrontOfCaret: function() {
- var data = this._selectMention(false);
- if (data !== null) {
- return data.range.cloneContents().textContent.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim();
- }
-
- return '';
- },
-
- _getDropdownMenuPosition: function() {
- var data = this._selectMention();
- if (data === null) {
- return null;
- }
-
- this._redactor.selection.save();
-
- data.selection.removeAllRanges();
- 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 || window.pageYOffset),
- left: Math.round(rect.left) + document.body.scrollLeft
- };
-
- if (this._lineHeight === null) {
- this._lineHeight = Math.round(rect.bottom - rect.top);
- }
-
- // restore caret position
- this._redactor.selection.restore();
-
- 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.range);
-
- var range = getSelection().getRangeAt(0);
- range.deleteContents();
- range.collapse(true);
-
- // Mentions only allow for one whitespace per match, putting the username in apostrophes
- // will allow an arbitrary number of spaces.
- var username = elData(item, 'username').trim();
- if (username.split(/\s/g).length > 2) {
- username = "'" + username.replace(/'/g, "''") + "'";
- }
-
- var text = document.createTextNode('@' + username + '\u00A0');
- range.insertNode(text);
-
- range = document.createRange();
- range.selectNode(text);
- range.collapse(false);
-
- data.selection.removeAllRanges();
- data.selection.addRange(range);
-
- this._hideDropdown();
- },
-
- _selectMention: function (skipCheck) {
- var selection = window.getSelection();
- if (!selection.rangeCount || !selection.isCollapsed) {
- return null;
- }
-
- var container = selection.anchorNode;
- if (container.nodeType === Node.TEXT_NODE) {
- // work-around for Firefox after suggestions have been presented
- container = container.parentNode;
- }
-
- // check if there is an '@' within the current range
- if (container.textContent.indexOf('@') === -1) {
- return null;
- }
-
- // check if we're inside code or quote blocks
- var editor = this._redactor.core.editor()[0];
- while (container && container !== editor) {
- if (['PRE', 'WOLTLAB-QUOTE'].indexOf(container.nodeName) !== -1) {
- return null;
- }
-
- container = container.parentNode;
- }
-
- 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;
- }
- 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('@');
-
- break;
- }
-
- 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 {
- 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 < 0) {
- 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;
- this._itemIndex = 0;
- },
-
- _ajaxSetup: function() {
- return {
- data: {
- actionName: 'getSearchResultList',
- className: 'wcf\\data\\user\\UserAction',
- interfaceName: 'wcf\\data\\ISearchAction',
- parameters: {
- data: {
- includeUserGroups: true,
- scope: 'mention'
- }
- }
- },
- silent: true
- };
- },
-
- _ajaxSuccess: function(data) {
- if (!Array.isArray(data.returnValues) || !data.returnValues.length) {
- this._hideDropdown();
-
- return;
- }
-
- if (this._dropdownMenu === null) {
- this._dropdownMenu = elCreate('ol');
- this._dropdownMenu.className = 'dropdownMenu';
-
- if (_dropdownContainer === null) {
- _dropdownContainer = elCreate('div');
- _dropdownContainer.className = 'dropdownMenuContainer';
- document.body.appendChild(_dropdownContainer);
- }
-
- _dropdownContainer.appendChild(this._dropdownMenu);
- }
-
- this._dropdownMenu.innerHTML = '';
-
- var callbackClick = this._setUsername.bind(this), link, listItem, user;
- for (var i = 0, length = data.returnValues.length; i < length; i++) {
- user = data.returnValues[i];
-
- listItem = elCreate('li');
- link = elCreate('a');
- link.addEventListener('mousedown', callbackClick);
- link.className = 'box16';
- link.innerHTML = '<span>' + user.icon + '</span> <span>' + StringUtil.escapeHTML(user.label) + '</span>';
- elData(link, 'user-id', user.objectID);
- elData(link, 'username', user.label);
-
- listItem.appendChild(link);
- this._dropdownMenu.appendChild(listItem);
- }
-
- this._dropdownMenu.classList.add('dropdownOpen');
- this._dropdownActive = true;
-
- this._updateDropdownPosition();
- }
- };
-
- return UiRedactorMention;
-});
--- /dev/null
+import * as Ajax from "../../Ajax";
+import { AjaxCallbackSetup, ResponseData } from "../../Ajax/Data";
+import * as Core from "../../Core";
+import * as StringUtil from "../../StringUtil";
+import UiCloseOverlay from "../CloseOverlay";
+import { RedactorEditor, WoltLabEventData } from "./Editor";
+
+interface DropDownPosition {
+ top: number;
+ left: number;
+}
+
+interface Mention {
+ range: Range;
+ selection: Selection;
+}
+
+interface MentionItem {
+ icon: string;
+ label: string;
+ objectID: number;
+}
+
+interface AjaxResponse extends ResponseData {
+ returnValues: MentionItem[];
+}
+
+let _dropdownContainer: HTMLElement | null = null;
+
+class UiRedactorMention {
+ protected _active = false;
+ protected _dropdownActive = false;
+ protected _dropdownMenu: HTMLOListElement | null = null;
+ protected _itemIndex = 0;
+ protected _lineHeight: number | null = null;
+ protected _mentionStart = "";
+ protected _redactor: RedactorEditor;
+ protected _timer: number | null = null;
+
+ constructor(redactor: RedactorEditor) {
+ this._redactor = redactor;
+
+ redactor.WoltLabEvent.register("keydown", (data) => this._keyDown(data));
+ redactor.WoltLabEvent.register("keyup", (data) => this._keyUp(data));
+
+ UiCloseOverlay.add(`UiRedactorMention-${redactor.core.element()[0].id}`, () => this._hideDropdown());
+ }
+
+ protected _keyDown(data: WoltLabEventData): void {
+ if (!this._dropdownActive) {
+ return;
+ }
+
+ const event = data.event as KeyboardEvent;
+
+ switch (event.key) {
+ case "Enter":
+ this._setUsername(null, this._dropdownMenu!.children[this._itemIndex].children[0] as HTMLElement);
+ break;
+
+ case "ArrowUp":
+ this._selectItem(-1);
+ break;
+
+ case "ArrowDown":
+ this._selectItem(1);
+ break;
+
+ default:
+ this._hideDropdown();
+ return;
+ }
+
+ event.preventDefault();
+ data.cancel = true;
+ }
+
+ protected _keyUp(data: WoltLabEventData): void {
+ const event = data.event as KeyboardEvent;
+
+ // ignore return key
+ if (event.key === "Enter") {
+ this._active = false;
+
+ return;
+ }
+
+ if (this._dropdownActive) {
+ data.cancel = true;
+
+ // ignore arrow up/down
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
+ return;
+ }
+ }
+
+ const text = this._getTextLineInFrontOfCaret();
+ if (text.length > 0 && text.length < 25) {
+ const match = /@([^,]{3,})$/.exec(text);
+ if (match) {
+ // if mentioning is at text begin or there's a whitespace character
+ // before the '@', everything is fine
+ if (!match.index || /\s/.test(text[match.index - 1])) {
+ this._mentionStart = match[1];
+
+ if (this._timer !== null) {
+ window.clearTimeout(this._timer);
+ this._timer = null;
+ }
+
+ this._timer = window.setTimeout(() => {
+ Ajax.api(this, {
+ parameters: {
+ data: {
+ searchString: this._mentionStart,
+ },
+ },
+ });
+
+ this._timer = null;
+ }, 500);
+ }
+ } else {
+ this._hideDropdown();
+ }
+ } else {
+ this._hideDropdown();
+ }
+ }
+
+ protected _getTextLineInFrontOfCaret(): string {
+ const data = this._selectMention(false);
+ if (data !== null) {
+ return data.range
+ .cloneContents()
+ .textContent!.replace(/\u200B/g, "")
+ .replace(/\u00A0/g, " ")
+ .trim();
+ }
+
+ return "";
+ }
+
+ protected _getDropdownMenuPosition(): DropDownPosition | null {
+ const data = this._selectMention();
+ if (data === null) {
+ return null;
+ }
+
+ this._redactor.selection.save();
+
+ data.selection.removeAllRanges();
+ data.selection.addRange(data.range);
+
+ // get the offsets of the bounding box of current text selection
+ const rect = data.selection.getRangeAt(0).getBoundingClientRect();
+ const offsets: DropDownPosition = {
+ 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);
+ }
+
+ // restore caret position
+ this._redactor.selection.restore();
+
+ return offsets;
+ }
+
+ protected _setUsername(event: MouseEvent | null, item?: HTMLElement): void {
+ if (event) {
+ event.preventDefault();
+ item = event.currentTarget as HTMLElement;
+ }
+
+ const 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.range);
+
+ let range = window.getSelection()!.getRangeAt(0);
+ range.deleteContents();
+ range.collapse(true);
+
+ // Mentions only allow for one whitespace per match, putting the username in apostrophes
+ // will allow an arbitrary number of spaces.
+ let username = item!.dataset.username!.trim();
+ if (username.split(/\s/g).length > 2) {
+ username = "'" + username.replace(/'/g, "''") + "'";
+ }
+
+ const text = document.createTextNode("@" + username + "\u00A0");
+ range.insertNode(text);
+
+ range = document.createRange();
+ range.selectNode(text);
+ range.collapse(false);
+
+ data.selection.removeAllRanges();
+ data.selection.addRange(range);
+
+ this._hideDropdown();
+ }
+
+ protected _selectMention(skipCheck?: boolean): Mention | null {
+ const selection = window.getSelection()!;
+ if (!selection.rangeCount || !selection.isCollapsed) {
+ return null;
+ }
+
+ let container = selection.anchorNode as HTMLElement;
+ if (container.nodeType === Node.TEXT_NODE) {
+ // work-around for Firefox after suggestions have been presented
+ container = container.parentElement!;
+ }
+
+ // check if there is an '@' within the current range
+ if (container.textContent!.indexOf("@") === -1) {
+ return null;
+ }
+
+ // check if we're inside code or quote blocks
+ const editor = this._redactor.core.editor()[0];
+ while (container && container !== editor) {
+ if (["PRE", "WOLTLAB-QUOTE"].indexOf(container.nodeName) !== -1) {
+ return null;
+ }
+
+ container = container.parentElement!;
+ }
+
+ let range = selection.getRangeAt(0);
+ let endContainer = range.startContainer;
+ let 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;
+ } else {
+ endOffset = endContainer.childNodes.length;
+ }
+ }
+ }
+
+ let startContainer = endContainer;
+ let startOffset = -1;
+ while (startContainer !== null) {
+ if (startContainer.nodeType !== Node.TEXT_NODE) {
+ return null;
+ }
+
+ 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 {
+ // 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
+ let 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 (/\S$/.test(text.replace(/\u200B/g, ""))) {
+ 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 {
+ range: range,
+ selection: selection,
+ };
+ }
+
+ protected _updateDropdownPosition(): void {
+ const offset = this._getDropdownMenuPosition();
+ if (offset === null) {
+ this._hideDropdown();
+
+ return;
+ }
+ offset.top += 7; // add a little vertical gap
+
+ const dropdownMenu = this._dropdownMenu!;
+ dropdownMenu.style.setProperty("left", `${offset.left}px`, "");
+ dropdownMenu.style.setProperty("top", `${offset.top}px`, "");
+
+ this._selectItem(0);
+
+ if (offset.top + dropdownMenu.offsetHeight + 10 > window.innerHeight + (window.scrollY || window.pageYOffset)) {
+ const top = offset.top - dropdownMenu.offsetHeight - 2 * this._lineHeight! + 7;
+ dropdownMenu.style.setProperty("top", `${top}px`, "");
+ }
+ }
+
+ protected _selectItem(step: number): void {
+ const dropdownMenu = this._dropdownMenu!;
+
+ // find currently active item
+ const item = dropdownMenu.querySelector(".active");
+ if (item !== null) {
+ item.classList.remove("active");
+ }
+
+ this._itemIndex += step;
+ if (this._itemIndex < 0) {
+ this._itemIndex = dropdownMenu.childElementCount - 1;
+ } else if (this._itemIndex >= dropdownMenu.childElementCount) {
+ this._itemIndex = 0;
+ }
+
+ dropdownMenu.children[this._itemIndex].classList.add("active");
+ }
+
+ protected _hideDropdown(): void {
+ if (this._dropdownMenu !== null) {
+ this._dropdownMenu.classList.remove("dropdownOpen");
+ }
+ this._dropdownActive = false;
+ this._itemIndex = 0;
+ }
+
+ _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
+ return {
+ data: {
+ actionName: "getSearchResultList",
+ className: "wcf\\data\\user\\UserAction",
+ interfaceName: "wcf\\data\\ISearchAction",
+ parameters: {
+ data: {
+ includeUserGroups: true,
+ scope: "mention",
+ },
+ },
+ },
+ silent: true,
+ };
+ }
+
+ _ajaxSuccess(data: AjaxResponse): void {
+ if (!Array.isArray(data.returnValues) || !data.returnValues.length) {
+ this._hideDropdown();
+
+ return;
+ }
+
+ if (this._dropdownMenu === null) {
+ this._dropdownMenu = document.createElement("ol");
+ this._dropdownMenu.className = "dropdownMenu";
+
+ if (_dropdownContainer === null) {
+ _dropdownContainer = document.createElement("div");
+ _dropdownContainer.className = "dropdownMenuContainer";
+ document.body.appendChild(_dropdownContainer);
+ }
+
+ _dropdownContainer.appendChild(this._dropdownMenu);
+ }
+
+ this._dropdownMenu.innerHTML = "";
+
+ data.returnValues.forEach((item) => {
+ const listItem = document.createElement("li");
+ const link = document.createElement("a");
+ link.addEventListener("mousedown", (ev) => this._setUsername(ev));
+ link.className = "box16";
+ link.innerHTML = `<span>${item.icon}</span> <span>${StringUtil.escapeHTML(item.label)}</span>`;
+ link.dataset.userId = item.objectID.toString();
+ link.dataset.username = item.label;
+
+ listItem.appendChild(link);
+ this._dropdownMenu!.appendChild(listItem);
+ });
+
+ this._dropdownMenu.classList.add("dropdownOpen");
+ this._dropdownActive = true;
+
+ this._updateDropdownPosition();
+ }
+}
+
+Core.enableLegacyInheritance(UiRedactorMention);
+
+export = UiRedactorMention;