One, foreword

If you’re often bothered by global CSS contamination while working with a rich text editor, and you’ve learned about the Shadow DOM at this point, you’ll probably have a cute idea for putting rich text in the Shadow DOM.

Shadow DOM and shadowRoot

Before we get started, we need to understand what shadow DOM is. Shadowdom mount point ShadowRoot.

I briefly summarize some of the features of shadow that I know:

1. ShadowRoot is equivalent to a neutered Document, but it has a separate CSS scope and no separate JS scope

What about a separate CSS domain? This is equivalent to setting the ALL: Initial CSS style for all elements in the Shadow DOM. The style of the internal element is the browser’s default, unmodified, and completely unaffected by the external style.

No separate JS field? ShadowRoot and Document access the same window object, and shadowRoot does not support the form of for JS import.

2. The MutationObserver cannot monitor shadow DOM changes

For example, we define a custom element < IS-editable > and enable shadowRoot. As long as < IS-editable > itself does not change, the MutationObserver will not be triggered regardless of how the shadow DOM inside it changes. Just like < Video Controls >, no matter how much you drag the progress bar, it will not trigger MutationObserver.

If you want to view shadow DOM for video, you can open the console, then open Settings, and check Show User Agent Shadow DOM.

Of course, by not listening, I mean that you can’t listen externally, but if you can just take elements from the Shadow DOM and bind them to the MutationObserver, you can also listen for changes

The test code

<! DOCTYPEhtml>
<html>
    <head>
        <meta charset="utf-8">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js" type="text/javascript" charset="utf-8"></script>
    </head>
    <body>
        <div class="demo">
            <h3>Edit area in shadowRoot</h3>
            <is-editable></is-editable>
            <h3>. Edit area in demo</h3>
        </div>
        
        <! Create a shadow DOM template using <template>
        <template id="tpl">
            <style type="text/css">
                :host {
                    display: inline-block;
                    width: 100%;
                    margin: 0 0 15px 0;
                }
                .editable {
                    border: 1px solid # 999999;
                    box-sizing: border-box;
                    padding: 5px 10px;
                }
                .editable:focus {
                    outline: none;
                }
            </style>
            <div class="editable" contenteditable="true">
                <h3>Title 3,<code>h3</code></h3>
                <p>Common text,<b>bold</b>.<i>italics</i></p>
                <p>Common text,<b>bold</b>.<i>italics</i></p>
            </div>
        </template>

        <script type="text/javascript">
            const tpl = document.querySelector('#tpl')
            const demo = document.querySelector('.demo')
            demo.append(tpl.cloneNode(true).content)
            
            MObserver.observeAll('.demo'.function() {
                console.log('External MutationObserver listening');
            })
        </script>

        <script type="text/javascript">
            customElements.define('is-editable'.class EditableElement extends HTMLElement {
                constructor() {
                    super(a)this.attachShadow({ mode: "open" })
                    // Find the 
       
                    const tpl = document.querySelector('#tpl')
                    // Clone the template and add the template contents to the current shadowRoot
                    this.shadowRoot.append(tpl.cloneNode(true).content)
                }

                mutationCallback() {
                    console.log('Internal MutationObserver listening');
                }

                // called when the custom element is first connected to the document DOM
                connectedCallback() {
                    const target = this.shadowRoot.querySelector('.editable')
                    MObserver.observeAll(target, this.mutationCallback)
                }

                // called when the custom element is disconnected from the document DOM
                disconnectedCallback() {
                    const target = this.shadowRoot.querySelector('.editable')
                    MObserver.remove(target, this.mutationCallback)
                }
            })
        </script>
    </body>
</html>
Copy the code

The test results

3. CSS loading

As mentioned in the previous article, shadowRoot has an independent CSS scope, so we need to manage the dependency of the CSS related to shadow DOM under each custom element.

For example, we need to package KateX as a custom element, so its JS part can be packaged directly with Webpack or imported directly in document with CDN, while its CSS part needs to be managed manually

Katex sample

<! DOCTYPEhtml>
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js" type="text/javascript" charset="utf-8"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" integrity="sha384-pK1WpvzWVBQiP0/GjnvRxV4mOb0oxFuyRxJlk6vVw146n3egcN5C925NCP7a7BY8" crossorigin="anonymous"></script>
</head>
<body>
    <template id="tpl">
        <! -- <link> Introduce CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-RZU/ijkSsFbcmivfdRBQDtwuwVqK7GMOw6IMvKyeWL2K5UAlyp6WonmB8m7Jd0Hn" crossorigin="anonymous">
        <div class="root"></div>
    </template>

    <script>
        customElements.define('is-katex'.class KatexElement extends HTMLElement {
            constructor() {
                super(a)this.attachShadow({ mode: "open" })
                const tpl = document.querySelector('#tpl')
                this.shadowRoot.append(tpl.cloneNode(true).content)
            }

            render() {
                const text = this.getAttribute('data-text')
                katex.render(text, this.shadowRoot.querySelector('.root'))}// called when the custom element is first connected to the document DOM
            connectedCallback() {
                this.render()
                MObserver.attributeFilter(this.this.render, ['data-text'])}// called when the custom element is disconnected from the document DOM
            disconnectedCallback() {
                MObserver.remove(this.this.render)
            }
        })
    </script>
</body>
</html>
Copy the code

If the rich text editor considered the extensibility of the plug-in design, then we can not simply introduce the way will not meet the requirements, so we need to rely on CSS for a set of systematic management.

In previous attempts, a simple CSS dependency loading mechanism was designed for this purpose. If you are interested, you can look at the source code. The detailed explanation is a bit long here

register.ts

gcss.ts

mount-gcss.ts

At the same time, it is worth noting that if you are using a dependent library under shadowRoot, its CSS is in js, not a separate.css file, then you can look for a replacement library. Because this library loads CSS directly into the document, and you can’t get it!

Compatibility processing selection

Still don’t know Selection? Look here

If your rich text editor relies on browser selection support, you can’t escape this part!

In shadowRoot of Chromium kernel browser, we can’t get valid selection through document.getSelectin or window.getSelection, We can only get a valid selection through Shadowroot. getSelection.

But on other browsers, whether in Document or shadowRoot, we must use document.getSelectin or window.getSelection to get the current valid selection.

The test code

<! DOCTYPEhtml>
<html>
    <body>
        <is-editable></is-editable>
        
        <script type="text/javascript">
            customElements.define('is-editable'.class EditableElement extends HTMLElement {
                constructor() {
                    super(a)const shadow = this.attachShadow({ mode: 'open'})
                    shadow.innerHTML = ` < div class = "root" > < p > < / I > < I > computer separated the < del > points according to the library to see Christmas < del > began to lose weight < / u > < u > separated band, pulls to see < / p > < P > Fitness card points  interface data feedback to the world of  excuses up to determine whether the 

tax cut points start

'
}})document.addEventListener('mouseup'.function() { // Use document.getSelection to get the selection console.log(`document.getSelection().getRangeAt(0)`); console.log(document.getSelection().getRangeAt(0)); // Use shadowroot.getSelection to get the selection const el = document.querySelector('is-editable') console.log(`el.shadowRoot.getSelection().getRangeAt(0)`); console.log(el.shadowRoot.getSelection().getRangeAt(0)); })
</script> </body> </html> Copy the code

The test results

conclusion

So much for the previous research, you’ll have to go into the details yourself.