quill icon indicating copy to clipboard operation
quill copied to clipboard

Shadow DOM support - continued

Open yananym opened this issue 5 years ago • 27 comments

Follow up of issues https://github.com/quilljs/quill/issues/2021 and https://github.com/quilljs/quill/issues/1472 to add support for Shadow DOM.

More and more apps are being built with Micro Front End architecture in mind, which means components including editor experience are delivered through as a web component, which means, Shadow DOM.

Cisco had been leveraging quill in Contact Center cloud enterprise solution, and now is moving to MFE architecture.

I will be opening a simple PR on behalf of Cisco to address it, please let me know if quill team would have bandwidth to review it within the next 3-4 months, it would help greatly!

yananym avatar Mar 10 '20 00:03 yananym

@yananym all power to you. It's a shame this Shadow DOM supporting implementation wasn't merged before becoming stale: https://github.com/web-padawan/quill But I'm sure something similar can be created for Quill 2.x.x

mike-shtil avatar Mar 11 '20 15:03 mike-shtil

Any update on this issue?

albert-sf avatar Mar 23 '20 20:03 albert-sf

Any update on this? I think as a community we must encourage the web component standard and we cannot be a bottle neck.

aanavaneeth avatar Mar 27 '20 18:03 aanavaneeth

I'm glad to see a relatively new issue addressing shadow DOM support. I'd be happy to help in any way. Will integrating the changes from https://github.com/web-padawan/quill into a fresh fork of quill be useful?

kr05 avatar Apr 03 '20 02:04 kr05

This is actually exactly what one of my engineers had done. He had reported that some Jasmine unit tests are failing and he was not able to resole them, i had not been able to find time to look into it as of yet. @kr05 Kevin would you be able to take a look at tests failures? i would be grateful! https://github.com/yananym/quill

yananym avatar Apr 03 '20 02:04 yananym

Of course, I'll see what I can do!

kr05 avatar Apr 03 '20 02:04 kr05

@yananym it still not work on Safari (my safari version: 13.1). But it's work on Chrome & Firefox, although I see some errors on the console

nguyen-van-quang avatar Apr 03 '20 04:04 nguyen-van-quang

Has anyone tried setting up the dev environment on WSL? I'm getting some issues with installing nokogiri. I'll keep trying, but if anyone has any suggestions I would appreciate it. I can provide more info as requested.

error 1 error 2

kr05 avatar Apr 03 '20 04:04 kr05

I try the https://github.com/yananym/quill, I didn't see setup issue on mac.

albert-sf avatar Apr 08 '20 16:04 albert-sf

@kr05 I think the error is realtively clear. you'll need to install pkg-config (program that helps C compiler discover libraries to link - it essentially generates -l* falgs to cc). You might also need to install libxml2 and other dependencies. Last time I used windows was 10 years ago so I can't provide more specific help but perhaps you can just apt-get install it in WSL?

I'm also interested seeing some progress here. Shadow dom support is not a deal breaker for me but I would still rather use the shadow dom if I could. Is anyone working on this at the moment? And if not are PRs implementing this accepted?

turboMaCk avatar Feb 22 '21 15:02 turboMaCk

@kr05 actually I think you find answers to your questions in this post https://davemateer.com/2020/10/20/running-jekyll-on-wsl2

turboMaCk avatar Feb 22 '21 15:02 turboMaCk

Any updates on this? I am using react-quill in shadow dom and I have strange issues with it.

dman777 avatar Mar 04 '21 20:03 dman777

@dman777 yes that's what I also experienced just with my hand-coded web-component embedded in elm application. For time being I would recommend you to abandon shadow DOM and just namespace things as best as you can. It seems to me that quill depends on some of the global stuff (event handlers on window and such) to work properly. Luckily these things are not a major problem if you're careful™. The limitation is probably the biggest problem for folks who need to integrate quill to code that itself is used in shadow dom like the micro frontend oriented apps. It seems there is no interest in maintainers on this so the options seems to be either to fork quill or to live with it as is.

turboMaCk avatar Mar 04 '21 20:03 turboMaCk

I'm another person who wants to use Quill but needs it inside a web component context. I can get Quill to import and render and it kind of works, but the issue preventing me from using it is that it can get into a state where every keypress sends the cursor back to the start and inserts a newline, so trying to type a word will produce each character on its own line and in reverse order. Clicking inside the text box while it's empty toggles this state somewhat reliably.

It only happens when it's inside a web component, but that's my only lead.

ghost avatar Mar 09 '21 07:03 ghost

@yujiri8 isn't it possible that you have quill in some vdom based FW (like React) and that every keypress causes react to render Quill again (insert after v dom diff) so the problem is not the quill itself but the fact you initialize it again after every key?

I'm 100% certain it's possible to embed quill as web-component (without shadow DOM). I think the problem is somewhere in your approach.

turboMaCk avatar Mar 09 '21 07:03 turboMaCk

The FW I'm using is Lit-Element. It only re-renders a component when an attribute changes, so keypresses should not trigger that.

