import { css, html, LitElement, unsafeCSS } from "lit"; import { property } from "lit/decorators.js"; import Quill from "uxl-quill/dist/quill"; import styles from "./styles.css?inline"; import { template } from "./template"; import { fixSpellCheckerIssue } from "./utils"; fixSpellCheckerIssue(Quill); export class PrimariaRichTextEditor extends LitElement { private textChangeTimeout: number | null = null; render() { return html` ${template(this)} `; } static styles = css` ${unsafeCSS(styles)} `; firstUpdated() { // const rte: PrimariaRichTextEditor = this; this.quill = new Quill(this.shadowRoot?.querySelector("#rte"), this._getOptions()); this.quill.on("text-change", () => this._debouncedEmitTextChangeEvent()); this.quill.root.addEventListener("paste", () => { this._debouncedEmitTextChangeEvent(); }); this.quill.on("selection-change", (range, oldRange, source) => { if (range) this.range = range; }); } _debouncedEmitTextChangeEvent() { if (this.textChangeTimeout) { clearTimeout(this.textChangeTimeout); } this.textChangeTimeout = window.setTimeout(() => { this._emitTextChangeEvent(); }, 200); } _emitTextChangeEvent() { const plainValue = this.quill.getText(); const values = { html: this.shadowRoot?.querySelector(".ql-editor")?.innerHTML, plain: plainValue, }; const textChanged = new CustomEvent("text-changed", { composed: true, detail: values, }); this.dispatchEvent(textChanged); this.range = this.getCursorPosition(); } @property() quill: Quill | null; @property() options: string; @property({ type: Object }) range: { index: number; length: number }; @property({ type: Array }) availableOptions: string[] = [ "bold", "italic", "underline", "strike", "blockquote", "code-block", "image", "video", "link", "color", "background", "ol", "ul", "subindex", "superindex", // 'outdent', // 'indent', "size", "header", "font", "align", // "clear", "undo", "redo", ]; @property({ type: Array }) availableFormats: string[] = [ "background", "bold", "color", "font", "code", "italic", "link", "size", "strike", "script", "underline", "blockquote", "header", // 'indent', "list", "align", "direction", "code-block", "formula", "image", "video", ]; @property() formats: string; _getOptions() { let toolbarOptions = []; const availableOpts = this.availableOptions; if (this.options === undefined || this.options.length === 0) { toolbarOptions = [ ["bold", "italic", "underline", "strike"], ["blockquote", "code-block"], [{ header: 1 }, { header: 2 }], [{ list: "ordered" }, { list: "bullet" }], [{ script: "sub" }, { script: "super" }], // [{indent: '-1'}, {indent: '+1'}], [{ direction: "rtl" }], [{ size: ["small", false, "large", "huge", "20px"] }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], [{ font: [] }], [{ align: [] }], ["image"], ["video"], // ["clean"], ]; } else { toolbarOptions = this.options.split(","); toolbarOptions.forEach((option) => { if (availableOpts.indexOf(option) <= -1) toolbarOptions.splice(toolbarOptions.indexOf(option), 1); }); toolbarOptions = toolbarOptions.map(function (option) { if (option == "color") return { color: [] }; else if (option == "ol") return { list: "ordered" }; else if (option == "ul") return { list: "bullet" }; else if (option == "subindex") return { script: "sub" }; else if (option == "superindex") return { script: "super" }; // else if (option == 'outdent') return {indent: '-1'}; // else if (option == 'indent') return {indent: '+1'}; else if (option == "size") return { size: ["small", false, "large", "huge"] }; else if (option == "header") return { header: [1, 2, 3, 4, 5, 6, false] }; else if (option == "background") return { background: [] }; else if (option == "font") return { font: [] }; else if (option == "align") return { align: [] }; /* else if (option == 'undo') return {undo: undo_icon}; else if (option == 'redo') return {redo: redo_icon}; */ else return option; }); // if (toolbarOptions.indexOf("clean") < 0) { // toolbarOptions.push("clean"); // } } /* const icons = Quill.import('ui/icons'); icons['undo'] = undo_icon; icons['redo'] = redo_icon; */ let formats = this.availableFormats; if (this.formats) { formats = this.formats.split(","); } return { modules: { toolbar: { container: toolbarOptions, handlers: { redo() { this.quill.history.redo(); }, undo() { this.quill.history.undo(); }, }, }, history: { delay: 1000, maxStack: 50, userOnly: false, }, keyboard: { bindings: { indent: { // highlight tab or tab at beginning of list, indent or blockquote key: "Tab", format: ["blockquote", "indent", "list"], handler(range, context) { if ( (context.collapsed && context.offset !== 0) || context.format?.list === "ordered" // disable ordered list auto-numbering on indentation ) return true; this.quill.format("indent", "+1", Quill.sources.USER); return false; }, }, "list autofill": { key: " ", shiftKey: null, collapsed: true, format: { "code-block": false, blockquote: false, table: false, }, prefix: /^\s*?(\d+\.|-|\*)$/, handler(range, context) { if (this.quill.scroll.query("list") == null) return true; const { length } = context.prefix; const line = this.quill.getLine(range.index); const offset = line[1]; if (offset > length) return true; this.quill.insertText(range.index, " ", Quill.sources.USER); this.quill.setSelection(range.index - length, Quill.sources.SILENT); return false; }, }, }, }, }, theme: "snow", formats: formats, }; } public getCursorPosition() { return this.quill.getSelection(); } }