quill icon indicating copy to clipboard operation
quill copied to clipboard

Support Shadow DOM v1

Open ergo opened this issue 7 years ago • 20 comments

Hello,

https://github.com/quilljs/quill/issues/1472 can we get this issue reopened or at least have a new one on the subject?

The stable spec is over a year old, chrome, safari and other webkit based browsers have full support, firefox nightly ships Shadow Dom behind a flag already and stable will probably land with support at the end of march.

ergo avatar Mar 14 '18 21:03 ergo

@jhchen Shadow DOM is becoming reality and many frameworks are taking advantage of it. I think it would be a great feature for 2.0 version of the modern Quill editor. Do you think version 2.0 will support it?

olegomon avatar May 07 '18 13:05 olegomon

Any news on this?

kaseyhinton avatar May 29 '18 16:05 kaseyhinton

bump. PLEASE add shadow dom support. this editor is so nice.

chwzr avatar Aug 05 '18 15:08 chwzr

Any news on this? GitHub is also using this, https://githubengineering.com/removing-jquery-from-github-frontend/. Firefox beta already has web components enabled, next stable will have them on. Chrome and Safari both desktop and mobile support them for a year now.

ergo avatar Sep 07 '18 16:09 ergo

Hello, I'm revisiting quill after about 4 months after not being able to use it due to this issue. Is it actively being worked on or is it simply not realistic...

kr05 avatar Apr 02 '19 18:04 kr05

We've now reached 88.66% global browser support for Shadow DOM (V1). We can ignore IE or Edge at this point, since libs like Lit are poly-filling and use Light DOM. Any updates or plans to implement support for 2019?

raphaelrauwolf avatar Jul 16 '19 16:07 raphaelrauwolf

This would be really helpful! I love quill but we can't really use it today in our Polymer/LitElement application... :(

ronnyroeller avatar Sep 26 '19 15:09 ronnyroeller

@ronnyroeller Try this fork https://github.com/web-padawan/quill/tree/shadow I've been using it with lit-element and shadow dom for a few months now

kjantzer avatar Sep 26 '19 15:09 kjantzer

Interesting, @web-padawan - any chance this can get merged back into master? Maybe now when Edge is also Chromium based it will get accepted.

ergo avatar Jan 25 '20 20:01 ergo

I have quill working in shadowdom almost perfectly without any changes to quill itself, however I have an issue in Firefox where the cursor will not display in the quill editor, but does know my cursors position. If I change tabs and tab back to my app the cursor will become visible. the same happens if I click into the inspector then back into the quill editor.

Arosner avatar Feb 20 '20 23:02 Arosner

If I change tabs and tab back to my app the cursor will become visible. the same happens if I click into the inspector then back into the quill editor.

how? do you have demo?

aanavaneeth avatar Feb 25 '20 15:02 aanavaneeth

@aanavaneeth Demo of quill almost working in shadow Dom or the tabbing thing? I'm not sure why the tabbing thing happens the way it does and am currently trying to fix that. Only happens in Firefox.

Regardless this weekend I can make a simple example repo and link it here. The hill I am dying on is this Firefox issue though.

Arosner avatar Feb 28 '20 18:02 Arosner

Demo of working in shadow DOM. Frankly, for now I am ok if it doesn't work perfectly in FF but hoping for it i n near future.

aanavaneeth avatar Mar 01 '20 11:03 aanavaneeth

I'm using the following polyfill workaround if it helps anyone, it is not perfect, but makes the editor fairly usable in Firefox.

It also has a small memory leak because of the event listener in the end that is not cleaned up if the quill instance is thrown away.

It is a mix of the shadow-selection-polyfill npm package and a script from stackoverflow (link in code comment):

import { getRange } from 'shadow-selection-polyfill';

// Shadow DOM fix based on https://stackoverflow.com/questions/67914657/quill-editor-inside-shadow-dom/67944380#67944380
export function quillSelectionFix(quill) {
  const normalizeNative = (nativeRange) => {

    // document.getSelection model has properties startContainer and endContainer
    // shadow.getSelection model has baseNode and focusNode
    // Unify formats to always look like document.getSelection

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

  // Hack Quill and replace document.getSelection with shadow.getSelection

  // eslint-disable-next-line no-param-reassign
  quill.selection.getNativeRange = () => {
    const dom = quill.root.getRootNode();
    // const selection = dom.getSelection instanceof Function ? dom.getSelection() : document.getSelection();
    const selection = getRange(dom);
    const range = normalizeNative(selection);

    return range;
  };

  // Subscribe to selection change separately,
  // because emitter in Quill doesn't catch this event in Shadow DOM

  document.addEventListener("selectionchange", () => {
    // Update selection and some other properties
    quill.selection.update();
  });
}

tirithen avatar Jun 18 '21 14:06 tirithen

@tirithen with this approach, there is a problem of copy/pasting in chrome

hojjat-afsharan avatar Mar 22 '23 07:03 hojjat-afsharan

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

Any news on this issue?

varminas avatar Nov 20 '23 12:11 varminas

@luin Now that Quill 2.0 has released, are we interested in a PR for this based on @jhefferman-sfdc's snippet above? I can submit one if so.

I've tested their changes on v2 within a shadow root and its working very well, so it shows promise.

justinbhopper avatar Apr 17 '24 15:04 justinbhopper

Quill 2.0.0 has same bugs with Web components: https://github.com/EasyWebApp/quill-cell/pull/1/commits/8bf3c6d8da477c71f6cbb5be1140460141b30f33

Reproduce

  1. open the test page code in Cloud IDE: https://gitpod.io/?autostart=true#https://github.com/EasyWebApp/quill-cell/pull/1/commits/8bf3c6d8da477c71f6cbb5be1140460141b30f33

  2. run pnpm i && npm start to open the test page

TechQuery avatar Apr 25 '24 13:04 TechQuery

Please note that the code snippet above isn't really the only thing that should be applied. This part also need an update:

https://github.com/quilljs/quill/blob/6590aa45ac6a60a64b59bccf7badba9667692f61/packages/quill/src/core/emitter.ts#L10

See also #1805 where shadow DOM support was originally prototyped for some other places where getRootNode() might be used instead of document (it's mostly about activeElement and getSelection() calls though).

web-padawan avatar Apr 25 '24 13:04 web-padawan