slab / quill

Quill is a modern WYSIWYG editor built for compatibility and extensibility
https://quilljs.com
BSD 3-Clause "New" or "Revised" License
42.35k stars 3.32k forks source link

Support Shadow DOM v1 #2021

Open ergo opened 6 years ago

ergo commented 6 years ago

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.

olegomon commented 6 years ago

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

kaseyhinton commented 6 years ago

Any news on this?

chwzr commented 5 years ago

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

ergo commented 5 years ago

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.

kr05 commented 5 years ago

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

raphaelrauwolf commented 4 years ago

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?

ronnyroeller commented 4 years ago

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

kjantzer commented 4 years ago

@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

ergo commented 4 years ago

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

Arosner commented 4 years ago

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.

aanavaneeth commented 4 years ago

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?

Arosner commented 4 years ago

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

aanavaneeth commented 4 years ago

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.

tirithen commented 3 years ago

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();
  });
}
hojjat-afsharan commented 1 year ago

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

jhefferman-sfdc commented 8 months ago

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);
varminas commented 7 months ago

Any news on this issue?

justinbhopper commented 2 months ago

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

TechQuery commented 2 months ago

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

web-padawan commented 2 months ago

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