Last active
April 7, 2024 08:13
-
-
Save bettysteger/d8f660030849ae85211d51d93e736a3b to your computer and use it in GitHub Desktop.
Editor.js [Inline Tool] Mentions
This file contains 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
import mentionsService from 'mentionsService.js' | |
import InlineMention from 'inlineMention.js' | |
/** | |
* this is just a code snippet on how to use the Mentions Service and Inline Tool | |
*/ | |
const editor = new EditorJS({ | |
holder: 'editorjs', | |
inlineToolbar: ['bold', 'italic', 'link', 'inlineMention'], | |
tools: { | |
inlineMention: InlineMention, | |
}, | |
onReady: () => { | |
// this will start listing to '@' input | |
mentionsService.init(editor) | |
}, | |
}) | |
/** | |
* Saving and getting mentioned user ids out of blocks | |
*/ | |
editor.save().then(outputData => { | |
const mentions = mentionUserIds(outputData.blocks); | |
console.log('mentions', mentions); | |
}); | |
function mentionUserIds(blocks) { | |
const content = blocks.map(block => block.data.text || '').join(''); | |
if (!content.includes('rel="tag"')) { return []; } | |
const mentions = []; | |
const tempEl = document.createElement('div'); | |
tempEl.innerHTML = content; | |
// See inlineMention.js: <a href="#user-id" rel="tag">@username</a> | |
tempEl.querySelectorAll('a[rel=tag][href]').forEach((a) => { | |
// remove # first character from href attribute | |
mentions.push(a.getAttribute('href').substring(1)); | |
}); | |
return mentions; | |
} |
This file contains 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
import mentionsService from "mentionsService.js"; | |
export default class InlineMention { | |
static get isInline() { | |
return true; | |
} | |
static get sanitize() { | |
return { | |
a: { | |
href: true, | |
rel: true | |
} | |
}; | |
} | |
get state() { | |
return this._state; | |
} | |
set state(state) { | |
this._state = state; | |
} | |
/** | |
* same as in editorjs/components/inline-tools/inline-tool-link.ts | |
*/ | |
get CSS() { | |
return { | |
input: 'ce-inline-tool-input', | |
inputShowed: 'ce-inline-tool-input--showed', | |
ul: 'tiny', | |
liActive: 'bg-muted' | |
}; | |
} | |
constructor({ api }) { | |
this.api = api; | |
this._state = false; | |
this.nodes = { | |
input: null, | |
userList: null, | |
anchor: null, | |
}; | |
} | |
/** | |
* Invisible button, will be triggered by typing @ | |
* @see mentionsService.init | |
*/ | |
render() { | |
return document.createElement('span'); | |
} | |
surround(range) {} | |
checkState() { | |
const a = this.api.selection.findParentTag('A'); | |
this.state = a?.rel === 'tag'; | |
if (this.state) { | |
this.showActions(a); | |
} else { | |
this.hideActions(); | |
} | |
} | |
renderActions() { | |
this.wrapper = document.createElement('div'); | |
this.nodes.input = document.createElement('input'); | |
this.nodes.input.placeholder = 'Search for a user'; | |
this.nodes.input.classList.add(this.CSS.input); | |
this.nodes.input.addEventListener('input', (e) => { | |
this.getMentions(e.target.value) | |
}); | |
this.nodes.input.addEventListener('keydown', (event) => { | |
if (event.keyCode === 13) { | |
this.enterPressed(event); | |
} | |
if (event.keyCode === 40 || event.keyCode === 38) { | |
this.downOrUpPressed(event); | |
} | |
}); | |
this.wrapper.appendChild(this.nodes.input); | |
return this.wrapper; | |
} | |
getMentions(q) { | |
mentionsService.getMentions(q).then((users) => { | |
this.nodes.userList.innerHTML = ''; | |
users.forEach((user, i) => { | |
const li = document.createElement('li'); | |
li.innerHTML = mentionsService.outputTemplate(user); | |
if (i === 0) { li.classList.add(this.CSS.liActive); } | |
li.addEventListener('click', (e) => { | |
e.preventDefault(); | |
const a = this.nodes.anchor | |
a.innerHTML = '@' + user.name; | |
a.href = '#' + user.id; | |
// adds a space after the mention | |
a.insertAdjacentHTML('afterend', ' '); | |
// focus after the space | |
this.setCursor(a.nextSibling, 1) | |
}); | |
this.nodes.userList.appendChild(li); | |
}); | |
}); | |
} | |
enterPressed(event) { | |
event.preventDefault(); | |
const firstLi = this.nodes.userList.querySelector(`li.${this.CSS.liActive}`); | |
if (!firstLi) { return; } | |
firstLi.click(); | |
} | |
downOrUpPressed(event) { | |
event.preventDefault(); | |
const currentLi = this.nodes.userList.querySelector(`li.${this.CSS.liActive}`); | |
const nextLi = event.keyCode === 40 ? currentLi?.nextSibling : currentLi?.previousSibling; | |
if (nextLi) { | |
currentLi.classList.remove(this.CSS.liActive); | |
nextLi.classList.add(this.CSS.liActive); | |
} | |
} | |
showActions(a) { | |
if (this.nodes.userList) { return; } | |
// remove input from original link inline tool | |
document.querySelector(`.${this.CSS.inputShowed}`).remove(); | |
if (a.href) { return; } | |
this.nodes.anchor = a; // save anchor for later | |
this.nodes.input.classList.add(`${this.CSS.inputShowed}`); | |
setTimeout(() => { this.nodes.input.focus(); }); // wait for input to be visible | |
this.nodes.userList = document.createElement('ul'); | |
this.nodes.userList.classList.add(this.CSS.ul); | |
this.wrapper.appendChild(this.nodes.userList); | |
this.getMentions(); | |
} | |
hideActions() { | |
this.nodes.input.classList.remove(this.CSS.inputShowed); | |
this.nodes.input.value = ''; | |
this.nodes.anchor = null; | |
if (this.nodes.userList) { | |
this.nodes.userList.remove(); | |
this.nodes.userList = null; | |
} | |
} | |
clear() { | |
this.hideActions(); | |
} | |
setCursor(element, offset) { | |
const range = document.createRange(); | |
const selection = window.getSelection(); | |
range.setStart(element, offset); | |
range.setEnd(element, offset); | |
selection.removeAllRanges(); | |
selection.addRange(range); | |
return range.getBoundingClientRect(); | |
} | |
} |
This file contains 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 trigger = '@'; | |
const keyCombos = { // will be set to true on keydown, check happens in keyup | |
'2': false, // Shift+2 | |
l: false, // ⌥+L on Mac on a german keyboard | |
q: false // AltGr+Q on Windows on a german keyboard | |
}; | |
/** | |
* Service methods for mentions in EditorJS | |
*/ | |
export default { | |
init: (editor) => { | |
editor.listeners.on(editor.ui.nodes.wrapper, 'keydown', (e) => { | |
keyCombos.l = e.altKey; | |
keyCombos['2'] = e.shiftKey; | |
keyCombos.q = e.key === 'AltGraph'; | |
}); | |
editor.listeners.on(editor.ui.nodes.wrapper, 'keyup', (e) => { | |
// just checking '@' is not enough, because when pressing key combos too fast the '@' is not registered | |
if(e.key === trigger || keyCombos[e.key]) { | |
keyCombos[e.key] = false; | |
// add inline mention tool to current block, by replacing @ (at the end) with <a rel="tag">@</a> | |
const currentIdx = editor.blocks.getCurrentBlockIndex(); | |
const currentBlock = editor.blocks.getBlockByIndex(currentIdx); | |
const paragraph = currentBlock.holder.querySelector('.ce-paragraph'); | |
const regex = new RegExp(`${trigger}$`); | |
if (!paragraph || !paragraph.innerHTML.match(regex)) { return; } | |
paragraph.innerHTML = paragraph.innerHTML.replace(regex, `<a rel="tag">${trigger}</a>`); | |
const anchor = editor.ui.nodes.wrapper.querySelector('a[rel="tag"]:not([href])') | |
editor.selection.expandToTag(anchor); // this will open up the inline tool, see inlineMention.js | |
} | |
}); | |
}, | |
getMentions: async (q) => { | |
return fetch(`/api/users?q=${q}`) | |
.then(response => response.json()) | |
.then(json => json.data); | |
}, | |
outputTemplate: (user) => { | |
return `<a href="#${user.id}" rel="tag">${user.name}</a>`; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very nice! I have another use case (not mentioning people) but I also needed to activate an inline tool by keyboard shortcut without necessarily selecting text, and your gist provided a great starting point.