import MicroEvent from "./contrib/microevent.js";
import MicroPlugin from "./contrib/microplugin.js";
import { Sifter } from '@orchidjs/sifter';
import { escape_regex } from '@orchidjs/unicode-variants';
import { highlight, removeHighlight } from "./contrib/highlight.js";
import * as constants from "./constants.js";
import getSettings from "./getSettings.js";
import { hash_key, get_hash, escape_html, debounce_events, getSelection, preventDefault, addEvent, loadDebounce, timeout, isKeyDown, getId, addSlashes, append, iterate } from "./utils.js";
import { getDom, isHtmlString, escapeQuery, triggerEvent, applyCSS, addClasses, removeClasses, parentMatch, getTail, isEmptyObject, nodeIndex, setAttr, replaceNode } from "./vanilla.js";
var instance_i = 0;
export default class TomSelect extends MicroPlugin(MicroEvent) {
    constructor(input_arg, user_settings) {
        super();
        this.order = 0;
        this.isOpen = false;
        this.isDisabled = false;
        this.isReadOnly = false;
        this.isInvalid = false; // @deprecated 1.8
        this.isValid = true;
        this.isLocked = false;
        this.isFocused = false;
        this.isInputHidden = false;
        this.isSetup = false;
        this.ignoreFocus = false;
        this.ignoreHover = false;
        this.hasOptions = false;
        this.lastValue = '';
        this.caretPos = 0;
        this.loading = 0;
        this.loadedSearches = {};
        this.activeOption = null;
        this.activeItems = [];
        this.optgroups = {};
        this.options = {};
        this.userOptions = {};
        this.items = [];
        this.refreshTimeout = null;
        instance_i++;
        var dir;
        var input = getDom(input_arg);
        if (input.tomselect) {
            throw new Error('Tom Select already initialized on this element');
        }
        input.tomselect = this;
        // detect rtl environment
        var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null);
        dir = computedStyle.getPropertyValue('direction');
        // setup default state
        const settings = getSettings(input, user_settings);
        this.settings = settings;
        this.input = input;
        this.tabIndex = input.tabIndex || 0;
        this.is_select_tag = input.tagName.toLowerCase() === 'select';
        this.rtl = /rtl/i.test(dir);
        this.inputId = getId(input, 'tomselect-' + instance_i);
        this.isRequired = input.required;
        // search system
        this.sifter = new Sifter(this.options, { diacritics: settings.diacritics });
        // option-dependent defaults
        settings.mode = settings.mode || (settings.maxItems === 1 ? 'single' : 'multi');
        if (typeof settings.hideSelected !== 'boolean') {
            settings.hideSelected = settings.mode === 'multi';
        }
        if (typeof settings.hidePlaceholder !== 'boolean') {
            settings.hidePlaceholder = settings.mode !== 'multi';
        }
        // set up createFilter callback
        var filter = settings.createFilter;
        if (typeof filter !== 'function') {
            if (typeof filter === 'string') {
                filter = new RegExp(filter);
            }
            if (filter instanceof RegExp) {
                settings.createFilter = (input) => filter.test(input);
            }
            else {
                settings.createFilter = (value) => {
                    return this.settings.duplicates || !this.options[value];
                };
            }
        }
        this.initializePlugins(settings.plugins);
        this.setupCallbacks();
        this.setupTemplates();
        // Create all elements
        const wrapper = getDom('<div>');
        const control = getDom('<div>');
        const dropdown = this._render('dropdown');
        const dropdown_content = getDom(`<div role="listbox" tabindex="-1">`);
        const classes = this.input.getAttribute('class') || '';
        const inputMode = settings.mode;
        var control_input;
        addClasses(wrapper, settings.wrapperClass, classes, inputMode);
        addClasses(control, settings.controlClass);
        append(wrapper, control);
        addClasses(dropdown, settings.dropdownClass, inputMode);
        if (settings.copyClassesToDropdown) {
            addClasses(dropdown, classes);
        }
        addClasses(dropdown_content, settings.dropdownContentClass);
        append(dropdown, dropdown_content);
        getDom(settings.dropdownParent || wrapper).appendChild(dropdown);
        // default controlInput
        if (isHtmlString(settings.controlInput)) {
            control_input = getDom(settings.controlInput);
            // set attributes
            var attrs = ['autocorrect', 'autocapitalize', 'autocomplete', 'spellcheck', 'aria-label'];
            iterate(attrs, (attr) => {
                if (input.getAttribute(attr)) {
                    setAttr(control_input, { [attr]: input.getAttribute(attr) });
                }
            });
            control_input.tabIndex = -1;
            control.appendChild(control_input);
            this.focus_node = control_input;
            // dom element
        }
        else if (settings.controlInput) {
            control_input = getDom(settings.controlInput);
            this.focus_node = control_input;
        }
        else {
            control_input = getDom('<input/>');
            this.focus_node = control;
        }
        this.wrapper = wrapper;
        this.dropdown = dropdown;
        this.dropdown_content = dropdown_content;
        this.control = control;
        this.control_input = control_input;
        this.setup();
    }
    /**
     * set up event bindings.
     *
     */
    setup() {
        const self = this;
        const settings = self.settings;
        const control_input = self.control_input;
        const dropdown = self.dropdown;
        const dropdown_content = self.dropdown_content;
        const wrapper = self.wrapper;
        const control = self.control;
        const input = self.input;
        const focus_node = self.focus_node;
        const passive_event = { passive: true };
        const listboxId = self.inputId + '-ts-dropdown';
        setAttr(dropdown_content, {
            id: listboxId
        });
        setAttr(focus_node, {
            role: 'combobox',
            'aria-haspopup': 'listbox',
            'aria-expanded': 'false',
            'aria-controls': listboxId
        });
        const control_id = getId(focus_node, self.inputId + '-ts-control');
        const query = "label[for='" + escapeQuery(self.inputId) + "']";
        const label = document.querySelector(query);
        const label_click = self.focus.bind(self);
        if (label) {
            addEvent(label, 'click', label_click);
            setAttr(label, { for: control_id });
            const label_id = getId(label, self.inputId + '-ts-label');
            setAttr(focus_node, { 'aria-labelledby': label_id });
            setAttr(dropdown_content, { 'aria-labelledby': label_id });
        }
        wrapper.style.width = input.style.width;
        wrapper.style.minWidth = input.style.minWidth;
        wrapper.style.maxWidth = input.style.maxWidth;
        if (self.plugins.names.length) {
            const classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
            addClasses([wrapper, dropdown], classes_plugins);
        }
        if ((settings.maxItems === null || settings.maxItems > 1) && self.is_select_tag) {
            setAttr(input, { multiple: 'multiple' });
        }
        if (settings.placeholder) {
            setAttr(control_input, { placeholder: settings.placeholder });
        }
        // if splitOn was not passed in, construct it from the delimiter to allow pasting universally
        if (!settings.splitOn && settings.delimiter) {
            settings.splitOn = new RegExp('\\s*' + escape_regex(settings.delimiter) + '+\\s*');
        }
        // debounce user defined load() if loadThrottle > 0
        // after initializePlugins() so plugins can create/modify user defined loaders
        if (settings.load && settings.loadThrottle) {
            settings.load = loadDebounce(settings.load, settings.loadThrottle);
        }
        addEvent(dropdown, 'mousemove', () => {
            self.ignoreHover = false;
        });
        addEvent(dropdown, 'mouseenter', (e) => {
            var target_match = parentMatch(e.target, '[data-selectable]', dropdown);
            if (target_match)
                self.onOptionHover(e, target_match);
        }, { capture: true });
        // clicking on an option should select it
        addEvent(dropdown, 'click', (evt) => {
            const option = parentMatch(evt.target, '[data-selectable]');
            if (option) {
                self.onOptionSelect(evt, option);
                preventDefault(evt, true);
            }
        });
        addEvent(control, 'click', (evt) => {
            var target_match = parentMatch(evt.target, '[data-ts-item]', control);
            if (target_match && self.onItemSelect(evt, target_match)) {
                preventDefault(evt, true);
                return;
            }
            // retain focus (see control_input mousedown)
            if (control_input.value != '') {
                return;
            }
            self.onClick();
            preventDefault(evt, true);
        });
        // keydown on focus_node for arrow_down/arrow_up
        addEvent(focus_node, 'keydown', (e) => self.onKeyDown(e));
        // keypress and input/keyup
        addEvent(control_input, 'keypress', (e) => self.onKeyPress(e));
        addEvent(control_input, 'input', (e) => self.onInput(e));
        addEvent(focus_node, 'blur', (e) => self.onBlur(e));
        addEvent(focus_node, 'focus', (e) => self.onFocus(e));
        addEvent(control_input, 'paste', (e) => self.onPaste(e));
        const doc_mousedown = (evt) => {
            // blur if target is outside of this instance
            // dropdown is not always inside wrapper
            const target = evt.composedPath()[0];
            if (!wrapper.contains(target) && !dropdown.contains(target)) {
                if (self.isFocused) {
                    self.blur();
                }
                self.inputState();
                return;
            }
            // retain focus by preventing native handling. if the
            // event target is the input it should not be modified.
            // otherwise, text selection within the input won't work.
            // Fixes bug #212 which is no covered by tests
            if (target == control_input && self.isOpen) {
                evt.stopPropagation();
                // clicking anywhere in the control should not blur the control_input (which would close the dropdown)
            }
            else {
                preventDefault(evt, true);
            }
        };
        const win_scroll = () => {
            if (self.isOpen) {
                self.positionDropdown();
            }
        };
        const input_invalid = () => {
            if (self.isValid) {
                self.isValid = false;
                self.isInvalid = true;
                self.refreshState();
            }
        };
        addEvent(input, 'invalid', input_invalid);
        addEvent(document, 'mousedown', doc_mousedown);
        addEvent(window, 'scroll', win_scroll, passive_event);
        addEvent(window, 'resize', win_scroll, passive_event);
        this._destroy = () => {
            input.removeEventListener('invalid', input_invalid);
            document.removeEventListener('mousedown', doc_mousedown);
            window.removeEventListener('scroll', win_scroll);
            window.removeEventListener('resize', win_scroll);
            if (label)
                label.removeEventListener('click', label_click);
        };
        // store original html and tab index so that they can be
        // restored when the destroy() method is called.
        this.revertSettings = {
            innerHTML: input.innerHTML,
            tabIndex: input.tabIndex
        };
        input.tabIndex = -1;
        input.insertAdjacentElement('afterend', self.wrapper);
        self.sync(false);
        settings.items = [];
        delete settings.optgroups;
        delete settings.options;
        self.refreshItems();
        self.close(false);
        self.inputState();
        self.isSetup = true;
        if (input.disabled) {
            self.disable();
        }
        else if (input.readOnly) {
            self.setReadOnly(true);
        }
        else {
            self.enable(); //sets tabIndex
        }
        self.on('change', this.onChange);
        addClasses(input, 'tomselected', 'ts-hidden-accessible');
        self.trigger('initialize');
        // preload options
        if (settings.preload === true) {
            self.preload();
        }
    }
    /**
     * Register options and optgroups
     *
     */
    setupOptions(options = [], optgroups = []) {
        // build options table
        this.addOptions(options);
        // build optgroup table
        iterate(optgroups, (optgroup) => {
            this.registerOptionGroup(optgroup);
        });
    }
    /**
     * Sets up default rendering functions.
     */
    setupTemplates() {
        var self = this;
        var field_label = self.settings.labelField;
        var field_optgroup = self.settings.optgroupLabelField;
        var templates = {
            'optgroup': (data) => {
                let optgroup = document.createElement('div');
                optgroup.className = 'optgroup';
                optgroup.appendChild(data.options);
                return optgroup;
            },
            'optgroup_header': (data, escape) => {
                return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
            },
            'option': (data, escape) => {
                return '<div>' + escape(data[field_label]) + '</div>';
            },
            'item': (data, escape) => {
                return '<div>' + escape(data[field_label]) + '</div>';
            },
            'option_create': (data, escape) => {
                return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
            },
            'no_results': () => {
                return '<div class="no-results">No results found</div>';
            },
            'loading': () => {
                return '<div class="spinner"></div>';
            },
            'not_loading': () => { },
            'dropdown': () => {
                return '<div></div>';
            }
        };
        self.settings.render = Object.assign({}, templates, self.settings.render);
    }
    /**
     * Maps fired events to callbacks provided
     * in the settings used when creating the control.
     */
    setupCallbacks() {
        var key, fn;
        var callbacks = {
            'initialize': 'onInitialize',
            'change': 'onChange',
            'item_add': 'onItemAdd',
            'item_remove': 'onItemRemove',
            'item_select': 'onItemSelect',
            'clear': 'onClear',
            'option_add': 'onOptionAdd',
            'option_remove': 'onOptionRemove',
            'option_clear': 'onOptionClear',
            'optgroup_add': 'onOptionGroupAdd',
            'optgroup_remove': 'onOptionGroupRemove',
            'optgroup_clear': 'onOptionGroupClear',
            'dropdown_open': 'onDropdownOpen',
            'dropdown_close': 'onDropdownClose',
            'type': 'onType',
            'load': 'onLoad',
            'focus': 'onFocus',
            'blur': 'onBlur'
        };
        for (key in callbacks) {
            fn = this.settings[callbacks[key]];
            if (fn)
                this.on(key, fn);
        }
    }
    /**
     * Sync the Tom Select instance with the original input or select
     *
     */
    sync(get_settings = true) {
        const self = this;
        const settings = get_settings ? getSettings(self.input, { delimiter: self.settings.delimiter, allowEmptyOption: self.settings.allowEmptyOption }) : self.settings;
        self.setupOptions(settings.options, settings.optgroups);
        self.setValue(settings.items || [], true); // silent prevents recursion
        self.lastQuery = null; // so updated options will be displayed in dropdown
    }
    /**
     * Triggered when the main control element
     * has a click event.
     *
     */
    onClick() {
        var self = this;
        if (self.activeItems.length > 0) {
            self.clearActiveItems();
            self.focus();
            return;
        }
        if (self.isFocused && self.isOpen) {
            self.blur();
        }
        else {
            self.focus();
        }
    }
    /**
     * @deprecated v1.7
     *
     */
    onMouseDown() { }
    /**
     * Triggered when the value of the control has been changed.
     * This should propagate the event to the original DOM
     * input / select element.
     */
    onChange() {
        triggerEvent(this.input, 'input');
        triggerEvent(this.input, 'change');
    }
    /**
     * Triggered on <input> paste.
     *
     */
    onPaste(e) {
        var self = this;
        if (self.isInputHidden || self.isLocked) {
            preventDefault(e);
            return;
        }
        // If a regex or string is included, this will split the pasted
        // input and create Items for each separate value
        if (!self.settings.splitOn) {
            return;
        }
        // Wait for pasted text to be recognized in value
        setTimeout(() => {
            var pastedText = self.inputValue();
            if (!pastedText.match(self.settings.splitOn)) {
                return;
            }
            var splitInput = pastedText.trim().split(self.settings.splitOn);
            iterate(splitInput, (piece) => {
                const hash = hash_key(piece);
                if (hash) {
                    if (this.options[piece]) {
                        self.addItem(piece);
                    }
                    else {
                        self.createItem(piece);
                    }
                }
            });
        }, 0);
    }
    /**
     * Triggered on <input> keypress.
     *
     */
    onKeyPress(e) {
        var self = this;
        if (self.isLocked) {
            preventDefault(e);
            return;
        }
        var character = String.fromCharCode(e.keyCode || e.which);
        if (self.settings.create && self.settings.mode === 'multi' && character === self.settings.delimiter) {
            self.createItem();
            preventDefault(e);
            return;
        }
    }
    /**
     * Triggered on <input> keydown.
     *
     */
    onKeyDown(e) {
        var self = this;
        self.ignoreHover = true;
        if (self.isLocked) {
            if (e.keyCode !== constants.KEY_TAB) {
                preventDefault(e);
            }
            return;
        }
        switch (e.keyCode) {
            // ctrl+A: select all
            case constants.KEY_A:
                if (isKeyDown(constants.KEY_SHORTCUT, e)) {
                    if (self.control_input.value == '') {
                        preventDefault(e);
                        self.selectAll();
                        return;
                    }
                }
                break;
            // esc: close dropdown
            case constants.KEY_ESC:
                if (self.isOpen) {
                    preventDefault(e, true);
                    self.close();
                }
                self.clearActiveItems();
                return;
            // down: open dropdown or move selection down
            case constants.KEY_DOWN:
                if (!self.isOpen && self.hasOptions) {
                    self.open();
                }
                else if (self.activeOption) {
                    let next = self.getAdjacent(self.activeOption, 1);
                    if (next)
                        self.setActiveOption(next);
                }
                preventDefault(e);
                return;
            // up: move selection up
            case constants.KEY_UP:
                if (self.activeOption) {
                    let prev = self.getAdjacent(self.activeOption, -1);
                    if (prev)
                        self.setActiveOption(prev);
                }
                preventDefault(e);
                return;
            // return: select active option
            case constants.KEY_RETURN:
                if (self.canSelect(self.activeOption)) {
                    self.onOptionSelect(e, self.activeOption);
                    preventDefault(e);
                    // if the option_create=null, the dropdown might be closed
                }
                else if (self.settings.create && self.createItem()) {
                    preventDefault(e);
                    // don't submit form when searching for a value
                }
                else if (document.activeElement == self.control_input && self.isOpen) {
                    preventDefault(e);
                }
                return;
            // left: modifiy item selection to the left
            case constants.KEY_LEFT:
                self.advanceSelection(-1, e);
                return;
            // right: modifiy item selection to the right
            case constants.KEY_RIGHT:
                self.advanceSelection(1, e);
                return;
            // tab: select active option and/or create item
            case constants.KEY_TAB:
                if (self.settings.selectOnTab) {
                    if (self.canSelect(self.activeOption)) {
                        self.onOptionSelect(e, self.activeOption);
                        // prevent default [tab] behaviour of jump to the next field
                        // if select isFull, then the dropdown won't be open and [tab] will work normally
                        preventDefault(e);
                    }
                    else if (self.settings.create && self.createItem()) {
                        preventDefault(e);
                    }
                }
                return;
            // delete|backspace: delete items
            case constants.KEY_BACKSPACE:
            case constants.KEY_DELETE:
                self.deleteSelection(e);
                return;
        }
        // don't enter text in the control_input when active items are selected
        if (self.isInputHidden && !isKeyDown(constants.KEY_SHORTCUT, e)) {
            preventDefault(e);
        }
    }
    /**
     * Triggered on <input> keyup.
     *
     */
    onInput(e) {
        if (this.isLocked) {
            return;
        }
        const value = this.inputValue();
        if (this.lastValue === value)
            return;
        this.lastValue = value;
        if (value == '') {
            this._onInput();
            return;
        }
        if (this.refreshTimeout) {
            window.clearTimeout(this.refreshTimeout);
        }
        this.refreshTimeout = timeout(() => {
            this.refreshTimeout = null;
            this._onInput();
        }, this.settings.refreshThrottle);
    }
    _onInput() {
        const value = this.lastValue;
        if (this.settings.shouldLoad.call(this, value)) {
            this.load(value);
        }
        this.refreshOptions();
        this.trigger('type', value);
    }
    /**
     * Triggered when the user rolls over
     * an option in the autocomplete dropdown menu.
     *
     */
    onOptionHover(evt, option) {
        if (this.ignoreHover)
            return;
        this.setActiveOption(option, false);
    }
    /**
     * Triggered on <input> focus.
     *
     */
    onFocus(e) {
        var self = this;
        var wasFocused = self.isFocused;
        if (self.isDisabled || self.isReadOnly) {
            self.blur();
            preventDefault(e);
            return;
        }
        if (self.ignoreFocus)
            return;
        self.isFocused = true;
        if (self.settings.preload === 'focus')
            self.preload();
        if (!wasFocused)
            self.trigger('focus');
        if (!self.activeItems.length) {
            self.inputState();
            self.refreshOptions(!!self.settings.openOnFocus);
        }
        self.refreshState();
    }
    /**
     * Triggered on <input> blur.
     *
     */
    onBlur(e) {
        if (document.hasFocus() === false)
            return;
        var self = this;
        if (!self.isFocused)
            return;
        self.isFocused = false;
        self.ignoreFocus = false;
        var deactivate = () => {
            self.close();
            self.setActiveItem();
            self.setCaret(self.items.length);
            self.trigger('blur');
        };
        if (self.settings.create && self.settings.createOnBlur) {
            self.createItem(null, deactivate);
        }
        else {
            deactivate();
        }
    }
    /**
     * Triggered when the user clicks on an option
     * in the autocomplete dropdown menu.
     *
     */
    onOptionSelect(evt, option) {
        var value, self = this;
        // should not be possible to trigger a option under a disabled optgroup
        if (option.parentElement && option.parentElement.matches('[data-disabled]')) {
            return;
        }
        if (option.classList.contains('create')) {
            self.createItem(null, () => {
                if (self.settings.closeAfterSelect) {
                    self.close();
                }
                else if (self.settings.clearAfterSelect) {
                    self.setTextboxValue();
                }
            });
        }
        else {
            value = option.dataset.value;
            if (typeof value !== 'undefined') {
                self.lastQuery = null;
                self.addItem(value);
                if (self.settings.closeAfterSelect) {
                    self.close();
                }
                else if (self.settings.clearAfterSelect) {
                    self.setTextboxValue();
                }
                if (!self.settings.hideSelected && evt.type && /click/.test(evt.type)) {
                    self.setActiveOption(option);
                }
            }
        }
    }
    /**
     * Return true if the given option can be selected
     *
     */
    canSelect(option) {
        if (this.isOpen && option && this.dropdown_content.contains(option)) {
            return true;
        }
        return false;
    }
    /**
     * Triggered when the user clicks on an item
     * that has been selected.
     *
     */
    onItemSelect(evt, item) {
        var self = this;
        if (!self.isLocked && self.settings.mode === 'multi') {
            preventDefault(evt);
            self.setActiveItem(item, evt);
            return true;
        }
        return false;
    }
    /**
     * Determines whether or not to invoke
     * the user-provided option provider / loader
     *
     * Note, there is a subtle difference between
     * this.canLoad() and this.settings.shouldLoad();
     *
     *	- settings.shouldLoad() is a user-input validator.
     *	When false is returned, the not_loading template
     *	will be added to the dropdown
     *
     *	- canLoad() is lower level validator that checks
     * 	the Tom Select instance. There is no inherent user
     *	feedback when canLoad returns false
     *
     */
    canLoad(value) {
        if (!this.settings.load)
            return false;
        if (this.loadedSearches.hasOwnProperty(value))
            return false;
        return true;
    }
    /**
     * Invokes the user-provided option provider / loader.
     *
     */
    load(value) {
        const self = this;
        if (!self.canLoad(value))
            return;
        addClasses(self.wrapper, self.settings.loadingClass);
        self.loading++;
        const callback = self.loadCallback.bind(self);
        self.settings.load.call(self, value, callback);
    }
    /**
     * Invoked by the user-provided option provider
     *
     */
    loadCallback(options, optgroups) {
        const self = this;
        self.loading = Math.max(self.loading - 1, 0);
        self.lastQuery = null;
        self.clearActiveOption(); // when new results load, focus should be on first option
        self.setupOptions(options, optgroups);
        self.refreshOptions(self.isFocused && !self.isInputHidden);
        if (!self.loading) {
            removeClasses(self.wrapper, self.settings.loadingClass);
        }
        self.trigger('load', options, optgroups);
    }
    preload() {
        var classList = this.wrapper.classList;
        if (classList.contains('preloaded'))
            return;
        classList.add('preloaded');
        this.load('');
    }
    /**
     * Sets the input field of the control to the specified value.
     *
     */
    setTextboxValue(value = '') {
        var input = this.control_input;
        var changed = input.value !== value;
        if (changed) {
            input.value = value;
            triggerEvent(input, 'update');
            this.lastValue = value;
        }
    }
    /**
     * Returns the value of the control. If multiple items
     * can be selected (e.g. <select multiple>), this returns
     * an array. If only one item can be selected, this
     * returns a string.
     *
     */
    getValue() {
        if (this.is_select_tag && this.input.hasAttribute('multiple')) {
            return this.items;
        }
        return this.items.join(this.settings.delimiter);
    }
    /**
     * Resets the selected items to the given value.
     *
     */
    setValue(value, silent) {
        var events = silent ? [] : ['change'];
        debounce_events(this, events, () => {
            this.clear(silent);
            this.addItems(value, silent);
        });
    }
    /**
     * Resets the number of max items to the given value
     *
     */
    setMaxItems(value) {
        if (value === 0)
            value = null; //reset to unlimited items.
        this.settings.maxItems = value;
        this.refreshState();
    }
    /**
     * Sets the selected item.
     *
     */
    setActiveItem(item, e) {
        var self = this;
        var eventName;
        var i, begin, end, swap;
        var last;
        if (self.settings.mode === 'single')
            return;
        // clear the active selection
        if (!item) {
            self.clearActiveItems();
            if (self.isFocused) {
                self.inputState();
            }
            return;
        }
        // modify selection
        eventName = e && e.type.toLowerCase();
        if (eventName === 'click' && isKeyDown('shiftKey', e) && self.activeItems.length) {
            last = self.getLastActive();
            begin = Array.prototype.indexOf.call(self.control.children, last);
            end = Array.prototype.indexOf.call(self.control.children, item);
            if (begin > end) {
                swap = begin;
                begin = end;
                end = swap;
            }
            for (i = begin; i <= end; i++) {
                item = self.control.children[i];
                if (self.activeItems.indexOf(item) === -1) {
                    self.setActiveItemClass(item);
                }
            }
            preventDefault(e);
        }
        else if ((eventName === 'click' && isKeyDown(constants.KEY_SHORTCUT, e)) || (eventName === 'keydown' && isKeyDown('shiftKey', e))) {
            if (item.classList.contains('active')) {
                self.removeActiveItem(item);
            }
            else {
                self.setActiveItemClass(item);
            }
        }
        else {
            self.clearActiveItems();
            self.setActiveItemClass(item);
        }
        // ensure control has focus
        self.inputState();
        if (!self.isFocused) {
            self.focus();
        }
    }
    /**
     * Set the active and last-active classes
     *
     */
    setActiveItemClass(item) {
        const self = this;
        const last_active = self.control.querySelector('.last-active');
        if (last_active)
            removeClasses(last_active, 'last-active');
        addClasses(item, 'active last-active');
        self.trigger('item_select', item);
        if (self.activeItems.indexOf(item) == -1) {
            self.activeItems.push(item);
        }
    }
    /**
     * Remove active item
     *
     */
    removeActiveItem(item) {
        var idx = this.activeItems.indexOf(item);
        this.activeItems.splice(idx, 1);
        removeClasses(item, 'active');
    }
    /**
     * Clears all the active items
     *
     */
    clearActiveItems() {
        removeClasses(this.activeItems, 'active');
        this.activeItems = [];
    }
    /**
     * Sets the selected item in the dropdown menu
     * of available options.
     *
     */
    setActiveOption(option, scroll = true) {
        if (option === this.activeOption) {
            return;
        }
        this.clearActiveOption();
        if (!option)
            return;
        this.activeOption = option;
        setAttr(this.focus_node, { 'aria-activedescendant': option.getAttribute('id') });
        setAttr(option, { 'aria-selected': 'true' });
        addClasses(option, 'active');
        if (scroll)
            this.scrollToOption(option);
    }
    /**
     * Sets the dropdown_content scrollTop to display the option
     *
     */
    scrollToOption(option, behavior) {
        if (!option)
            return;
        const content = this.dropdown_content;
        const height_menu = content.clientHeight;
        const scrollTop = content.scrollTop || 0;
        const height_item = option.offsetHeight;
        const y = option.getBoundingClientRect().top - content.getBoundingClientRect().top + scrollTop;
        if (y + height_item > height_menu + scrollTop) {
            this.scroll(y - height_menu + height_item, behavior);
        }
        else if (y < scrollTop) {
            this.scroll(y, behavior);
        }
    }
    /**
     * Scroll the dropdown to the given position
     *
     */
    scroll(scrollTop, behavior) {
        const content = this.dropdown_content;
        if (behavior) {
            content.style.scrollBehavior = behavior;
        }
        content.scrollTop = scrollTop;
        content.style.scrollBehavior = '';
    }
    /**
     * Clears the active option
     *
     */
    clearActiveOption() {
        if (this.activeOption) {
            removeClasses(this.activeOption, 'active');
            setAttr(this.activeOption, { 'aria-selected': null });
        }
        this.activeOption = null;
        setAttr(this.focus_node, { 'aria-activedescendant': null });
    }
    /**
     * Selects all items (CTRL + A).
     */
    selectAll() {
        const self = this;
        if (self.settings.mode === 'single')
            return;
        const activeItems = self.controlChildren();
        if (!activeItems.length)
            return;
        self.inputState();
        self.close();
        self.activeItems = activeItems;
        iterate(activeItems, (item) => {
            self.setActiveItemClass(item);
        });
    }
    /**
     * Determines if the control_input should be in a hidden or visible state
     *
     */
    inputState() {
        var self = this;
        if (!self.control.contains(self.control_input))
            return;
        setAttr(self.control_input, { placeholder: self.settings.placeholder });
        if (self.activeItems.length > 0 || (!self.isFocused && self.settings.hidePlaceholder && self.items.length > 0)) {
            self.setTextboxValue();
            self.isInputHidden = true;
        }
        else {
            if (self.settings.hidePlaceholder && self.items.length > 0) {
                setAttr(self.control_input, { placeholder: '' });
            }
            self.isInputHidden = false;
        }
        self.wrapper.classList.toggle('input-hidden', self.isInputHidden);
    }
    /**
     * Get the input value
     */
    inputValue() {
        return this.control_input.value.trim();
    }
    /**
     * Gives the control focus.
     */
    focus() {
        var self = this;
        if (self.isDisabled || self.isReadOnly)
            return;
        self.ignoreFocus = true;
        if (self.control_input.offsetWidth) {
            self.control_input.focus();
        }
        else {
            self.focus_node.focus();
        }
        setTimeout(() => {
            self.ignoreFocus = false;
            self.onFocus();
        }, 0);
    }
    /**
     * Forces the control out of focus.
     *
     */
    blur() {
        this.focus_node.blur();
        this.onBlur();
    }
    /**
     * Returns a function that scores an object
     * to show how good of a match it is to the
     * provided query.
     *
     * @return {function}
     */
    getScoreFunction(query) {
        return this.sifter.getScoreFunction(query, this.getSearchOptions());
    }
    /**
     * Returns search options for sifter (the system
     * for scoring and sorting results).
     *
     * @see https://github.com/orchidjs/sifter.js
     * @return {object}
     */
    getSearchOptions() {
        var settings = this.settings;
        var sort = settings.sortField;
        if (typeof settings.sortField === 'string') {
            sort = [{ field: settings.sortField }];
        }
        return {
            fields: settings.searchField,
            conjunction: settings.searchConjunction,
            sort: sort,
            nesting: settings.nesting
        };
    }
    /**
     * Searches through available options and returns
     * a sorted array of matches.
     *
     */
    search(query) {
        var result, calculateScore;
        var self = this;
        var options = this.getSearchOptions();
        // validate user-provided result scoring function
        if (self.settings.score) {
            calculateScore = self.settings.score.call(self, query);
            if (typeof calculateScore !== 'function') {
                throw new Error('Tom Select "score" setting must be a function that returns a function');
            }
        }
        // perform search
        if (query !== self.lastQuery) {
            self.lastQuery = query;
            // temp fix for https://github.com/orchidjs/tom-select/issues/987
            // UI crashed when more than 30 same chars in a row, prevent search and return empt result
            if (/(.)\1{15,}/.test(query)) {
                query = '';
            }
            result = self.sifter.search(query, Object.assign(options, { score: calculateScore }));
            self.currentResults = result;
        }
        else {
            result = Object.assign({}, self.currentResults);
        }
        // filter out selected items
        if (self.settings.hideSelected) {
            result.items = result.items.filter((item) => {
                let hashed = hash_key(item.id);
                return !(hashed !== null && self.items.indexOf(hashed) !== -1);
            });
        }
        return result;
    }
    /**
     * Refreshes the list of available options shown
     * in the autocomplete dropdown menu.
     *
     */
    refreshOptions(triggerDropdown = true) {
        var i, j, k, n, optgroup, optgroups, html, has_create_option, active_group;
        var create;
        const groups = {};
        const groups_order = [];
        var self = this;
        var query = self.inputValue();
        const same_query = query === self.lastQuery || (query == '' && self.lastQuery == null);
        var results = self.search(query);
        var active_option = null;
        var show_dropdown = self.settings.shouldOpen || false;
        var dropdown_content = self.dropdown_content;
        if (same_query) {
            active_option = self.activeOption;
            if (active_option) {
                active_group = active_option.closest('[data-group]');
            }
        }
        // build markup
        n = results.items.length;
        if (typeof self.settings.maxOptions === 'number') {
            n = Math.min(n, self.settings.maxOptions);
        }
        if (n > 0) {
            show_dropdown = true;
        }
        // get fragment for group and the position of the group in group_order
        const getGroupFragment = (optgroup, order) => {
            let group_order_i = groups[optgroup];
            if (group_order_i !== undefined) {
                let order_group = groups_order[group_order_i];
                if (order_group !== undefined) {
                    return [group_order_i, order_group.fragment];
                }
            }
            let group_fragment = document.createDocumentFragment();
            group_order_i = groups_order.length;
            groups_order.push({ fragment: group_fragment, order, optgroup });
            return [group_order_i, group_fragment];
        };
        // render and group available options individually
        for (i = 0; i < n; i++) {
            // get option dom element
            let item = results.items[i];
            if (!item)
                continue;
            let opt_value = item.id;
            let option = self.options[opt_value];
            if (option === undefined)
                continue;
            let opt_hash = get_hash(opt_value);
            let option_el = self.getOption(opt_hash, true);
            // toggle 'selected' class
            if (!self.settings.hideSelected) {
                option_el.classList.toggle('selected', self.items.includes(opt_hash));
            }
            optgroup = option[self.settings.optgroupField] || '';
            optgroups = Array.isArray(optgroup) ? optgroup : [optgroup];
            for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
                optgroup = optgroups[j];
                let order = option.$order;
                let self_optgroup = self.optgroups[optgroup];
                if (self_optgroup === undefined && typeof self.settings.optionGroupRegister === 'function') {
                    var regGroup;
                    if (regGroup = self.settings.optionGroupRegister.apply(self, [optgroup])) {
                        self.registerOptionGroup(regGroup);
                    }
                }
                self_optgroup = self.optgroups[optgroup];
                if (self_optgroup === undefined) {
                    optgroup = '';
                }
                else {
                    order = self_optgroup.$order;
                }
                const [group_order_i, group_fragment] = getGroupFragment(optgroup, order);
                // nodes can only have one parent, so if the option is in mutple groups, we need a clone
                if (j > 0) {
                    option_el = option_el.cloneNode(true);
                    setAttr(option_el, { id: option.$id + '-clone-' + j, 'aria-selected': null });
                    option_el.classList.add('ts-cloned');
                    removeClasses(option_el, 'active');
                    // make sure we keep the activeOption in the same group
                    if (self.activeOption && self.activeOption.dataset.value == opt_value) {
                        if (active_group && active_group.dataset.group === optgroup.toString()) {
                            active_option = option_el;
                        }
                    }
                }
                group_fragment.appendChild(option_el);
                if (optgroup != '') {
                    groups[optgroup] = group_order_i;
                }
            }
        }
        // sort optgroups
        if (self.settings.lockOptgroupOrder) {
            groups_order.sort((a, b) => {
                return a.order - b.order;
            });
        }
        // render optgroup headers & join groups
        html = document.createDocumentFragment();
        iterate(groups_order, (group_order) => {
            let group_fragment = group_order.fragment;
            let optgroup = group_order.optgroup;
            if (!group_fragment || !group_fragment.children.length)
                return;
            let group_heading = self.optgroups[optgroup];
            if (group_heading !== undefined) {
                let group_options = document.createDocumentFragment();
                let header = self.render('optgroup_header', group_heading);
                append(group_options, header);
                append(group_options, group_fragment);
                let group_html = self.render('optgroup', { group: group_heading, options: group_options });
                append(html, group_html);
            }
            else {
                append(html, group_fragment);
            }
        });
        dropdown_content.innerHTML = '';
        append(dropdown_content, html);
        // highlight matching terms inline
        if (self.settings.highlight) {
            removeHighlight(dropdown_content);
            if (results.query.length && results.tokens.length) {
                iterate(results.tokens, (tok) => {
                    highlight(dropdown_content, tok.regex);
                });
            }
        }
        // helper method for adding templates to dropdown
        var add_template = (template) => {
            let content = self.render(template, { input: query });
            if (content) {
                show_dropdown = true;
                dropdown_content.insertBefore(content, dropdown_content.firstChild);
            }
            return content;
        };
        // add loading message
        if (self.loading) {
            add_template('loading');
            // invalid query
        }
        else if (!self.settings.shouldLoad.call(self, query)) {
            add_template('not_loading');
            // add no_results message
        }
        else if (results.items.length === 0) {
            add_template('no_results');
        }
        // add create option
        has_create_option = self.canCreate(query);
        if (has_create_option) {
            create = add_template('option_create');
        }
        // activate
        self.hasOptions = results.items.length > 0 || has_create_option;
        if (show_dropdown) {
            if (results.items.length > 0) {
                if (!active_option && self.settings.mode === 'single' && self.items[0] != undefined) {
                    active_option = self.getOption(self.items[0]);
                }
                if (!dropdown_content.contains(active_option)) {
                    let active_index = 0;
                    if (create && !self.settings.addPrecedence) {
                        active_index = 1;
                    }
                    active_option = self.selectable()[active_index];
                }
            }
            else if (create) {
                active_option = create;
            }
            if (triggerDropdown && !self.isOpen) {
                self.open();
                self.scrollToOption(active_option, 'auto');
            }
            self.setActiveOption(active_option);
        }
        else {
            self.clearActiveOption();
            if (triggerDropdown && self.isOpen) {
                self.close(false); // if create_option=null, we want the dropdown to close but not reset the textbox value
            }
        }
    }
    /**
     * Return list of selectable options
     *
     */
    selectable() {
        return this.dropdown_content.querySelectorAll('[data-selectable]');
    }
    /**
     * Adds an available option. If it already exists,
     * nothing will happen. Note: this does not refresh
     * the options list dropdown (use `refreshOptions`
     * for that).
     *
     * Usage:
     *
     *   this.addOption(data)
     *
     */
    addOption(data, user_created = false) {
        const self = this;
        // @deprecated 1.7.7
        // use addOptions( array, user_created ) for adding multiple options
        if (Array.isArray(data)) {
            self.addOptions(data, user_created);
            return false;
        }
        const key = hash_key(data[self.settings.valueField]);
        if (key === null || self.options.hasOwnProperty(key)) {
            return false;
        }
        data.$order = data.$order || ++self.order;
        data.$id = self.inputId + '-opt-' + data.$order;
        self.options[key] = data;
        self.lastQuery = null;
        if (user_created) {
            self.userOptions[key] = user_created;
            self.trigger('option_add', key, data);
        }
        return key;
    }
    /**
     * Add multiple options
     *
     */
    addOptions(data, user_created = false) {
        iterate(data, (dat) => {
            this.addOption(dat, user_created);
        });
    }
    /**
     * @deprecated 1.7.7
     */
    registerOption(data) {
        return this.addOption(data);
    }
    /**
     * Registers an option group to the pool of option groups.
     *
     * @return {boolean|string}
     */
    registerOptionGroup(data) {
        var key = hash_key(data[this.settings.optgroupValueField]);
        if (key === null)
            return false;
        data.$order = data.$order || ++this.order;
        this.optgroups[key] = data;
        return key;
    }
    /**
     * Registers a new optgroup for options
     * to be bucketed into.
     *
     */
    addOptionGroup(id, data) {
        var hashed_id;
        data[this.settings.optgroupValueField] = id;
        if (hashed_id = this.registerOptionGroup(data)) {
            this.trigger('optgroup_add', hashed_id, data);
        }
    }
    /**
     * Removes an existing option group.
     *
     */
    removeOptionGroup(id) {
        if (this.optgroups.hasOwnProperty(id)) {
            delete this.optgroups[id];
            this.clearCache();
            this.trigger('optgroup_remove', id);
        }
    }
    /**
     * Clears all existing option groups.
     */
    clearOptionGroups() {
        this.optgroups = {};
        this.clearCache();
        this.trigger('optgroup_clear');
    }
    /**
     * Updates an option available for selection. If
     * it is visible in the selected items or options
     * dropdown, it will be re-rendered automatically.
     *
     */
    updateOption(value, data) {
        const self = this;
        var item_new;
        var index_item;
        const value_old = hash_key(value);
        const value_new = hash_key(data[self.settings.valueField]);
        // sanity checks
        if (value_old === null)
            return;
        const data_old = self.options[value_old];
        if (data_old == undefined)
            return;
        if (typeof value_new !== 'string')
            throw new Error('Value must be set in option data');
        const option = self.getOption(value_old);
        const item = self.getItem(value_old);
        data.$order = data.$order || data_old.$order;
        delete self.options[value_old];
        // invalidate render cache
        // don't remove existing node yet, we'll remove it after replacing it
        self.uncacheValue(value_new);
        self.options[value_new] = data;
        // update the option if it's in the dropdown
        if (option) {
            if (self.dropdown_content.contains(option)) {
                const option_new = self._render('option', data);
                replaceNode(option, option_new);
                if (self.activeOption === option) {
                    self.setActiveOption(option_new);
                }
            }
            option.remove();
        }
        // update the item if we have one
        if (item) {
            index_item = self.items.indexOf(value_old);
            if (index_item !== -1) {
                self.items.splice(index_item, 1, value_new);
            }
            item_new = self._render('item', data);
            if (item.classList.contains('active'))
                addClasses(item_new, 'active');
            replaceNode(item, item_new);
        }
        // invalidate last query because we might have updated the sortField
        self.lastQuery = null;
    }
    /**
     * Removes a single option.
     *
     */
    removeOption(value, silent) {
        const self = this;
        value = get_hash(value);
        self.uncacheValue(value);
        delete self.userOptions[value];
        delete self.options[value];
        self.lastQuery = null;
        self.trigger('option_remove', value);
        self.removeItem(value, silent);
    }
    /**
     * Clears all options.
     */
    clearOptions(filter) {
        const boundFilter = (filter || this.clearFilter).bind(this);
        this.loadedSearches = {};
        this.userOptions = {};
        this.clearCache();
        const selected = {};
        iterate(this.options, (option, key) => {
            if (boundFilter(option, key)) {
                selected[key] = option;
            }
        });
        this.options = this.sifter.items = selected;
        this.lastQuery = null;
        this.trigger('option_clear');
    }
    /**
     * Used by clearOptions() to decide whether or not an option should be removed
     * Return true to keep an option, false to remove
     *
     */
    clearFilter(option, value) {
        if (this.items.indexOf(value) >= 0) {
            return true;
        }
        return false;
    }
    /**
     * Returns the dom element of the option
     * matching the given value.
     *
     */
    getOption(value, create = false) {
        const hashed = hash_key(value);
        if (hashed === null)
            return null;
        const option = this.options[hashed];
        if (option != undefined) {
            if (option.$div) {
                return option.$div;
            }
            if (create) {
                return this._render('option', option);
            }
        }
        return null;
    }
    /**
     * Returns the dom element of the next or previous dom element of the same type
     * Note: adjacent options may not be adjacent DOM elements (optgroups)
     *
     */
    getAdjacent(option, direction, type = 'option') {
        var self = this, all;
        if (!option) {
            return null;
        }
        if (type == 'item') {
            all = self.controlChildren();
        }
        else {
            all = self.dropdown_content.querySelectorAll('[data-selectable]');
        }
        for (let i = 0; i < all.length; i++) {
            if (all[i] != option) {
                continue;
            }
            if (direction > 0) {
                return all[i + 1];
            }
            return all[i - 1];
        }
        return null;
    }
    /**
     * Returns the dom element of the item
     * matching the given value.
     *
     */
    getItem(item) {
        if (typeof item == 'object') {
            return item;
        }
        var value = hash_key(item);
        return value !== null
            ? this.control.querySelector(`[data-value="${addSlashes(value)}"]`)
            : null;
    }
    /**
     * "Selects" multiple items at once. Adds them to the list
     * at the current caret position.
     *
     */
    addItems(values, silent) {
        var self = this;
        var items = Array.isArray(values) ? values : [values];
        items = items.filter(x => self.items.indexOf(x) === -1);
        const last_item = items[items.length - 1];
        items.forEach(item => {
            self.isPending = (item !== last_item);
            self.addItem(item, silent);
        });
    }
    /**
     * "Selects" an item. Adds it to the list
     * at the current caret position.
     *
     */
    addItem(value, silent) {
        var events = silent ? [] : ['change', 'dropdown_close'];
        debounce_events(this, events, () => {
            var item, wasFull;
            const self = this;
            const inputMode = self.settings.mode;
            const hashed = hash_key(value);
            if (hashed && self.items.indexOf(hashed) !== -1) {
                if (inputMode === 'single') {
                    self.close();
                }
                if (inputMode === 'single' || !self.settings.duplicates) {
                    return;
                }
            }
            if (hashed === null || !self.options.hasOwnProperty(hashed))
                return;
            if (inputMode === 'single')
                self.clear(silent);
            if (inputMode === 'multi' && self.isFull())
                return;
            item = self._render('item', self.options[hashed]);
            if (self.control.contains(item)) { // duplicates
                item = item.cloneNode(true);
            }
            wasFull = self.isFull();
            self.items.splice(self.caretPos, 0, hashed);
            self.insertAtCaret(item);
            if (self.isSetup) {
                // update menu / remove the option (if this is not one item being added as part of series)
                if (!self.isPending && self.settings.hideSelected) {
                    let option = self.getOption(hashed);
                    let next = self.getAdjacent(option, 1);
                    if (next) {
                        self.setActiveOption(next);
                    }
                }
                //remove input value when enabled
                if (self.settings.clearAfterSelect) {
                    self.setTextboxValue();
                }
                // refreshOptions after setActiveOption(),
                // otherwise setActiveOption() will be called by refreshOptions() with the wrong value
                if (!self.isPending && !self.settings.closeAfterSelect) {
                    self.refreshOptions(self.isFocused && inputMode !== 'single');
                }
                // hide the menu if the maximum number of items have been selected or no options are left
                if (self.settings.closeAfterSelect != false && self.isFull()) {
                    self.close();
                }
                else if (!self.isPending) {
                    self.positionDropdown();
                }
                self.trigger('item_add', hashed, item);
                if (!self.isPending) {
                    self.updateOriginalInput({ silent: silent });
                }
            }
            if (!self.isPending || (!wasFull && self.isFull())) {
                self.inputState();
                self.refreshState();
            }
        });
    }
    /**
     * Removes the selected item matching
     * the provided value.
     *
     */
    removeItem(item = null, silent) {
        const self = this;
        item = self.getItem(item);
        if (!item)
            return;
        var i, idx;
        const value = item.dataset.value;
        i = nodeIndex(item);
        item.remove();
        if (item.classList.contains('active')) {
            idx = self.activeItems.indexOf(item);
            self.activeItems.splice(idx, 1);
            removeClasses(item, 'active');
        }
        self.items.splice(i, 1);
        self.lastQuery = null;
        if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
            self.removeOption(value, silent);
        }
        if (i < self.caretPos) {
            self.setCaret(self.caretPos - 1);
        }
        self.updateOriginalInput({ silent: silent });
        self.refreshState();
        self.positionDropdown();
        self.trigger('item_remove', value, item);
    }
    /**
     * Invokes the `create` method provided in the
     * TomSelect options that should provide the data
     * for the new item, given the user input.
     *
     * Once this completes, it will be added
     * to the item list.
     *
     */
    createItem(input = null, callback = () => { }) {
        // triggerDropdown parameter @deprecated 2.1.1
        if (arguments.length === 3) {
            callback = arguments[2];
        }
        if (typeof callback != 'function') {
            callback = () => { };
        }
        var self = this;
        var caret = self.caretPos;
        var output;
        input = input || self.inputValue();
        if (!self.canCreate(input)) {
            const hash = hash_key(input);
            if (hash) {
                if (this.options[input]) {
                    self.addItem(input);
                }
            }
            callback();
            return false;
        }
        self.lock();
        var created = false;
        var create = (data) => {
            self.unlock();
            if (!data || typeof data !== 'object')
                return callback();
            var value = hash_key(data[self.settings.valueField]);
            if (typeof value !== 'string') {
                return callback();
            }
            self.setTextboxValue();
            self.addOption(data, true);
            self.setCaret(caret);
            self.addItem(value);
            callback(data);
            created = true;
        };
        if (typeof self.settings.create === 'function') {
            output = self.settings.create.call(this, input, create);
        }
        else {
            output = {
                [self.settings.labelField]: input,
                [self.settings.valueField]: input,
            };
        }
        if (!created) {
            create(output);
        }
        return true;
    }
    /**
     * Re-renders the selected item lists.
     */
    refreshItems() {
        var self = this;
        self.lastQuery = null;
        if (self.isSetup) {
            self.addItems(self.items);
        }
        self.updateOriginalInput();
        self.refreshState();
    }
    /**
     * Updates all state-dependent attributes
     * and CSS classes.
     */
    refreshState() {
        const self = this;
        self.refreshValidityState();
        const isFull = self.isFull();
        const isLocked = self.isLocked;
        self.wrapper.classList.toggle('rtl', self.rtl);
        const wrap_classList = self.wrapper.classList;
        wrap_classList.toggle('focus', self.isFocused);
        wrap_classList.toggle('disabled', self.isDisabled);
        wrap_classList.toggle('readonly', self.isReadOnly);
        wrap_classList.toggle('required', self.isRequired);
        wrap_classList.toggle('invalid', !self.isValid);
        wrap_classList.toggle('locked', isLocked);
        wrap_classList.toggle('full', isFull);
        wrap_classList.toggle('input-active', self.isFocused && !self.isInputHidden);
        wrap_classList.toggle('dropdown-active', self.isOpen);
        wrap_classList.toggle('has-options', isEmptyObject(self.options));
        wrap_classList.toggle('has-items', self.items.length > 0);
    }
    /**
     * Update the `required` attribute of both input and control input.
     *
     * The `required` property needs to be activated on the control input
     * for the error to be displayed at the right place. `required` also
     * needs to be temporarily deactivated on the input since the input is
     * hidden and can't show errors.
     */
    refreshValidityState() {
        var self = this;
        if (!self.input.validity) {
            return;
        }
        self.isValid = self.input.validity.valid;
        self.isInvalid = !self.isValid;
    }
    /**
     * Determines whether or not more items can be added
     * to the control without exceeding the user-defined maximum.
     *
     * @returns {boolean}
     */
    isFull() {
        return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
    }
    /**
     * Refreshes the original <select> or <input>
     * element to reflect the current state.
     *
     */
    updateOriginalInput(opts = {}) {
        const self = this;
        var option, label;
        const empty_option = self.input.querySelector('option[value=""]');
        if (self.is_select_tag) {
            const selected = [];
            const has_selected = self.input.querySelectorAll('option:checked').length;
            function AddSelected(option_el, value, label) {
                if (!option_el) {
                    option_el = getDom('<option value="' + escape_html(value) + '">' + escape_html(label) + '</option>');
                }
                // don't move empty option from top of list
                // fixes bug in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1725293
                if (option_el != empty_option) {
                    self.input.append(option_el);
                }
                selected.push(option_el);
                // marking empty option as selected can break validation
                // fixes https://github.com/orchidjs/tom-select/issues/303
                if (option_el != empty_option || has_selected > 0) {
                    option_el.selected = true;
                }
                return option_el;
            }
            // unselect all selected options
            self.input.querySelectorAll('option:checked').forEach((option_el) => {
                option_el.selected = false;
            });
            // nothing selected?
            if (self.items.length == 0 && self.settings.mode == 'single') {
                AddSelected(empty_option, "", "");
                // order selected <option> tags for values in self.items
            }
            else {
                self.items.forEach((value) => {
                    option = self.options[value];
                    label = option[self.settings.labelField] || '';
                    if (selected.includes(option.$option)) {
                        const reuse_opt = self.input.querySelector(`option[value="${addSlashes(value)}"]:not(:checked)`);
                        AddSelected(reuse_opt, value, label);
                    }
                    else {
                        option.$option = AddSelected(option.$option, value, label);
                    }
                });
            }
        }
        else {
            self.input.value = self.getValue();
        }
        if (self.isSetup) {
            if (!opts.silent) {
                self.trigger('change', self.getValue());
            }
        }
    }
    /**
     * Shows the autocomplete dropdown containing
     * the available options.
     */
    open() {
        var self = this;
        if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull()))
            return;
        self.isOpen = true;
        setAttr(self.focus_node, { 'aria-expanded': 'true' });
        self.refreshState();
        applyCSS(self.dropdown, { visibility: 'hidden', display: 'block' });
        self.positionDropdown();
        applyCSS(self.dropdown, { visibility: 'visible', display: 'block' });
        self.focus();
        self.trigger('dropdown_open', self.dropdown);
    }
    /**
     * Closes the autocomplete dropdown menu.
     */
    close(setTextboxValue = true) {
        var self = this;
        var trigger = self.isOpen;
        if (setTextboxValue) {
            // before blur() to prevent form onchange event
            self.setTextboxValue();
            if (self.settings.mode === 'single' && self.items.length) {
                self.inputState();
            }
        }
        self.isOpen = false;
        setAttr(self.focus_node, { 'aria-expanded': 'false' });
        applyCSS(self.dropdown, { display: 'none' });
        if (self.settings.hideSelected) {
            self.clearActiveOption();
        }
        self.refreshState();
        if (trigger)
            self.trigger('dropdown_close', self.dropdown);
    }
    /**
     * Calculates and applies the appropriate
     * position of the dropdown if dropdownParent = 'body'.
     * Otherwise, position is determined by css
     */
    positionDropdown() {
        if (this.settings.dropdownParent !== 'body') {
            return;
        }
        var context = this.control;
        var rect = context.getBoundingClientRect();
        var top = context.offsetHeight + rect.top + window.scrollY;
        var left = rect.left + window.scrollX;
        applyCSS(this.dropdown, {
            width: rect.width + 'px',
            top: top + 'px',
            left: left + 'px'
        });
    }
    /**
     * Resets / clears all selected items
     * from the control.
     *
     */
    clear(silent) {
        var self = this;
        if (!self.items.length)
            return;
        var items = self.controlChildren();
        iterate(items, (item) => {
            self.removeItem(item, true);
        });
        self.inputState();
        if (!silent)
            self.updateOriginalInput();
        self.trigger('clear');
    }
    /**
     * A helper method for inserting an element
     * at the current caret position.
     *
     */
    insertAtCaret(el) {
        const self = this;
        const caret = self.caretPos;
        const target = self.control;
        target.insertBefore(el, target.children[caret] || null);
        self.setCaret(caret + 1);
    }
    /**
     * Removes the current selected item(s).
     *
     */
    deleteSelection(e) {
        var direction, selection, caret, tail;
        var self = this;
        direction = (e && e.keyCode === constants.KEY_BACKSPACE) ? -1 : 1;
        selection = getSelection(self.control_input);
        // determine items that will be removed
        const rm_items = [];
        if (self.activeItems.length) {
            tail = getTail(self.activeItems, direction);
            caret = nodeIndex(tail);
            if (direction > 0) {
                caret++;
            }
            iterate(self.activeItems, (item) => rm_items.push(item));
        }
        else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) {
            const items = self.controlChildren();
            let rm_item;
            if (direction < 0 && selection.start === 0 && selection.length === 0) {
                rm_item = items[self.caretPos - 1];
            }
            else if (direction > 0 && selection.start === self.inputValue().length) {
                rm_item = items[self.caretPos];
            }
            if (rm_item !== undefined) {
                rm_items.push(rm_item);
            }
        }
        if (!self.shouldDelete(rm_items, e)) {
            return false;
        }
        preventDefault(e, true);
        // perform removal
        if (typeof caret !== 'undefined') {
            self.setCaret(caret);
        }
        while (rm_items.length) {
            self.removeItem(rm_items.pop());
        }
        self.inputState();
        self.positionDropdown();
        self.refreshOptions(false);
        return true;
    }
    /**
     * Return true if the items should be deleted
     */
    shouldDelete(items, evt) {
        const values = items.map(item => item.dataset.value);
        // allow the callback to abort
        if (!values.length || (typeof this.settings.onDelete === 'function' && this.settings.onDelete.call(this, values, evt) === false)) {
            return false;
        }
        return true;
    }
    /**
     * Selects the previous / next item (depending on the `direction` argument).
     *
     * > 0 - right
     * < 0 - left
     *
     */
    advanceSelection(direction, e) {
        var last_active, adjacent, self = this;
        if (self.rtl)
            direction *= -1;
        if (self.inputValue().length)
            return;
        // add or remove to active items
        if (isKeyDown(constants.KEY_SHORTCUT, e) || isKeyDown('shiftKey', e)) {
            last_active = self.getLastActive(direction);
            if (last_active) {
                if (!last_active.classList.contains('active')) {
                    adjacent = last_active;
                }
                else {
                    adjacent = self.getAdjacent(last_active, direction, 'item');
                }
                // if no active item, get items adjacent to the control input
            }
            else if (direction > 0) {
                adjacent = self.control_input.nextElementSibling;
            }
            else {
                adjacent = self.control_input.previousElementSibling;
            }
            if (adjacent) {
                if (adjacent.classList.contains('active')) {
                    self.removeActiveItem(last_active);
                }
                self.setActiveItemClass(adjacent); // mark as last_active !! after removeActiveItem() on last_active
            }
            // move caret to the left or right
        }
        else {
            self.moveCaret(direction);
        }
    }
    moveCaret(direction) { }
    /**
     * Get the last active item
     *
     */
    getLastActive(direction) {
        let last_active = this.control.querySelector('.last-active');
        if (last_active) {
            return last_active;
        }
        var result = this.control.querySelectorAll('.active');
        if (result) {
            return getTail(result, direction);
        }
    }
    /**
     * Moves the caret to the specified index.
     *
     * The input must be moved by leaving it in place and moving the
     * siblings, due to the fact that focus cannot be restored once lost
     * on mobile webkit devices
     *
     */
    setCaret(new_pos) {
        this.caretPos = this.items.length;
    }
    /**
     * Return list of item dom elements
     *
     */
    controlChildren() {
        return Array.from(this.control.querySelectorAll('[data-ts-item]'));
    }
    /**
     * Disables user input on the control. Used while
     * items are being asynchronously created.
     */
    lock() {
        this.setLocked(true);
    }
    /**
     * Re-enables user input on the control.
     */
    unlock() {
        this.setLocked(false);
    }
    /**
     * Disable or enable user input on the control
     */
    setLocked(lock = this.isReadOnly || this.isDisabled) {
        this.isLocked = lock;
        this.refreshState();
    }
    /**
     * Disables user input on the control completely.
     * While disabled, it cannot receive focus.
     */
    disable() {
        this.setDisabled(true);
        this.close();
    }
    /**
     * Enables the control so that it can respond
     * to focus and user input.
     */
    enable() {
        this.setDisabled(false);
    }
    setDisabled(disabled) {
        this.focus_node.tabIndex = disabled ? -1 : this.tabIndex;
        this.isDisabled = disabled;
        this.input.disabled = disabled;
        this.control_input.disabled = disabled;
        this.setLocked();
    }
    setReadOnly(isReadOnly) {
        this.isReadOnly = isReadOnly;
        this.input.readOnly = isReadOnly;
        this.control_input.readOnly = isReadOnly;
        this.setLocked();
    }
    /**
     * Completely destroys the control and
     * unbinds all event listeners so that it can
     * be garbage collected.
     */
    destroy() {
        var self = this;
        var revertSettings = self.revertSettings;
        self.trigger('destroy');
        self.off();
        self.wrapper.remove();
        self.dropdown.remove();
        self.input.innerHTML = revertSettings.innerHTML;
        self.input.tabIndex = revertSettings.tabIndex;
        removeClasses(self.input, 'tomselected', 'ts-hidden-accessible');
        self._destroy();
        delete self.input.tomselect;
    }
    /**
     * A helper method for rendering "item" and
     * "option" templates, given the data.
     *
     */
    render(templateName, data) {
        var id, html;
        const self = this;
        if (typeof this.settings.render[templateName] !== 'function') {
            return null;
        }
        // render markup
        html = self.settings.render[templateName].call(this, data, escape_html);
        if (!html) {
            return null;
        }
        html = getDom(html);
        // add mandatory attributes
        if (templateName === 'option' || templateName === 'option_create') {
            if (data[self.settings.disabledField]) {
                setAttr(html, { 'aria-disabled': 'true' });
            }
            else {
                setAttr(html, { 'data-selectable': '' });
            }
        }
        else if (templateName === 'optgroup') {
            id = data.group[self.settings.optgroupValueField];
            setAttr(html, { 'data-group': id });
            if (data.group[self.settings.disabledField]) {
                setAttr(html, { 'data-disabled': '' });
            }
        }
        if (templateName === 'option' || templateName === 'item') {
            const value = get_hash(data[self.settings.valueField]);
            setAttr(html, { 'data-value': value });
            // make sure we have some classes if a template is overwritten
            if (templateName === 'item') {
                addClasses(html, self.settings.itemClass);
                setAttr(html, { 'data-ts-item': '' });
            }
            else {
                addClasses(html, self.settings.optionClass);
                setAttr(html, {
                    role: 'option',
                    id: data.$id
                });
                // update cache
                data.$div = html;
                self.options[value] = data;
            }
        }
        return html;
    }
    /**
     * Type guarded rendering
     *
     */
    _render(templateName, data) {
        const html = this.render(templateName, data);
        if (html == null) {
            throw 'HTMLElement expected';
        }
        return html;
    }
    /**
     * Clears the render cache for a template. If
     * no template is given, clears all render
     * caches.
     *
     */
    clearCache() {
        iterate(this.options, (option) => {
            if (option.$div) {
                option.$div.remove();
                delete option.$div;
            }
        });
    }
    /**
     * Removes a value from item and option caches
     *
     */
    uncacheValue(value) {
        const option_el = this.getOption(value);
        if (option_el)
            option_el.remove();
    }
    /**
     * Determines whether or not to display the
     * create item prompt, given a user input.
     *
     */
    canCreate(input) {
        return this.settings.create && (input.length > 0) && this.settings.createFilter.call(this, input);
    }
    /**
     * Wraps this.`method` so that `new_fn` can be invoked 'before', 'after', or 'instead' of the original method
     *
     * this.hook('instead','onKeyDown',function( arg1, arg2 ...){
     *
     * });
     */
    hook(when, method, new_fn) {
        var self = this;
        var orig_method = self[method];
        self[method] = function () {
            var result, result_new;
            if (when === 'after') {
                result = orig_method.apply(self, arguments);
            }
            result_new = new_fn.apply(self, arguments);
            if (when === 'instead') {
                return result_new;
            }
            if (when === 'before') {
                result = orig_method.apply(self, arguments);
            }
            return result;
        };
    }
}
;
//# sourceMappingURL=tom-select.js.map