On 3/9/21, Marek Fajkus [email protected] wrote:

@yujiri8 isn't it possible that you have quill in some vdom based FW (like React) and that every keypress causes react to render Quill again (insert after v dom diff) so the problem is not the quill itself but the fact you initialize it again after every key?

I'm 100% certain it's possible to embed quill as web-component (without shadow DOM). I think the problem is somewhere in your approach.

-- You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub: https://github.com/quilljs/quill/issues/2961#issuecomment-793507929

ghost avatar Mar 09 '21 14:03 ghost

I would start by checking your data flow to see what causes it but it's definitely not because of a web component. For instance call to setContent and some other resets the position of cursor. My approach is to let quill manage it's own state and just hook into events but never to call these methods that sets the content on quill instance unless I need to (I'm loading a new data). This approach works for me though I admit it involved some trickery to wrap this stateful interface so it can work within declarative ui.

turboMaCk avatar Mar 09 '21 15:03 turboMaCk

I'm not calling setContent or any other post-constructor API. This is a minimal element that experiences it:

import {LitElement, html, css} from 'lit-element';
import * as Quill from 'quill';

customElements.define('test-component', class extends LitElement {
	static get properties() {
		return {}
	}
	static get styles() {
		return [styles, css`
		@import 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
		:host([hidden]) { display: none; }
		:host { display: block; }
		`];
	}
	render() {
		return html`
		<div id="quill"></div>
		`;
	}
	firstUpdated() {
		this.quill = new Quill(this.shadowRoot.getElementById('quill'), {theme: 'snow'});
	}
});

ghost avatar Mar 09 '21 15:03 ghost

This is the vanila JS (ES2015) custom elment wrapper that works:

const toolbarOptions = [
  [{ 'header': [1, 2, false] }],
  ['bold', 'underline', 'italic', 'strike'],        // toggled buttons
  [{ 'background': [] }, { 'color': [] }],          // dropdown with defaults from theme
  [{ 'script': 'super' }, { 'script': 'sub'}],      // superscript/subscript
  [{ 'list': 'bullet' }, { 'list': 'ordered'}],
  [{ 'indent': '+1'}, { 'indent': '-1' }],          // outdent/indent

  ['code-block', 'blockquote'],
  ['clean']                                         // remove formatting button
];


class MyQuill extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

    // Setup container
    this.editorContainer = document.createElement('div');
  }

  connectedCallback() {
    this.prepend(this.editorContainer);

    const quill = new Quill(this.editorContainer, {
      modules: {
        toolbar: toolbarOptions
      },
      theme: 'snow' // or 'bubble'
    });
    this._quill = quill;
  }
}

customElements.define("my-quill", MyQuill);

Your example is using shadow dom as it says in code:

this.shadowRoot.getElementById('quill')

as I said:

web-component (without shadow DOM).

turboMaCk avatar Mar 09 '21 16:03 turboMaCk

as I said:

web-component (without shadow DOM).

Oh, I missed that part of your reply. But yes I need it inside shadow DOM. And that is the title of this issue, after all.

ghost avatar Mar 09 '21 19:03 ghost

In this example, editor is created inside Shadow Root. https://codepen.io/artemiusgreat/pen/XWMPdWG The main concern so far is that inline formatting doesn't work when initiated from Toolbar module by clicking Bold or Italic buttons on the panel. The reason is that window.getSelection always returns empty selection inside the Shadow Root. The good thing is that it somehow works when inline formatting is initiated from Keyboard module by pressing CTRL + B or CTRL + I. I'm digging into the code, but if somebody already resolved this I would appreciate some guidance.

artemiusgreat avatar Jun 10 '21 04:06 artemiusgreat

Kind of fixed. https://stackoverflow.com/a/67944380/437393

thank you very mush!

littleweb avatar Jun 18 '21 09:06 littleweb

Wanted to note a couple of interesting details which I saw mentioned elsewhere

  • web-padawan's personal quill fork has been moved to vaadin/quill
  • some selection-api-related issues on safari are purportedly improved via a different polyfill

robrez avatar Aug 26 '21 19:08 robrez

Happy new year everyone. Anyone else still eagerly awaiting some traction here? Please feel free to provide some comments/thoughts on my branch - https://github.com/robrez/quill/pull/1

robrez avatar Jan 07 '22 05:01 robrez

@yananym all power to you. It's a shame this Shadow DOM supporting implementation wasn't merged before becoming stale: https://github.com/web-padawan/quill But I'm sure something similar can be created for Quill 2.x.x

parse not work.

littleweb avatar Jan 26 '22 06:01 littleweb

Kind of fixed. https://stackoverflow.com/a/67944380/437393

I noticed that copy-pasting text is not working with this approach, which can be observed here. Does anyone have a workaround for this?

C0ncept95 avatar Jul 26 '22 14:07 C0ncept95

