UiRedactorMention.prototype = {
init: function(redactor) {
this._active = false;
- this._caret = null;
this._dropdownActive = false;
this._dropdownMenu = null;
this._itemIndex = 0;
break;
default:
+ this._hideDropdown();
return;
break;
}
}
},
- _setUsername: function(event, item) {
- if (event) {
- event.preventDefault();
- item = event.currentTarget;
+ _getTextLineInFrontOfCaret: function() {
+ /** @var Range range */
+ var range = window.getSelection().getRangeAt(0);
+ if (!range.collapsed) {
+ return '';
}
- /*if (this._timer !== null) {
- this._timer.stop();
- this._timer = 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();
}
- this._proxy.abortPrevious();*/
- var selection = window.getSelection();
+ var text = range.startContainer.textContent.substr(0, range.startOffset);
- // restore caret position
- selection.removeAllRanges();
- selection.addRange(this._caret);
+ return text.replace(/\u200B/g, '').replace(/\u00A0/g, '');
+ },
+
+ _getDropdownMenuPosition: function() {
+ var data = this._selectMention();
+ if (data === null) {
+ return null;
+ }
- var orgRange = selection.getRangeAt(0).cloneRange();
+ this._redactor.selection.save();
- // allow redactor to undo this
- this._redactor.buffer.set();
+ data.selection.removeAllRanges();
+ data.selection.addRange(data.newRange);
- var startContainer = orgRange.startContainer;
- var startOffset = orgRange.startOffset - (this._mentionStart.length + 1);
+ // 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,
+ left: Math.round(rect.left) + document.body.scrollLeft
+ };
- // 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);
+ if (this._lineHeight === null) {
+ this._lineHeight = Math.round(rect.bottom - rect.top - window.scrollY);
}
- var newRange = document.createRange();
- newRange.setStart(startContainer, startOffset);
- newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
+ // restore caret position
+ this._redactor.selection.restore();
- selection.removeAllRanges();
- selection.addRange(newRange);
+ 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.newRange);
var range = getSelection().getRangeAt(0);
range.deleteContents();
range.collapse(true);
- var text = document.createTextNode('@' + elData(item, 'username') + '\u00A0');
+ var text = document.createTextNode('@' + elData(item, 'username') + ' ');
range.insertNode(text);
- newRange = document.createRange();
- newRange.selectNode(text);
- newRange.collapse(false);
-
- selection.removeAllRanges();
- selection.addRange(newRange);
+ range = document.createRange();
+ range.selectNode(text);
+ range.collapse(false);
- 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 () {
+ var selection = window.getSelection();
+ if (!selection.rangeCount || !selection.isCollapsed) {
+ 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();
- }
+ var originalRange = selection.getRangeAt(0).cloneRange();
- var text = range.startContainer.textContent.substr(0, range.startOffset);
+ // mark the entire text, starting from the '@' to the current cursor position
+ var newRange = document.createRange();
- // 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;
- }
-
- if (textBackup[i] === '@' && i && /\s/.test(textBackup[i - 1])) {
- hadSpace = false;
- text = '';
- }
-
- text += textBackup[i];
- }
- else {
- hadSpace = false;
- text = '';
+ var startContainer = originalRange.startContainer;
+ var startOffset = originalRange.startOffset - (this._mentionStart.length + 1);
+
+ if (startContainer.nodeType === Node.ELEMENT_NODE) {
+ startContainer = startContainer.childNodes[originalRange.startOffset];
+ startOffset = startContainer.length - (this._mentionStart.length + 1);
+ }
+
+ // 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
+ return null;
}
+
+ startOffset = startContainer.length - (this._mentionStart.length + 1) - (originalRange.startOffset - 1);
}
- return text;
+ try {
+ newRange.setStart(startContainer, startOffset);
+ newRange.setEnd(originalRange.startContainer, originalRange.startOffset);
+ }
+ catch (e) {
+ 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;
+ }
+
+ return {
+ newRange: newRange,
+ originalRange: originalRange,
+ 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) {
+ 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');
+ }
+ },
+
+ _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() {
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;
}
};