Source: map/LayerTree.js

import { ContextMenu } from "../ui/ContextMenu.js";
import { SidebarElement } from "../ui/sidebar/SidebarElement.js";
import { GeoJSONLayer } from "./layer/GeoJSONLayer/GeoJSONLayer.js";
import { WMSLayer } from "./layer/WMSLayer/WMSLayer.js";
import { LayerLegend } from "./utils/LayerLegend.js";
import { CsvLayer } from "./layer/CsvLayer/CsvLayer.js";
import { displayMessage, saveAsFile } from "../utils/utils.js";
import { retrieveDataTransfer } from "../ui/dnd/util.js";
import { Window } from "../ui/Window.js";
import { ImporterWindow } from "./importer/ImporterWindow.js";
import { Folder } from "./layer/Folder/Folder.js";
import { FilterLayer } from "./layer/FilterLayer/FilterLayer.js";
import { WFSLayer } from "./layer/WFSLayer/WFSLayer.js";
import { StyleSettings } from "./utils/StyleSettings.js";
import { LayerSettings } from "./utils/LayerSettings.js";
import "./LayerTree.css";

export { LayerTree };

/**
 * LayerTree class that defines the layer tree and manipulates it
 * 
 * @author awalter <andreas.walter@awi.de>
 * @author sjaswal <shahzeib.jaswal@awi.de>
 * @author rhess <robin.hess@awi.de>
 * 
 * @memberof vef.map
 */
class LayerTree extends SidebarElement {

    constructor(target, layerManager, options) {

        super(target, {
            "layertree_add_layer": [],
            "layertree_select": [],
            "layertree_deselect": [],
            "layertree_show_info": [],
            "layertree_zoom_layer": [],
            "layertree_rename_layer": [],
            "layertree_reorder": []
        });

        const defaultOptions = {
            title: "Layer Tree",
            addLayerMethods: [],
            indexUrl: null,
            cswUrl: null,
            maxClickedLayers: 10
        };

        // Merge default options with given options
        this.options_ = { ...defaultOptions, ...options };
        this.layerManager_ = layerManager;

        this.items_ = {};
        this.deleteMode_ = false;
        this.editMode_ = false;
        this.contextMenu_ = new ContextMenu();
        this.openedFoldersOnSearch_ = [];

        this.initHtmlElement_();

        if (options.structure) this.setStructure(options.structure);

        // remove event for layers
        this.layerManager_.on("layermanager_remove_layer", layer => {
            try {
                this.removeItem(layer.uniqueId);
            } catch (e) {
                console.warn("could not remove layer from LayerTree: " + layer.uniqueId);
            }
        });
    }

    /**
     * @private
     */
    download_(item) {
        if (item.layer instanceof Folder) {
            const features = [];
            const children = item.element.querySelectorAll('.list-item');
            for (let i = 0; i < children.length; ++i) {
                const id = children[i].dataset.id;
                const layer = this.getItem(id).layer;
                if (layer instanceof GeoJSONLayer) {
                    features.push(layer.exportGeometry("object"));
                }
            }

            const json = JSON.stringify({
                type: "FeatureCollection",
                features: features
            });
            const blob = new Blob([json], { type: "text/plain;charset=utf-8" });

            saveAsFile(blob, item.layer.title + ".json");
        } else if (item.layer instanceof GeoJSONLayer) {
            item.layer.exportGeometry("download");
        }
    }

    /**
     * @private
     */
    openStyleSettings_(pointerEvent, layer) {
        if (!this.styleSettings_) this.styleSettings_ = new StyleSettings();
        this.styleSettings_.setOptions({ pointerEvent: pointerEvent });
        this.styleSettings_.open(layer);
    }

    /**
     * @private
     */
    openLayerSettings_(pointerEvent, layer) {
        if (!this.layerSettings_) this.layerSettings_ = new LayerSettings();
        this.layerSettings_.setOptions({ pointerEvent: pointerEvent });
        this.layerSettings_.open(layer);
    }

    clearTree() {
        for (let i in this.items_) {
            this.deselectNodes(i);
        }

        const children = this.list_.querySelectorAll(".list-item");
        for (let i = 0; i < children.length; ++i) {
            children[i].remove();
        }

        this.items_ = {};
    }

    setStructure(structure) {
        this.clearTree();

        const activeNodes = [];

        for (let i in structure) {
            const parent = structure[i].parent;
            const children = structure[i].children;
            for (let j in children) {
                const id = children[j];
                const layer = this.layerManager_.getLayerById(id);
                if (layer) {
                    const role = (layer instanceof Folder) ? "folder" : "layer";
                    const item = this.addItem_(parent, id, role);
                    if ((item.role == "layer") && item.layer.active) activeNodes.push(id);
                }
            }
        }

        for (let i in activeNodes) {
            this.selectNodes(activeNodes[i]);
        }

        this.initDragDrop_();
        this.setLayerIndex_();
    }

    /**
     * Get the Layer order structure of the LayerTree
     * 
     * @returns {object[]} structure
     */
    getStructure() {
        const nodes = this.getNodes();

        const structure = {};

        for (let i = 0; i < nodes.length; ++i) {
            const id = nodes[i].parentElement.dataset.id || "#";
            if (!(id in structure)) structure[id] = [];
            structure[id].push(nodes[i].dataset.id);
        }

        const orderedStructure = [];

        const recursiveSort = (id) => {
            orderedStructure.push({
                parent: id,
                children: structure[id]
            });

            if (id in structure) {
                for (let key of structure[id]) {
                    if (key in structure) recursiveSort(key)
                }
            }
        };

        recursiveSort("#");

        return orderedStructure;
    }

    /**
     * Get the layertree's options-object
     */
    getOptions() {
        return {
            title: this.options_.title,
            addLayerMethods: this.options_.addLayerMethods,
            indexUrl: this.options_.indexUrl,
            cswUrl: this.options_.cswUrl,
            maxClickedLayers: this.options_.maxClickedLayers,
            structure: this.getStructure()
        };
    }

    /**
     * Get a layerManager list-Item by passing a uniqueId or
     * an instance of a HTMLElement list-item
     * 
     * @param {string | HTMLElement} target
     */
    getItem(target) {
        if (target instanceof HTMLElement) {
            target = target.dataset.id;
        }

        if (typeof target == "string") {
            const item = this.items_[target];
            if (typeof item == "object") {
                // copy the object, so the original cannot be changed
                return Object.assign({}, item);
            }
            throw new Error("invalid id: layer with the id '" + target + "' does not exist");
        }

        throw new Error("invalid type: target must be a string or an instance of HTMLElement");
    }