We are also using Quill and WebComponents ( Lit Element ) as part of our rich / fully featured text editor. We are currently still suffering from the fact Selection API doesn't understand the shadow DOM in Safari / Firefox this polyfil: https://github.com/GoogleChromeLabs/shadow-selection-polyfill together with this function: https://stackoverflow.com/questions/67914657/quill-editor-inside-shadow-dom/67944380#67944380 solved the issue on firefox (the entire toolbar and selection of text and trying to apply the toolbar functionality on the selected text didn't work without this polyfil. some issues were not resolved by the polyfil. for example: same issue of @yujiri8 https://github.com/quilljs/quill/issues/2961#issuecomment-793999566 HOWEVER - the biggest problem is with Safari -> not even one polyfil worked for us. anyone knows what other companies that use LitElement and ShadowDOM with rich text editors do with Safari ?

itamarzil123 avatar Nov 01 '22 11:11 itamarzil123

In case anyone is still trying to solve this, here's what I have (seems to work ok in Chromium, Gecko, Webkit browsers, including copy/paste, selection, keyboard shortcuts, etc. Note: for Safari it requires > Safari 17 which has the new Selection API

All issues seemed to boil down to three problems:

  1. Getting Selection.Range: document.getSelection doesn't work in Native Shadow and each browser has a different implementation
  2. Checking Focus: document.activeElement needs to be replaced by shadowRoot.activeElement
  3. Setting Selection.Range: Selection.addRange does not work in Safari in Native Shadow but can be replaced with Selection.setBaseAndExtent

Here's a codepen example: https://codepen.io/John-Hefferman/pen/yLZygKo?editors=1000 Here's the monkeypatch:

   const hasShadowRootSelection = !!(document.createElement('div').attachShadow({ mode: 'open' }).getSelection);
    // Each browser engine has a different implementation for retrieving the Range
    const getNativeRange = (rootNode) => {
        try {
            if (hasShadowRootSelection) {
                // In Chromium, the shadow root has a getSelection function which returns the range
                return rootNode.getSelection().getRangeAt(0);
            } else {
                const selection = window.getSelection();
                if (selection.getComposedRanges) {
                    // Webkit range retrieval is done with getComposedRanges (see: https://bugs.webkit.org/show_bug.cgi?id=163921)
                    return selection.getComposedRanges(rootNode)[0];
                } else {
                    // Gecko implements the range API properly in Native Shadow: https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt
                    return selection.getRangeAt(0);
                }
            }
        } catch {
            return null;
        }
    }

    /** 
     * Original implementation uses document.active element which does not work in Native Shadow.
     * Replace document.activeElement with shadowRoot.activeElement
     **/
    quill.selection.hasFocus = function () {
        const rootNode = quill.root.getRootNode();
        return rootNode.activeElement === quill.root;
    }

    /** 
     * Original implementation uses document.getSelection which does not work in Native Shadow. 
     * Replace document.getSelection with shadow dom equivalent (different for each browser)
     **/
    quill.selection.getNativeRange = function () {
        const rootNode = quill.root.getRootNode();
        const nativeRange = getNativeRange(rootNode);
        return !!nativeRange ? quill.selection.normalizeNative(nativeRange) : null;
    };

    /**
     * Original implementation relies on Selection.addRange to programatically set the range, which does not work
     * in Webkit with Native Shadow. Selection.addRange works fine in Chromium and Gecko.
     **/
        quill.selection.setNativeRange = function (startNode, startOffset) {
            var endNode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : startNode;
            var endOffset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : startOffset;
            var force = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
            if (startNode != null && (quill.selection.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) {
                return;
            }
            var selection = document.getSelection();
            if (selection == null) return;
            if (startNode != null) {
                if (!quill.selection.hasFocus()) quill.selection.root.focus();
                var native = (quill.selection.getNativeRange() || {}).native;
                if (native == null || force || startNode !== native.startContainer || startOffset !== native.startOffset || endNode !== native.endContainer || endOffset !== native.endOffset) {
                    if (startNode.tagName == "BR") {
                        startOffset = [].indexOf.call(startNode.parentNode.childNodes, startNode);
                        startNode = startNode.parentNode;
                    }
                    if (endNode.tagName == "BR") {
                        endOffset = [].indexOf.call(endNode.parentNode.childNodes, endNode);
                        endNode = endNode.parentNode;
                    }
                    selection.setBaseAndExtent(startNode, startOffset, endNode, endOffset);
                }
            } else {
                selection.removeAllRanges();
                quill.selection.root.blur();
                document.body.focus();
            }
        }

    /**
     * Subscribe to selection change separately, because emitter in Quill doesn't catch this event in Shadow DOM
     **/
    const handleSelectionChange = function () {
        quill.selection.update();
    };

    document.addEventListener("selectionchange", handleSelectionChange);

jhefferman-sfdc avatar Oct 23 '23 20:10 jhefferman-sfdc

@jhefferman-sfdc Thank you for this! I went in circles for hours trying to find a fix for this and finally found your comment, and you wrote it only a couple days ago! Great timing. Thanks again.

vialoh avatar Oct 25 '23 09:10 vialoh