Created
April 7, 2017 16:35
-
-
Save felipeochoa/0bf29abda9dcaaf401a0f25faf27e605 to your computer and use it in GitHub Desktop.
@-mentions for Quill
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @-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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
hello, thanks for help!
Exists any way to use ALT + 64 equal SHIFT + 2 ?
I'm trying to do that, but not have success..