    /**
     * Select a node by passing a uniqueId or
     * an instance of a HTMLElement list-item
     * 
     * @param {string/HTMLElement} target
     */
    selectNodes(target) {
        for (let i = 0; i < arguments.length; ++i) {
            const item = this.getItem(arguments[0]);
            const node = item.element;

            if (node.getAttribute("selected") == "true") continue;

            this.fullSelected_(node);
            this.fire("layertree_select", item.layer);

            const children = node.querySelectorAll('.list-item[selected="false"]');
            for (let c = 0; c < children.length; ++c) {
                if (children[c].getAttribute("disabled") == "true") continue;
                this.selectNodes(children[c]);
            }
            const messageIcon = node.querySelector(".td-item-message i.item-message");
            if (messageIcon) messageIcon.style.display = "block";

            this.setParent_(node.parentNode);
        }
    }

    /**
     * Deselect a node by passing a uniqueId or
     * an instance of a HTMLElement list-item
     * 
     * @param {string/HTMLElement} target
     */
    deselectNodes(target) {
        for (let i = 0; i < arguments.length; ++i) {
            const item = this.getItem(arguments[0]);
            const node = item.element;

            if (node.getAttribute("selected") == "false") continue;

            this.deselected_(node);
            this.fire("layertree_deselect", item.layer);

            const children = node.querySelectorAll('.list-item[selected="true"]');
            for (let c = 0; c < children.length; ++c) {
                if (children[c].getAttribute("disabled") == "true") continue;
                this.deselectNodes(children[c]);
            }

            const messageIcon = node.querySelector(".td-item-message i.item-message");
            if (messageIcon && (messageIcon.dataset.level < 1)) messageIcon.style.display = "none";

            this.setParent_(node.parentNode);
        }
    }

    /**
     * Enable a node by passing a uniqueId or
     * an instance of a HTMLElement list-item
     * 
     * @param {string/HTMLElement} target
     */
    enableNodes(target) {
        for (let i = 0; i < arguments.length; ++i) {
            const item = this.getItem(arguments[0]);
            const node = item.element;

            const children = node.querySelectorAll('.list-item');
            for (let c = 0; c < children.length; ++c) {
                if (children[c].getAttribute("disabled") == "false") continue;
                this.enableNodes(children[c]);
            }

            if (node.getAttribute("disabled") == "false") continue;

            node.setAttribute("disabled", "false");
        }
    }

    /**
     * Disable a node by passing a uniqueId or
     * an instance of a HTMLElement list-item
     * 
     * @param {string/HTMLElement} target
     */
    disableNodes(target) {
        for (let i = 0; i < arguments.length; ++i) {
            const item = this.getItem(arguments[0]);
            const node = item.element;

            const children = node.querySelectorAll('.list-item');
            for (let c = 0; c < children.length; ++c) {
                if (children[c].getAttribute("disabled") == "true") continue;
                this.disableNodes(children[c]);
            }

            if (node.getAttribute("disabled") == "true") continue;

            node.setAttribute("disabled", "true");
        }
    }

    /**
     * show loading animation for a given target layer.
     * uses recursion for handling folders
     * 
     * @param {string/HTMLElement} target
     */
    showLoadingIcon(target) {
        try {
            const el = (target instanceof HTMLElement) ? target : this.getItem(target).element;
            el.querySelector(`.item-svg`).style.display = "flex";

            if (el.parentNode.classList.contains("list-item")) {
                this.showLoadingIcon(el.parentNode);
            }
        } catch (e) {
            console.warn("Layer not found in the LayerTree: cannot show loading icon");
        }
    }

    /**
     * hide loading animation for a given target layer.
     * uses recursion for handling folders
     * 
     * @param {string/HTMLElement} target
     */
    hideLoadingIcon(target) {
        try {
            const el = (target instanceof HTMLElement) ? target : this.getItem(target).element;
            let isLoading = false;

            const children = el.querySelectorAll(".list-item");
            for (let i = 0; i < children.length; ++i) {
                const icon = children[i].querySelector('.item-svg');
                if (icon.style.display != "none") {
                    isLoading = true;
                    break;
                }
            }

            if (!isLoading) {
                el.querySelector(`.item-svg`).style.display = "none";
                if (el.parentNode.classList.contains("list-item")) {
                    this.hideLoadingIcon(el.parentNode);
                }
            }
        } catch (e) {
            console.warn("Layer not found in the LayerTree: cannot hide loading icon");
        }
    }

    /**
     * Display a message icon with a hover text
     * 
     * @param {string/HTMLElement} target
     * @param {object[]} messages
     */
    showMessages(target, messages) {
        try {
            target = (target instanceof HTMLElement) ? target : this.getItem(target).element;
        } catch (e) {
            console.warn("Layer not found in the LayerTree: cannot show loading icon");
        }

        const wrapper = target.querySelector(`.td-item-message`);
        wrapper.innerHTML = "";

        if (messages.length == 0) return;

        // let maxLevel = 0;
        let messageContent = document.createElement("div");
        messageContent.classList.add("layer-tree-message-content");

        const levels = [{ color: "var(--primary-color)", icon: "warning" }, { color: "#ffbb00", icon: "warning-filled" }, { color: "var(--error-color)", icon: "warning-filled" }];

        for (let i = 0; i < messages.length; ++i) {
            const message = messages[i];
            if (message.level >= levels.length) message.level = levels.length - 1;
            // if (message.level > maxLevel) maxLevel = message.level;

            messageContent.innerHTML += `
                <div class="message-wrapper">
                    <div class="color-bar"><i class="vef vef-${levels[message.level].icon}" style="color:${levels[message.level].color};"></i></div>
                    <div class="message-content">${message.content}</div>
                </div>
            `;
        }

        const icon = document.createElement("i");
        // icon.classList.add("item-message", "vef", "vef-" + levels[maxLevel].icon);
        icon.classList.add("item-message", "vef", "vef-" + levels[0].icon);
        // icon.style.color = levels[maxLevel].color;
        icon.style.color = levels[0].color;
        icon.style.width = "100%";
        icon.style.height = "100%";
        icon.style.display = "block";
        // icon.dataset.level = maxLevel;
        icon.dataset.level = 0;
        wrapper.appendChild(icon);

        if (!this.messageWindow_) {
            this.messageWindow_ = new Window(this.getElement(), {
                width: "330px",
                height: "unset",
                open: false,
                mode: "slim",
                title: null
            });
            const windowElement = this.messageWindow_.getElement();
            windowElement.classList.add("pointer-left", "layer-tree-message-window");
            windowElement.style.pointerEvents = "none";
            windowElement.querySelector(".fas.fa-times").style.display = "none";
        }

        icon.addEventListener("mouseover", () => {
            let rect = icon.getBoundingClientRect();
            this.messageWindow_.setOptions({
                top: (rect.top - 3) + "px",
                left: (rect.left + rect.width + 15) + "px",
                open: true,
                content: messageContent
            });
        });

        // close info window when leaving
        icon.addEventListener("mouseleave", () => this.messageWindow_.close());
    }

    /**
     * @returns {NodeList} NodeList containing all selected list-item elements
     */
    getSelectedNodes() {
        return this.list_.querySelectorAll('.list-item[selected="true"]');
    }

    getSelectedNodeLayers() {
        return this.list_.querySelectorAll('.list-item[selected="true"].list-item[role="layer"]');
    }

    getSelectedLayers() {
        const selectedNodes = this.getSelectedNodes();
        const selectedLayers = {};
        for (let i = 0; i < selectedNodes.length; ++i) {
            const id = selectedNodes[i].dataset.id;
            selectedLayers[id] = this.layerManager_.getLayerById(id);
        }
        return selectedLayers;
    }

    /**
     * @returns {NodeList} NodeList containing all list-item elements
     */
    getNodes() {
        return this.list_.querySelectorAll('.list-item');
    }

    /**
     * @returns {Object[]} Array of selected items
     */
    getSelectedItems() {
        const selectedItems = [];
        for (let i in this.items_) {
            const item = this.items_[i];
            if (item.element.getAttribute("selected") == "true") {
                selectedItems.push(Object.assign({}, item));
            }
        }
        return selectedItems;
    }

    /**
     * @returns {Object[]} Array of all items
     */
    getItems() {
        const items = [];
        for (let i in this.items_) {
            items.push({ ...{}, ...this.items_[i] });
        }
        return items;
    }

    /**
     * @param {string | HTMLElement} target
     * @returns {number} get the z-index of a layer
     */
    getLayerIndex(target) {
        const item = this.getItem(target);
        return (typeof item == "object") ? item.index : null;
    }

    /**
     * remove context menu events from a layer
     * 
     * @param {object} item 
     */
    removeContextMenuEntry_(item) {
        this.contextMenu_.unregisterElement(item.element.querySelector(".layertree-content-table"));
    }

    /**
     * bind context menu events to a layer
     * 
     * @param {object} item 
     */
    addContextMenuEntry_(item) {
        // available for all items
        const callbacks = [
            {
                icon: "vef vef-rename",
                text: "Rename",
                callback: () => this.editTitle(item.element.querySelector(".table-title"), item)
            },
            {
                icon: "fas fa-trash",
                text: "Delete",
                callback: () => this.removeItem(item.id)
            }
        ];

        const l = item.layer;

        if (l instanceof Folder) {

            // add download button to folders containing GeoJSONLayers
            callbacks.unshift({
                icon: "fas fa-download",
                text: "Download",
                callback: () => this.download_(item),
                condition: () => {
                    const children = item.element.querySelectorAll('.list-item');
                    for (let i = 0; i < children.length; ++i) {
                        const id = children[i].dataset.id;
                        const layer = this.getItem(id).layer;
                        if (layer instanceof GeoJSONLayer) return true;
                    }
                    return false;
                }
            });

            // add upload button to folders containing CsvLayers
            callbacks.unshift({
                icon: "fas fa-upload",
                text: "Reload Data File",
                callback: () => CsvLayer.requestFileUpload(),
                condition: () => {
                    const children = item.element.querySelectorAll('.list-item');
                    for (let i = 0; i < children.length; ++i) {
                        const id = children[i].dataset.id;
                        const layer = this.getItem(id).layer;
                        if (layer instanceof CsvLayer) return true;
                    }
                    return false;
                }
            });
        } else {
            callbacks.unshift({
                icon: "fas fa-expand-arrows-alt",
                text: "Zoom to Layer",
                callback: () => this.fire('layertree_zoom_layer', item.layer)
            });

            if (l instanceof GeoJSONLayer) {
                callbacks.unshift({
                    icon: "fas fa-download",
                    text: "Download",
                    callback: () => this.download_(item)
                });
            }

            if ((l instanceof WMSLayer) && !l.filterDisabled) {
                callbacks.unshift({
                    icon: "vef vef-legend-small",
                    text: "Show Legend",
                    callback: () => {
                        const legend = new LayerLegend(null, this.layerManager_);
                        legend.on("close", () => {
                            legend.off("close");
                            legend.dispose();
                        });
                        legend.open(l);
                    }
                });
            }

            if ((l instanceof WMSLayer) || (l instanceof WFSLayer)) {
                callbacks.unshift({
                    icon: "fas fa-sync-alt",
                    text: "Reload Metadata",
                    callback: () => this.layerManager_.reloadServiceMetadata(l.serviceUrl, l.type)
                });
            }

            if (l instanceof CsvLayer) {
                callbacks.unshift({
                    icon: "fas fa-upload",
                    text: "Reload Data File",
                    callback: () => CsvLayer.requestFileUpload(l.fileIdentifier)
                });
            }
        }

        //callbacks.push({
        //    icon: "fas fa-cog",
        //    text: "Options",
        //    callback: (e) => this.openLayerSettings_(e, item.layer)
        //});

        if (l.opacitySettings || l.styleSettings || l.colorScaleSettings) {
            callbacks.push({
                icon: "fas fa-palette",
                text: "Style",
                callback: (e) => this.openStyleSettings_(e, item.layer)
            });
        }

        callbacks.unshift({
            icon: "fas fa-info",
            text: "Information",
            callback: () => this.fire("layertree_show_info", item.layer)
        });

        this.contextMenu_.registerElement(item.element.querySelector(".layertree-content-table"), callbacks);
    }

    /**
     * internal method for adding items
     * 
     * @param {string | HTMLElement} parent
     * @param {string | Object} layer
     * @param {string} role layer or folder
     * @param {string} position top, bottom (default: bottom)
     *
     * @private
     */
    addItem_(parent, layer, role, position) {
        parent = parent || "#";
        role = (role == "folder") ? "folder" : "layer";

        position = (position) ? position.toLowerCase() : "bottom";

        // validate layer
        if (typeof layer == "string") {
            const cacheLayer = this.layerManager_.getLayerById(layer);
            if (!cacheLayer) throw new Error(`layer with id="${layer}" does not exist in the LayerManager`);
            layer = cacheLayer;
        } else if (typeof layer == "object") {
            const cacheLayer = this.layerManager_.getLayerById(layer.uniqueId);
            if (!cacheLayer) this.layerManager_.addLayer(layer);
        } else {
            throw new Error("invalid type for layer argument");
        }

        // check if layer already exists in the LayerTree
        if (this.items_[layer.uniqueId]) {
            throw new Error(`Layer with id "${layer.uniqueId}" already exists in the LayerTree`);
        }

        const item = {
            id: layer.uniqueId,
            layer: layer,
            role: role,
            title: layer.title,
            index: 0,
            element: null
        };

        this.createListElement_(item);

        // set parent element
        if (parent == '#') {
            this.list_.insertAdjacentElement((position == "top") ? "afterbegin" : "beforeend", item.element);
        } else {
            if (position == "top") {
                this.getItem(parent).element.querySelector(".layertree-content-table").insertAdjacentElement("afterend", item.element);
            } else {
                this.getItem(parent).element.insertAdjacentElement("beforeend", item.element);
            }
        }

        this.items_[layer.uniqueId] = item;

        // fire add layer event
        this.fire("layertree_add_layer", layer);

        // add context menu event
        this.addContextMenuEntry_(item);

        // display messages if available
        this.showMessages(item.element, layer.getMessages());
        layer.on("layer_message", messages => this.showMessages(item.element, messages));
        layer.on("layer_update", l => {
            const span = item.element.querySelector(".table-title");
            item.title = l.title;
            span.innerText = l.title;
            span.title = l.title;
        });

        return item;
    }

    /**
     * Add an item (layer or folder) to the LayerTree.
     * {@code layer} can be uniqueId or a layer-object
     * 
     * @param {string | HTMLElement} parent
     * @param {string | Object} layer
     * @param {string} role layer or folder
     * @param {string} position top, bottom (default: bottom)
     */
    addItem(parent, layer, role, position) {
        const item = this.addItem_(parent, layer, role, position);
        if (item.layer.active) this.selectNodes(layer.uniqueId);

        if (!(item.layer instanceof FilterLayer)) this.initDragDrop_(item.element);
        this.setLayerIndex_();

        return item;
    }

    /**
     * Add layer to the LayerTree. {@code layer}
     * can be uniqueId or a layer-object
     * 
     * @param {string | HTMLElement} parent
     * @param {string | Object} layer
     * @param {string} position top, bottom (default: bottom)
     */
    addLayer(parent, layer, position) {
        return this.addItem(parent, layer, "layer", position);
    }

    /**
     * Add folder to the LayerTree. {@code layer}
     * can be uniqueId or a layer-object
     * 
     * @param {string | HTMLElement} parent
     * @param {string | Object} layer
     * @param {string} position top, bottom (default: bottom)
     */
    addFolder(parent, layer, position) {
        return this.addItem(parent, layer, "folder", position);
    }

    /**
     * Remove a Layer or Folder from the layertree
     * 
     * @param {string | HTMLElement} target
     */
    removeItem(target) {
        const item = this.getItem(target);
        const element = item.element;

        this.deselectNodes(target);

        // remove children
        const children = element.querySelectorAll(".list-item");
        for (let i = 0; i < children.length; ++i) {
            this.removeContextMenuEntry_(this.items_[children[i].dataset.id]);
            delete this.items_[children[i].dataset.id];
        }

        this.removeContextMenuEntry_(item);
        delete this.items_[item.id];

        element.remove();
        this.setLayerIndex_();
    }

    /**
     * @private
     */
    setLayerIndex_() {
        const items = this.list_.querySelectorAll('.list-item')

        for (let i = 0; i < items.length; ++i) {
            const item = this.items_[items[i].dataset.id];
            item.index = 1000 - i;
            if (item.layer.setZIndex) {
                item.layer.setZIndex(item.index);
            } else if (item.layer.bringToBack) {
                item.layer.bringToBack();
            }

            // Check if an element hast children to set the attribute data-hasChildren.
            // Used for in CSS to recognize Folders, because the .list-item:has(.list-item) does not Work in Firefox
            const children = items[i].querySelectorAll('.list-item');
            items[i].dataset.hasChildren = (children.length > 0);

            // limit max amount of layers that can be selected
            if ((this.options_.maxClickedLayers > 0) && (item.layer instanceof Folder)) {
                let layerCount = 0;
                for (let j = 0; j < children.length; ++j) {
                    const child = this.items_[children[j].dataset.id];
                    if (!(child.layer instanceof Folder) && !(child.layer instanceof FilterLayer)) {
                        ++layerCount
                    }
                }
                const icon = items[i].querySelector(".fa-stack");
                if (layerCount > this.options_.maxClickedLayers) {
                    items[i].dataset.preventSelection = true;
                    icon.title = "This folder cannot be selected directly, because contains too many layers.";
                } else {
                    items[i].dataset.preventSelection = false;
                    icon.title = "";
                }
            }

        }
    }

    /**
     * Create the main structure of the LayerTree
     * 
     * -----------------------------------------------------------
     * <div>
     *   <h3>TITLE</h3>
     *   <ul class="list-layers">
     *     <li data-id="LAYER_NAME" class="list-item" selected="true" role="layer"></li> 
     *     <ul data-id="LAYER_NAME" class="list-item" selected="true" role="folder" active="true">
     *       <li data-id="LAYER_NAME" class="list-item" selected="true" role="layer"></li>
     *     </ul>
     *   </ul>
     * </div>
     * -----------------------------------------------------------
     * @private
     */
    initHtmlElement_() {
        this.setTitle(this.options_.title);
        this.getElement().classList.add("layer-tree-container");

        const list = document.createElement("ul");
        list.classList.add("list-layers");
        const tools = this.initTools_();

        // add elements to content container
        let layers = this.getContentContainer();
        layers.classList.add('layer-tree');
        layers.appendChild(tools);
        layers.appendChild(list);

        this.list_ = list;
        this.tools_ = tools;
    }

