Source: xxe/part/NodePath.js

/**
 * {@link XMLEditor} part used to display the full path of explicitly or
 * implicitly selected node.
 */
class NodePath extends HTMLElement {
    constructor() {
        super();

        this._xmlEditor = null;
        this._onNodeClicked = this.onNodeClicked.bind(this);
    }

    // -----------------------------------------------------------------------
    // Event handlers
    // -----------------------------------------------------------------------

    onNodeClicked(event) {
        XUI.Util.consumeEvent(event);

        if (this._xmlEditor === null || // Should not happen.
            (event.type === "click" && event.detail !== 1)) { // Click count.
            return;
        }
        let docView = this._xmlEditor.documentView;

        let uid = event.target.getAttribute("data-uid");
        if (uid) {
            let view = docView.getNodeView(uid, /*reportError*/ false);
            if (view !== null) {
                let selectItem;
                if (!Object.is(docView.selected, view) ||
                    docView.selected2 !== null) {
                    selectItem = docView.selectNode(view, /*show*/ true);
                } else {
                    selectItem = Promise.resolve(true);
                }

                if (event.type === "contextmenu") {
                    const where = event.target.getBoundingClientRect();
                    selectItem.then((selected) => {
                        if (selected) {
                            let cmd = docView.getCommand("contextualMenu");
                            if (cmd) {
                                cmd.showContextualMenu(docView, where.left,
                                                       where.bottom);
                            }
                        }
                    });
                } else { // It's a click event.
                    if (event.shiftKey) {
                        selectItem.then((selected) => {
                            if (selected) {
                                docView.executeCommand(EXECUTE_NORMAL,
                                                       "selectNode",
                                                       "children");
                            }
                        });
                    }
                }
            }
        }
    }
    
    // -----------------------------------------------------------------------
    // Used by XMLEditor
    // -----------------------------------------------------------------------

    set xmlEditor(editor) {
        this._xmlEditor = editor;
    }
    
    nodePathChanged(items) {
        XUI.Util.removeAllChildren(this);
        if (items === null) {
            // No document is being edited.
            return;
        }

        this.appendChild(this.createDocumentSegment(this._xmlEditor));
        
        let seg = null;
        const count = items.length;
        for (let i = 0; i < count; i += 3) {
            let uid = items[i];
            let tag = items[i+1];
            let info = items[i+2];
            
            this.appendChild(this.createSegmentSeparator());
            
            seg = this.createNodeSegment(uid, tag, info);
            this.appendChild(seg);
        }

        if (seg !== null) {
            seg.scrollIntoView({ block: "center", inline: "end" });
        }
    }

    createDocumentSegment(editor) {
        let seg = document.createElement("span");
        seg.textContent = XUI.StockIcon["doc"];
        this.updateDocumentSegment(seg, editor);
        
        return seg;
    }

    updateDocumentSegment(seg, editor) {
        let classNames = "xui-small-icon xxe-ndpth-doc";
        let tooltip = "Location:\n" + editor.documentURI;
        
        if (editor.readOnlyDocument) {
            classNames += " xxe-ndpth-doc-ro";
            tooltip += "\n(read-only)";
        } else if (editor.saveNeeded) {
            classNames += " xxe-ndpth-doc-mod";
            tooltip += "\n(modified)";
        }

        if (editor.configurationName !== null) {
            tooltip += "\nConfiguration: " + editor.configurationName;
        }

        if (editor.diffSupport >= 1) {
            tooltip += "\nComparison of revisions: enabled.";
            if (editor.diffSupport >= 2) {
                tooltip += " All revisions stored in the document.";
            }
        }
        
        seg.className = classNames;
        seg.setAttribute("title", tooltip);
    }
    
    createSegmentSeparator() {
        let seg = document.createElement("span");
        seg.className = "xui-small-icon xxe-ndpth-separ";
        seg.textContent = XUI.StockIcon["right-open"];
        
        return seg;
    }
    
    createNodeSegment(uid, tag, info) {
        let classNames = "xxe-ndpth-node";
        let tooltip = tag;
        
        if (info.length > 0) {
            const lines = info.split('\n');
            const lineCount = lines.length;
            
            if (lineCount > 0) {
                let flags = lines[0];
                if (flags.length > 0) {
                    if (flags.indexOf("sel") >= 0) {
                        classNames += " xxe-ndpth-node-sel";
                    }
                    if (flags.indexOf("ro") >= 0) {
                        classNames += " xxe-ndpth-node-ro";
                    }
                }
                
                if (lineCount > 1) {
                    // Element attributes.
                    for (let i = 1; i < lineCount; i += 2) {
                        let name = lines[i];
                        let value = lines[i+1];

                        tooltip += "\n\u2022 ";
                        tooltip += name;
                        tooltip += "=";
                        let quote = (value.indexOf("\"") >= 0)? "'" : "\"";
                        tooltip += quote;
                        tooltip += value;
                        tooltip += quote;
                    }
                }
            }
        }
        
        let seg = document.createElement("span");
        seg.setAttribute("data-uid", uid);
        seg.className = classNames;
        
        tooltip += `\n\n\
-\u00A0Click to select.`;
        if (!tag.startsWith("#")) {
            tooltip += `\n\
-\u00A0Shift-click to select child nodes.`;
        }
        tooltip += `\n\
-\u00A0Right-click to display contextual menu.`;
        seg.setAttribute("title", tooltip);
        
        if (tag.length > 40) {
            tag = tag.substring(0, 39) + "\u2026";
        }
        seg.textContent = tag;
        
        seg.addEventListener("click", this._onNodeClicked);
        seg.addEventListener("contextmenu", this._onNodeClicked);
        
        return seg;
    }

    documentStateChanged() {
        let seg = this.firstElementChild;
        if (seg !== null) {
            this.updateDocumentSegment(seg, this._xmlEditor);
        }
    }
}

window.customElements.define("xxe-node-path", NodePath);