Search code examples
javascriptweb-componentnative-web-component

WYSIWYG editor web component


I try to create a WYSIWYG web component and I have problem defining a reusable function inside the web component class. The contents of my index.html file is the following:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>

    <cm-richtexteditor text=""></cm-richtexteditor>

    <br />

    <cm-richtexteditor text="<p>Aris</p>"></cm-richtexteditor>

    <script type="text/javascript">
        class CMRichTextEditor extends HTMLElement {
            constructor() {
                super();
                this.attachShadow({ mode: 'open' });
            }

            get text() {
                let value = this.getAttribute('text');
                if (!value || value == "")
                    value = "<p><br></p>";
                return value;
            }
            set text(val) { this.setAttribute('text', val); }

            static get observedAttributes() {
                return ['text'];
            }

            connectedCallback() {
                this.render();
            }

            render() {
                this.shadowRoot.innerHTML = `<style type="text/css">
    #componentContainer {
        border: 1px solid #cccccc;
    }

    #toolbar {
        padding: 4px;
    }

    #toolbar button {
        font-size: 12px;
        font-family: inherit;
        border: 1px solid #777777;
        background-color: white;
        padding: 5px;
        margin: 0 10px 0 0;
    }

    .activeIcon {
        background-color: #777777 !important;
        color: white !important;
    }

    #editorContainer {
        height: 300px;
        border-top: 1px solid #cccccc;
        outline: none;
        padding: 5px;
        overflow: auto;
    }

    #editorContainer img {
        max-width: 250px;
    }

    #editorContainer p {
        margin: 0;
    }
</style>

<div id="componentContainer">
    <div id="toolbar">
        <button id="boldButton" title="Bold">B</button>
        <button id="underlineButton" title="Underline">U</button>
        <button id="unorderedListButton" title="Bullet List">UL</button>
        <button id="numberedListButton" title="Numbered List">OL</button>
        <button id="imageButton" title="Picture">Picture</button>
    </div>
    <div id="editorContainer" contenteditable="true" spellcheck="false">${this.text}</div>
</div>`;

                document.execCommand('defaultParagraphSeparator', false, 'p');

                // Bold menu
                this.shadowRoot.querySelector('#boldButton').addEventListener('click', function () {
                    document.execCommand('bold');
                });

                // Underline menu
                this.shadowRoot.querySelector('#underlineButton').addEventListener('click', function () {
                    document.execCommand('underline');
                });

                // List menu
                this.shadowRoot.querySelector('#unorderedListButton').addEventListener('click', function () {
                    document.execCommand('insertUnorderedList');
                });

                // List menu
                this.shadowRoot.querySelector('#numberedListButton').addEventListener('click', function () {
                    document.execCommand('insertOrderedList');
                });

                // Picture menu
                this.shadowRoot.querySelector('#imageButton').addEventListener('click', function () {
                    document.execCommand('insertImage', false, 'http://usefulangle.com/img/posts/17-1px.jpg');
                });

                // Check menu options to be highlighted on keyup and click event
                this.shadowRoot.querySelector('#editorContainer').addEventListener('keyup', this.UpdateActiveStatus.bind(this));
                this.shadowRoot.querySelector('#editorContainer').addEventListener('click', this.UpdateActiveStatus.bind(this));
                this.shadowRoot.querySelector('#editorContainer').addEventListener('keyup', this.ContentsChanged.bind(this));
            }

            ContentsChanged() {
                // editor container
                var editorContainer = this.shadowRoot.querySelector('#editorContainer');

                // make sure that we have at least one paragraph tag
                var contents = editorContainer.innerHTML;
                if (contents == "") {
                    contents = "<p><br></p>";
                    editorContainer.innerHTML = contents;
                }

                // set the text property of the web component to contain the html that the user typed
                this.setAttribute('text', editorContainer.innerHTML);
            }


            UpdateActiveStatus() {
                // editor container
                var editorContainer = this.shadowRoot.querySelector('#editorContainer');

                // will hold format codes of all ranges
                var rangesFormats = [];

                var end_element, cur_element;

                // for all ranges
                for (var i = 0; i < this.shadowRoot.getSelection().rangeCount; i++) {
                    // Start container of range
                    var start_element = this.shadowRoot.getSelection().getRangeAt(i).startContainer;

                    // End container of range
                    end_element = this.shadowRoot.getSelection().getRangeAt(i).endContainer;

                    // Will hold parent tags of a range
                    var range_parent_tags = [];

                    // If starting node and final node are the same
                    if (start_element.isEqualNode(end_element)) {
                        // If the current element lies inside the editor container then don't consider the range
                        // This happens when editor container is clicked
                        if (editorContainer.isEqualNode(start_element)) {
                            rangesFormats.push([]);
                            continue;
                        }

                        cur_element = start_element.parentNode;

                        // Get all parent tags till editor container
                        while (!editorContainer.isEqualNode(cur_element)) {
                            range_parent_tags.push(cur_element.nodeName);
                            cur_element = cur_element.parentNode;
                        }
                    }

                    // Push tags of current range
                    rangesFormats.push(range_parent_tags);
                }

                // Find common formats for all ranges
                rangesFormats = rangesFormats.filter((item, index) => rangesFormats.indexOf(item) === index)[0];

                console.log(rangesFormats);

                // Activate or deactivate the toolbar icons
                if (rangesFormats.indexOf('B') != -1)
                    this.shadowRoot.querySelector("#boldButton").classList.add("activeIcon");
                else
                    this.shadowRoot.querySelector("#boldButton").classList.remove("activeIcon");

                if (rangesFormats.indexOf('U') != -1)
                    this.shadowRoot.querySelector("#underlineButton").classList.add("activeIcon");
                else
                    this.shadowRoot.querySelector("#underlineButton").classList.remove("activeIcon");

                if (rangesFormats.indexOf('UL') != -1)
                    this.shadowRoot.querySelector("#unorderedListButton").classList.add("activeIcon");
                else
                    this.shadowRoot.querySelector("#unorderedListButton").classList.remove("activeIcon");

                if (rangesFormats.indexOf('OL') != -1)
                    this.shadowRoot.querySelector("#numberedListButton").classList.add("activeIcon");
                else
                    this.shadowRoot.querySelector("#numberedListButton").classList.remove("activeIcon");
            }
        }

        customElements.define('cm-richtexteditor', CMRichTextEditor);
    </script>
</body>
</html>

This code has two problems. The first problem is that when I click one of the buttons it does not update the active state of the button. For example when I click the Bold button I have to call the UpdateActiveStatus function.

// Bold menu
this.shadowRoot.querySelector('#boldButton').addEventListener('click', function () {
    document.execCommand('bold');
    this.UpdateActiveStatus();
});

The problem is that I get the following error:

Uncaught TypeError: this.UpdateActiveStatus is not a function at HTMLButtonElement.<anonymous>

Does anyone knows what is the right way to call this function?

My second problem is that if I select some text in the first editor and click the bold button of the second editor this will have as a result to change the format of the first editor because this is the location of the page selected text.

Any possible solution would be appreciated.


Solution

  • I found how to solve my problem and I will write the solution just in case someone else has the same problem. Inside a web component we can call a function using the getRootNode method of the object so I changed the following line:

    this.UpdateActiveStatus();
    

    using the following code:

    this.getRootNode().host.UpdateActiveStatus();
    

    For my second problem I created the following function and I called it before I modify the contents of the editor.

    SelfFocus() {
        this.shadowRoot.querySelector('#editorContainer').focus();
    }