integration/cauldron/InspectorConceptBinding.js

/**
 *  InspectorConceptBinding - Allow inspection of Concepts in the inspector
 *  
 *  This code is licensed under the MIT License (MIT).
 *  
 *  Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the “Software”), to deal
 *  in the Software without restriction, including without limitation the rights 
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 *  copies of the Software, and to permit persons to whom the Software is 
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 *  THE SOFTWARE.
 *  
 */

/**
 *
 */
class InspectorConceptBinding {
    /**
     * Inspects the given TreeNode and if supported, returns a map of editable attributes
     * @param {TreeNode} treeNode
     * @returns {Cauldron.InspectorElement[]}
     */
    static inspect(treeNode) {
        let elements = [];
        let concept = null;
        let datastore = null;
       
        switch (treeNode.type){
            case "ConceptInstanceNode":
                concept = treeNode.context.concept;
                datastore = treeNode.context.datastore;

                let propertyEditors = new Cauldron.InspectorSegment("Properties", elements);
                elements.push(propertyEditors);
                for (const property of concept.properties.values()){
                    if (datastore.isPropertyMapped(concept, property) || property.isDerived()){
                        propertyEditors.push(new Cauldron.InspectorPropertyEditor(treeNode.context, property, treeNode.getTreeBrowser()));                
                    }
                }

                return elements;
                break;
            case "ConceptNode":
                concept = treeNode.context;
                
                if (concept.properties.size>0){
                    let propertiesView = new Cauldron.InspectorSegment("Properties", elements);
                    elements.push(propertiesView);
                    for (const property of concept.properties.values()){
                        propertiesView.push(new Cauldron.InspectorPropertyView(treeNode.context, property, treeNode.getTreeBrowser()));                
                    }
                }

                if (concept.behaviours.size>0){
                    let actionsView = new Cauldron.InspectorSegment("Actions", elements);
                    elements.push(actionsView);
                    for (const behaviour of concept.behaviours.values()){
                        actionsView.push(new Cauldron.InspectorBehaviourView(treeNode.context, behaviour, treeNode.getTreeBrowser()));                
                    }                
                }

                return elements;
                break;
            default:
                return null;
        } 
    }
}
window.Cauldron.InspectorConceptBinding = InspectorConceptBinding;
Cauldron.CauldronInspector.registerContentBinding(InspectorConceptBinding, 10);

class InspectorBehaviourView extends Cauldron.InspectorElement {
    constructor(concept, behaviour, treeBrowser) {
        super();
        
        let label = document.createElement("span");
        label.classList.add("cauldron-inspector-element-label");
        label.textContent = behaviour.name;

        let triggersCausingView = document.createElement("span");
        triggersCausingView.classList.add("cauldron-inspector-element-field");
        let triggerList = "";
        for (let triggerName of behaviour.triggers.values()){
            let trigger = concept.getTrigger(triggerName);
            let triggerNameReal = "'"+triggerName+"'";
            try {
                triggerNameReal = trigger.constructor.name;
            } catch (ex){}
            triggerList += "@"+triggerNameReal+" ";
        }        
        triggersCausingView.textContent = triggerList;

        // TODO: Jump to code link here

        this.html.appendChild(label);
        this.html.appendChild(triggersCausingView);
    }
}
window.Cauldron.InspectorBehaviourView = InspectorBehaviourView;

class InspectorPropertyView extends Cauldron.InspectorElement {
    constructor(concept, property, treeBrowser) {
        super();
        
        let label = document.createElement("span");
        label.classList.add("cauldron-inspector-element-label");
        label.textContent = property.name;

        let theType = document.createElement("span");
        theType.classList.add("cauldron-inspector-element-field");
        theType.textContent = property.type + (property.isDerived()?" (derived)":"");

        this.html.appendChild(label);
        this.html.appendChild(theType);
    }
}
window.Cauldron.InspectorPropertyView = InspectorPropertyView;



class InspectorPropertyEditor extends Cauldron.InspectorElement {
    /**
     *
     * @param {Element} domElement
     * @param {String} attrName
     * @param {String} overrideLabel
     */
    constructor(conceptInstance, property, browser) {
        super();
        this.browser = browser;

        let self = this;

        this.conceptInstance = conceptInstance;
        this.property = property;

        if(property.isDerived()) {
            this.readOnly = true;
        }

        if (property.type==="array"){
            this.editor = document.createElement("div");            
        } else if (property.type==="boolean"){
            this.editor = document.createElement("input");            
            this.editor.setAttribute("type", "checkbox");
        } else if (property.type==="number"){
            this.editor = document.createElement("input");
            this.editor.setAttribute("type", "number");
            this.editor.setAttribute("step", "1");
        } else {
            this.editor = document.createElement("input");
            this.editor.setAttribute("contenteditable", "true");
            this.editor.setAttribute("spellcheck", "false");
        }
        this.editor.classList.add("cauldron-inspector-element-editor");

        if(this.readOnly) {
            this.editor.setAttribute("disabled", true);
        }

        this.label = document.createElement("span");
        this.label.classList.add("cauldron-inspector-element-label");
        this.label.textContent = property.name;

        this.html.append(this.label);

        this.editorContainer = document.createElement("div");
        this.editorContainer.appendChild(this.editor);
        this.editorContainer.classList.add("cauldron-inspector-element-editor-container");
        this.editorContainer.classList.add("cauldron-inspector-element-field");

        this.html.appendChild(this.editorContainer);

        this.html.classList.add("inspector-property");
        
        this.html.addEventListener("click", ()=>{
            EventSystem.triggerEventAsync("Varv.DOMView.HighlightProperty", property);
        });        
        
        if(this.property.isConceptType()) {
            this.autocompleteDiv = document.createElement("div");
            this.autocompleteDiv.classList.add("cauldron-inspector-element-autocomplete");
            this.autocompleteDiv.classList.add("hidden");
            this.editorContainer.appendChild(this.autocompleteDiv);

            if(!this.readOnly) {
                this.editor.addEventListener("focus", () => {
                    self.autoComplete(self.editor.value);
                });
                document.addEventListener("pointerdown", (evt) => {
                    if (evt.target.closest(".cauldron-inspector-element-editor-container") !== self.editorContainer) {
                        self.autocompleteDiv.innerHTML = "";
                        self.autocompleteDiv.classList.add("hidden");
                    }
                });
            }
            
            // Add the click-to-locate feature
            if (property.type!=="array"){
                this.locatorContainer = document.createElement("span");
                this.editorContainer.append(this.locatorContainer);
            }            
        }
        
        this.html.addEventListener("keydown", (event)=>{
            if(event.code === "Enter") {
                event.preventDefault();
            }
        });                

        // Send changes to concept
        this.html.addEventListener("input", (evt)=>{
            if(self.property.isConceptType()) {
                self.autoComplete(self.editor.value);
            }

            self.persistValue();
        });

        // Fetch changes from concept
        this.valueUpdaterCallback = async function onFieldUpdate(uuid, value){
            let mark = VarvPerformance.start();
            if (uuid===self.conceptInstance.uuid){
                if (property.type==="array"){
                    self.editor.innerHTML="";
                    let value = await property.getValue(conceptInstance.uuid);
                    for(let i = 0; i < value.length; i++){ 
                        let entry = value[i];
                        let entryElement = document.createElement("div");
                        entryElement.style.display = "flex";
                        entryElement.style.alignItems = "center";

                        if (!property.isDerived()){
                            let deleter = IconRegistry.createIcon("mdc:delete");
                            deleter.style.cursor = "pointer";
                            deleter.style.flex = "1 1 0%";
                            deleter.style.fontSize = "1.5em";
                            deleter.addEventListener("click", ()=>{
                                // Delete this entry
                                value.splice(i, 1);
                                property.setValue(conceptInstance.uuid, value); 
                            });    
                            entryElement.appendChild(deleter);
                        }
                                                
                        let theText = document.createElement("div");
                        theText.style.flex = "1 1 100%";
                        theText.innerText = entry;
                        entryElement.appendChild(theText);
                        
                        
                        if (property.isConceptArrayType()){
                            let link = self.getConceptLink(entry);
                            link.style.flex = "1 1 0%";
                            entryElement.appendChild(link);
                        }
                        
                        self.editor.appendChild(entryElement);                        
                    }   
                    
                    if (!property.isDerived()){
                        let adder = document.createElement("button");
                        adder.innerText = "Add Entry";
                        adder.addEventListener("click", ()=>{
                            let newValue = prompt ("Value to add", "");
                            newValue = self.convertArrayItem(newValue, property.options.items);
                            value.push(newValue);
                            property.setValue(conceptInstance.uuid, value);
                        });                    
                        self.editor.appendChild(adder);
                    }
                } else if (property.type==="boolean"){
                    self.editor.checked = value;
                } else {
                    self.editor.value = value;
                    
                    if (property.isConceptType()){
                        self.updateLocator();
                    }
                }
            }
            VarvPerformance.stop("InspectorConceptBinding.valueUpdaterCallback", mark);
        };

        let possiblePromiseOrValue = property.getValue(conceptInstance.uuid);
        if(possiblePromiseOrValue instanceof Promise) {
            possiblePromiseOrValue.then((value)=>{
                self.valueUpdaterCallback(self.conceptInstance.uuid, value);
            });
        } else {
            self.valueUpdaterCallback(self.conceptInstance.uuid, possiblePromiseOrValue);
        }
        property.addUpdatedCallback(this.valueUpdaterCallback);
    }

