gdy-quill-editor
Version:
基于Quill的vue编辑器组件 npm@0.1.6
309 lines (273 loc) • 8.95 kB
JavaScript
import Quill from 'quill'
import Fuse from 'fuse.js'
import emojiList from '../config/emoji-list.js'
const Module = Quill.import('core/module')
class ShortNameEmoji extends Module {
constructor(quill, options) {
super(quill, options)
this.emojiList = options.emojiList
this.fuse = new Fuse(options.emojiList, options.fuse)
this.quill = quill
this.onClose = options.onClose
this.onOpen = options.onOpen
this.container = document.createElement('ul')
this.container.classList.add('emoji_completions')
this.quill.container.appendChild(this.container)
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.atIndex = null
this.focusedButton = null
this.isWhiteSpace = function (ch) {
let whiteSpace = false
if (/\s/.test(ch)) {
whiteSpace = true
}
return whiteSpace
}
quill.keyboard.addBinding(
{
// TODO: Once Quill supports using event.key change this to ":"
key: 186, // ":" instead of 190 in Safari. Since it's the same key it doesn't matter if we register both.
shiftKey: true,
},
this.triggerPicker.bind(this)
)
quill.keyboard.addBinding(
{
key: 59, // gecko based browsers (firefox) use 59 as the keycode for semicolon, which makes a colon character when combined with shift
shiftKey: true,
},
this.triggerPicker.bind(this)
)
quill.keyboard.addBinding(
{
key: 39, // ArrowRight
collapsed: true,
},
this.handleArrow.bind(this)
)
quill.keyboard.addBinding(
{
key: 40, // ArrowRight
collapsed: true,
},
this.handleArrow.bind(this)
)
// TODO: Add keybindings for Enter (13) and Tab (9) directly on the quill editor
}
triggerPicker(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,
':',
'emoji-shortname',
Quill.sources.USER
)
const atSignBounds = this.quill.getBounds(range.index)
this.quill.setSelection(range.index + 1, Quill.sources.SILENT)
this.atIndex = range.index
const paletteMaxPos = atSignBounds.left + 250
if (paletteMaxPos > this.quill.container.offsetWidth) {
this.container.style.left = atSignBounds.left - 250 + 'px'
} else {
this.container.style.left = atSignBounds.left + 'px'
}
this.container.style.top = atSignBounds.top + atSignBounds.height + 'px'
this.open = true
this.quill.on('text-change', this.onTextChange)
this.quill.once('selection-change', this.onSelectionChange)
this.onOpen && this.onOpen()
}
handleArrow() {
if (!this.open) return true
this.buttons[0].classList.remove('emoji-active')
this.buttons[0].focus()
if (this.buttons.length > 1) {
this.buttons[1].focus()
}
}
update() {
const sel = this.quill.getSelection().index
if (this.atIndex >= sel) {
// Deleted the at character
return this.close(null)
}
// Using: fuse.js
this.query = this.quill.getText(this.atIndex + 1, sel - this.atIndex - 1)
try {
if (event && this.isWhiteSpace(this.query)) {
this.close(null)
return
}
} catch (e) {
console.warn(e)
}
this.query = this.query.trim()
let emojis = this.fuse.search(this.query)
emojis.sort(function (a, b) {
return a.emoji_order - b.emoji_order
})
if (
this.query.length < this.options.fuse.minMatchCharLength ||
emojis.length === 0
) {
this.container.style.display = 'none'
return
}
if (emojis.length > 15) {
// return only 15
emojis = emojis.slice(0, 15)
}
this.renderCompletions(emojis)
}
maybeUnfocus() {
if (this.container.querySelector('*:focus')) return
this.close(null)
}
renderCompletions(emojis) {
try {
if (event) {
if (event.key === 'Enter' || event.keyCode === 13) {
this.close(emojis[0], 1)
this.container.style.display = 'none'
return
} else if (event.key === 'Tab' || event.keyCode === 9) {
this.quill.disable()
this.buttons[0].classList.remove('emoji-active')
this.buttons[1].focus()
return
}
}
if (event) {
return
}
} catch (e) {
console.warn(e)
}
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild)
}
const buttons = Array(emojis.length)
this.buttons = buttons
const handler = (i, emoji) => (event) => {
if (event.key === 'ArrowRight' || event.keyCode === 39) {
event.preventDefault()
buttons[Math.min(buttons.length - 1, i + 1)].focus()
} else if (event.key === 'Tab' || event.keyCode === 9) {
event.preventDefault()
if (i + 1 === buttons.length) {
buttons[0].focus()
return
}
buttons[Math.min(buttons.length - 1, i + 1)].focus()
} else if (event.key === 'ArrowLeft' || event.keyCode === 37) {
event.preventDefault()
buttons[Math.max(0, i - 1)].focus()
} else 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.quill.enable()
this.close(emoji)
}
}
emojis.forEach((emoji, i) => {
const li = makeElement(
'li',
{},
makeElement(
'button',
{ type: 'button' },
makeElement('span', {
className: 'button-emoji ap ap-' + emoji.name,
innerHTML: emoji.code_decimal,
}),
// makeElement('span', {className: "matched"}, this.query),
// makeElement('span', {className: "unmatched"}, emoji.shortname.slice(this.query.length+1))
makeElement('span', { className: 'unmatched' }, emoji.shortname)
)
)
this.container.appendChild(li)
buttons[i] = li.firstChild
// Events will be GC-ed with button on each re-render:
buttons[i].addEventListener('keydown', handler(i, emoji))
buttons[i].addEventListener('mousedown', () => this.close(emoji))
buttons[i].addEventListener('focus', () => (this.focusedButton = i))
buttons[i].addEventListener('unfocus', () => (this.focusedButton = null))
})
this.container.style.display = 'block'
// emoji palette on top
if (this.quill.container.classList.contains('top-emoji')) {
const x = this.container.querySelectorAll('li')
let i
for (i = 0; i < x.length; i++) {
x[i].style.display = 'block'
}
const windowHeight = window.innerHeight
const editorPos = this.quill.container.getBoundingClientRect().top
if (editorPos > windowHeight / 2 && this.container.offsetHeight > 0) {
this.container.style.top = '-' + this.container.offsetHeight + 'px'
}
}
buttons[0].classList.add('emoji-active')
}
close(value, trailingDelete = 0) {
this.quill.enable()
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)
if (value) {
this.quill.deleteText(
this.atIndex,
this.query.length + 1 + trailingDelete,
Quill.sources.USER
)
this.quill.insertEmbed(this.atIndex, 'emoji', value, Quill.sources.USER)
setTimeout(() => this.quill.setSelection(this.atIndex + 1), 0)
}
this.quill.focus()
this.open = false
this.onClose && this.onClose(value)
}
}
ShortNameEmoji.DEFAULTS = {
emojiList: emojiList,
fuse: {
shouldSort: true,
threshold: 0.1,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['shortname'],
},
}
function makeElement(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
}
export default ShortNameEmoji