Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shadow DOM support - continued #2961

Closed
yananym opened this issue Mar 10, 2020 · 34 comments
Closed

Shadow DOM support - continued #2961

yananym opened this issue Mar 10, 2020 · 34 comments

Comments

@yananym
Copy link

yananym commented Mar 10, 2020

Follow up of issues #2021 and #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!

@mike-shtil
Copy link

@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

@albert-sf
Copy link

albert-sf commented Mar 23, 2020

Any update on this issue?

@aanavaneeth
Copy link

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

@kr05
Copy link

kr05 commented Apr 3, 2020

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?

@yananym
Copy link
Author

yananym commented Apr 3, 2020

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

@kr05
Copy link

kr05 commented Apr 3, 2020

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

@nguyen-van-quang
Copy link

@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

@kr05
Copy link

kr05 commented Apr 3, 2020

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

@albert-sf
Copy link

albert-sf commented Apr 8, 2020

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

@turboMaCk
Copy link

turboMaCk commented Feb 22, 2021

@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
Copy link

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

@dman777
Copy link

dman777 commented Mar 4, 2021

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

@turboMaCk
Copy link

@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.

@ghost
Copy link

ghost commented Mar 9, 2021

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.

@turboMaCk
Copy link

@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.

@ghost
Copy link

ghost commented Mar 9, 2021 via email

@turboMaCk
Copy link

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.

@ghost
Copy link

ghost commented Mar 9, 2021

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'});
	}
});

@turboMaCk
Copy link

turboMaCk commented Mar 9, 2021

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).

@ghost
Copy link

ghost commented Mar 9, 2021

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.

@artemiusgreat
Copy link

artemiusgreat commented Jun 10, 2021

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
Copy link

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

@littleweb
Copy link

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

thank you very mush!

@robrez
Copy link

robrez commented Aug 26, 2021

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
Copy link

robrez commented Jan 7, 2022

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

@littleweb
Copy link

@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.

@C0ncept95
Copy link

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?

@itamarzil123
Copy link

itamarzil123 commented Nov 1, 2022

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 #2961 (comment)
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 ?

@jhefferman-sfdc
Copy link

jhefferman-sfdc commented Oct 23, 2023

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);

@vialoh
Copy link

vialoh commented Oct 25, 2023

@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.

@turboMaCk
Copy link

I wonder if this solution can't be turned into patch that could get accepted. cc @dlitsman how do you feel about @jhefferman-sfdc's solution?

@jhefferman-sfdc
Copy link

If folks are generally OK with this approach, I'd be happy to rework it so it functions in Light DOM too and submit a PR for review? Let me know.

@Thilo84
Copy link

Thilo84 commented Dec 20, 2023

@jhefferman-sfdc I'm very happy with your code. This fixed our problem too. Thanks a lot!

@quill-bot
Copy link

Quill 2.0 has been released (announcement post) with many changes and fixes. If this is still an issue please create a new issue after reviewing our updated Contributing guide 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests