import { __esDecorate, __runInitializers } from "tslib";
import { html, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { cancelEvent } from '@api-client/core/lib/events/Utils.js';
import ApiElement from '../../ApiElement.js';
import '../../../md/icons/ui-icon.js';
import '../../../md/icon-button/ui-icon-button.js';
let AppNavigation = (() => {
    let _classSuper = ApiElement;
    let _focusedItem_decorators;
    let _focusedItem_initializers = [];
    let _focusedItem_extraInitializers = [];
    let _selected_decorators;
    let _selected_initializers = [];
    let _selected_extraInitializers = [];
    let _edited_decorators;
    let _edited_initializers = [];
    let _edited_extraInitializers = [];
    let _noVisualSelection_decorators;
    let _noVisualSelection_initializers = [];
    let _noVisualSelection_extraInitializers = [];
    let __nameInput_decorators;
    let __nameInput_initializers = [];
    let __nameInput_extraInitializers = [];
    let _label_decorators;
    let _label_initializers = [];
    let _label_extraInitializers = [];
    return class AppNavigation extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _focusedItem_decorators = [state()];
            _selected_decorators = [property({ type: String })];
            _edited_decorators = [property({ type: String })];
            _noVisualSelection_decorators = [property({ type: Boolean, reflect: true })];
            __nameInput_decorators = [query('.name-change input')];
            _label_decorators = [property({ type: String })];
            __esDecorate(this, null, _focusedItem_decorators, { kind: "accessor", name: "focusedItem", static: false, private: false, access: { has: obj => "focusedItem" in obj, get: obj => obj.focusedItem, set: (obj, value) => { obj.focusedItem = value; } }, metadata: _metadata }, _focusedItem_initializers, _focusedItem_extraInitializers);
            __esDecorate(this, null, _selected_decorators, { kind: "accessor", name: "selected", static: false, private: false, access: { has: obj => "selected" in obj, get: obj => obj.selected, set: (obj, value) => { obj.selected = value; } }, metadata: _metadata }, _selected_initializers, _selected_extraInitializers);
            __esDecorate(this, null, _edited_decorators, { kind: "accessor", name: "edited", static: false, private: false, access: { has: obj => "edited" in obj, get: obj => obj.edited, set: (obj, value) => { obj.edited = value; } }, metadata: _metadata }, _edited_initializers, _edited_extraInitializers);
            __esDecorate(this, null, _noVisualSelection_decorators, { kind: "accessor", name: "noVisualSelection", static: false, private: false, access: { has: obj => "noVisualSelection" in obj, get: obj => obj.noVisualSelection, set: (obj, value) => { obj.noVisualSelection = value; } }, metadata: _metadata }, _noVisualSelection_initializers, _noVisualSelection_extraInitializers);
            __esDecorate(this, null, __nameInput_decorators, { kind: "accessor", name: "_nameInput", static: false, private: false, access: { has: obj => "_nameInput" in obj, get: obj => obj._nameInput, set: (obj, value) => { obj._nameInput = value; } }, metadata: _metadata }, __nameInput_initializers, __nameInput_extraInitializers);
            __esDecorate(this, null, _label_decorators, { kind: "accessor", name: "label", static: false, private: false, access: { has: obj => "label" in obj, get: obj => obj.label, set: (obj, value) => { obj.label = value; } }, metadata: _metadata }, _label_initializers, _label_extraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        /**
         * The list of keys of opened items with children. This should be clear each time
         * when the main data property change.
         */
        opened = new Set();
        /**
         * The key of the currently focused item.
         */
        focused;
        #focusedItem_accessor_storage = __runInitializers(this, _focusedItem_initializers, void 0);
        /**
         * The currently focused node.
         * In case there are headers in the list, this may not be a list item.
         */
        get focusedItem() { return this.#focusedItem_accessor_storage; }
        set focusedItem(value) { this.#focusedItem_accessor_storage = value; }
        /**
         * To be set in the child class constructor to configure whether a name change is possible.
         */
        allowRename = __runInitializers(this, _focusedItem_extraInitializers);
        #selected_accessor_storage = __runInitializers(this, _selected_initializers, void 0);
        /**
         * The currently selected navigation item.
         * @attr
         */
        get selected() { return this.#selected_accessor_storage; }
        set selected(value) { this.#selected_accessor_storage = value; }
        #edited_accessor_storage = (__runInitializers(this, _selected_extraInitializers), __runInitializers(this, _edited_initializers, void 0));
        /**
         * When set it renders a name change input for the item.
         * This is the `key` of the object.
         *
         * Note, when the user finish editing the name it calls the `_commitName(key, kind, name)` method.
         * This requires the `_allowRename` property to be set to `true` in the constructor.
         * @attr
         */
        get edited() { return this.#edited_accessor_storage; }
        set edited(value) { this.#edited_accessor_storage = value; }
        #noVisualSelection_accessor_storage = (__runInitializers(this, _edited_extraInitializers), __runInitializers(this, _noVisualSelection_initializers, void 0));
        /**
         * When set it prohibits the element to make visual selection (via the css) when an item is selected.
         * This is helpful when one navigation can be visually references in multiple places (like split layout).
         * @attr
         */
        get noVisualSelection() { return this.#noVisualSelection_accessor_storage; }
        set noVisualSelection(value) { this.#noVisualSelection_accessor_storage = value; }
        #_nameInput_accessor_storage = (__runInitializers(this, _noVisualSelection_extraInitializers), __runInitializers(this, __nameInput_initializers, void 0));
        /**
         * A reference to the currently rendered name input, if any.
         */
        get _nameInput() { return this.#_nameInput_accessor_storage; }
        set _nameInput(value) { this.#_nameInput_accessor_storage = value; }
        #label_accessor_storage = (__runInitializers(this, __nameInput_extraInitializers), __runInitializers(this, _label_initializers, void 0));
        /**
         * When set it adds a menu entry with the navigation label.
         * @attribute
         */
        get label() { return this.#label_accessor_storage; }
        set label(value) { this.#label_accessor_storage = value; }
        /**
         * A reference to the currently dragged item.
         * This logic assumes that the drag and drop is local to this element.
         */
        draggingItem = __runInitializers(this, _label_extraInitializers);
        constructor() {
            super();
            this.addEventListener('click', this.handleClick.bind(this));
        }
        connectedCallback() {
            super.connectedCallback();
            this.setAttribute('role', 'navigation');
            this.setAttribute('tabindex', '0');
        }
        willUpdate(cp) {
            super.willUpdate(cp);
            if (cp.has('selected') && this.selected) {
                this.ensureTreeVisibility(this.selected);
            }
        }
        updated(cp) {
            super.updated(cp);
            if (cp.has('edited') && this.edited) {
                const node = this._nameInput;
                if (node) {
                    node.focus();
                }
                this.ensureTreeVisibility(this.edited);
            }
            if (cp.has('focusedItem') && this.focusedItem) {
                const old = cp.get('focusedItem');
                this.manageFocus(this.focusedItem, old);
            }
        }
        reset() {
            this.opened.clear();
            this.focused = undefined;
            this.focusedItem = undefined;
            this.edited = undefined;
        }
        /**
         * This method is called when the `_focusedItem` property is updated.
         * It removes tabindex from the old element (if any) and sets tabindex and focuses on the new item (if any).
         * When there's no new item, it sets the `tabindex` on self.
         *
         * @param current The new focused element, if any
         * @param old The old focused element, if any.
         */
        manageFocus(current, old) {
            if (old) {
                // don't set `-1` here as the item can have children which event with tabindex '0' won't be focusable.
                old.removeAttribute('tabindex');
            }
            if (current) {
                current.setAttribute('tabindex', '0');
                current.focus();
                // current.scrollIntoView();
                // remove tabindex from self so when the user returns to this navigation it will focus directly on
                // this item.
                if (this.hasAttribute('tabindex')) {
                    this.removeAttribute('tabindex');
                }
            }
            else if (!this.hasAttribute('tabindex')) {
                // restore tabindex on self.
                this.setAttribute('tabindex', '0');
            }
        }
        /**
         * Makes sure the node represented by the passed key is visible in the UI.
         * This opens all parents on the way up to the root and scrolls to the node, when needed.
         * @param key The key of the object to ensure visibility for.
         */
        ensureTreeVisibility(key) {
            const node = this.shadowRoot?.querySelector(`li[data-key="${key}"]`);
            if (!node) {
                return;
            }
            if (node.offsetParent !== null) {
                // node is visible
                return;
            }
            const { opened: _opened } = this;
            let parent = this.findParentListItem(node);
            const toOpen = new Set();
            while (parent) {
                const id = parent.dataset.key;
                if (!_opened.has(id)) {
                    toOpen.add(id);
                }
                parent = this.findParentListItem(parent);
            }
            if (toOpen.size) {
                this.opened = this.opened.union(toOpen);
                this.requestUpdate();
            }
        }
        /**
         * Toggles a group of tree items.
         *
         * @param key The key of the group parent.
         */
        toggleGroup(key) {
            if (this.opened.has(key)) {
                this.opened.delete(key);
            }
            else {
                this.opened.add(key);
            }
            this.requestUpdate();
        }
        /**
         * Allows to programmatically open a folder.
         * It opens all parents if they are not opened.
         * @param key The key of the folder to open.
         */
        openGroup(key) {
            const node = this.shadowRoot?.querySelector(`li[data-key="${key}"]`);
            if (!node) {
                return;
            }
            if (this.opened.has(key)) {
                return;
            }
            this.opened.add(key);
            // open all parents
            let parent = this.findParentListItem(node);
            while (parent) {
                const parentKey = parent.dataset.key;
                this.opened.add(parentKey);
                parent = this.findParentListItem(parent);
            }
            this.requestUpdate();
        }
        /**
         * Allows to programmatically close a folder.
         * @param key The key of the folder to open.
         */
        closeGroup(key) {
            if (this.opened.has(key)) {
                this.opened.delete(key);
                this.requestUpdate();
            }
        }
        /**
         * A function to determine whether the passed Element is an active list item.
         *
         * @param node The Element to test
         * @returns true when the passed element is a `li` element and is not disabled.
         */
        isValidListItem(node) {
            return (node &&
                node.localName === 'li' &&
                node.getAttribute('aria-disabled') !== 'true' &&
                node.hasAttribute('data-key') &&
                node.hasAttribute('data-kind'));
        }
        /**
         * Marks the item as currently focused.
         *
         * Note, focused item is not the same as selected item. Focus is reflected in the UI without changing the selection.
         *
         * Also note, this only sets the `_focused` and `_focusedItem` properties. After the element renders
         * the implementation should change the `tabindex` of the focused item to `0`.
         */
        focusListItem(node) {
            this.focused = node.dataset.key;
            this.focusedItem = node;
        }
        /**
         * Finds a valid list item that is a parent to the current item.
         *
         * @param node The current node located somewhere in the tree.
         * @returns The nearest parent list item or undefined when not found.
         */
        findParentListItem(node) {
            let parent = node.parentElement;
            while (parent) {
                if (this.isValidListItem(parent)) {
                    return parent;
                }
                // when the `parentElement` is ShadowRoot then it's parent is `null` so the loop breaks.
                parent = parent.parentElement;
            }
            return undefined;
        }
        /**
         * From a `<li>` it finds the last `<li>` descendant in the tree.
         *
         * ```html
         * <ul>
         *  <li> <-- this is the current node
         *    <div>...</div>
         *    <ul>
         *       <li>...</li>
         *       <li>  <-- this is the last list item.
         *          <div>...</div>
         *          <ul hidden> <- this is ignored.
         *            <li></li>
         *            <li></li>  <-- this is not the last list item because the parent list is hidden.
         *          </ul>
         *      </li>
         *    </ul>
         *  </li>
         * </ul>
         * ```
         *
         * @param node The current list item to find the last descendant for.
         * @returns The last list item or undefined.
         */
        findLastDescendant(node) {
            const list = node.querySelector('ul:not([hidden])');
            if (!list) {
                return undefined;
            }
            const li = list.querySelector('li[data-key][data-kind]:last-of-type:not([aria-disabled="true"])');
            if (!li) {
                return undefined;
            }
            const result = this.findLastDescendant(li);
            return result || li;
        }
        /**
         * Focuses on a list descendant of a folder item.
         * It does nothing when the `node` is not a folder item or the folder has no selectable children.
         *
         * @param node The node to use as a reference. The current focused node by default.
         * @returns true when focused on any of the descendants
         */
        focusFirstDescendant(node = this.focusedItem) {
            if (!node) {
                return false;
            }
            const list = node.querySelector('ul:not([aria-hidden="true"])');
            if (!list) {
                return false;
            }
            const child = list.querySelector('li[data-key][data-kind]:not([aria-disabled="true"])');
            if (!child) {
                return false;
            }
            this.focusListItem(child);
            return true;
        }
        /**
         * Focuses on the next node relative to the passed node.
         *
         * @param node The node to use as a starting point. When not set it uses the currently focused node.
         * @returns true when focused on any of the next siblings
         */
        focusNextSibling(node = this.focusedItem) {
            if (!node) {
                return false;
            }
            let current = node.nextElementSibling;
            while (current) {
                if (this.isValidListItem(current)) {
                    this.focusListItem(current);
                    return true;
                }
                current = current.nextElementSibling;
            }
            return false;
        }
        /**
         * Focuses on the previous node relative to the passed node.
         *
         * @param node The node to use as a starting point. When not set it uses the currently focused node.
         * @returns true when focused on any of the previous siblings
         */
        focusPreviousSibling(node = this.focusedItem) {
            if (!node) {
                return false;
            }
            let current = node.previousElementSibling;
            while (current) {
                if (this.isValidListItem(current)) {
                    const last = this.findLastDescendant(current);
                    if (last) {
                        this.focusListItem(last);
                        return true;
                    }
                    this.focusListItem(current);
                    return true;
                }
                current = current.previousElementSibling;
            }
            return false;
        }
        /**
         * Focuses on a first list item that accepts focus relative to the given node.
         *
         * @param node The node to use as a reference. The current focused node by default.
         * @returns true when focused on any of the parents
         */
        focusFirstParent(node) {
            if (!node) {
                return false;
            }
            const parent = this.findParentListItem(node);
            if (parent) {
                this.focusListItem(parent);
                return true;
            }
            return false;
        }
        findClosestListItem(e) {
            const target = e.composedPath()[0];
            return target.closest('li[data-key][data-kind]:not([aria-disabled="true"])') || undefined;
            // const path = e.composedPath()
            // while (path.length) {
            //   const target = path.shift() as Node
            //   if (!target) {
            //     break
            //   }
            //   if (target.nodeType !== Node.ELEMENT_NODE) {
            //     continue
            //   }
            //   if (!this._isValidListItem(target as Element)) {
            //     continue
            //   }
            //   return target as HTMLLIElement
            // }
            // return undefined
        }
        /**
         * Dispatched the `select` custom event with the `key` and `kind` on the detail.
         *
         * @param key The key of the selected item.
         * @param kind The kind of the selected item
         * @param parent Optional parent item.
         */
        notifySelection(key, kind, parent, root) {
            this.dispatchEvent(new CustomEvent('select', {
                detail: { key, kind, parent, root },
            }));
        }
        /**
         * A handler for the `dragstart` event dispatched when the item is draggable.
         * Calls the `setupDataTransfer()` if has valid target.
         */
        handleItemDragStart(e) {
            if (e.defaultPrevented) {
                return;
            }
            const node = this.findClosestListItem(e);
            const dt = e.dataTransfer;
            if (!dt || !node) {
                return;
            }
            this.draggingItem = node;
            // do not cancel this event
            this.setupDataTransfer(node, dt);
        }
        /**
         * A handler for the `dragend` event dispatched when the item is draggable.
         * @param e The dragend event.
         */
        handleItemDragEnd(e) {
            if (e.defaultPrevented) {
                return;
            }
            const dt = e.dataTransfer;
            if (!dt || !this.draggingItem) {
                return;
            }
            this.unloadDataTransfer(this.draggingItem, dt);
            this.draggingItem = undefined;
            // const node = this.findClosestListItem(e)
            // const dt = e.dataTransfer
            // if (!dt || !node) {
            //   return
            // }
            // e.preventDefault()
            // this.unloadDataTransfer(node, dt)
        }
        handleItemDragOver(e) {
            if (e.defaultPrevented) {
                return;
            }
            const node = this.findClosestListItem(e);
            if (!node) {
                return;
            }
            const dt = e.dataTransfer;
            if (!dt) {
                return;
            }
            // This is required to enable drop event.
            e.preventDefault();
            const validTarget = this.isValidDropTarget(node, dt);
            if (validTarget) {
                node.classList.add('drop-target');
            }
            if (node.classList.contains('parent-item')) {
                this.scheduleGroupOpen(node.dataset.key, node);
            }
            this.processDragOver(node, dt, validTarget);
        }
        handleItemDragLeave(e) {
            if (e.defaultPrevented) {
                return;
            }
            // const node = e.composedPath()[0] as HTMLElement
            // if (node.localName !== 'li') {
            //   // We get a lot of false positive here when the user drags over the list item.
            //   // which interfeers with the dragleave event.
            //   // Because of that, we onlt react when the leave hapens on a list item only.
            //   return
            // }
            const node = this.findClosestListItem(e);
            if (!node) {
                return;
            }
            const dt = e.dataTransfer;
            if (!dt) {
                return;
            }
            node.classList.remove('drop-target');
            if (node.classList.contains('parent-item')) {
                this.cancelGroupOpen(node.dataset.key);
            }
            this.processDragLeave(node, dt);
        }
        handleItemDrop(e) {
            if (e.defaultPrevented) {
                return;
            }
            const node = this.findClosestListItem(e);
            if (!node) {
                return;
            }
            const dt = e.dataTransfer;
            if (!dt) {
                return;
            }
            // This is required to prohibit the default action.
            e.preventDefault();
            node.classList.remove('drop-target');
            this.unloadDataTransfer(node, dt);
            this.processDrop(node, dt);
        }
        /**
         * Sets the common properties on the `DataTransfer`. Implementations can override this method to add
         * own properties.
         */
        setupDataTransfer(item, dt) {
            const kind = item.dataset.kind;
            const key = item.dataset.key;
            if (!key || !kind) {
                return;
            }
            dt.setData('text/kind', kind);
            dt.setData(kind, '');
            dt.setData('text/key', key);
            dt.setData('text/plain', key);
            dt.setData('text/source', this.localName);
            dt.effectAllowed = 'copyMove';
            // Setup the image
            const clone = item.cloneNode(true);
            // removes children + toggle buttons
            clone.querySelectorAll('ul,.group-toggle-icon').forEach((i) => {
                i.remove();
            });
            // Setup the dragged width.
            const content = item.querySelector('.list-item-content') || item;
            clone.style.width = `${content.clientWidth}px`;
            // Marks the clone as a drag image and selected
            clone.classList.add('drag-image', 'selected');
            const root = this.shadowRoot;
            root.appendChild(clone);
            dt.setDragImage(clone, 0, 0);
        }
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        unloadDataTransfer(item, dt) {
            const root = this.shadowRoot;
            root.querySelectorAll('.drag-image').forEach((i) => {
                i.remove();
            });
        }
        handleNameInputKeydown(e) {
            e.stopPropagation();
            if (e.key === 'Escape') {
                this.edited = undefined;
            }
            else if (e.key === 'Enter') {
                const input = e.target;
                const { key, kind } = input.dataset;
                if (!key || !kind) {
                    return;
                }
                this.commitName(key, kind, input.value.trim());
            }
        }
        /**
         * Handles an event when something is dragged over another item.
         * Mind that it can be anything (files, text, etc.) so the implementation should
         * check the type of the data transfer.
         *
         * @param item The item that an object is dragged overt that item.
         * @param dt Data transfer instance.
         * @param validTarget True when the target is a valid drop target.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        processDragOver(item, dt, validTarget) {
            // ...
        }
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        processDragLeave(item, dt) {
            // ...
        }
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        processDrop(item, dt) {
            // ...
        }
        /**
         * To be implemented by the child class.
         * Checks whether the target is a valid drop target.
         * When used, it simplifies the sub-class logic by using this classes drag and drop handling.
         * @param target The target item that the object is dragged over.
         * @param dt The data transfer instance.
         * @returns true when the target is a valid drop target.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        isValidDropTarget(target, dt) {
            return false;
        }
        /**
         * A map of timeouts for opening groups.
         */
        groupOpenTimeouts = new Map();
        /**
         * Schedules opening a group after a delay.
         * @param key The key of the group to open.
         * @param item The list item element.
         */
        scheduleGroupOpen(key, item) {
            // console.log(Date.now(), 'SchemaDesignNavigation.scheduleGroupOpen', key)
            if (item.ariaExpanded === 'true' || this.groupOpenTimeouts.has(key)) {
                // console.log(Date.now(), 'NOT NEEDED', key, item.ariaExpanded === 'true', this.groupOpenTimeouts.has(key))
                return;
            }
            const timeoutId = window.setTimeout(() => {
                this.openGroup(key);
                this.groupOpenTimeouts.delete(key);
            }, 700);
            this.groupOpenTimeouts.set(key, timeoutId);
        }
        /**
         * Cancels a scheduled group open.
         * @param key The key of the group to cancel.
         */
        cancelGroupOpen(key) {
            // console.log(Date.now(), 'SchemaDesignNavigation.cancelGroupOpen', key)
            const timeoutId = this.groupOpenTimeouts.get(key);
            if (timeoutId !== undefined) {
                window.clearTimeout(timeoutId);
                this.groupOpenTimeouts.delete(key);
            }
        }
        handleNameCommit(e) {
            e.preventDefault();
            const input = e.currentTarget.previousElementSibling;
            if (!input || input.localName !== 'input') {
                return;
            }
            const { key, kind } = input.dataset;
            if (!key || !kind) {
                return;
            }
            this.commitName(key, kind, input.value.trim());
        }
        /**
         * This should be overwritten by the implementation that support name change.
         * Method called when the user request to change a name of an item.
         *
         * @param key The key of the item.
         * @param kind The kind of the item.
         * @param name THe new name.
         */
        commitName(key, kind, name) {
            // eslint-disable-next-line no-console
            console.warn(`The commit name method is not set in the child class for ${key}, ${kind}, and ${name}.`);
        }
        handleDoubleClick(e) {
            if (e.defaultPrevented) {
                return;
            }
            const node = this.findClosestListItem(e);
            if (!node) {
                return;
            }
            this.handleEnterAction(node);
        }
        handleClick(e) {
            if (e.defaultPrevented) {
                return;
            }
            const node = this.findClosestListItem(e);
            if (!node) {
                if (e.target === this) {
                    this.deselect();
                }
                return;
            }
            this.handleEnterAction(node);
            this.focusListItem(node);
            this.requestUpdate();
        }
        handleKeydown(e) {
            const node = this.findClosestListItem(e);
            if (!node) {
                return;
            }
            switch (e.key) {
                case 'ArrowRight':
                    this.handleRightAction(node);
                    e.preventDefault();
                    break;
                case 'ArrowLeft':
                    this.handleLeftAction(node);
                    e.preventDefault();
                    break;
                case 'ArrowDown':
                    this.handleDownAction(node);
                    e.preventDefault();
                    break;
                case 'ArrowUp':
                    this.handleUpAction(node);
                    e.preventDefault();
                    break;
                case 'Home':
                    this.handleHomeAction();
                    e.preventDefault();
                    break;
                case 'End':
                    this.handleEndAction();
                    e.preventDefault();
                    break;
                case 'Enter':
                    this.handleEnterAction(node);
                    e.preventDefault();
                    break;
                default:
            }
        }
        renderLabel() {
            const { label } = this;
            if (!label) {
                return nothing;
            }
            return html ` <div class="navigation-title">${label}</div> `;
        }
        deselect() {
            this.dispatchEvent(new Event('deselect'));
            this.selected = undefined;
            this.focused = undefined;
            this.focusedItem = undefined;
        }
        /**
         * Performs the default action of the currently focused node.
         * For parent nodes, it opens or closes the node.
         * In single-select trees, if the node has no children, selects the current node
         * if not already selected (which is the default action).
         */
        handleEnterAction(node) {
            const key = node.dataset.key;
            const isFolder = node.classList.contains('parent-item');
            if (isFolder) {
                this.toggleGroup(key);
            }
            // don't guard this with checking whether the selection changed.
            // In a tabbed / split layout the item can be closed while the navigation item still can
            // be selected.
            this.selected = key;
            this.notifySelection(key, node.dataset.kind, node.dataset.parent, node.dataset.root);
        }
        /**
         * 1. When focus is on a closed node, opens the node; focus does not move.
         * 2. When focus is on a open node, moves focus to the first child node.
         * 3. When focus is on an end node (a tree item with no children), does nothing.
         */
        handleRightAction(node) {
            const isFolder = node.classList.contains('parent-item');
            if (!isFolder) {
                return;
            }
            const key = node.dataset.key;
            if (!this.opened.has(key)) {
                // #1
                this.opened.add(key);
                this.requestUpdate();
                return;
            }
            const size = Number(node.dataset.size);
            if (Number.isNaN(size) || !size) {
                // #3
                return;
            }
            // #2
            this.focusFirstDescendant(node);
        }
        /**
         * 1. When focus is on an open node, closes the node.
         * 2. When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
         * 3. When focus is on a closed `tree`, does nothing.
         */
        handleLeftAction(node) {
            const key = node.dataset.key;
            const isFolder = node.classList.contains('parent-item');
            if (isFolder) {
                if (this.opened.has(key)) {
                    // #1
                    this.opened.delete(key);
                    this.requestUpdate();
                    return;
                }
            }
            // #2
            this.focusFirstParent(node);
            // note, #3 won't happen here as the outer tree is always opened.
        }
        /**
         * Moves focus to the next node that is focusable without opening or closing a node.
         */
        handleDownAction(node) {
            const isFolder = node.classList.contains('parent-item');
            // try focusing on any child
            if (isFolder && this.focusFirstDescendant(node)) {
                return;
            }
            // try focus on any next item.
            if (this.focusNextSibling(node)) {
                return;
            }
            let parent = this.findParentListItem(node);
            while (parent) {
                if (this.focusNextSibling(parent)) {
                    return;
                }
                parent = this.findParentListItem(parent);
            }
        }
        /**
         * Moves focus to the previous node that is focusable without opening or closing a node.
         */
        handleUpAction(node) {
            if (this.focusPreviousSibling(node)) {
                return;
            }
            this.focusFirstParent(node);
        }
        handleHomeAction() {
            const first = this.shadowRoot.querySelector('ul');
            if (!first) {
                return;
            }
            const children = Array.from(first.children);
            for (const node of children) {
                if (this.isValidListItem(node)) {
                    this.focusListItem(node);
                    break;
                }
            }
        }
        handleEndAction() {
            const first = this.shadowRoot.querySelector('ul');
            if (!first) {
                return;
            }
            const children = Array.from(first.children).reverse();
            for (const node of children) {
                if (this.isValidListItem(node)) {
                    this.focusListItem(node);
                    break;
                }
            }
        }
        /**
         *
         * @param contents The children contents.
         * @returns A predefined template for the outer list element.
         */
        renderOuterList(contents) {
            return html `
      <ul
        role="tree"
        aria-multiselectable="false"
        aria-labelledby="${this.label ? 'navigation-title' : nothing}"
        @dblclick="${this.handleDoubleClick}"
        @keydown="${this.handleKeydown}"
        @dragstart="${this.handleItemDragStart}"
        @dragend="${this.handleItemDragEnd}"
        @dragover="${this.handleItemDragOver}"
        @dragleave="${this.handleItemDragLeave}"
        @drop="${this.handleItemDrop}"
        class="root"
      >
        ${contents}
      </ul>
    `;
        }
        renderParentListItem(key, kind, label, children, opts = {}) {
            const { selected, focused: _focused, edited } = this;
            const opened = this.opened.has(key);
            const classes = {
                'parent-item': true,
                'selected': selected === key,
                'focused': _focused === key,
                opened,
            };
            return html `
      <li
        class="${classMap(classes)}"
        role="treeitem"
        aria-selected="false"
        aria-expanded="${opened ? 'true' : 'false'}"
        data-parent="${ifDefined(opts.parent)}"
        data-root="${ifDefined(opts.root)}"
        data-key="${key}"
        data-kind="${kind}"
        draggable="${opts.draggable ? 'true' : 'false'}"
        aria-disabled="${opts.disabled ? 'true' : 'false'}"
      >
        ${edited === key ? this.renderNameInput(key, kind, label) : this.renderParentListItemContent(label, opts)}
        <ul
          role="group"
          id="group${key}"
          data-parent="${ifDefined(opts.parent)}"
          ?hidden="${!opened}"
          aria-hidden="${opened ? 'false' : 'true'}"
        >
          ${children}
        </ul>
      </li>
    `;
        }
        renderParentListItemContent(label, opts = {}) {
            return html `
      <div class="list-item-content parent-content">
        <ui-icon icon="chevronRight" class="group-toggle-icon"></ui-icon>
        ${opts.parentIcon ? html `<ui-icon icon="${opts.parentIcon}" class="object-icon"></ui-icon>` : ''}
        <span class="label" title="${label}">${label}</span>
      </div>
    `;
        }
        renderListItem(key, kind, label, content, opts = {}) {
            const { selected, focused, edited } = this;
            if (edited === key) {
                const input = this.renderNameInput(key, kind, label);
                return html `
        <li
          role="treeitem"
          aria-selected="false"
          class="name-change-item"
          data-parent="${ifDefined(opts.parent)}"
          data-root="${ifDefined(opts.root)}"
          data-key="${key}"
          data-kind="${kind}"
        >
          ${input}
        </li>
      `;
            }
            const classes = {
                selected: selected === key,
                focused: focused === key,
            };
            return html `
      <li
        class="${classMap(classes)}"
        role="treeitem"
        aria-selected="false"
        data-parent="${ifDefined(opts.parent)}"
        data-root="${ifDefined(opts.root)}"
        data-key="${key}"
        data-kind="${kind}"
        draggable="${opts.draggable ? 'true' : 'false'}"
        aria-disabled="${opts.disabled ? 'true' : 'false'}"
      >
        <div class="list-item-content">${content}</div>
      </li>
    `;
        }
        renderNameInput(key, kind, oldName) {
            return html `
      <div class="name-change" @click="${cancelEvent}" @keydown="${cancelEvent}" @dblclick="${cancelEvent}">
        <input
          type="text"
          .value=${oldName || ''}
          @keydown="${this.handleNameInputKeydown}"
          data-key="${key}"
          data-kind="${kind}"
        />
        <ui-icon-button @click="${this.handleNameCommit}" aria-label="Confirm name change" class="name-commit-button">
          <ui-icon icon="check"></ui-icon>
        </ui-icon-button>
      </div>
    `;
        }
        renderItemContent(icon, label) {
            return html `
      <ui-icon icon="${icon}" class="object-icon"></ui-icon>
      <span class="label" title="${label}">${label}</span>
    `;
        }
    };
})();
/**
 * A base class for navigation elements helping creating specific implementation of the view.
 * The implementations sets their own properties with the data that are used to generate the view.
 *
 * The generated menu must have the following structure created by the implementations:
 *
 * ```html
 * <ul aria-label="..." aria-multiselectable="false">
 *  <li role="treeitem" data-key="..." data-kind="..." data-draggable="true|false">
 *    <div class="list-item-content"><span class="label" title="...">...</span></div>
 *  </li>
 *  <li role="treeitem" data-key="..." data-kind="..." data-draggable="true" aria-expanded="true" class="parent-item">
 *    <div class="list-item-content">
 *      <ui-icon icon="chevronRight" class="group-toggle-icon"></ui-icon>
 *      <span class="label" title="...">...</span>
 *    </div>
 *    <ul role="group" data-parent="..." ?hidden aria-hidden="true|false">
 *      <li role="treeitem" data-key="..." data-parent="..." data-kind="..." data-draggable="true|false">
 *        <div class="list-item-content">
 *          <ui-icon icon="..." class="item-icon"></ui-icon>
 *          <span class="label" title="...">...</span>
 *        </div>
 *      </li>
 *    </ul>
 *  </li>
 * </ul>
 * ```
 *
 * Additionally, the child element must include styles from './AppNavigation.styles.js`.
 *
 * Assumptions when running the menu:
 *
 * - all `li` items that do not have `aria-disabled` attribute are considered active tree items (learn more why not to use "disabled" attribute https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled)
 * - all `li` elements have `data-key` and `data-kind` attributes set. Elements without these properties are ignored.
 * - all `ul` and `li` elements that have a parent also have `data-parent` with the value of the parent's key.
 *    When this is not set the element is considered a top level element.
 * - the implementations does not manage the `tabindex` of any item. Instead set the `_focusedItem` to
 *    the currently focused element and the base will manage the focus for you (after the update is complete).
 *
 * @fires {CustomEvent} select - Custom, non bubbling event when a selection is made.
 *        It has the `key` and `kind` properties on the detail panel with optional `parent` id.
 * @fires {Event} deselect - Custom, non bubbling event when the selection is cleared.
 */
export default AppNavigation;
//# sourceMappingURL=AppNavigationElement.js.map