    /**
     * @private
     */
    initTools_() {
        const tools = document.createElement("div");

        // delete button
        this.addTool("fas fa-trash delete-button", (enabled) => this.setDeleteMode_(enabled), "Remove Layers", true);

        // edit button
        this.addTool("vef vef-rename edit-button", (enabled) => this.setEditMode_(enabled), "Rename Layers", true);

        // Add Layer Button
        this.addTool("vef vef-add-layer", (toggle, e) => {
            this.initImporter_();
            this.dataImporter_.setOptions({
                pointerEvent: e,
                open: true
            });
        }, "Add Layers");

        // Add Folder Button
        this.addFolderForm_ = this.initAddFolderForm_();
        tools.appendChild(this.addFolderForm_);

        // search button
        this.addSearchForm_ = this.initAddSearchForm_();
        tools.appendChild(this.addSearchForm_);

        return tools;
    }

    /**
     * @private
     */
    initFormTool_(placeholder, buttonText, toolIcon, toolText, toolEvent, formEvent) {
        const formContainer = document.createElement("div");
        formContainer.classList.add("text-input-field");
        formContainer.style.display = "none";
        formContainer.innerHTML = `
                <i class="fas fa-times"></i><!--
             --><input spellcheck="false" placeholder="${placeholder}" type="text"/><button><i class="fas fa-plus"></i> ${buttonText}<!--
             --></button>
            `;
        const form = formContainer.querySelector("input");
        form.addEventListener("keypress", (e) => {
            if (e.code == "Enter") formEvent(form)
        });
        this.addTool(toolIcon, toolEvent, toolText);
        formContainer.querySelector(".fa-times").setAttribute("tabindex", "0");
        formContainer.querySelector("button").setAttribute("tabindex", "0");
        formContainer.querySelector("button").addEventListener("click", () => formEvent(form));
        formContainer.querySelector(".fa-times").addEventListener("click", () => formContainer.style.display = "none");
        formContainer.querySelector(".fa-times").addEventListener("keydown", (e) => {
            if (e.key != "Enter") return;
            formContainer.style.display = "none"
        });

        return formContainer;
    }

    /**
     * @private
     */
    initSearchTool_(placeholder, buttonText, toolIcon, toolText, toolEvent, formEvent) {
        const formContainer = document.createElement("div");
        formContainer.classList.add("text-input-field");
        formContainer.style.display = "none";
        formContainer.innerHTML = `
                <i class="fas fa-times"></i><!--
             --><input spellcheck="false" placeholder="${placeholder}" type="text"/><button><i class="fas fa-search"></i> ${buttonText}<!--
             --></button>
            `;
        const form = formContainer.querySelector("input");
        form.addEventListener("keyup", (e) => {
            formEvent(form)
        });
        formContainer.querySelector(".fa-times").setAttribute("tabindex", "0");
        formContainer.querySelector("button").setAttribute("tabindex", "-1");
        formContainer.querySelector("button").addEventListener("click", () => formEvent(form));
        formContainer.querySelector(".fa-times").addEventListener("click", (e) => {
            formContainer.style.display = "none";
            this.resetSearch();
        });
        formContainer.querySelector(".fa-times").addEventListener("keydown", (e) => {
            if (e.key != "Enter") return;
            formContainer.style.display = "none";
            this.resetSearch();
        });
        this.addTool(toolIcon, toolEvent, toolText);

        return formContainer;
    }

    /**
     * @private
     */
    initAddFolderForm_() {
        let formContainer;
        const addFolder = (form) => {
            const value = form.value;
            if (value) {
                this.addFolder("#", new Folder({ title: value }), "top");
                formContainer.style.display = "none";
            }
        };

        formContainer = this.initFormTool_("New Folder", "FOLDER", "vef vef-add-folder", "Add Folder", () => this.showAddFolderForm_(), addFolder);
        return formContainer;
    }

    /**
     * @private
     */
    initAddSearchForm_() {
        let formContainer;
        const doSearch = (form) => {
            const value = form.value;
            if (value) {
                this.doSearch(value);
            } else {
                this.resetSearch();
            }
        };
        formContainer = this.initSearchTool_("Seachbar ...", "SEARCH", "fas fa-search", "Search", () => this.showSearchForm_(), doSearch);
        return formContainer;
    }

    /**
     * @private
     */
    initImporter_() {
        if (this.dataImporter_) return;

        const importer = new ImporterWindow(null, {
            catalog: {
                url: this.options_.indexUrl
            }
        });

        importer.on("add_layers", data => {
            if (!Array.isArray(data)) data = [data];
            for (let i in data) {
                if (!data[i]) continue;
                displayMessage("Added Layers to the Layer-Tree", 7000);
                this.addStructuredLayers(data[i].layers, data[i].structure, "top");
            }
        });

        importer.on("show_info", layer => {
            this.fire("layertree_show_info", layer);
        });

        this.dataImporter_ = importer;
    }

    /**
     * Add layers according to a predefined directory structure
     * 
     * @param {object} layers 
     * @param {object} structure 
     * @param {string} position top, bottom (default=bottom). Only relevant for root level layers
     */
    addStructuredLayers(layers, structure, position) {
        for (let parent in structure) {
            for (let id of structure[parent]) {
                const pos = (parent == "#") ? position : "bottom";
                if (id in structure) {
                    this.addFolder(parent, layers[id], pos);
                } else {
                    this.addLayer(parent, layers[id], pos);
                }
            }
        }
    }

    /**
     * @private
     */
    showAddFolderForm_() {
        const input = this.addFolderForm_.querySelector("input");
        input.value = "";
        this.addSearchForm_.style.display = "none";
        this.addFolderForm_.style.display = "flex";
        input.focus();
    }

    /**
     * @private
     */
    showSearchForm_() {
        const input = this.addSearchForm_.querySelector("input");
        input.value = "";
        this.addFolderForm_.style.display = "none";
        this.addSearchForm_.style.display = "flex";
        input.focus();
    }

    /**
     * enable or disable delete mode
     * 
     * @param {boolean} enabled
     * 
     * @private
     */
    setDeleteMode_(enabled) {
        const tree = this.getContentContainer();

        if (enabled) {
            tree.classList.add('delete-mode');
        } else {
            tree.classList.remove('delete-mode');
        }

        this.deleteMode_ = enabled;
    }

    setEditMode_(enabled) {
        const tree = this.getContentContainer();

        if (enabled) {
            tree.classList.add('edit-mode');
        } else {
            tree.classList.remove('edit-mode');
        }

        this.editMode_ = enabled;
    }

