1 define(['Ajax', 'Environment', 'EventHandler', 'Ui/Alignment'], function(Ajax
, Environment
, EventHandler
, UiAlignment
) {
4 var _dropdownContainer
= null;
6 function UiRedactorMention(redactor
) { this.init(redactor
); }
7 UiRedactorMention
.prototype = {
8 init: function(redactor
) {
11 this._dropdownActive
= false;
12 this._dropdownMenu
= null;
14 this._lineHeight
= null;
15 this._mentionStart
= '';
16 this._redactor
= redactor
;
19 redactor
.WoltLabEvent
.register('keydown', this._keyDown
.bind(this));
20 redactor
.WoltLabEvent
.register('keyup', this._keyUp
.bind(this));
23 _keyDown: function(data
) {
24 if (!this._dropdownActive
) {
28 /** @var Event event */
29 var event
= data
.event
;
31 switch (event
.which
) {
34 this._setUsername(null, this._dropdownMenu
.children
[this._itemIndex
].children
[0]);
52 event
.preventDefault();
56 _keyUp: function(data
) {
57 /** @var Event event */
58 var event
= data
.event
;
61 if (event
.which
=== 13) {
67 var text
= this._getTextLineInFrontOfCaret();
69 var match
= text
.match(/@([^,]{3,})$/);
71 // if mentioning is at text begin or there's a whitespace character
72 // before the '@', everything is fine
73 if (!match
.index
|| text
[match
.index
- 1].match(/\s/)) {
74 this._mentionStart
= match
[1];
76 if (this._timer
!== null) {
77 window
.clearTimeout(this._timer
);
81 this._timer
= window
.setTimeout((function() {
85 searchString
: this._mentionStart
103 _setUsername: function(event
, item
) {
105 event
.preventDefault();
106 item
= event
.currentTarget
;
109 /*if (this._timer !== null) {
113 this._proxy.abortPrevious();*/
115 var selection
= window
.getSelection();
117 // restore caret position
118 selection
.removeAllRanges();
119 selection
.addRange(this._caret
);
121 var orgRange
= selection
.getRangeAt(0).cloneRange();
123 // allow redactor to undo this
124 this._redactor
.buffer
.set();
126 var startContainer
= orgRange
.startContainer
;
127 var startOffset
= orgRange
.startOffset
- (this._mentionStart
.length
+ 1);
129 // navigating with the keyboard before hitting enter will cause the text node to be split
130 if (startOffset
< 0) {
131 startContainer
= startContainer
.previousSibling
;
132 startOffset
= startContainer
.length
- (this._mentionStart
.length
+ 1) - (orgRange
.startOffset
- 1);
135 var newRange
= document
.createRange();
136 newRange
.setStart(startContainer
, startOffset
);
137 newRange
.setEnd(orgRange
.startContainer
, orgRange
.startOffset
);
139 selection
.removeAllRanges();
140 selection
.addRange(newRange
);
142 var range
= getSelection().getRangeAt(0);
143 range
.deleteContents();
144 range
.collapse(true);
146 var text
= document
.createTextNode('@' + elData(item
, 'username') + '\u00A0');
147 range
.insertNode(text
);
149 newRange
= document
.createRange();
150 newRange
.selectNode(text
);
151 newRange
.collapse(false);
153 selection
.removeAllRanges();
154 selection
.addRange(newRange
);
156 this._redactor
.selection
.save();
158 this._hideDropdown();
161 _getTextLineInFrontOfCaret: function() {
162 /** @var Range range */
163 var range
= window
.getSelection().getRangeAt(0);
164 if (!range
.collapsed
) {
168 // in Firefox, blurring and refocusing the browser creates separate text nodes
169 if (Environment
.browser() === 'firefox' && range
.startContainer
.nodeType
=== Node
.TEXT_NODE
) {
170 range
.startContainer
.parentNode
.normalize();
173 var text
= range
.startContainer
.textContent
.substr(0, range
.startOffset
);
175 // remove unicode zero-width space and non-breaking space
176 var textBackup
= text
;
178 var hadSpace
= false;
179 for (var i
= 0; i
< textBackup
.length
; i
++) {
180 var byte = textBackup
.charCodeAt(i
).toString(16);
181 if (byte !== '200b' && (!/\s/.test(textBackup
[i
]) || ((byte === 'a0' || byte === '20') && !hadSpace
))) {
182 if (byte === 'a0' || byte === '20') {
186 if (textBackup
[i
] === '@' && i
&& /\s/.test(textBackup
[i
- 1])) {
191 text
+= textBackup
[i
];
202 _ajaxSetup: function() {
205 actionName
: 'getSearchResultList',
206 className
: 'wcf\\data\\user\\UserAction',
207 interfaceName
: 'wcf\\data\\ISearchAction',
210 includeUserGroups
: false
217 _ajaxSuccess: function(data
) {
218 if (!Array
.isArray(data
.returnValues
) || !data
.returnValues
.length
) {
219 this._hideDropdown();
224 if (this._dropdownMenu
=== null) {
225 this._dropdownMenu
= elCreate('ol');
226 this._dropdownMenu
.className
= 'dropdownMenu';
228 if (_dropdownContainer
=== null) {
229 _dropdownContainer
= elCreate('div');
230 _dropdownContainer
.className
= 'dropdownMenuContainer';
231 document
.body
.appendChild(_dropdownContainer
);
234 _dropdownContainer
.appendChild(this._dropdownMenu
);
237 this._dropdownMenu
.innerHTML
= '';
239 var callbackClick
= this._setUsername
.bind(this), link
, listItem
, user
;
240 for (var i
= 0, length
= data
.returnValues
.length
; i
< length
; i
++) {
241 user
= data
.returnValues
[i
];
243 listItem
= elCreate('li');
244 link
= elCreate('a');
245 link
.addEventListener(WCF_CLICK_EVENT
, callbackClick
);
246 link
.className
= 'box16';
247 link
.innerHTML
= '<span>' + user
.icon
+ '</span> <span>' + user
.label
+ '</span>';
248 elData(link
, 'user-id', user
.objectID
);
249 elData(link
, 'username', user
.label
);
251 listItem
.appendChild(link
);
252 this._dropdownMenu
.appendChild(listItem
);
255 this._dropdownMenu
.classList
.add('dropdownOpen');
256 this._dropdownActive
= true;
258 this._updateDropdownPosition();
261 _getDropdownMenuPosition: function() {
262 this._redactor
.selection
.save();
264 var selection
= window
.getSelection();
265 var orgRange
= selection
.getRangeAt(0).cloneRange();
267 // mark the entire text, starting from the '@' to the current cursor position
268 var newRange
= document
.createRange();
269 newRange
.setStart(orgRange
.startContainer
, orgRange
.startOffset
- (this._mentionStart
.length
+ 1));
270 newRange
.setEnd(orgRange
.startContainer
, orgRange
.startOffset
);
272 selection
.removeAllRanges();
273 selection
.addRange(newRange
);
275 // get the offsets of the bounding box of current text selection
276 var rect
= selection
.getRangeAt(0).getBoundingClientRect();
278 top
: Math
.round(rect
.bottom
) + window
.scrollY
,
279 left
: Math
.round(rect
.left
) + document
.body
.scrollLeft
282 if (this._lineHeight
=== null) {
283 this._lineHeight
= Math
.round(rect
.bottom
- rect
.top
- window
.scrollY
);
286 // restore caret position
287 this._redactor
.selection
.restore();
289 this._caret
= orgRange
;
294 _updateDropdownPosition: function() {
296 var offset
= this._getDropdownMenuPosition();
297 offset
.top
+= 7; // add a little vertical gap
299 this._dropdownMenu
.style
.setProperty('left', offset
.left
+ 'px', '');
300 this._dropdownMenu
.style
.setProperty('top', offset
.top
+ 'px', '');
304 if (offset
.top
+ this._dropdownMenu
.offsetHeight
+ 10 > window
.innerHeight
+ window
.scrollY
) {
305 this._dropdownMenu
.classList
.add('dropdownArrowBottom');
307 this._dropdownMenu
.style
.setProperty('top', offset
.top
- this._dropdownMenu
.offsetHeight
- 2 * this._lineHeight
+ 7 + 'px', '');
310 this._dropdownMenu
.classList
.remove('dropdownArrowBottom');
315 // ignore errors that are caused by pressing enter to
316 // often in a short period of time
320 _selectItem: function(step
) {
321 // find currently active item
322 var item
= elBySel('.active', this._dropdownMenu
);
324 item
.classList
.remove('active');
327 this._itemIndex
+= step
;
328 if (this._itemIndex
=== -1) {
329 this._itemIndex
= this._dropdownMenu
.childElementCount
- 1;
331 else if (this._itemIndex
=== this._dropdownMenu
.childElementCount
) {
335 this._dropdownMenu
.children
[this._itemIndex
].classList
.add('active');
338 _hideDropdown: function() {
339 if (this._dropdownMenu
!== null) this._dropdownMenu
.classList
.remove('dropdownOpen');
340 this._dropdownActive
= false;
344 return UiRedactorMention
;