# Source: AutocompleteField.js

    /**
     * A text field supporting autocompletion "a la XXE desktop".
     * <p>Part of the XUI module which, for now, has an undocumented API.
     */
    export class AutocompleteField extends HTMLElement {
        constructor() {
            super();

            this._lenientTextChecker = null;

            this._onSelectionChanged = this.onSelectionChanged.bind(this);
            this._onDoubleClickList = this.onDoubleClickList.bind(this);
            this._onKeydownList = this.onKeydownList.bind(this);

            this._field = null;
            this._list = null;
            this._completionKey = null;
            this._completionKeyIsAlt = this._completionKeyIsCtrl =
                this._completionKeyIsMeta = this._completionKeyIsShift = false;
            this._attrBeingChanged = null;
        }

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

        connectedCallback() {
            if (this._field === null) {
                // User code may have already added its own classes.
                this.classList.add("xui-control");

                if (!this.hasAttribute("for")) {
                    this.for = null;
                }

                if (!this.hasAttribute("acceptmode")) {
                    this.acceptMode = "strict";
                }

                // Force parsing completion key spec.
                this.completionKey = this.getAttribute("completionkey");

                // Nothing to do for "singleclicktoaccept".

                this._field = document.createElement("input");
                // User code may have already added its own classes.
                this._field.classList.add("xui-control", "xui-acfld-field");
                this._field.setAttribute("type", "text");
                this._field.setAttribute("autocomplete", "off");
                this._field.setAttribute("spellcheck", "false");
                this.appendChild(this._field);

                this._field.addEventListener("keydown", this.onKeydown.bind(this));
                this._field.addEventListener("input", this.onInput.bind(this));
            }
        }

        static get observedAttributes() {
            return [ "for", "acceptmode", "completionkey", "singleclicktoaccept" ];
        }

        attributeChangedCallback(attrName, oldVal, newVal) {
            if (this._attrBeingChanged !== null) {
                return;
            }

            switch (attrName) {
            case "for":
                this.for = newVal;
                break;
            case "acceptmode":
                this.acceptMode = newVal;
                break;
            case "completionkey":
                this.completionKey = newVal;
                break;
            case "singleclicktoaccept":
                this.singleClickToAccept = newVal;
                break;
            }
        }

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

        // --------------------------
        // XUI.List listeners
        // --------------------------

        onSelectionChanged(event) {
            let sel = event.detail.selection;
            if (sel < 0) {
                this._field.value = "";
            } else {
                this._field.value = this._list.getLabel(sel);
                if (event.detail.clickCount === 1 && this.singleClickToAccept) {
                    this.acceptSelection();
                }
            }
        }

        onDoubleClickList(event) {
            if (event.button === 0) {
                // Second click on primary button.
                Util.consumeEvent(event);

                if (!this.singleClickToAccept) {
                    this.acceptSelection();
                }
            }
        }

        acceptSelection() {
            if (this._list === null) {
                return false;
            }

            let acceptedText = this._field.value.trim();
            if (acceptedText.length === 0) {
                return false;
            }

            let selectedItem = null;
            let selectedIndex = this._list.getSelection();
            if (selectedIndex >= 0) { // (Hence cannot be disabled.)
                let selectedLabel = this._list.getLabel(selectedIndex);
                // Typing a prefix suffices, not matter acceptMode.
                if (selectedLabel.startsWith(acceptedText)) {
                    selectedItem = this._list.get(selectedIndex);
                    this._field.value = acceptedText = selectedLabel;
                }
            }

            if (selectedItem === null) {
                // What has been typed does not match the selection, if any.
                if (this.acceptMode === "lenient") {
                    if (this._lenientTextChecker !== null) {
                        let checked = false;
                        try {
                          checked = this._lenientTextChecker(acceptedText);
                        } catch (error) {
                          console.error(`XUI.AutocompleteField.lenientTextChecker \
    has failed: ${error}`);
                        }
                        if (!checked) {
                            acceptedText = null;
                        }
                    }
                    // Otherwise, any non-empty text is OK.
                } else {
                    // Strict mode.
                    acceptedText = null;
                }
            }

            if (acceptedText === null) {
                this._field.select();
                return false;
            }

            let event = new CustomEvent("selectionaccepted",
                                        { bubbles: false, composed: false,
                                          cancelable: false,
                                          detail: { acceptedText: acceptedText,
                                                    selectedIndex: selectedIndex,
                                                    selectedItem: selectedItem } });
            this.dispatchEvent(event);

            return true;
        }

        onKeydownList(event) {
            if (event.key === "Enter") { // Whatever its modifiers.
                Util.consumeEvent(event);

                let sel = this._list.getSelection();
                if (sel >= 0) {
                    let label = this._list.getLabel(sel);
                    if (label !== this._field.value) {
                        this._field.value = label;
                        // Just in case.
                        this._list.setAnchor(sel);
                        this._list.ensureIsVisible(sel);
                    }

                    this.acceptSelection();
                }
            }
        }

        // --------------------------
        // Input type=text listeners
        // --------------------------

        onKeydown(event) {
            if (this._list === null) {
                return;
            }

            let isHotKey = true; // Whatever its modifiers.

            switch (event.key) {
            case "ArrowUp":
            case "Up":
            case "ArrowDown":
            case "Down":
                {
                    let sel = this._list.getSelection();
                    let label;
                    if (sel >= 0 &&
                        (label = this._list.getLabel(sel)) !== this._field.value) {
                        this._field.value = label;
                        // Just in case.
                        this._list.setAnchor(sel);
                        this._list.ensureIsVisible(sel);
                    } else {
                        if (event.key === "ArrowUp" || event.key === "Up") {
                            this._list.onUpKey();
                        } else {
                            this._list.onDownKey();
                        }
                    }
                }
                break;
            case "Enter":
                this.acceptSelection();
                break;
            default:
                if (event.key === this._completionKey &&
                    event.altKey === this._completionKeyIsAlt &&
                    event.ctrlKey === this._completionKeyIsCtrl &&
                    event.metaKey === this._completionKeyIsMeta &&
                    event.shiftKey === this._completionKeyIsShift) {
                    this.autoComplete();
                } else {
                    isHotKey = false;
                }
                break;
            }

            if (isHotKey) {
                Util.consumeEvent(event);
            }
        }

        autoComplete() {
            if (this._list === null) {
                return;
            }

            let prefix = this._field.value.trim();
            if (prefix.length === 0) {
                return;
            }

            let index = this.findByPrefix(prefix);
            if (index < 0) {
                return;
            }

            let curLabel = this._list.getLabel(index);
            let nextLabels = [];

            const count = this._list.length;
            for (let i = index+1; i < count; ++i) {
                let label = this._list.getLabel(i);
                if (!label.startsWith(prefix)) {
                    // Here we assume that items are sorted by their labels.
                    break;
                }

                nextLabels.push(label);
            }

            let longestPrefix;
            if (nextLabels.length === 0) {
                longestPrefix = curLabel;
            } else {
                let j = prefix.length;
                const curLabelLength = curLabel.length;
                loop: for (; j < curLabelLength; ++j) {
                    let c = curLabel.charAt(j);

                    for (let nextLabel of nextLabels) {
                        if (j >= nextLabel.length ||
                            nextLabel.charAt(j) !== c) {
                            break loop;
                        }
                    }
                }

                longestPrefix = curLabel.substring(0, j);
            }

            if (longestPrefix !== prefix) {
                this.text = longestPrefix; // This invokes onInput. See below.
            }
        }

        findByPrefix(text) {
            let index = -1;
            const items = this._list.getAll(/*copy*/ false);
            const count = items.length;
            for (let i = 0; i < count; ++i) {
                let item = items[i];
                if (this._list.itemLabel(item).startsWith(text)) {
                    index = i;
                    break;
                }
            }

            return index;
        }

        onInput(event) {
            if (this._list === null) {
                return;
            }

            let prefix = this._field.value.trim();
            if (prefix.length === 0) {
                this._list.clearSelection();
                // Anchor is not set, so we rely on default behavior here.
                this._list.ensureIsVisible(0);
                return;
            }

            let index = this.findByPrefix(prefix);
            if (index < 0) {
                this._list.clearSelection();
                // Be conservative. Do not scroll the list.
                // Anchor is not set, so we rely on default behavior here.
            } else {
                if (this._list.isDisabled(index)) {
                    this._list.clearSelection();
                } else {
                    this._list.setSelection(index);
                }
                this._list.setAnchor(index);
                this._list.ensureIsVisible(index);
            }
        }

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

        get for() {
            return this.getAttribute("for");
        }

        set for(id) {
            this._attrBeingChanged = "for";

            if (this._list !== null) {
                this._list.removeEventListener("selectionchanged",
                                               this._onSelectionChanged);
                this._list.removeEventListener("dblclick", this._onDoubleClickList);
                this._list.removeEventListener("keydown", this._onKeydownList);
                this._list = null;
            }

            if (id !== null && (id = id.trim()).length > 0) {
                this._list = document.getElementById(id);
                if (this._list !== null && this._list.localName !== "xui-list") {
                    this._list = null;
                }
            }

            if (this._list === null) {
                this.removeAttribute("for");
            } else {
                this._list.selectionMode = "single";
                this._list.addEventListener("selectionchanged",
                                            this._onSelectionChanged);
                this._list.addEventListener("dblclick", this._onDoubleClickList);
                this._list.addEventListener("keydown", this._onKeydownList);

                // Clicking on an item already selected in the list will not cause
                // the selectionchanged event to be sent hence, without this
                // trick, acceptSelection would not be invoked.
                this._list.singleClickToAccept = this.singleClickToAccept;

                this.setAttribute("for", id);
            }

            this._attrBeingChanged = null;
        }

        get acceptMode() {
            return this.getAttribute("acceptmode");
        }

        set acceptMode(mode) {
            this._attrBeingChanged = "acceptmode";

            this.setAttribute("acceptmode", (mode === "lenient")? mode : "strict");

            this._attrBeingChanged = null;
        }

        get completionKey() {
            return this.getAttribute("completionkey");
        }

        set completionKey(spec) {
            this._attrBeingChanged = "completionkey";

            this._completionKey = null;
            this._completionKeyIsAlt = this._completionKeyIsCtrl =
                this._completionKeyIsMeta = this._completionKeyIsShift = false;

            if (spec !== null && spec.length > 0) {
                let segments = spec.toLowerCase().split(/[-+]/); // Do not trim.

                if (segments.length > 0) {
                    // Key names are listed here
                    // https://developer.mozilla.org/
                    //    en-US/docs/Web/API/KeyboardEvent/key/Key_Values

                    this._completionKey = segments[segments.length-1];
                    if (this._completionKey === "space") {
                        this._completionKey = " "; // Space.
                    }

                    segments.pop();
                    if (segments.length > 0) {
                        for (let segment of segments) {
                            switch (segment) {
                            case "alt":
                                this._completionKeyIsAlt = true;
                                break;
                            case "ctrl":
                                this._completionKeyIsCtrl = true;
                                break;
                            case "meta":
                                this._completionKeyIsMeta = true;
                                break;
                            case "shift":
                                this._completionKeyIsShift = true;
                                break;
                            case "mod":
                                if (Util.PLATFORM_IS_MAC_OS) {
                                    this._completionKeyIsMeta = true;
                                } else {
                                    this._completionKeyIsCtrl = true;
                                }
                                break;
                            }
                        }
                    }
                }
            }

            if (this._completionKey === null) {
                this.removeAttribute("completionkey");
            } else {
                this.setAttribute("completionkey", spec);
            }

            this._attrBeingChanged = null;
        }

        get singleClickToAccept() {
            return this.hasAttribute("singleclicktoaccept");
        }

        set singleClickToAccept(single) {
            this._attrBeingChanged = "singleclicktoaccept";

            if (single) {
                this.setAttribute("singleclicktoaccept", "singleclicktoaccept");
            } else {
                this.removeAttribute("singleclicktoaccept");
            }

            this._attrBeingChanged = null;

            if (this._list !== null) {
                this._list.singleClickToAccept = single;
            }
        }

        get lenientTextChecker() {
            return this._lenientTextChecker;
        }

        set lenientTextChecker(checker) {
            this._lenientTextChecker = checker;
        }

        get text() {
            return this._field.value;
        }

        set text(value) {
            if (value === null) {
                value = "";
            }
            this._field.value = value;
            this.onInput(/*event*/ null);
        }

        get autofocus() {
            return this._field.autofocus;
        }

        set autofocus(focus) {
            this._field.autofocus = focus;
        }
    }

    window.customElements.define("xui-autocomplete-field", AutocompleteField);