    getTemplate_(content) {
        return `
            <table class="layertree-content-table">
                <tr>
                    <td class="td-item-chevron" data-id="item-chevron-${content.uniqueId}">
                        <i class="fas"></i>
                    </td>
                    <td class="td-item-select" data-id="item-select-${content.uniqueId}" select-id="item-select-${content.uniqueId}" tabindex="0">
                        ${content.selectElement}
                    </td>
                    <td class="td-item-icon" data-id="item-icon-${content.uniqueId}" select-id="item-select-${content.uniqueId}"><i></i></td>
                    <td class="td-item-title" select-id="item-select-${content.uniqueId}">
                        <span data-id="item-title-${content.uniqueId}" class="table-title" title="${content.title}">${content.title}</span>
                    </td>
                    <td class="td-item-svg">
                        <span class="item-svg" style="display: none"> 
                            <svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" style="margin: auto; shape-rendering: auto;" width="20px" height="20px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
                                <circle cx="50" cy="50" r="32" stroke-width="8" stroke="var(--primary-color)" stroke-dasharray="50.26548245743669 50.26548245743669" fill="none" stroke-linecap="round">
                                <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="0 50 50;360 50 50"></animateTransform>
                                </circle>
                            </svg>
                        </span>
                    </td>
                    <td class="td-item-message"></td>
                    <td class="td-item-menu">
                        <span class="item-menu context-dropdown"><i class="fas fa-caret-down"></i></span>
                    </td>
                </tr>
            </table>
        `;
    }

    /**
     * combines the searching values 
     * 
     * @param {Object} obj
     * @returns {String} returns modified string
     * @private
     */
    combineSearchFilterOptions(obj) {
        if (typeof obj == "string") {
            return obj.toLowerCase();
        } else if (Array.isArray(obj)) {
            return obj.reduce((acc, val) => {
                if (typeof val != "string") return acc;
                return acc + " " + val.toLowerCase();
            }, "");
        }
        return "";
    }

    /**
     * searches for a value and shows folder and layer that match
     * 
     * @param {String} value 
     * 
     * @private
     */
    doSearch(value) {
        this.resetSearch();
        value = value.toLowerCase();

        const properties = ["title", "name", "serviceKeywords", "keywords", "originalTitle", "serviceTitle", , "serviceAbstract", "abstract"];
        const keep = [];
        const removed = [];

        for (let i in this.items_) {
            let searchingValues = "";
            const item = this.items_[i];

            for (let property of properties) {
                if (property in item.layer) {
                    searchingValues += this.combineSearchFilterOptions(item.layer[property]);
                }
            }

            if (searchingValues.includes(value)) {
                const parents = this.getParentElements(item.element);
                parents.forEach(parent => {
                    const parentItem = this.getItem(parent);
                    if (!parentItem.layer.expanded) {
                        this.open_(parentItem);
                        this.openedFoldersOnSearch_.push(parentItem);
                    }
                });

                keep.push(item.element, ...parents, ...this.getChildrenElements(item.element));
            } else {
                removed.push(item.element);
            }
        }

        removed.forEach(removedElement => {
            if (!keep.includes(removedElement)) {
                removedElement.classList.add("search-hidden");
            }
        });
    }

    /**
     * returns all parent elements of a given element
     * 
     * @param {HTMLElement} element 
     * 
     * @returns {HTMLElement} parent
     */
    getParentElements(element) {
        let parents = [];
        const parent = element.parentElement;
        if (parent.classList.contains("list-item")) {
            parents.push(parent);
            parents = parents.concat(this.getParentElements(parent));
        }
        return parents;
    }

    /**
     * returns the children elements of an item 
     * 
     * @param {HTMLElement} item 
     * @returns {HTMLElement[]} Array of children Elements
     */
    getChildrenElements(element) {
        const arr = [];
        const children = element.querySelectorAll(".list-item");
        for (let i = 0; i < children.length; ++i) {
            arr.push(children[i]);
        }
        return arr;
    }

    /**
     * reset structure without clearing the selected layer or folder
     */
    resetSearch() {
        for (let i in this.items_) {
            this.items_[i].element.classList.remove("search-hidden");
        }

        this.openedFoldersOnSearch_.forEach(folder => {
            this.close_(folder);
        });
        this.openedFoldersOnSearch_ = [];
    }

    /**
     * @private
     */
    open_(item) {
        const el = item.element;
        const chevronElement = el.querySelector('.td-item-chevron>i')
        chevronElement.classList.remove("fa-chevron-right");
        chevronElement.classList.add("fa-chevron-down");

        item.layer.expanded = true;
        el.setAttribute("expanded", "true");
    }

    /**
     * @private
     */
    close_(item) {
        const el = item.element;
        const chevronElement = el.querySelector('.td-item-chevron>i')
        chevronElement.classList.remove("fa-chevron-down");
        chevronElement.classList.add("fa-chevron-right");

        item.layer.expanded = false;
        el.setAttribute("expanded", "false");
    }

    /**
     * @private
     */
    deselected_(el) {
        const selectionElement = el.querySelector('.td-item-select>span>i[class*="tree-checkbox"]');
        selectionElement.classList.remove("tree-checkbox-full-selected");
        selectionElement.classList.remove("tree-checkbox-part-selected");
        selectionElement.classList.add("tree-checkbox");
        el.setAttribute('selected', 'false');
    }

    /**
     * @private
     */
    fullSelected_(el) {
        const selectionElement = el.querySelector('.td-item-select>span>i[class*="tree-checkbox"]');
        selectionElement.classList.remove("tree-checkbox");
        selectionElement.classList.remove("tree-checkbox-part-selected");
        selectionElement.classList.add("tree-checkbox-full-selected");
        el.setAttribute('selected', 'true');
    }

    /**
     * @private
     */
    partlySelected_(el) {
        const selectionElement = el.querySelector('.td-item-select>span>i[class*="tree-checkbox"]');
        selectionElement.classList.remove("tree-checkbox-full-selected");
        selectionElement.classList.remove("tree-checkbox");
        selectionElement.classList.add("tree-checkbox-part-selected");
        el.setAttribute('selected', 'true');
    }