    convertArrayItem(value, itemsType){
        if (itemsType === 'number')
            return parseFloat(value);
        
        if (itemsType === "boolean")
            return value === "true";
        
        return value;
    }

    updateLocator() {
        this.locatorContainer.innerHTML = "";
        this.locatorContainer.appendChild(this.getConceptLink(this.editor.value));
    }

    persistValue() {
        try {
            let value = null;
            if (this.property.type==="boolean"){
                value = this.editor.checked;
            } else {
                value = this.property.typeCast(this.editor.value);
                if (this.property.isConceptType()){
                    this.updateLocator();
                }
            }
            this.property.setValue(this.conceptInstance.uuid, value);

            //What is this method?
            this.setFailing(false);
        } catch (ex){
            this.setFailing(true);
        };
    }

    async autoComplete() {
        const self = this;

        let allUUIDS = await VarvEngine.getAllUUIDsFromType(this.property.type, true);

        this.autocompleteDiv.innerHTML = "";

        let ul = document.createElement("ul");

        allUUIDS.forEach((uuid)=>{
            let li = document.createElement("li");
            li.textContent = uuid;
            ul.appendChild(li);

            li.addEventListener("mouseenter", ()=>{
                EventSystem.triggerEventAsync("Varv.DOMView.HighlightInstance", uuid);
            });

            li.addEventListener("mouseleave", ()=>{
                EventSystem.triggerEventAsync("Varv.DOMView.ClearHighlights");
            });

            li.addEventListener("click", ()=>{
                self.editor.value = uuid;
                self.autocompleteDiv.classList.add("hidden");
                self.persistValue();
            });
        });

        this.autocompleteDiv.appendChild(ul);
        this.autocompleteDiv.classList.remove("hidden");
    }

    getConceptLink(uuid){
        let linker = IconRegistry.createIcon("mdc:gps_fixed");
        linker.style.cursor = "pointer";
        linker.addEventListener("click", ()=>{
            let treeNodes = this.browser.findTreeNode(uuid);
            if(treeNodes.length > 0) {
                let treeNode = treeNodes[0];
                treeNode.reveal();
                treeNode.select();
            }            
        });        
        return linker;
    }

    destroy() {
        super.destroy();
        this.property.removeUpdatedCallback(this.valueUpdaterCallback);
    }
    
    focus(){
        this.editor.select();
    }
}

window.Cauldron.InspectorPropertyEditor = InspectorPropertyEditor;
 


EventSystem.registerEventCallback("TreeBrowser.Selection", ({detail: {selection: selection}})=>{
    if (selection.context && selection.context instanceof Concept){
        let concept = selection.context;
        EventSystem.triggerEventAsync("Varv.DOMView.HighlightConcept", concept);
    } else if (selection.context && selection.context.uuid) {        
        EventSystem.triggerEventAsync("Varv.DOMView.HighlightInstance", selection.context.uuid);
    } else {
        EventSystem.triggerEventAsync("Varv.DOMView.ClearHighlights");
    }
});