Quill editor inside Shadow DOM

1.8k Views Asked by At

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.

3

There are 3 best solutions below

2
On BEST ANSWER

Done.

This fix doesn't cover missing shadow.getSelection in Safari but all other browsers should work. In my case, I needed only Chrome.

var quill = new Quill(editorControl, {
  modules: {
    toolbar: [
      [{ header: [1, 2, false] }],
      ['bold', 'italic', 'underline'],
      ['image', 'code-block']
    ]
  },
  placeholder: 'Compose an epic...',
  theme: 'snow' 
});

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 

quill.selection.getNativeRange = () => {
  
  const dom = quill.root.getRootNode();
  const selection = dom.getSelection();
  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", (...args) => {

  // Update selection and some other properties

  quill.selection.update()
});
0
On

Full HTML file

<html>
        <head>
                <meta charset="utf-8">
                <title>CKEditor</title>
                <script type="text/javascript" src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
        </head>
        <body>
                <div class="container" name="NewEditor"></div>

        <script>
                let container = document.querySelector(".container");
                let shadow = container.attachShadow({ mode: 'open' });
                const ele = document.createElement('div');
                console.log(shadow.getRootNode())
                shadow.innerHTML = `<link href="https://cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet" />
                <link rel="stylesheet" type="text/css" href="https://cdn.quilljs.com/1.3.6/quill.bubble.css">
                `;
                shadow.appendChild(ele);
                var quill = new Quill(ele, {
                  "modules": {
                    "toolbar": [
                        [
                            "bold",
                            "italic",
                            "underline",
                            "strike",
                            "blockquote",
                            "code-block",
                            {
                                "list": "ordered"
                            },
                            {
                                "list": "bullet"
                            },
                            {
                                "script": "sub"
                            },
                            {
                                "script": "super"
                            },
                            {
                                "indent": -1
                            },
                            {
                                "indent": "+1"
                            },
                            {
                                "direction": "rtl"
                            },
                            {
                                "size": [
                                    "small",
                                    false,
                                    "large",
                                    "huge"
                                ]
                            },
                            {
                                "color": []
                            },
                            {
                                "background": []
                            },
                            {
                                "font": []
                            },
                            {
                                "align": []
                            },
                            "clean",
                            "link",
                            "image",
                            "video"
                        ]
                    ]
                  },
                  "placeholder": "",
                    "readOnly": false,
                    "theme": "snow",
                    "value": {
                        "ops": [
                            {
                                "attributes": {
                                    "bold": true
                                },
                                "insert": "Bold"
                            }
                        ]
                    }
                });
                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
                };
                quill.selection.getNativeRange = () => {
                  const dom = quill.root.getRootNode();
                  const selection = dom.getSelection();
                  const range =  normalizeNative(selection);
                  // if(selection.focusOffset !== selection.baseOffset){
                  //       range.length = 0;
                  //       document.activeElement.shadowRoot.querySelector('.ql-editor').focus();
                  // }
                  return range;
                };
                document.addEventListener("selectionchange", (...args) => {
                  quill.selection.update()
                });
        </script>
        </body>
</html>

But still 'bubble' theme not working properly. Getting document.activeElement problem

Error Line in this iamge

0
On

I just implemented a nice approach in my WebComponent - use a iframe tag in your ShadowDOM, and load another HTML page with Quill.