    /**
     * Create a layer or folder ui-element with events for the LayerTree
     * @private
     */
    createListElement_(item) {
        const fragment = document.createDocumentFragment();

        const el = document.createElement("ul");
        item.element = el;

        el.dataset.id = item.id;
        el.classList.add('list-item');
        el.setAttribute('role', item.role);
        el.setAttribute('selected', "false");
        el.setAttribute('disabled', "false");

        // Add Table element to Document Fragment
        fragment.appendChild(el);

        // Folder specific Icon Elements
        const selectDummy = `
            <span class="fa-stack">
                <i class="far fa-square fa-stack-1x"></i>
                <i class="fas fa-check fa-stack-1x tree-checkbox"></i>
                <i class="fas fa-times fa-stack-1x delete-button"></i>
                <i class="fas fa-square fa-stack-1x prevent-select-icon"></i>
            </span>
        `;

        // Get Table Template and fill it with Icons
        el.innerHTML = this.getTemplate_({
            uniqueId: item.id,
            title: item.title,
            selectElement: selectDummy
        });

        // active=true => folder is 'open'
        // active=false => folder is 'closed'
        const triggerFolder = (setActive) => {
            if (setActive) {
                this.open_(item);
            } else {
                this.close_(item);
            }
        }

        el.querySelector('.td-item-icon>i').classList = item.layer.icon;
        triggerFolder(item.layer.expanded);

        // Add click event for open/close folder (use timeout to not close instantly on double click)
        let folderClickTimeout = null;
        let elChevron = fragment.querySelector("[data-id='item-chevron-" + item.id + "']");
        elChevron.onclick = (event) => {
            if (!folderClickTimeout) {
                triggerFolder(!item.layer.expanded);
                folderClickTimeout = setTimeout(() => {
                    folderClickTimeout = null;
                }, 400);
            }
        };

        const elSelect = fragment.querySelector(".td-item-select");
        const elIcon = fragment.querySelector(".td-item-icon");
        const elTitle = fragment.querySelector(".td-item-title");
        const elContextMenu = fragment.querySelector(".item-menu.context-dropdown");

        // Add click event for select/deselect
        let clickTimeout = null;
        const triggerSelect = () => {
            if ((el.dataset.preventSelection === "true") && (el.getAttribute("selected") !== "true")) {
                this.open_(item);
                return;
            }
            if (clickTimeout) {
                // open/close folder on double click
                clearTimeout(clickTimeout);
                clickTimeout = null;
                triggerFolder(!item.layer.expanded);
            } else {
                // select folder on single click (with timeout)
                if (this.deleteMode_ || this.editMode_) return;
                if (el.getAttribute("disabled") == "true") return;
                if (el.getAttribute("selected") === "true") {
                    this.deselectNodes(item.id);
                } else {
                    this.selectNodes(item.id);
                }
                clickTimeout = setTimeout(() => {
                    clickTimeout = null;
                }, 400);
            }
        };

        elIcon.addEventListener("click", triggerSelect);

        elTitle.addEventListener("click", (e) => {
            if (this.editMode_) {
                this.editTitle(e.target, item);
            } else {
                triggerSelect();
            }
        });

        elSelect.addEventListener("click", () => {
            if (this.deleteMode_) {
                this.removeItem(item.id);
            } else {
                triggerSelect();
            }
        });
        elSelect.addEventListener("keydown", (e) => {
            if (e.key != "Enter") return;
            if (this.deleteMode_) {
                this.removeItem(item.id);
            } else {
                triggerSelect();
            }
        })

        elContextMenu.addEventListener("click", (e) => {
            this.contextMenu_.open(el.querySelector(".layertree-content-table"), e.pageX, e.pageY);
        });
    }

    /** 
     * @private
     */
    setParent_(parent) {
        if (parent.classList.contains("list-layers")) return;

        const childrenLength = parent.querySelectorAll('.list-item').length;
        const selectedChildrenLength = parent.querySelectorAll('.list-item[selected="true"]').length;
        const selected = (parent.getAttribute("selected") == "true") ? true : false;

        if (selectedChildrenLength == 0) {
            this.deselected_(parent);
            if (selected) this.fire("layertree_deselect", this.getItem(parent)?.layer);
        }
        else if (childrenLength == selectedChildrenLength) {
            this.fullSelected_(parent);
            if (!selected) this.fire("layertree_select", this.getItem(parent)?.layer);
        }
        else if (childrenLength > selectedChildrenLength) {
            this.partlySelected_(parent);
            if (!selected) this.fire("layertree_select", this.getItem(parent)?.layer);
        }

        this.setParent_(parent.parentNode);
    }

    /**
     * Moves Items up and down by the given amount of steps
     * 
     * @param {object} item 
     * @param {number} steps 
     */
    moveItem(item, steps) {
        item = this.getItem(item);

        const nodes = this.getNodes();

        for (let i = 0; i < nodes.length; ++i) {
            if (item.element == nodes[i]) {

                const oldParent = nodes[i].parentElement;

                let target, newParent;
                let offset = i + steps;
                while ((offset < nodes.length) && (offset >= 0) && (!newParent || !newParent.contains(nodes[i]) || nodes[i].contains(target))) {
                    target = nodes[offset];
                    newParent = target.parentElement;
                    offset = offset + steps;
                }

                if (!nodes[i].contains(target)) {

                    if ((oldParent != newParent) || (offset < i)) {
                        target.insertAdjacentElement("beforebegin", nodes[i]);

                        if (oldParent != newParent) {
                            this.setParent_(oldParent);
                            this.setParent_(newParent);
                        }
                    } else if (offset > i) {
                        target.insertAdjacentElement("afterend", nodes[i]);
                    }

                    this.setLayerIndex_();
                    this.fire("layertree_reorder", item.layer);
                }

                return;
            }
        }

    }

