@cairn214/fluent-editor
Version:
A rich text editor based on Quill 2.0, which extends rich modules and formats on the basis of Quill. It's powerful and out-of-the-box.
326 lines (325 loc) • 12.2 kB
JavaScript
"use strict";
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
const Quill = require("quill");
const editor_utils = require("../config/editor.utils.cjs.js");
const constants = require("./constants.cjs.js");
const MentionLink = require("./MentionLink.cjs.js");
const Delta = Quill.import("delta");
const Parchment = Quill.import("parchment");
const { Scope } = Parchment;
class Mention {
// @ts-ignore
constructor(quill, options) {
this.quill = quill;
this.activeMentionIndex = 0;
this.searchTerm = "";
this.needInsertBr = true;
this.defaultOptions = {
defaultLink: "#",
target: "_blank",
mentionChar: constants.DEFAULT_MENTION_CHAR,
maxHeight: 200,
renderMentionItem(data) {
let mentionItem = data.name || data.id;
if (this.itemKey) {
mentionItem = data[this.itemKey];
}
const dom = document.createElement("SPAN");
dom.textContent = mentionItem;
return dom;
},
renderMentionText(data) {
let mentionText = data.name || data.id;
if (this.itemKey) {
mentionText = data[this.itemKey];
}
return `${mentionText}`;
},
containerClass: "ql-mention-list-container",
listClass: "ql-mention-list",
listHideClass: "ql-mention-list--hide",
itemClass: "ql-mention-item",
itemActiveClass: "ql-mention-item--active",
itemKey: "name",
searchKey: "name",
// dataAttributes: ['id'],
select(_data) {
},
remove(_data) {
}
};
this.handleTextChange = (_delta, _oldDelta, source) => {
setTimeout(() => {
if (Quill.sources.USER === source) {
const range = this.quill.getSelection();
if (!range) {
return;
}
const caretPos = this.latestCaretPos = range.index;
const content = this.quill.getContents();
const beforeCaretText = content.reduce((newText, op) => {
if (typeof op.insert === "string") {
return newText += op.insert;
} else {
return newText += " ";
}
}, "");
const mentionCharPos = beforeCaretText.lastIndexOf(this.options.mentionChar);
if (mentionCharPos > -1) {
const searchTerm = beforeCaretText.substring(mentionCharPos + this.options.mentionChar.length, caretPos);
this.searchTerm = searchTerm;
if (!"".startsWith.call(searchTerm, " ")) {
this.latestMentionCharPos = mentionCharPos;
this.searchMentionListByTerm(searchTerm);
} else {
this.hideMentionList();
}
} else {
this.hideMentionList();
}
}
});
};
this.handleArrowUpKey = () => {
if (this.isOpen()) {
this.activeMentionIndex = (this.activeMentionIndex + this.latestMentionList.length - 1) % this.latestMentionList.length;
this.highlightMentionItem(this.activeMentionIndex);
return false;
}
return true;
};
this.handleArrowDownKey = () => {
if (this.isOpen()) {
this.activeMentionIndex = (this.activeMentionIndex + 1) % this.latestMentionList.length;
this.highlightMentionItem(this.activeMentionIndex);
return false;
}
return true;
};
this.handleEnterKey = () => {
if (this.isOpen()) {
this.selectMentionItem();
this.needInsertBr = false;
}
return true;
};
this.handleEscapeKey = () => {
if (this.isOpen()) {
this.hideMentionList();
return false;
}
return true;
};
if (!options.search) {
console.warn("please provide a search function!");
return;
}
this.options = Object.assign(this.defaultOptions, options);
const container = document.createElement("div");
container.classList.add("ql-mention-list-container");
if (this.options.containerClass !== "ql-mention-list-container") {
container.classList.add(this.options.containerClass);
}
this.mentionListEL = document.createElement("ul");
this.mentionListEL.classList.add(this.options.listClass, this.options.listHideClass);
this.mentionListEL.style.cssText += `
max-height: ${this.options.maxHeight}px;
`;
quill.on(Quill.events.TEXT_CHANGE, this.handleTextChange);
quill.keyboard.addBinding({ key: "ArrowUp" }, this.handleArrowUpKey);
quill.keyboard.addBinding({ key: "ArrowDown" }, this.handleArrowDownKey);
quill.keyboard.addBinding({ key: "Enter" }, this.handleEnterKey);
quill.keyboard.addBinding({ key: "Tab" }, this.handleEnterKey);
quill.keyboard.addBinding({ key: "Escape" }, this.handleEscapeKey);
quill.keyboard.bindings.Enter.unshift(quill.keyboard.bindings.Enter.pop());
quill.keyboard.bindings.Tab.unshift(quill.keyboard.bindings.Tab.pop());
quill.keyboard.bindings.Escape.unshift(quill.keyboard.bindings.Escape.pop());
const customKeyboardEnter = {
key: "Enter",
shiftKey: null,
handler: (range, context) => {
const lineFormats = Object.keys(context.format).reduce(
(formats, format) => {
if (this.quill.scroll.query(format, Scope.BLOCK) && !Array.isArray(context.format[format])) {
formats[format] = context.format[format];
}
return formats;
},
{}
);
let selectionIndex = range.index - this.searchTerm.length;
let delta = new Delta().retain(range.index).delete(range.length);
if (this.needInsertBr) {
delta = delta.insert("\n", lineFormats);
selectionIndex = range.index + 1;
}
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(selectionIndex, Quill.sources.SILENT);
this.quill.focus();
Object.keys(context.format).forEach((name) => {
if (!editor_utils.isNullOrUndefined(lineFormats[name])) return;
if (Array.isArray(context.format[name])) return;
if (name === "code" || name === "link") return;
this.quill.format(name, context.format[name], Quill.sources.USER);
});
this.needInsertBr = true;
}
};
quill.keyboard.bindings.Enter = quill.keyboard.bindings.Enter.map((item) => {
const buildinKeyboardEnter = item.format === void 0 && item.shiftKey === null;
if (buildinKeyboardEnter) {
return customKeyboardEnter;
} else {
return item;
}
});
this.on("click", this.handleMouseClick);
this.on("mouseover", this.handleMouseEnter);
quill.emitter.on(constants.ON_MENTION_LINK_REMOVE, async ({ mention, name }) => {
const [result] = mention && [mention] || await this.options.search(name);
this.options.remove(result);
});
container.appendChild(this.mentionListEL);
quill.container.parentElement.insertBefore(container, quill.container);
}
static register() {
Quill.register(MentionLink.default);
}
on(eventName, callback) {
this.mentionListEL.addEventListener(eventName, (evt) => {
let target = evt.target;
let targetItemEL;
while (this.mentionListEL.contains(target) && target !== this.mentionListEL) {
if (target.classList.contains(this.options.itemClass)) {
targetItemEL = target;
}
target = target.parentElement;
}
if (targetItemEL) {
callback.call(this, targetItemEL, this.getMentionItemIndex(targetItemEL));
}
});
}
getMentionItemIndex(itemEl) {
return [].reduce.call(this.mentionListEL.children, (index, item, idx) => item === itemEl ? idx : index, -1);
}
handleMouseClick(_itemEl, index) {
this.selectMentionItem(index, true);
this.quill.focus();
}
handleMouseEnter(_itemEl, index) {
this.activeMentionIndex = index;
this.highlightMentionItem(index);
}
getActiveMentionItem() {
return this.mentionListEL.querySelector(`.${this.options.itemActiveClass}`);
}
isOpen() {
return !this.mentionListEL.classList.contains(this.options.listHideClass);
}
async searchMentionListByTerm(term) {
const mentionList = await this.options.search(term);
this.latestMentionList = mentionList;
if (!mentionList || mentionList.length === 0) {
return this.hideMentionList();
}
this.showMentionList(mentionList);
}
showMentionList(mentionList) {
if (!this.isOpen()) {
this.mentionListEL.classList.remove(this.options.listHideClass);
}
this.activeMentionIndex = 0;
this.setMentionListPos();
this.render(mentionList);
}
hideMentionList() {
if (this.isOpen()) {
this.activeMentionIndex = 0;
this.mentionListEL.classList.add(this.options.listHideClass);
}
}
setMentionListPos() {
const cursorIndex = this.quill.selection.savedRange.index;
const cursorBounds = this.quill.getBounds(cursorIndex);
const { left, top } = cursorBounds;
const container = this.quill.container;
const hostElement = container.parentNode;
const { left: editorLeft, top: editorTop } = container.getBoundingClientRect();
const { left: hostElementLeft, top: hostElementTop } = hostElement.getBoundingClientRect();
const relativeLeft = editorLeft - hostElementLeft;
const relativeTop = editorTop - hostElementTop;
const menuLeft = left + relativeLeft - 5;
const menuTop = top + relativeTop + 20;
this.mentionListEL.style.cssText += `
left: ${menuLeft}px;
top: ${menuTop}px;
`;
}
render(mentionList) {
const wrapEl = document.createElement("div");
[].forEach.call(mentionList, (mentionItem, index) => {
const mentionItemEl = document.createElement("li");
mentionItemEl.classList.add(this.options.itemClass);
if (index === this.activeMentionIndex) {
mentionItemEl.classList.add(this.options.itemActiveClass);
}
const renderResult = this.options.renderMentionItem(mentionItem);
if (typeof renderResult === "string") {
mentionItemEl.insertAdjacentHTML("afterbegin", renderResult);
} else {
mentionItemEl.insertAdjacentElement("afterbegin", renderResult);
}
wrapEl.appendChild(mentionItemEl);
});
this.mentionListEL.innerHTML = wrapEl.innerHTML;
}
highlightMentionItem(index) {
const oldActiveItem = this.getActiveMentionItem();
if (oldActiveItem) {
oldActiveItem.classList.remove(this.options.itemActiveClass);
}
const newActiveItem = this.mentionListEL.querySelector(`.${this.options.itemClass}:nth-of-type(${index + 1})`);
if (newActiveItem) {
newActiveItem.classList.add(this.options.itemActiveClass);
this.scrollIntoView(newActiveItem);
}
}
scrollIntoView(node) {
const nodeAsAny = node;
if (nodeAsAny.scrollIntoViewIfNeeded) {
nodeAsAny.scrollIntoViewIfNeeded(false);
return;
}
if (node.scrollIntoView) {
node.scrollIntoView(false);
}
}
selectMentionItem(index = this.activeMentionIndex, isClick) {
const activeMentionItem = this.latestMentionList[index];
this.insertMentionBlot(activeMentionItem, isClick);
this.options.select(activeMentionItem);
this.hideMentionList();
}
insertMentionBlot(activeMentionItem, isClick) {
const mention = this.options.renderMentionText(activeMentionItem);
const delta = new Delta().retain(this.latestMentionCharPos).delete(this.latestCaretPos - this.latestMentionCharPos).insert({
[MentionLink.default.blotName]: {
char: this.options.mentionChar,
text: mention,
mention: activeMentionItem,
link: activeMentionItem.link || this.options.defaultLink,
target: activeMentionItem.target || this.options.target,
searchKey: this.options.searchKey
}
});
if (isClick) {
this.quill.updateContents(delta, Quill.sources.USER);
} else {
this.quill.updateContents(delta, Quill.sources.API);
}
this.quill.setSelection(this.latestMentionCharPos + 1, Quill.sources.API);
}
}
exports.default = Mention;
//# sourceMappingURL=Mention.cjs.js.map