# Source: List.js

    /**
     * A list which supports a "rich rendering" of the items it contains
     * (item values may be rendered using HTML).
     * <p>Part of the XUI module which, for now, has an undocumented API.
     */
    export class List extends HTMLElement {
        constructor() {
            super();

            this._singleClickToAccept = false;

            this.addEventListener("click", this.onClick.bind(this));
            this.addEventListener("keydown", this.onKeydown.bind(this));

            this._connected = false;
            this._labelMaker = null;
            this._htmlRenderer = null;
            this._disabledChecker = null;
            this._items = [];
            this._selection = [];
            this._anchor = this._extent = -1;
        }

        // -----------------------------------------------------------------------
        // Custom element
        // -----------------------------------------------------------------------

        connectedCallback() {
            if (!this._connected) {
                this._connected = true;

                // User code may have already added its own classes.
                this.classList.add("xui-control");
                // tabindex="0" makes this element focusable and tab-able.
                this.setAttribute("tabindex", "0");

                if (!this.hasAttribute("selectionmode")) {
                    this.selectionMode = "single";
                }
            }
        }

        static get observedAttributes() {
            return [ "selectionmode" ];
        }

        attributeChangedCallback(attrName, oldVal, newVal) {
            if (newVal === "single" && oldVal === "multiple") {
                let index = this.getSelection();
                if (index >= 0) {
                    this.setSelection(index);
                }
            }
        }

        // -----------------------------------------------------------------------
        // Input events
        // -----------------------------------------------------------------------

        get singleClickToAccept() {
            return this._singleClickToAccept;
        }

        set singleClickToAccept(single) {
            // For use by AutocompleteField.
            this._singleClickToAccept = single;
        }

        onClick(event) {
            if (event.button === 0 && event.detail === 1) {
                // First click on primary button.
                Util.consumeEvent(event);

                if (this.length > 0) {
                    let index = this.clickEventToIndex(event);
                    if (index >= 0) {
                        if (event.shiftKey) {
                            this.extendSelection(index, /*clickCount*/ 1);
                        } else if (Util.modKey(event)) {
                            this.toggleSelection(index, /*clickCount*/ 1);
                        } else {
                            let changed =
                                this.changeSelection(index, /*clickCount*/ 1);

                            if (!changed &&
                                this.singleClickToAccept &&
                                this.isSingleSelectionMode()) {
                                // Clicking on an item already selected in the
                                // list will not cause the selectionchanged event
                                // to be sent hence, without this trick,
                                // AutocompleteField.acceptSelection would not be
                                // invoked.

                                this.selectionChanged(/*prevSelection*/ [], 1);
                            }
                        }
                    }
                }

                this.focus();
            }
        }

        clickEventToIndex(event) {
            let itemDiv = null;
            let elem = event.target;
            while (elem !== null && !Object.is(elem, this)) {
                if (elem.classList.contains("xui-lst-item")) {
                    itemDiv = elem;
                    break;
                }

                elem = elem.parentElement;
            }

            let index = -1;
            if (itemDiv !== null) {
                let i = 0;
                let child = this.firstElementChild;
                while (child !== null) {
                    if (Object.is(child, itemDiv)) {
                        index = i;
                        break;
                    }

                    ++i;
                    child = child.nextElementSibling;
                }
            }

            return index;
        }

        extendSelection(index, clickCount=0) {
            if (this.isSingleSelectionMode()) {
                this.changeSelection(index, clickCount);
                return;
            }

            // ---

            let curAnchor = this._anchor;
            if (curAnchor < 0) {
                this.setAnchor(0);
                curAnchor = this._anchor;
            }

            let from, to;
            if (index < curAnchor) {
                from = index;
                to = curAnchor;
            } else if (index > curAnchor) {
                from = curAnchor;
                to = index;
            } else {
                from = to = index;
            }
            let sel = [];
            for (let i = from; i <= to; ++i) {
                sel.push(i);
            }
            if (!List.sameSelection(sel, this._selection)) {
                // This clears anchor and extent.
                let prevSelection = this.setSelection(...sel);
                this.selectionChanged(prevSelection, clickCount);
            }

            this.setAnchor(curAnchor); // Restore anchor.
            this._extent = index;
        }

        static sameSelection(a1, a2) {
            let count;
            if ((count = a1.length) !== a2.length) {
                return false;
            }

            for (let i = 0; i < count; ++i) {
                if (a1[i] !== a2[i]) {
                    return false;
                }
            }

            return true;
        }

        toggleSelection(index, clickCount=0) {
            let sel = [ ...this._selection ]; // Never modify this._selection
            let pos = sel.indexOf(index);
            if (pos >= 0) {
                // Selected ==> unselect.
                sel.splice(pos, 1);
            } else {
                // Not selected ==> select.
                if (this.isSingleSelectionMode()) {
                    sel = [ index ];
                } else {
                    sel.push(index);
                }
            }
            let prevSelection = this.setSelection(...sel);
            this.selectionChanged(prevSelection, clickCount);

            this.setAnchor(index);
        }

        changeSelection(index, clickCount=0) {
            let changed = false;
            if (this._selection.length !== 1 || this._selection[0] !== index) {
                let prevSelection = this.setSelection(index);
                changed = this.selectionChanged(prevSelection, clickCount);
            }
            // Otherwise, nothing to do.

            this.setAnchor(index);

            return changed;
        }

        selectionChanged(prevSelection, clickCount=0) {
            if (List.sameSelection(this._selection, prevSelection)) {
                return false;
            }

            let event = new CustomEvent("selectionchanged",
                                        { bubbles: false, composed: false,
                                          cancelable: false,
                                          detail: { selection: this.getSelection(),
                                                    clickCount: clickCount } });
            this.dispatchEvent(event);

            return true;
        }

        onKeydown(event) {
            let isHotKey = true;
            switch (event.key) {
            // Ctrl-PageUp and Ctrl-PageDown not sent by Chrome & Firefox.
            case "PageUp":
                this.onUpKey(event.shiftKey, Util.modKey(event), 10);
                break;
            case "PageDown":
                this.onDownKey(event.shiftKey, Util.modKey(event), 10);
                break;
            case "ArrowUp":
            case "Up":
                this.onUpKey(event.shiftKey, Util.modKey(event));
                break;
            case "ArrowDown":
            case "Down":
                this.onDownKey(event.shiftKey, Util.modKey(event));
                break;
            case "Home":
                this.onHomeKey(event.shiftKey, Util.modKey(event));
                break;
            case "End":
                this.onEndKey(event.shiftKey, Util.modKey(event));
                break;
            case " ": // Space bar
                this.onSpaceKey(event.shiftKey, Util.modKey(event));
                break;
            case "a":
                this.onAKey(event.shiftKey, Util.modKey(event));
                break;
            default:
                isHotKey = false;
                break;
            }

            if (isHotKey) {
                // Whatever its modifiers.
                Util.consumeEvent(event);
            }
        }

        // ---------------------------------------
        // Low-level API used by AutocompleteField
        // ---------------------------------------

        onUpKey(isShift, isMod, skip=1) {
            if (this.length === 0) {
                return;
            }

            let curExtent = this._extent;
            if (this._anchor < 0) {
                this.setAnchor(this.length-1);
                curExtent = this.length + skip - 1; // See nextExtent below.
            }

            let nextExtent = curExtent - skip;
            if (nextExtent < 0) {
                nextExtent = 0;
            }

            if (isShift) {
                this.extendSelection(nextExtent);
            } else if (isMod) {
                this.setAnchor(nextExtent);
            } else {
                this.changeSelection(nextExtent);
            }

            if (this._extent >= 0) {
                this.ensureIsVisible(this._extent);
            }
        }

        onDownKey(isShift, isMod, skip=1) {
            if (this.length === 0) {
                return;
            }

            let curExtent = this._extent;
            if (this._anchor < 0) {
                this.setAnchor(0);
                curExtent = -skip; // See nextExtent below.
            }

            let nextExtent = curExtent + skip;
            if (nextExtent >= this.length) {
                nextExtent = this.length-1;
            }

            if (isShift) {
                this.extendSelection(nextExtent);
            } else if (isMod) {
                this.setAnchor(nextExtent);
            } else {
                this.changeSelection(nextExtent);
            }

            if (this._extent >= 0) {
                this.ensureIsVisible(this._extent);
            }
        }

        onHomeKey(isShift, isMod) {
            if (this.length === 0) {
                return;
            }

            if (this._anchor < 0) {
                this.setAnchor(0);
            }

            let nextExtent = 0;

            if (isShift) {
                this.extendSelection(nextExtent);
            } else if (isMod) {
                this.setAnchor(nextExtent);
            } else {
                this.changeSelection(nextExtent);
            }

            if (this._extent >= 0) {
                this.ensureIsVisible(this._extent);
            }
        }

        onEndKey(isShift, isMod) {
            if (this.length === 0) {
                return;
            }

            if (this._anchor < 0) {
                this.setAnchor(this.length-1);
            }

            let nextExtent = this.length-1;

            if (isShift) {
                this.extendSelection(nextExtent);
            } else if (isMod) {
                this.setAnchor(nextExtent);
            } else {
                this.changeSelection(nextExtent);
            }

            if (this._extent >= 0) {
                this.ensureIsVisible(this._extent);
            }
        }

        onSpaceKey(isShift, isMod) {
            if (this.length === 0) {
                return;
            }

            if (this._anchor >= 0) {
                if (isMod) {
                    this.toggleSelection(this._anchor);
                } else {
                    this.changeSelection(this._anchor);
                }
            }

            if (this._extent >= 0) {
                this.ensureIsVisible(this._extent);
            }
        }

        onAKey(isShift, isMod) {
            if (this.length === 0) {
                return;
            }

            if (isMod) {
                let prevSelection = this.selectAll();
                this.selectionChanged(prevSelection);
            }

            if (this._extent >= 0) {
                this.ensureIsVisible(this._extent);
            }
        }

        // -----------------------------------------------------------------------
        // API
        // -----------------------------------------------------------------------

        get selectionMode() {
            return this.getAttribute("selectionmode");
        }

        set selectionMode(mode) {
            this.setAttribute("selectionmode",
                              (mode === "multiple")? mode : "single");
        }

        isSingleSelectionMode() {
            return (this.selectionMode === "single");
        }

        get labelMaker() {
            return this._labelMaker;
        }

        set labelMaker(maker) {
            this._labelMaker = maker;
        }

        get htmlRenderer() {
            return this._htmlRenderer;
        }

        set htmlRenderer(renderer) {
            this._htmlRenderer = renderer;
        }

        get disabledChecker() {
            return this._disabledChecker;
        }

        set disabledChecker(checker) {
            this._disabledChecker = checker;
        }

        get length() {
            return this._items.length;
        }

        get(index) {
            let item = null;
            if (index >= 0 && index < this.length) {
                item = this._items[index];
            }

            return item;
        }

        getLabel(index) {
            let label = null;
            if (index >= 0 && index < this.length) {
                label = this.itemLabel(this._items[index]);
            }

            return label;
        }

        itemLabel(item) {
            let label = null;
            if (this._labelMaker !== null) {
                try {
                    label = this._labelMaker(item);
                } catch (error) {
                    console.error(`XUI.List.labelMaker has failed: ${error}`);
                }
            }
            if (label === null) {
                label = item.toString();
            }

            return label;
        }

        isDisabled(index) {
            let disabled = false;
            if (index >= 0 && index < this.length) {
                disabled = this.itemIsDisabled(this._items[index]);
            }

            return disabled;
        }

        itemIsDisabled(item) {
            let disabled = false;
            if (this._disabledChecker !== null) {
                try {
                    disabled = this._disabledChecker(item);
                } catch (error) {
                    console.error(`XUI.List.disabledChecker has failed: ${error}`);
                }
            }

            return disabled;
        }

        getAll(copy=true) {
            if (copy) {
                return [ ...this._items ]; // A copy that can be modified.
            } else {
                return this._items; // Must not be modified. Use with care.
            }
        }

        setAll(items) {
            this.removeAll();

            this._items = [ ...items ]; // Our own, private, copy.

            let i = 0;
            for (let item of items) {
                let child = document.createElement("div");
                child.setAttribute("class", "xui-lst-item");
                this.populateChild(child, item, i++);

                this.appendChild(child);
            }

            this.update(0);

            this.ensureIsVisible(0);
        }

        removeAll() {
            this._items = [];
            this._selection = [];
            this._anchor = -1;

            Util.removeAllChildren(this);
        }

        populateChild(child, item, index) {
            child.innerHTML = this.itemInnerHTML(item, index);
            if (this.itemIsDisabled(item)) {
                child.classList.add("xui-lst-disabled");
            } else {
                child.classList.remove("xui-lst-disabled");
            }
        }

        itemInnerHTML(item, index) {
            let html = null;
            if (this._htmlRenderer !== null) {
                // index may be rendered by client code too.
                try {
                    html = this._htmlRenderer(item, index);
                } catch (error) {
                    console.error(`XUI.List.htmlRenderer has failed: ${error}`);
                }
            }

            if (html === null) {
                html = Util.escapeHTML(this.itemLabel(item));
                if (html.indexOf('\n') >= 0) {
                    html = html.replaceAll('\n', "<br>");
                }
            }

            return html;
        }

        update(from) {
           this._selection = [];
           this._anchor = -1;

           let i = 0;
           let child = this.firstElementChild;
           while (child !== null) {
               let classList = child.classList;

               if (i >= from) {
                   classList.remove("xui-lst-item0", "xui-lst-item1");
                   classList.add("xui-lst-item" + String(i % 2));
               }

               if (classList.contains("xui-lst-selected")) {
                   this._selection.push(i);
               }
               if (classList.contains("xui-lst-anchor")) {
                   this._anchor = i;
               }

               ++i;
               child = child.nextElementSibling;
           }
        }

        ensureIsVisible(index) {
            const count = this.length;
            if (count === 0) {
                // Nothing to do.
                return;
            }

            if (index < 0) {
                index = 0;
            } else if (index >= count) {
                index = count-1;
            }
            this.children[index].scrollIntoView({ block: "nearest",
                                                  inline: "nearest" });
        }

        set(index, item) {
            let replaced = null;
            if (index >= 0 && index < this.length) {
                replaced = this._items[index];
                this._items[index] = item;

                this.populateChild(this.children[index], item, index);
            }

            return replaced;
        }

        remove(index) {
            let removed = null;
            if (index >= 0 && index < this.length) {
                removed = this._items[index];
                this._items.splice(index, 1);

                this.removeChild(this.children[index]);

                this.update(index);
            }

            return removed;
        }

        add(...newItems) {
            this.insert(this.length, ...newItems);
        }

        insert(index, ...newItems) {
            if (newItems.length === 0) {
                // Nothing to do.
                return;
            }

            const count = this.length;
            if (index < 0) {
                index = 0;
            } else if (index > count) {
                index = count;
            }
            let before = (index === count)? /*at end*/ null : this.children[index];

            let i = index;
            for (let newItem of newItems) {
                let child = document.createElement("div");
                child.setAttribute("class", "xui-lst-item");
                this.populateChild(child, newItem, i++);

                this.insertBefore(child, before);
            }

            this._items.splice(index, 0, ...newItems);

            this.update(index);
        }

        getSelection() {
            if (this.isSingleSelectionMode()) {
                return (this._selection.length > 0)? this._selection[0] : -1;
            } else {
                return [ ...this._selection ]; // A copy that can be modified.
            }
        }

        selectAll() {
            let sel = [];
            const count = this.length;
            for (let i = 0; i < count; ++i) {
                sel.push(i);
            }
            return this.setSelection(...sel);
        }

        clearSelection() {
            return this.setSelection();
        }

        setSelection(...indices) {
            if (this.isSingleSelectionMode() && indices.length > 0) {
                indices = indices.slice(0, 1);
            }

            let prevSelection = this._selection;
            this._selection = [];

            const count = this.length;
            for (let i = 0; i < count; ++i) {
                let item = this._items[i];
                let childClassList = this.children[i].classList;

                if (indices.includes(i) && !this.itemIsDisabled(item)) {
                    this._selection.push(i);

                    childClassList.add("xui-lst-selected");
                } else {
                    childClassList.remove("xui-lst-selected");
                }
            }

            // Forget about these selection marks.
            this.setAnchor(-1);

            return prevSelection;
        }

        // ---------------------------------------
        // Low-level API used by AutocompleteField
        // ---------------------------------------

        setAnchor(index) {
            let oldAnchor = this._anchor;
            this._anchor = this._extent = -1;
            if (index >= 0 && index < this.length) {
                this._anchor = this._extent = index;
            }

            if (this._anchor !== oldAnchor) {
                if (oldAnchor >= 0) {
                    this.children[oldAnchor].classList.remove("xui-lst-anchor");
                }
                if (this._anchor >= 0) {
                    this.children[this._anchor].classList.add("xui-lst-anchor");
                }
            }
        }

        get anchor() {
            return this._anchor;
        }

        get extent() {
            return this._extent;
        }
    }

    window.customElements.define("xui-list", List);