inspector-component/content-bindings/inspector-htmlnode/InspectorHTMLBinding.js

/**
 *  Inspector HTML Bindings
 *  Visual inspector of HTML and DOM elements
 * 
 *  Copyright 2020, 2021 Rolf Bagge, Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *    
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
**/
    
/**
 * Visual inspector of HTML and DOM elements
 * @type type
 */
class InspectorHTMLBinding {
    /**
     * Inspects the given TreeNode and if supported, returns a map of editable attributes
     * @param {TreeNode} treeNode
     * @returns {Cauldron.InspectorElement[]}
     */
    static inspect(treeNode, inspector) {
        if(treeNode.type === "DomTreeNode") {
            let elements = [];

            InspectorHTMLBinding.focusEditor = new Cauldron.InspectorAttributeEditor(treeNode.context, "id");
            
            let primaryFold = new Cauldron.InspectorSegment("Attributes", elements);
            elements.push(primaryFold);
            primaryFold.push(InspectorHTMLBinding.focusEditor);
            let primaryAttributes = ["class", "name"];
            
            switch (treeNode.context.tagName){
                case "IMG":
                    primaryAttributes = [...primaryAttributes, "src", "alt", "width", "height"];
                    break;
                case "SCRIPT":
                    primaryAttributes = [...primaryAttributes, "src", "type"];
                    break;
                case "IFRAME":
                    primaryAttributes.push("src");
                    break;
                case "A":
                    primaryAttributes.push("href");
                    break;
                case "INPUT":
                    primaryAttributes = [...primaryAttributes, "type", "value"];
                    break;
                case "OPTION":
                    primaryAttributes.push("value");
                    break;
                case "LINK":
                    primaryAttributes = [...primaryAttributes, "type", "rel", "href"];
                    break;
                case "LABEL":
                    primaryAttributes.push("for");
                    break;
                case "BUTTON":
                    primaryAttributes.push("type");
                    break;
                case "FORM":
                    primaryAttributes = [...primaryAttributes, "action", "method"];
                    break;
            }                        
            for (let attribute of primaryAttributes){
                primaryFold.push(new Cauldron.InspectorAttributeEditor(treeNode.context, attribute));                
            }
            primaryAttributes.push("id"); // We already added this manually

            // Add other attributes present which are not primary ones
            let fold = new Cauldron.InspectorSegment("Additional Attributes", elements);
            elements.push(fold);
            for (let attributeEntry of treeNode.context.attributes){
                if (!primaryAttributes.includes(attributeEntry.name)){
                    fold.push(new Cauldron.InspectorAttributeEditor(treeNode.context, attributeEntry.name));                
                }
            }
            fold.push(new Cauldron.InspectorAttributeAdder(treeNode.context, inspector));
            
            return elements;
        }

        return null;
    }
}

window.Cauldron.InspectorHTMLBinding = InspectorHTMLBinding;

Cauldron.CauldronInspector.registerContentBinding(InspectorHTMLBinding, 10);

class InspectorAttributeAdder extends Cauldron.InspectorElement {
    constructor(domElement, inspector){
        super();
        let label = document.createElement("label");
        label.classList.add("cauldron-inspector-element-label");        
        let adderButton = document.createElement("button");
        adderButton.innerText = "Add Attribute";
        adderButton.classList.add("cauldron-inspector-element-editor");
        this.html.append(label);
        this.html.append(adderButton);
        
        adderButton.addEventListener("click", ()=>{
            let attributeName = prompt("Attribute Name:");
            if (attributeName !== null && attributeName!==""){
                domElement.setAttribute(attributeName,"");
                inspector.reinspect();
            }
        });
    }
}
window.Cauldron.InspectorAttributeAdder = InspectorAttributeAdder;


class InspectorAttributeEditor extends Cauldron.InspectorElement {
    /**
     *
     * @param {Element} domElement
     * @param {String} attrName
     * @param {String} overrideLabel
     */
    constructor(domElement, attrName, overrideLabel= null) {
        super();

        let self = this;

        this.domElement = domElement;
        this.attrName = attrName;

        this.editor = document.createElement("input");
        this.editor.classList.add("cauldron-inspector-element-field");
        this.editor.classList.add("cauldron-inspector-element-editor");
        this.editor.setAttribute("contenteditable", "true");
        this.editor.setAttribute("spellcheck", "false");

        this.label = document.createElement("span");
        this.label.classList.add("cauldron-inspector-element-label");
        this.label.textContent = overrideLabel==null?this.attrName:overrideLabel;

        this.html.append(this.label);
        this.html.appendChild(this.editor);
        this.html.classList.add("inspector-htmlnode");

        this.editor.value = this.domElement.getAttribute(this.attrName);

        this.observer = new MutationObserver((mutations)=>{
            //handleMutations(mutations);
        });

        function handleMutations(mutations) {
            //Attribute changed, update editor
            if(mutations.length > 0) {
                self.editor.value = self.domElement.getAttribute(self.attrName);
            }
        }

        function startObserver() {
            self.observer.observe(self.domElement, {
                attributes: true,
                attributeFilter: [self.attrName]
            });
        }

        function pauseObserver() {
            let mutationRecords = self.observer.takeRecords();
            self.observer.disconnect();
            handleMutations(mutationRecords);
        }

        startObserver();

        this.html.addEventListener("keydown", (event)=>{
            if(event.code === "Enter") {
                event.preventDefault();
            }
        });

        this.html.addEventListener("input", (evt)=>{
            pauseObserver();
            self.domElement.setAttribute(self.attrName, self.editor.value);
            setTimeout(()=>{
                startObserver();
            }, 0);
        });
    }

    destroy() {
        super.destroy();
        this.observer.disconnect();
    }
    
    focus(){
        this.editor.select();
    }
}

window.Cauldron.InspectorAttributeEditor = InspectorAttributeEditor;

EventSystem.registerEventCallback("TreeBrowser.Keyup", ({detail: {evt: evt, treeNode: treeNode}})=>{
    console.log(evt);
    if(evt.key === "F2" && InspectorHTMLBinding.focusEditor) {
        InspectorHTMLBinding.focusEditor.focus();
    }
});