    /** 
     * @private
     */
    initDragDrop_(targetElement) {
        this.draggedLayer_ = null;
        this.dropTimeout = null;
        this.dropInside = false;

        const setDropMarker = (item, position) => {
            switch (position) {
                case "before":
                    item.classList.add("drop-insert-before");
                    item.classList.remove("drop-insert-after");
                    item.classList.remove("drop-insert-inside");
                    break;
                case "inside":
                    item.classList.add("drop-insert-inside");
                    item.classList.remove("drop-insert-before");
                    item.classList.remove("drop-insert-after");
                    break;
                case "after":
                    item.classList.add("drop-insert-after");
                    item.classList.remove("drop-insert-before");
                    item.classList.remove("drop-insert-inside");
                    break;
                default:
                    item.classList.remove("drop-insert-before");
                    item.classList.remove("drop-insert-after");
                    item.classList.remove("drop-insert-inside");
            }
        };

        const getDropPosition = (table, e) => {
            const item = table.parentElement;
            const isFolder = item.getAttribute("role") == "folder";

            if (isFolder && this.dropInside) {
                return "inside";
            } else {
                const bounds = table.getBoundingClientRect();
                const center = bounds.top + (bounds.height / 2);
                if (e.clientY > center) {
                    if (isFolder && (item.getAttribute("active") === "true")) {
                        return "inside";
                    } else {
                        return "after";
                    }
                } else {
                    return "before";
                }
            }
        };

        const applyDragDrop = (dropItem, draggedLayer, position) => {
            const oldParent = draggedLayer.parentNode;
            const loading = draggedLayer.querySelector(`.item-svg`).style.display == "flex";

            // hide the loading icon, so the parent folder gets updated
            if (loading) this.hideLoadingIcon(draggedLayer);

            // move layer to the new parent
            if (position == "before") {
                dropItem.insertAdjacentElement("beforebegin", draggedLayer);
            } else if (position == "inside") {
                const table = dropItem.querySelector(".layertree-content-table");
                table.insertAdjacentElement("afterend", draggedLayer);
            } else {
                dropItem.insertAdjacentElement("afterend", draggedLayer);
            }

            // update parent folder checked/unchecked status
            const newParent = draggedLayer.parentNode;
            if (newParent != oldParent) {
                this.setParent_(oldParent);
                this.setParent_(newParent);
            }

            // show the loading icon, so the new parent folder gets updated
            if (loading) this.showLoadingIcon(draggedLayer);

            this.fire("layertree_reorder", draggedLayer);
        };

        const clearDropTimeout = () => {
            if (this.dropTimeout) clearTimeout(this.dropTimeout);
            this.dropTimeout = null;
            this.dropInside = false;
        }

        const setDropTimeout = (item) => {
            setTimeout(() => {
                clearDropTimeout(true);
                if (item.getAttribute("role") == "folder") {
                    this.dropTimeout = setTimeout(() => {
                        this.dropInside = true;
                        this.dropTimeout = null;
                    }, 1000);
                }
            }, 0);
        };

        const initEvents = (item) => {

            item.setAttribute('draggable', true);

            // use the table as a drop target (not the item)
            // easier to define the position in folders
            const table = item.querySelector(".layertree-content-table");

            item.addEventListener('dragstart', (e) => {
                e.stopPropagation();
                e.dataTransfer.setData("layer_id", item.dataset.id);
                e.target.classList.add("drag");
                this.draggedLayer_ = e.target;
            });

            item.addEventListener('dragend', (e) => {
                e.stopPropagation();
                setTimeout(() => {
                    clearDropTimeout();
                    if (!this.draggedLayer_) return;
                    this.draggedLayer_.classList.remove("drag");
                    this.draggedLayer_ = null;
                }, 0);
            });

            // dragleave is fired when hovering child elements
            // a counter helps to identify when the parent is truly left
            let dragEnterCount = 0;

            const isLayer = (e) => {
                return !!this.draggedLayer_ || e.dataTransfer.types.includes("layer_transfer_id");
            };

            table.addEventListener('dragenter', e => {
                e.preventDefault();
                if (!isLayer(e)) return;
                if (dragEnterCount++ == 0) setDropTimeout(item);
            });

            table.addEventListener('dragleave', e => {
                e.preventDefault();
                if (!isLayer(e)) return;
                if (--dragEnterCount == 0) {
                    clearDropTimeout();
                    setDropMarker(item);
                }
            });

            table.addEventListener('dragover', e => {
                e.dataTransfer.dropEffect = "move";
                e.preventDefault();
                if (!isLayer(e)) return;
                if (!this.draggedLayer_ || !this.draggedLayer_.contains(item)) {
                    setDropMarker(item, getDropPosition(table, e));
                }
            });

            table.addEventListener('drop', e => {
                e.preventDefault();
                e.stopPropagation();
                if (!isLayer(e)) return;

                dragEnterCount = 0;
                setDropMarker(item);
                if (this.draggedLayer_ && (item != this.draggedLayer_) && !this.draggedLayer_.contains(item)) {
                    applyDragDrop(item, this.draggedLayer_, getDropPosition(table, e));
                    this.setLayerIndex_();
                } else {
                    const transferId = e.dataTransfer.getData("layer_transfer_id");
                    if (!transferId) return;
                    const layer = retrieveDataTransfer(transferId);
                    if (!layer) return;
                    const layerItem = this.addLayer("#", layer);
                    applyDragDrop(item, layerItem.element, getDropPosition(table, e));
                    this.setLayerIndex_();
                }

            })
        }

        if (targetElement) {
            // init drag drop only for one element
            initEvents(targetElement);
        } else {
            // init drag drop for all elements
            for (let id in this.items_) {
                const item = this.items_[id];
                if (!(item.layer instanceof FilterLayer)) initEvents(item.element);
            }
        }

    }

    /**
     * Checks if an entry already exists in layerTree
     * returns true if yes
     * 
     * @param {String} id 
     */
    hasEntry(id) {
        let items = this.getItems();
        for (let i = 0; i < items.length; i++) {
            if (items[i].id == id) {
                return true;
            }
        }
        return false;
    }

    /**
     * Method to edit Layer and Folder names
     * 
     * @param {HTMLElement} span
     */
    editTitle(span, item) {
        let that = this;
        let triggered = false;

        if (span && span.tagName.toUpperCase() === "SPAN") {
            // hide the span
            span.style.display = "none";

            // get the title clicked on
            let title = span.innerText;

            // create input field
            let input = document.createElement("input");
            input.draggable = true;
            input.type = "text";
            input.value = title;
            input.style = "width: 100%;"
            span.parentNode.insertBefore(input, span);

            // focus on input and attach events for blur and pressing enter key
            input.focus();
            input.addEventListener("blur", () => {
                if (!triggered) {
                    triggered = true;
                    restoreSpan();
                }
            });

            input.addEventListener("keydown", (e) => {
                if (e.key == "Enter") {
                    if (!triggered) {
                        triggered = true;
                        restoreSpan();
                    }
                }
            });

            input.addEventListener("click", (e) => {
                e.stopPropagation();
            });

            input.addEventListener("dragstart", (e) => {
                e.preventDefault();
                e.stopPropagation();
            });

            // restore the span with the new title
            function restoreSpan() {
                // remove input
                span.parentNode.removeChild(input);

                // update span and layer
                const value = input.value.trim();
                item.layer.title = (value.length) ? value : title
                span.innerText = item.layer.title;
                span.title = item.layer.title;

                // show the span again
                span.style.display = "";

                that.fire("layertree_rename_layer", item.layer);
            }
        }
    }

    getFolderTitle(elem) {
        return elem.querySelector(".td-item-title > span").innerHTML;
    }

}