Skip to content

Instantly share code, notes, and snippets.

@felipeochoa
Created April 7, 2017 16:35
Show Gist options
  • Save felipeochoa/0bf29abda9dcaaf401a0f25faf27e605 to your computer and use it in GitHub Desktop.
Save felipeochoa/0bf29abda9dcaaf401a0f25faf27e605 to your computer and use it in GitHub Desktop.
@-mentions for Quill
const quillModules = {
toolbar: ["bold", "italic", "underline", "strike"],
// mentions is added in constructor
keyboard: {
bindings: {
tab: {
key: 9,
handler: function(range, context) {
return this.quill.mentionHandler(range.context);
}
},
enter: null // added in constructor
}
}
};
function CommentForm() {
this.quillModules = Object.assign({}, quillModules);
this.quillModules.mentions = {
getUsers: () => USER_LIST
};
const forceSubmit = this.createComment;
this.quillModules.keyboard = Object.assign({}, this.quillModules.keyboard);
this.quillModules.keyboard.bindings = Object.assign({}, this.quillModules.keyboard.bindings);
this.quillModules.keyboard.bindings.enter = {
key: 13,
handler: function(range, context) {
// Needs to be done this way to avoid the default binding in Quill
if (this.quill.mentionHandler(range, context)) {
forceSubmit();
};
}
};
const that = this;
this.quillModules.keyboard.bindings.escape = {
key: 27,
handler: function() {
if (this.quill.mentionDialogOpen) return true;
that.handleEscape();
}
};
}
// @-mentions for Quill
import Quill from 'quill';
const Delta = Quill.import('delta');
const Inline = Quill.import('blots/inline');
// TODO: Re-implement this as an embed
class MentionBlot extends Inline {
static create(id) {
const node = super.create();
node.dataset.id = id;
return node;
}
static formats(node) {
return node.dataset.id;
}
format(name, value) {
if (name === "mention" && value) {
this.domNode.setAttribute("data-id", value);
} else {
super.format(name, value);
}
}
formats() {
const formats = super.formats();
formats['mention'] = MentionBlot.formats(this.domNode);
return formats;
}
}
MentionBlot.blotName = "mention";
MentionBlot.tagName = "SPAN";
MentionBlot.className = "mention";
Quill.register({
'formats/mention': MentionBlot
});
const h = (tag, attrs, ...children) => {
const elem = document.createElement(tag);
Object.keys(attrs).forEach(key => elem[key] = attrs[key]);
children.forEach(child => {
if (typeof child === "string")
child = document.createTextNode(child);
elem.appendChild(child);
});
return elem;
};
class Mentions {
constructor(quill, {onClose, onOpen, getUsers, container}) {
this.quill = quill;
this.onClose = onClose;
this.onOpen = onOpen;
this.getUsers = getUsers;
if (typeof container === "string") {
this.container = this.quill.container.parentNode.querySelector(container);
} else if (container === undefined) {
this.container = h("ul", {});
this.quill.container.parentNode.appendChild(this.container);
} else {
this.container = container;
}
this.container.classList.add("ql-mention-menu");
this.container.style.position = "absolute";
this.container.style.display = "none";
this.onSelectionChange = this.maybeUnfocus.bind(this);
this.onTextChange = this.update.bind(this);
this.open = false;
this.quill.mentionDialogOpen = false;
this.atIndex = null;
this.focusedButton = null;
this.buttons = [];
this.users = [];
quill.keyboard.addBinding({
// TODO: Once Quill supports using event.key (#1091) use that instead of shift-2
key: 50, // 2
shiftKey: true,
}, this.onAtKey.bind(this));
quill.keyboard.addBinding({
key: 40, // ArrowDown
collapsed: true,
format: ["mention"]
}, this.handleArrow.bind(this));
quill.keyboard.addBinding({
key: 27, // Escape
collapsed: null,
format: ["mention"]
}, this.handleEscape.bind(this));
quill.mentionHandler = this.handleEnterTab.bind(this);
}
onAtKey(range, context) {
if (this.open) return true;
if (range.length > 0) {
this.quill.deleteText(range.index, range.length, Quill.sources.USER);
}
this.quill.insertText(range.index, "@", "mention", "@@placeholder", Quill.sources.USER);
const atSignBounds = this.quill.getBounds(range.index);
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.atIndex = range.index;
this.container.style.left = atSignBounds.left + "px";
this.container.style.top = atSignBounds.top + atSignBounds.height + "px",
this.open = true;
this.quill.mentionDialogOpen = true;
this.quill.on('text-change', this.onTextChange);
this.quill.once('selection-change', this.onSelectionChange);
this.update();
this.onOpen && this.onOpen();
}
handleArrow() {
if (!this.open) return true;
this.buttons[0].focus();
}
handleEnterTab() {
if (!this.open) return true;
this.close(this.users[0]);
}
handleEscape() {
if (!this.open) return true;
this.close();
}
update() {
const sel = this.quill.getSelection().index;
if (this.atIndex >= sel) { // Deleted the at character
return this.close(null);
}
this.originalQuery = this.quill.getText(this.atIndex + 1, sel - this.atIndex - 1);
this.query = this.originalQuery.toLowerCase();
// TODO: Should use fuse.js or similar fuzzy-matcher
this.users = this.getUsers()
.filter(u => u.name.toLowerCase().startsWith(this.query))
.sort((u1, u2) => u1.name > u2.name);
this.renderCompletions(this.users);
}
maybeUnfocus() {
if (this.container.querySelector("*:focus")) return;
this.close(null);
}
renderCompletions(users) {
while (this.container.firstChild) this.container.removeChild(this.container.firstChild);
const buttons = Array(users.length);
this.buttons = buttons;
const handler = (i, user) => event => {
if (event.key === "ArrowDown" || event.keyCode === 40) {
event.preventDefault();
buttons[Math.min(buttons.length - 1, i + 1)].focus();
} else if (event.key === "ArrowUp" || event.keyCode === 38) {
event.preventDefault();
buttons[Math.max(0, i - 1)].focus();
} else if (event.key === "Enter" || event.keyCode === 13
|| event.key === " " || event.keyCode === 32
|| event.key === "Tab" || event.keyCode === 9) {
event.preventDefault();
this.close(user);
} else if (event.key === "Escape" || event.keyCode === 27) {
event.preventDefault();
this.close();
}
};
users.forEach((user, i) => {
const li = h('li', {},
h('button', {type: "button"},
h('span', {className: "matched"}, "@" + user.name.slice(0, this.query.length)),
h('span', {className: "unmatched"}, user.name.slice(this.query.length))));
this.container.appendChild(li);
buttons[i] = li.firstChild;
// Event-handlers will be GC-ed with button on each re-render:
buttons[i].addEventListener('keydown', handler(i, user));
buttons[i].addEventListener("mousedown", () => this.close(user));
buttons[i].addEventListener("focus", () => this.focusedButton = i);
buttons[i].addEventListener("unfocus", () => this.focusedButton = null);
});
this.container.style.display = "block";
}
close(value) {
this.container.style.display = "none";
while (this.container.firstChild) this.container.removeChild(this.container.firstChild);
this.quill.off('selection-change', this.onSelectionChange);
this.quill.off('text-change', this.onTextChange);
let delta = new Delta()
.retain(this.atIndex)
.delete(this.query.length + 1);
let newIndex;
if (value) {
const {id, name} = value;
delta = delta
.insert("@" + name, {mention: id})
.insert(" ");
newIndex = this.atIndex + name.length + 2;
} else {
delta = delta.insert("@" + this.originalQuery);
newIndex = this.atIndex + this.originalQuery.length + 1;
}
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(newIndex, 0, Quill.sources.SILENT);
this.quill.focus();
this.open = false;
this.quill.mentionDialogOpen = false;
this.onClose && this.onClose(value);
}
}
Quill.register('modules/mentions', Mentions);
@VictorOliveira-Atlas
Copy link

VictorOliveira-Atlas commented Aug 29, 2022

hello, thanks for help!
Exists any way to use ALT + 64 equal SHIFT + 2 ?

I'm trying to do that, but not have success..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment