@enso-ui/discussions
Version:
327 lines (265 loc) • 9.06 kB
JavaScript
import Quill from 'quill';
import debounce from 'lodash/debounce';
import route from '@enso-ui/ui/src/modules/plugins/route';
import Keys from './constants/keys';
import './blots/mention';
const Options = {
mentionChar: '@',
allowedChars: /^[a-zA-Z0-9_]*$/,
maxChars: 10,
offsetTop: 2,
offsetLeft: 0,
};
class Mention {
constructor(quill, options) {
this.options = Object.assign(Options, options);
this.quill = quill;
this.fetchItems = debounce(() => this.fetcher(), 200);
this.init();
this.setKeyboardHandlers();
this.setQuillEvents();
}
init() {
this.container = null;
this.mentionList = null;
this.itemIndex = 0;
this.mentionCharPosition = null;
this.cursorPosition = null;
this.items = [];
this.query = null;
}
setQuillEvents() {
this.quill.on('text-change', (delta, oldDelta, source) => {
if (source === 'user') {
this.onChange();
}
});
this.quill.on('selection-change', (range) => {
if (range && range.length === 0) {
this.onChange();
return;
}
this.destroyMention();
});
}
onChange() {
const range = this.quill.getSelection();
if (range === null) {
return;
}
this.cursorPosition = range.index;
const startPosition = Math.max(0, this.cursorPosition - this.options.maxChars);
const textBefore = this.quill.getText(startPosition, this.cursorPosition - startPosition);
const mentionCharIndex = textBefore.lastIndexOf(this.options.mentionChar);
if (mentionCharIndex === 0 || (mentionCharIndex > 0
&& ['\n', ' ', ' '].includes(textBefore[mentionCharIndex - 1]))) {
this.mentionCharPosition = this.cursorPosition
- (textBefore.length - mentionCharIndex);
this.query = textBefore.substring(mentionCharIndex + 1);
if (this.queryIsValid()) {
this.fetchItems();
return;
}
}
this.destroyMention();
}
fetcher() {
axios.get(route('core.comments.users'), {
params: { query: this.query, limit: 10 },
}).then(({ data }) => this.renderList(data));
}
prioritizeKeyboardBinding(key) {
this.quill.keyboard.bindings[key]
.unshift(this.quill.keyboard.bindings[key].pop());
}
renderList(items) {
if (!items || !items.length) {
this.destroyMention();
return;
}
this.createContainer();
this.setItems(items);
this.itemIndex = 0;
this.selectItem();
this.showMentionList();
}
createContainer() {
if (this.container) {
return;
}
this.container = document.createElement('div');
this.container.className = 'mention-container';
this.container.style.cssText = 'display: none; position: absolute;';
this.mentionList = document.createElement('ul');
this.mentionList.className = 'mention-list';
this.container.appendChild(this.mentionList);
document.body.appendChild(this.container);
}
onItemClick(e) {
e.stopImmediatePropagation();
e.preventDefault();
this.itemIndex = e.currentTarget.dataset.index;
this.selectItem();
this.insertSelection();
}
setItems(items) {
this.items = items;
this.mentionList.innerHTML = '';
for (let i = 0; i < this.items.length; i++) {
const li = document.createElement('li');
li.className = 'mention-item';
li.innerHTML = this.options.template(this.highlight(this.items[i]));
li.dataset.index = i;
li.onclick = this.onItemClick.bind(this);
this.mentionList.appendChild(li);
}
}
showMentionList() {
this.container.style.visibility = 'hidden';
this.container.style.display = '';
const { top, left } = this.getContainerPosition();
this.container.style.top = `${top}px`;
this.container.style.left = `${left}px`;
this.container.style.visibility = 'visible';
}
getContainerPosition() {
const containerPos = this.quill.container.getBoundingClientRect();
const mentionCharPosition = this.quill.getBounds(this.mentionCharPosition);
let top = window.pageYOffset
+ containerPos.top
+ mentionCharPosition.bottom
+ this.options.offsetTop;
let left = window.pageXOffset
+ containerPos.left
+ mentionCharPosition.left
+ this.options.offsetLeft;
if (this.containerExceedsVertically(top)) {
const overMentionCharPos = window.pageYOffset
+ containerPos.top
+ mentionCharPosition.top;
const containerHeight = this.container.offsetHeight
+ this.options.offsetTop;
top = overMentionCharPos - containerHeight;
}
if (this.containerExceedsHorizontally(left)) {
const containerWidth = this.container.offsetWidth
+ this.options.offsetLeft;
const browserWidth = window.pageXOffset
+ document.documentElement.clientWidth;
left = browserWidth - containerWidth;
}
return { top, left };
}
containerExceedsVertically(top) {
return top + this.container.offsetHeight
> window.pageYOffset + window.innerHeight;
}
containerExceedsHorizontally(left) {
return left + this.container.offsetWidth
> window.pageXOffset + document.documentElement.clientWidth;
}
selectItem() {
for (let i = 0; i < this.mentionList.childNodes.length; i += 1) {
this.mentionList.childNodes[i].classList.remove('selected');
}
this.mentionList.childNodes[this.itemIndex].classList.add('selected');
const itemHeight = this.mentionList.childNodes[this.itemIndex].offsetHeight;
this.container.scrollTop = this.itemIndex * itemHeight;
}
itemData() {
return {
id: this.items[this.itemIndex].id,
value: this.options.template(this.items[this.itemIndex]),
};
}
insertSelection() {
this.quill.deleteText(
this.mentionCharPosition,
this.cursorPosition - this.mentionCharPosition,
);
this.quill.insertEmbed(
this.mentionCharPosition,
'mention',
this.itemData(),
);
this.quill.insertText(this.mentionCharPosition + 1, ' ');
this.quill.setSelection(this.mentionCharPosition + 2);
this.destroyMention();
}
highlight(user) {
if (!this.query) {
return user;
}
user.person.name = user.person.name
.replace(new RegExp(`(${this.query})`, 'gi'), '<b>$1</b>');
return user;
}
destroyMention() {
if (this.container) {
this.container.remove();
this.init();
}
}
nextItem() {
this.itemIndex = (this.itemIndex + 1) % this.items.length;
this.selectItem();
}
prevItem() {
this.itemIndex = ((this.itemIndex + this.items.length) - 1) % this.items.length;
this.selectItem();
}
queryIsValid() {
return this.query.length >= 0
&& this.options.allowedChars.test(this.query);
}
setKeyboardHandlers() {
this.quill.keyboard.addBinding({
key: Keys.TAB,
}, () => {
if (this.container) {
this.insertSelection();
return false;
}
return true;
});
this.prioritizeKeyboardBinding(Keys.TAB);
this.quill.keyboard.addBinding({
key: Keys.ENTER,
}, () => {
if (this.container) {
this.insertSelection();
return false;
}
return true;
});
this.prioritizeKeyboardBinding(Keys.ENTER);
this.quill.keyboard.addBinding({
key: Keys.ESCAPE,
}, () => {
if (this.container) {
this.destroyMention();
return false;
}
return true;
});
this.quill.keyboard.addBinding({
key: Keys.UP,
}, () => {
if (this.container) {
this.prevItem();
return false;
}
return true;
});
this.quill.keyboard.addBinding({
key: Keys.DOWN,
}, () => {
if (this.container) {
this.nextItem();
return false;
}
return true;
});
}
}
Quill.register('modules/mention', Mention);