class RichTextEditor extends HTMLElement { constructor() { super(); this.loadExternalScripts(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` ${RichTextEditor.header()} ${RichTextEditor.template()} `; } connectedCallback() { this.myQuill = this.mountQuill(); } loadExternalScripts() { const links = ["https://cdn.quilljs.com/1.3.6/quill.snow.css", "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css", "https://fonts.googleapis.com/css?family=Mirza|Roboto|Slabo+27px|Sofia|Inconsolata|Ubuntu|Akronim|Monoton&display=swap"] links.forEach(link => { const css = document.createElement("link"); css.href = link; css.rel = "stylesheet" document.head.appendChild(css); }) } static template() { return `
`; } static header() { return ` `; } async mountQuill() { // Register the customs format with Quill const lib = await import("https://cdn.jsdelivr.net/npm/shadow-selection-polyfill"); const getRange = lib.getRange; const Font = Quill.import('formats/font'); Font.whitelist = ['mirza', 'roboto', 'sofia', 'slabo', 'inconsolata', 'ubuntu', 'cursive', 'Akronim', 'Monoton']; const Link = Quill.import('formats/link'); Link.sanitize = function (url) { // modify url if desired return url; } const SizeStyle = Quill.import('attributors/style/size'); SizeStyle.whitelist = ['10px', '18px', '32px', '50px', '64px']; Quill.register(SizeStyle, true); Quill.register(Link, true); Quill.register(Font, true); const icons = Quill.import('ui/icons'); const icon = `` icons['link'] = icon; const editorContainer = this.shadowRoot.querySelector('#editor-container') const toolbarContainer = this.shadowRoot.querySelector('#toolbar-container') const myQuill = new Quill(editorContainer, { modules: { toolbar: { container: toolbarContainer, }, }, theme: 'snow' }); const normalizeNative = (nativeRange) => { if (nativeRange) { const range = nativeRange; if (range.baseNode) { range.startContainer = nativeRange.baseNode; range.endContainer = nativeRange.focusNode; range.startOffset = nativeRange.baseOffset; range.endOffset = nativeRange.focusOffset; if (range.endOffset < range.startOffset) { range.startContainer = nativeRange.focusNode; range.endContainer = nativeRange.baseNode; range.startOffset = nativeRange.focusOffset; range.endOffset = nativeRange.baseOffset; } } if (range.startContainer) { return { start: { node: range.startContainer, offset: range.startOffset }, end: { node: range.endContainer, offset: range.endOffset }, native: range }; } } return null }; myQuill.selection.getNativeRange = () => { const dom = myQuill.root.getRootNode(); const selection = getRange(dom); const range = normalizeNative(selection); return range; }; let fromEditor = false; editorContainer.addEventListener("pointerup", (e) => { fromEditor = false; }); editorContainer.addEventListener("pointerout", (e) => { fromEditor = false; }); editorContainer.addEventListener("pointerdown", (e) => { fromEditor = true; }); document.addEventListener("selectionchange", () => { if (fromEditor) { myQuill.selection.update() } }); myQuill.on('text-change', () => { // keep qull data inside _data to communicate with Gradio document.querySelector("#rich-text-root")._data = myQuill.getContents() }) return myQuill } } customElements.define('rich-text-editor', RichTextEditor);