1 define(['Ajax', 'Environment', 'Ui/CloseOverlay'], function(Ajax
, Environment
, UiCloseOverlay
) {
4 var _dropdownContainer
= null;
6 function UiRedactorMention(redactor
) { this.init(redactor
); }
7 UiRedactorMention
.prototype = {
8 init: function(redactor
) {
10 this._dropdownActive
= false;
11 this._dropdownMenu
= null;
13 this._lineHeight
= null;
14 this._mentionStart
= '';
15 this._redactor
= redactor
;
18 redactor
.WoltLabEvent
.register('keydown', this._keyDown
.bind(this));
19 redactor
.WoltLabEvent
.register('keyup', this._keyUp
.bind(this));
21 UiCloseOverlay
.add('UiRedactorMention-' + redactor
.core
.element()[0].id
, this._hideDropdown
.bind(this));
24 _keyDown: function(data
) {
25 if (!this._dropdownActive
) {
29 /** @var Event event */
30 var event
= data
.event
;
32 switch (event
.which
) {
35 this._setUsername(null, this._dropdownMenu
.children
[this._itemIndex
].children
[0]);
54 event
.preventDefault();
58 _keyUp: function(data
) {
59 /** @var Event event */
60 var event
= data
.event
;
63 if (event
.which
=== 13) {
69 if (this._dropdownActive
) {
72 // ignore arrow up/down
73 if (event
.which
=== 38 || event
.which
=== 40) {
78 var text
= this._getTextLineInFrontOfCaret();
79 if (text
.length
> 0 && text
.length
< 25) {
80 var match
= text
.match(/@([^,]{3,})$/);
82 // if mentioning is at text begin or there's a whitespace character
83 // before the '@', everything is fine
84 if (!match
.index
|| text
[match
.index
- 1].match(/\s/)) {
85 this._mentionStart
= match
[1];
87 if (this._timer
!== null) {
88 window
.clearTimeout(this._timer
);
92 this._timer
= window
.setTimeout((function() {
96 searchString
: this._mentionStart
106 this._hideDropdown();
110 this._hideDropdown();
114 _getTextLineInFrontOfCaret: function() {
115 var data
= this._selectMention(false);
117 return data
.range
.cloneContents().textContent
.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim();
123 _getDropdownMenuPosition: function() {
124 var data
= this._selectMention();
129 this._redactor
.selection
.save();
131 data
.selection
.removeAllRanges();
132 data
.selection
.addRange(data
.range
);
134 // get the offsets of the bounding box of current text selection
135 var rect
= data
.selection
.getRangeAt(0).getBoundingClientRect();
137 top
: Math
.round(rect
.bottom
) + window
.scrollY
,
138 left
: Math
.round(rect
.left
) + document
.body
.scrollLeft
141 if (this._lineHeight
=== null) {
142 this._lineHeight
= Math
.round(rect
.bottom
- rect
.top
- window
.scrollY
);
145 // restore caret position
146 this._redactor
.selection
.restore();
151 _setUsername: function(event
, item
) {
153 event
.preventDefault();
154 item
= event
.currentTarget
;
157 var data
= this._selectMention();
159 this._hideDropdown();
164 // allow redactor to undo this
165 this._redactor
.buffer
.set();
167 data
.selection
.removeAllRanges();
168 data
.selection
.addRange(data
.range
);
170 var range
= getSelection().getRangeAt(0);
171 range
.deleteContents();
172 range
.collapse(true);
174 var text
= document
.createTextNode('@' + elData(item
, 'username') + '\u00A0');
175 range
.insertNode(text
);
177 range
= document
.createRange();
178 range
.selectNode(text
);
179 range
.collapse(false);
181 data
.selection
.removeAllRanges();
182 data
.selection
.addRange(range
);
184 this._hideDropdown();
187 _selectMention: function (skipCheck
) {
188 var selection
= window
.getSelection();
189 if (!selection
.rangeCount
|| !selection
.isCollapsed
) {
193 // check if there is an '@' within the current range
194 if (selection
.anchorNode
.textContent
.indexOf('@') === -1) {
198 // check if we're inside code or quote blocks
199 var container
= selection
.anchorNode
, editor
= this._redactor
.core
.editor()[0];
200 while (container
&& container
!== editor
) {
201 if (['PRE', 'WOLTLAB-QUOTE'].indexOf(container
.nodeName
) !== -1) {
205 container
= container
.parentNode
;
208 var range
= selection
.getRangeAt(0);
209 var endContainer
= range
.startContainer
;
210 var endOffset
= range
.startOffset
;
212 // find the appropriate end location
213 while (endContainer
.nodeType
=== Node
.ELEMENT_NODE
) {
214 if (endOffset
=== 0 && endContainer
.childNodes
.length
=== 0) {
215 // invalid start location
219 // startOffset for elements will always be after a node index
220 // or at the very start, which means if there is only text node
221 // and the caret is after it, startOffset will equal `1`
222 endContainer
= endContainer
.childNodes
[(endOffset
? endOffset
- 1 : 0)];
224 if (endContainer
.nodeType
=== Node
.TEXT_NODE
) {
225 endOffset
= endContainer
.textContent
.length
;
228 endOffset
= endContainer
.childNodes
.length
;
233 var startContainer
= endContainer
;
234 var startOffset
= -1;
235 while (startContainer
!== null) {
236 if (startContainer
.nodeType
!== Node
.TEXT_NODE
) {
240 if (startContainer
.textContent
.indexOf('@') !== -1) {
241 startOffset
= startContainer
.textContent
.lastIndexOf('@');
246 startContainer
= startContainer
.previousSibling
;
249 if (startOffset
=== -1) {
250 // there was a non-text node that was in our way
255 // mark the entire text, starting from the '@' to the current cursor position
256 range
= document
.createRange();
257 range
.setStart(startContainer
, startOffset
);
258 range
.setEnd(endContainer
, endOffset
);
261 window
.console
.debug(e
);
265 if (skipCheck
=== false) {
266 // check if the `@` occurs at the very start of the container
267 // or at least has a whitespace in front of it
270 text
= startContainer
.textContent
.substr(0, startOffset
);
273 while (startContainer
= startContainer
.previousSibling
) {
274 if (startContainer
.nodeType
=== Node
.TEXT_NODE
) {
275 text
= startContainer
.textContent
+ text
;
282 if (text
.replace(/\u200B/g, '').match(/\S$/)) {
287 // check if new range includes the mention text
288 if (range
.cloneContents().textContent
.replace(/\u200B/g, '').replace(/\u00A0/g, '').trim().replace(/^@/, '') !== this._mentionStart
) {
300 _updateDropdownPosition: function() {
301 var offset
= this._getDropdownMenuPosition();
302 if (offset
=== null) {
303 this._hideDropdown();
307 offset
.top
+= 7; // add a little vertical gap
309 this._dropdownMenu
.style
.setProperty('left', offset
.left
+ 'px', '');
310 this._dropdownMenu
.style
.setProperty('top', offset
.top
+ 'px', '');
314 if (offset
.top
+ this._dropdownMenu
.offsetHeight
+ 10 > window
.innerHeight
+ window
.scrollY
) {
315 this._dropdownMenu
.style
.setProperty('top', offset
.top
- this._dropdownMenu
.offsetHeight
- 2 * this._lineHeight
+ 7 + 'px', '');
319 _selectItem: function(step
) {
320 // find currently active item
321 var item
= elBySel('.active', this._dropdownMenu
);
323 item
.classList
.remove('active');
326 this._itemIndex
+= step
;
327 if (this._itemIndex
=== -1) {
328 this._itemIndex
= this._dropdownMenu
.childElementCount
- 1;
330 else if (this._itemIndex
=== this._dropdownMenu
.childElementCount
) {
334 this._dropdownMenu
.children
[this._itemIndex
].classList
.add('active');
337 _hideDropdown: function() {
338 if (this._dropdownMenu
!== null) this._dropdownMenu
.classList
.remove('dropdownOpen');
339 this._dropdownActive
= false;
342 _ajaxSetup: function() {
345 actionName
: 'getSearchResultList',
346 className
: 'wcf\\data\\user\\UserAction',
347 interfaceName
: 'wcf\\data\\ISearchAction',
350 includeUserGroups
: false
357 _ajaxSuccess: function(data
) {
358 if (!Array
.isArray(data
.returnValues
) || !data
.returnValues
.length
) {
359 this._hideDropdown();
364 if (this._dropdownMenu
=== null) {
365 this._dropdownMenu
= elCreate('ol');
366 this._dropdownMenu
.className
= 'dropdownMenu';
368 if (_dropdownContainer
=== null) {
369 _dropdownContainer
= elCreate('div');
370 _dropdownContainer
.className
= 'dropdownMenuContainer';
371 document
.body
.appendChild(_dropdownContainer
);
374 _dropdownContainer
.appendChild(this._dropdownMenu
);
377 this._dropdownMenu
.innerHTML
= '';
379 var callbackClick
= this._setUsername
.bind(this), link
, listItem
, user
;
380 for (var i
= 0, length
= data
.returnValues
.length
; i
< length
; i
++) {
381 user
= data
.returnValues
[i
];
383 listItem
= elCreate('li');
384 link
= elCreate('a');
385 link
.addEventListener('mousedown', callbackClick
);
386 link
.className
= 'box16';
387 link
.innerHTML
= '<span>' + user
.icon
+ '</span> <span>' + user
.label
+ '</span>';
388 elData(link
, 'user-id', user
.objectID
);
389 elData(link
, 'username', user
.label
);
391 listItem
.appendChild(link
);
392 this._dropdownMenu
.appendChild(listItem
);
395 this._dropdownMenu
.classList
.add('dropdownOpen');
396 this._dropdownActive
= true;
398 this._updateDropdownPosition();
402 return UiRedactorMention
;