;(function (factory) {
    if (typeof define === "function" && define.amd) {
        define(["jquery"], factory)
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("jquery"))
    } else {
        factory(jQuery)
    }
})(function ($) {
    var $document = $(window.document),
        instanceNum = 0,
        eventStringRE = /\w\b/g,
        keyMap = {
            13: "enter",
            27: "escape",
            40: "downArrow",
            38: "upArrow",
        }

    function Fastsearch(inputElement, options) {
        this.init.apply(this, arguments)
    }

    $.extend(Fastsearch.prototype, {
        init: function (inputElement, options) {
            options = this.options = $.extend(
                true,
                {},
                Fastsearch.defaults,
                options
            )

            this.$input = $(inputElement)
            this.$el =
                options.wrapSelector instanceof $
                    ? options.wrapSelector
                    : this.$input.closest(options.wrapSelector)

            Fastsearch.pickTo(options, this.$el.data(), [
                "url",
                "onItemSelect",
                "noResultsText",
                "inputIdName",
                "apiInputName",
            ])

            options.url = options.url || this.$el.attr("action")

            this.ens = ".fastsearch" + ++instanceNum
            this.itemSelector = Fastsearch.selectorFromClass(options.itemClass)
            this.focusedItemSelector = Fastsearch.selectorFromClass(
                options.focusedItemClass
            )

            this.events()
        },

        namespaceEvents: function (events) {
            var eventNamespace = this.ens

            return events.replace(eventStringRE, function (match) {
                return match + eventNamespace
            })
        },

        events: function () {
            var self = this,
                options = this.options

            this.$input
                .on(this.namespaceEvents("keyup focus click"), function (e) {
                    keyMap[e.keyCode] !== "enter" && self.handleTyping()
                })
                .on(this.namespaceEvents("keydown"), function (e) {
                    keyMap[e.keyCode] === "enter" &&
                        options.preventSubmit &&
                        e.preventDefault()

                    if (self.hasResults && self.resultsOpened) {
                        switch (keyMap[e.keyCode]) {
                            case "downArrow":
                                e.preventDefault()
                                self.navigateItem("down")
                                break
                            case "upArrow":
                                e.preventDefault()
                                self.navigateItem("up")
                                break
                            case "enter":
                                self.onEnter(e)
                                break
                        }
                    }
                })

            this.$el.on(
                this.namespaceEvents("click"),
                this.itemSelector,
                function (e) {
                    e.preventDefault()
                    self.handleItemSelect($(this))
                }
            )

            options.mouseEvents &&
                this.$el
                    .on(
                        this.namespaceEvents("mouseleave"),
                        this.itemSelector,
                        function (e) {
                            $(this).removeClass(options.focusedItemClass)
                        }
                    )
                    .on(
                        this.namespaceEvents("mouseenter"),
                        this.itemSelector,
                        function (e) {
                            self.$resultItems.removeClass(
                                options.focusedItemClass
                            )
                            $(this).addClass(options.focusedItemClass)
                        }
                    )
        },

        handleTyping: function () {
            var inputValue = $.trim(this.$input.val()),
                self = this

            if (inputValue.length < this.options.minQueryLength) {
                this.hideResults()
            } else if (inputValue === this.query) {
                this.showResults()
            } else {
                clearTimeout(this.keyupTimeout)

                this.keyupTimeout = setTimeout(function () {
                    self.$el.addClass(self.options.loadingClass)

                    self.query = inputValue

                    self.getResults(function (data) {
                        self.showResults(
                            self.storeResponse(data).generateResults(data)
                        )
                    })
                }, this.options.typeTimeout)
            }
        },

        getResults: function (callback) {
            var self = this,
                options = this.options,
                formValues = this.$el
                    .find("input, textarea, select")
                    .serializeArray()

            if (options.apiInputName) {
                formValues.push({
                    name: options.apiInputName,
                    value: this.$input.val(),
                })
            }

            $.get(options.url, formValues, function (data) {
                callback(
                    options.parseResponse
                        ? options.parseResponse.call(self, data, self)
                        : data
                )
            })
        },

        storeResponse: function (data) {
            this.responseData = data
            this.hasResults = data.length !== 0

            return this
        },

        generateResults: function (data) {
            var $allResults = $("<div>"),
                options = this.options

            if (options.template) {
                return $(options.template(data, this))
            }

            if (data.length === 0) {
                $allResults.html(
                    '<p class="' +
                        options.noResultsClass +
                        '">' +
                        (typeof options.noResultsText === "function"
                            ? options.noResultsText.call(this)
                            : options.noResultsText) +
                        "</p>"
                )
            } else {
                if (this.options.responseType === "html") {
                    $allResults.html(data)
                } else {
                    this[
                        "generate" +
                            (data[0][options.responseFormat.groupItems]
                                ? "GroupedResults"
                                : "SimpleResults")
                    ](data, $allResults)
                }
            }

            return $allResults.children()
        },

        generateSimpleResults: function (data, $cont) {
            var self = this

            this.itemModels = data

            $.each(data, function (i, item) {
                $cont.append(self.generateItem(item))
            })
        },

        generateGroupedResults: function (data, $cont) {
            var self = this,
                options = this.options,
                format = options.responseFormat

            this.itemModels = []

            $.each(data, function (i, groupData) {
                var $group = $(
                    '<div class="' + options.groupClass + '">'
                ).appendTo($cont)

                groupData[format.groupCaption] &&
                    $group.append(
                        '<h3 class="' +
                            options.groupTitleClass +
                            '">' +
                            groupData[format.groupCaption] +
                            "</h3>"
                    )

                $.each(groupData.items, function (i, item) {
                    self.itemModels.push(item)
                    $group.append(self.generateItem(item))
                })

                options.onGroupCreate &&
                    options.onGroupCreate.call(self, $group, groupData, self)
            })
        },

        generateItem: function (item) {
            var options = this.options,
                format = options.responseFormat,
                url = item[format.url],
                html = item[format.html] || item[format.label],
                $tag = $("<" + (url ? "a" : "span") + ">")
                    .html(html)
                    .addClass(options.itemClass)

            url && $tag.attr("href", url)

            options.onItemCreate &&
                options.onItemCreate.call(this, $tag, item, this)

            return $tag
        },

        showResults: function ($content) {
            if (!$content && this.resultsOpened) {
                return
            }

            this.$el
                .removeClass(this.options.loadingClass)
                .addClass(this.options.resultsOpenedClass)

            if (this.options.flipOnBottom) {
                this.checkDropdownPosition()
            }

            this.$resultsCont =
                this.$resultsCont ||
                $("<div>")
                    .addClass(this.options.resultsContClass)
                    .appendTo(this.$el)

            if ($content) {
                this.$resultsCont.html($content)
                this.$resultItems = this.$resultsCont.find(this.itemSelector)
                this.options.onResultsCreate &&
                    this.options.onResultsCreate.call(
                        this,
                        this.$resultsCont,
                        this.responseData,
                        this
                    )
            }

            if (!this.resultsOpened) {
                this.documentCancelEvents("on")
                this.$input.trigger("openingResults")
            }

            if (
                this.options.focusFirstItem &&
                this.$resultItems &&
                this.$resultItems.length
            ) {
                this.navigateItem("down")
            }

            this.resultsOpened = true
        },

        checkDropdownPosition: function () {
            var flipOnBottom = this.options.flipOnBottom
            var offset =
                typeof flipOnBottom === "boolean" && flipOnBottom
                    ? 400
                    : flipOnBottom
            var isFlipped =
                this.$input.offset().top + offset > $document.height()

            this.$el.toggleClass(this.options.resultsFlippedClass, isFlipped)
        },

        documentCancelEvents: function (setup, onCancel) {
            var self = this

            if (setup === "off" && this.closeEventsSetuped) {
                $document.off(this.ens)
                this.closeEventsSetuped = false
                return
            } else if (setup === "on" && !this.closeEventsSetuped) {
                $document.on(this.namespaceEvents("click keyup"), function (e) {
                    if (
                        keyMap[e.keyCode] === "escape" ||
                        (!$(e.target).is(self.$el) &&
                            !$.contains(self.$el.get(0), e.target) &&
                            $.contains(document.documentElement, e.target))
                    ) {
                        onCancel ? onCancel.call(self) : self.hideResults()
                    }
                })

                this.closeEventsSetuped = true
            }
        },

        navigateItem: function (direction) {
            var $currentItem = this.$resultItems.filter(
                    this.focusedItemSelector
                ),
                maxPosition = this.$resultItems.length - 1

            if ($currentItem.length === 0) {
                this.$resultItems
                    .eq(direction === "up" ? maxPosition : 0)
                    .addClass(this.options.focusedItemClass)
                return
            }

            var currentPosition = this.$resultItems.index($currentItem),
                nextPosition =
                    direction === "up"
                        ? currentPosition - 1
                        : currentPosition + 1

            nextPosition > maxPosition && (nextPosition = 0)
            nextPosition < 0 && (nextPosition = maxPosition)

            $currentItem.removeClass(this.options.focusedItemClass)

            this.$resultItems
                .eq(nextPosition)
                .addClass(this.options.focusedItemClass)
        },

        navigateDown: function () {
            this.navigateItem("down")
        },

        navigateUp: function () {
            this.navigateItem("up")
        },

        onEnter: function (e) {
            var $currentItem = this.$resultItems.filter(
                this.focusedItemSelector
            )

            if ($currentItem.length) {
                e.preventDefault()
                this.handleItemSelect($currentItem)
            }
        },

        handleItemSelect: function ($item) {
            var selectOption = this.options.onItemSelect,
                model = this.itemModels.length
                    ? this.itemModels[this.$resultItems.index($item)]
                    : {}

            this.$input.trigger("itemSelected")

            if (selectOption === "fillInput") {
                this.fillInput(model)
            } else if (selectOption === "follow") {
                window.location.href = $item.attr("href")
            } else if (typeof selectOption === "function") {
                selectOption.call(this, $item, model, this)
            }
        },

        fillInput: function (model) {
            var options = this.options,
                format = options.responseFormat

            this.query = model[format.label]
            this.$input.val(model[format.label]).trigger("change")

            if (options.fillInputId && model.id) {
                if (!this.$inputId) {
                    var inputIdName =
                        options.inputIdName || this.$input.attr("name") + "_id"

                    this.$inputId = this.$el.find(
                        'input[name="' + inputIdName + '"]'
                    )

                    if (!this.$inputId.length) {
                        this.$inputId = $(
                            '<input type="hidden" name="' + inputIdName + '" />'
                        ).appendTo(this.$el)
                    }
                }

                this.$inputId.val(model.id).trigger("change")
            }

            this.hideResults()
        },

        hideResults: function () {
            if (this.resultsOpened) {
                this.resultsOpened = false
                this.$el.removeClass(this.options.resultsOpenedClass)
                this.$input.trigger("closingResults")
                this.documentCancelEvents("off")
            }

            return this
        },

        clear: function () {
            this.hideResults()
            this.$input.val("").trigger("change")

            return this
        },

        destroy: function () {
            $document.off(this.ens)

            this.$input.off(this.ens)

            this.$el
                .off(this.ens)
                .removeClass(this.options.resultsOpenedClass)
                .removeClass(this.options.loadingClass)

            if (this.$resultsCont) {
                this.$resultsCont.remove()
                delete this.$resultsCont
            }

            delete this.$el.data().fastsearch
        },
    })

    $.extend(Fastsearch, {
        pickTo: function (dest, src, keys) {
            $.each(keys, function (i, key) {
                dest[key] = (src && src[key]) || dest[key]
            })

            return dest
        },

        selectorFromClass: function (classes) {
            return "." + classes.replace(/\s/g, ".")
        },
    })

    Fastsearch.defaults = {
        wrapSelector: "form", // fastsearch container defaults to closest form. Provide selector for something other
        url: null, // plugin will get data from data-url propery, url option or container action attribute
        responseType: "JSON", // default expected server response type - can be set to html if that is what server returns
        preventSubmit: false, // prevent submit of form with enter keypress

        resultsContClass: "fs_results", // html classes
        resultsOpenedClass: "fsr_opened",
        resultsFlippedClass: "fsr_flipped",
        groupClass: "fs_group",
        itemClass: "fs_result_item",
        groupTitleClass: "fs_group_title",
        loadingClass: "loading",
        noResultsClass: "fs_no_results",
        focusedItemClass: "focused",

        typeTimeout: 140, // try not to hammer server with request for each keystroke if possible
        minQueryLength: 2, // minimal number of characters needed for plugin to send request to server

        template: null, // provide your template function if you need one - function(data, fastsearchApi)
        mouseEvents: !(
            "ontouchstart" in window ||
            navigator.maxTouchPoints > 0 ||
            navigator.msMaxTouchPoints > 0
        ), // detect if client is touch enabled so plugin can decide if mouse specific events should be set.
        focusFirstItem: false,
        flipOnBottom: false,

        responseFormat: {
            // Adjust where plugin looks for data in your JSON server response
            url: "url",
            html: "html",
            label: "label",
            groupCaption: "caption",
            groupItems: "items",
        },

        fillInputId: true, // on item select plugin will try to write selected id from item data model to input
        inputIdName: null, // on item select plugin will try to write selected id from item data model to input with this name

        apiInputName: null, // by default plugin will post input name as query parameter - you can provide custom one here

        noResultsText: "No results found",
        onItemSelect: "follow", // by default plugin follows selected link - other options available are "fillInput" and custom callback - function($item, model, fastsearchApi)

        parseResponse: null, // parse server response with your handler and return processed data - function(response, fastsearchApi)
        onResultsCreate: null, // adjust results element - function($allResults, data, fastsearchApi)
        onGroupCreate: null, // adjust group element when created - function($group, groupModel, fastsearchApi)
        onItemCreate: null, // adjust item element when created - function($item, model, fastsearchApi)
    }

    $.fastsearch = Fastsearch

    $.fn.fastsearch = function (options) {
        return this.each(function () {
            if (!$.data(this, "fastsearch")) {
                $.data(this, "fastsearch", new Fastsearch(this, options))
            }
        })
    }

    return $
})

;(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["jquery", "fastsearch"], factory)
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("jquery"), require("fastsearch"))
    } else {
        factory(root.jQuery)
    }
})(this, function ($) {
    var $document = $(document),
        instanceNum = 0,
        Fastsearch = $.fastsearch,
        pickTo = Fastsearch.pickTo,
        selectorFromClass = Fastsearch.selectorFromClass

    function Fastselect(inputElement, options) {
        this.init.apply(this, arguments)
    }

    $.extend(Fastselect.prototype, {
        init: function (inputElement, options) {
            this.$input = $(inputElement)

            this.options = pickTo(
                $.extend(true, {}, Fastselect.defaults, options, {
                    placeholder: this.$input.attr("placeholder"),
                }),
                this.$input.data(),
                [
                    "url",
                    "loadOnce",
                    "apiParam",
                    "initialValue",
                    "userOptionAllowed",
                ]
            )

            this.ens = ".fastselect" + ++instanceNum
            this.hasCustomLoader = this.$input.is("input")
            this.isMultiple = !!this.$input.attr("multiple")
            this.userOptionAllowed =
                this.hasCustomLoader &&
                this.isMultiple &&
                this.options.userOptionAllowed

            this.optionsCollection = new OptionsCollection(
                pickTo({ multipleValues: this.isMultiple }, this.options, [
                    "url",
                    "loadOnce",
                    "parseData",
                    "matcher",
                ])
            )

            this.setupDomElements()
            this.setupFastsearch()
            this.setupEvents()
        },

        setupDomElements: function () {
            this.$el = $("<div>").addClass(this.options.elementClass)

            this[
                this.isMultiple ? "setupMultipleElement" : "setupSingleElement"
            ](function () {
                this.updateDomElements()
                this.$controls.appendTo(this.$el)
                this.$el.insertAfter(this.$input)
                this.$input.detach().appendTo(this.$el)
            })
        },

        setupSingleElement: function (onDone) {
            var initialOptions = this.processInitialOptions(),
                toggleBtnText =
                    initialOptions && initialOptions.length
                        ? initialOptions[0].text
                        : this.options.placeholder

            this.$el.addClass(this.options.singleModeClass)
            this.$controls = $("<div>").addClass(this.options.controlsClass)
            this.$toggleBtn = $("<div>")
                .addClass(this.options.toggleButtonClass)
                .text(toggleBtnText)
                .appendTo(this.$el)
            this.$queryInput = $("<input>")
                .attr("placeholder", this.options.searchPlaceholder)
                .addClass(this.options.queryInputClass)
                .appendTo(this.$controls)

            onDone.call(this)
        },

        setupMultipleElement: function (onDone) {
            var self = this,
                options = self.options,
                initialOptions = this.processInitialOptions()

            this.$el.addClass(options.multipleModeClass)
            this.$controls = $("<div>").addClass(options.controlsClass)
            this.$queryInput = $("<input>")
                .addClass(options.queryInputClass)
                .appendTo(this.$controls)

            initialOptions &&
                $.each(initialOptions, function (i, option) {
                    self.addChoiceItem(option)
                })

            onDone.call(this)
        },

        updateDomElements: function () {
            this.$el.toggleClass(
                this.options.noneSelectedClass,
                !this.optionsCollection.hasSelectedValues()
            )
            this.adjustQueryInputLayout()
        },

        processInitialOptions: function () {
            var self = this,
                options

            if (this.hasCustomLoader) {
                options = this.options.initialValue

                $.isPlainObject(options) && (options = [options])
            } else {
                options = $.map(
                    this.$input.find("option:selected").get(),
                    function (option) {
                        var $option = $(option)

                        return {
                            text: $option.text(),
                            value: $option.attr("value"),
                        }
                    }
                )
            }

            options &&
                $.each(options, function (i, option) {
                    self.optionsCollection.setSelected(option)
                })

            return options
        },

        addChoiceItem: function (optionModel) {
            $(
                '<div data-text="' +
                    optionModel.text +
                    '" data-value="' +
                    optionModel.value +
                    '" class="' +
                    this.options.choiceItemClass +
                    '">' +
                    $("<div>").html(optionModel.text).text() +
                    '<button class="' +
                    this.options.choiceRemoveClass +
                    '" type="button">×</button>' +
                    "</div>"
            ).insertBefore(this.$queryInput)
        },

        setupFastsearch: function () {
            var self = this,
                options = this.options,
                fastSearchParams = {}

            pickTo(fastSearchParams, options, [
                "resultsContClass",
                "resultsOpenedClass",
                "resultsFlippedClass",
                "groupClass",
                "itemClass",
                "focusFirstItem",
                "groupTitleClass",
                "loadingClass",
                "noResultsClass",
                "noResultsText",
                "focusedItemClass",
                "flipOnBottom",
            ])

            this.fastsearch = new Fastsearch(
                this.$queryInput.get(0),
                $.extend(fastSearchParams, {
                    wrapSelector: this.isMultiple ? this.$el : this.$controls,

                    minQueryLength: 0,
                    typeTimeout: this.hasCustomLoader ? options.typeTimeout : 0,
                    preventSubmit: true,
                    fillInputId: false,

                    responseFormat: {
                        label: "text",
                        groupCaption: "label",
                    },

                    onItemSelect: function ($item, model, fastsearch) {
                        var maxItems = options.maxItems

                        if (
                            self.isMultiple &&
                            maxItems &&
                            self.optionsCollection.getValues().length >
                                maxItems - 1
                        ) {
                            options.onMaxItemsReached &&
                                options.onMaxItemsReached(this)
                        } else {
                            self.setSelectedOption(model)
                            self.writeToInput()
                            !self.isMultiple && self.hide()
                            options.clearQueryOnSelect && fastsearch.clear()

                            if (self.userOptionAllowed && model.isUserOption) {
                                fastsearch.$resultsCont.remove()
                                delete fastsearch.$resultsCont
                                self.hide()
                            }

                            options.onItemSelect &&
                                options.onItemSelect.call(
                                    self,
                                    $item,
                                    model,
                                    self,
                                    fastsearch
                                )
                        }
                    },

                    onItemCreate: function ($item, model) {
                        model.$item = $item
                        model.selected &&
                            $item.addClass(options.itemSelectedClass)

                        if (self.userOptionAllowed && model.isUserOption) {
                            $item
                                .text(
                                    self.options.userOptionPrefix + $item.text()
                                )
                                .addClass(self.options.userOptionClass)
                        }

                        options.onItemCreate &&
                            options.onItemCreate.call(self, $item, model, self)
                    },
                })
            )

            this.fastsearch.getResults = function () {
                if (
                    self.userOptionAllowed &&
                    self.$queryInput.val().length > 1
                ) {
                    self.renderOptions()
                }

                self.getOptions(function () {
                    self.renderOptions(true)
                })
            }
        },

        getOptions: function (onDone) {
            var options = this.options,
                self = this,
                params = {}

            if (this.hasCustomLoader) {
                var query = $.trim(this.$queryInput.val())

                if (query && options.apiParam) {
                    params[options.apiParam] = query
                }

                this.optionsCollection.fetch(params, onDone)
            } else {
                !this.optionsCollection.models &&
                    this.optionsCollection.reset(
                        this.gleanSelectData(this.$input)
                    )
                onDone()
            }
        },

        namespaceEvents: function (events) {
            return Fastsearch.prototype.namespaceEvents.call(this, events)
        },

        setupEvents: function () {
            var self = this,
                options = this.options

            if (this.isMultiple) {
                this.$el.on(this.namespaceEvents("click"), function (e) {
                    $(e.target).is(selectorFromClass(options.controlsClass)) &&
                        self.$queryInput.focus()
                })

                this.$queryInput
                    .on(this.namespaceEvents("keyup"), function (e) {
                        // if (self.$queryInput.val().length === 0 && e.keyCode === 8) {
                        //     console.log('TODO implement delete');
                        // }

                        self.adjustQueryInputLayout()
                        self.show()
                    })
                    .on(this.namespaceEvents("focus"), function () {
                        self.show()
                    })

                this.$el.on(
                    this.namespaceEvents("click"),
                    selectorFromClass(options.choiceRemoveClass),
                    function (e) {
                        var $choice = $(e.currentTarget).closest(
                            selectorFromClass(options.choiceItemClass)
                        )

                        self.removeSelectedOption(
                            {
                                value: $choice.attr("data-value"),
                                text: $choice.attr("data-text"),
                            },
                            $choice
                        )
                    }
                )
            } else {
                this.$el.on(
                    this.namespaceEvents("click"),
                    selectorFromClass(options.toggleButtonClass),
                    function () {
                        self.$el.hasClass(options.activeClass)
                            ? self.hide()
                            : self.show(true)
                    }
                )
            }
        },

        adjustQueryInputLayout: function () {
            if (this.isMultiple && this.$queryInput) {
                var noneSelected = this.$el.hasClass(
                    this.options.noneSelectedClass
                )

                this.$queryInput.toggleClass(
                    this.options.queryInputExpandedClass,
                    noneSelected
                )

                if (noneSelected) {
                    this.$queryInput.attr({
                        style: "",
                        placeholder: this.options.placeholder,
                    })
                } else {
                    this.$fakeInput =
                        this.$fakeInput ||
                        $("<span>").addClass(this.options.fakeInputClass)
                    this.$fakeInput.text(
                        this.$queryInput.val().replace(/\s/g, "&nbsp;")
                    )
                    this.$queryInput
                        .removeAttr("placeholder")
                        .css(
                            "width",
                            this.$fakeInput
                                .insertAfter(this.$queryInput)
                                .width() + 20
                        )
                    this.$fakeInput.detach()
                }
            }
        },

        show: function (focus) {
            this.$el.addClass(this.options.activeClass)
            focus ? this.$queryInput.focus() : this.fastsearch.handleTyping()

            this.documentCancelEvents("on")
        },

        hide: function () {
            this.$el.removeClass(this.options.activeClass)

            this.documentCancelEvents("off")
        },

        documentCancelEvents: function (setup) {
            Fastsearch.prototype.documentCancelEvents.call(
                this,
                setup,
                this.hide
            )
        },

        setSelectedOption: function (option) {
            if (this.optionsCollection.isSelected(option.value)) {
                return
            }

            this.optionsCollection.setSelected(option)

            var selectedModel = this.optionsCollection.findWhere(function (
                model
            ) {
                return model.value === option.value
            })

            if (this.isMultiple) {
                this.$controls && this.addChoiceItem(option)
            } else {
                this.fastsearch &&
                    this.fastsearch.$resultItems.removeClass(
                        this.options.itemSelectedClass
                    )
                this.$toggleBtn && this.$toggleBtn.text(option.text)
            }

            selectedModel &&
                selectedModel.$item.addClass(this.options.itemSelectedClass)

            this.updateDomElements()
        },

        removeSelectedOption: function (option, $choiceItem) {
            var removedModel = this.optionsCollection.removeSelected(option)

            if (removedModel && removedModel.$item) {
                removedModel.$item.removeClass(this.options.itemSelectedClass)
            }

            if ($choiceItem) {
                $choiceItem.remove()
            } else {
                this.$el
                    .find(
                        selectorFromClass(this.options.choiceItemClass) +
                            '[data-value="' +
                            option.value +
                            '"]'
                    )
                    .remove()
            }

            this.updateDomElements()
            this.writeToInput()
        },

        writeToInput: function () {
            var values = this.optionsCollection.getValues(),
                delimiter = this.options.valueDelimiter,
                formattedValue = this.isMultiple
                    ? this.hasCustomLoader
                        ? values.join(delimiter)
                        : values
                    : values[0]

            this.$input.val(formattedValue).trigger("change")
        },

        renderOptions: function (filter) {
            var query = this.$queryInput.val()
            var data

            if (this.optionsCollection.models) {
                data = (
                    filter
                        ? this.optionsCollection.filter(query)
                        : this.optionsCollection.models
                ).slice(0)
            } else {
                data = []
            }

            if (this.userOptionAllowed) {
                var queryInList =
                    this.optionsCollection.models &&
                    this.optionsCollection.findWhere(function (model) {
                        return model.value === query
                    })

                query &&
                    !queryInList &&
                    data.unshift({
                        text: query,
                        value: query,
                        isUserOption: true,
                    })
            }

            this.fastsearch.showResults(
                this.fastsearch.storeResponse(data).generateResults(data)
            )
        },

        gleanSelectData: function ($select) {
            var self = this,
                $elements = $select.children()

            if ($elements.eq(0).is("optgroup")) {
                return $.map($elements.get(), function (optgroup) {
                    var $optgroup = $(optgroup)

                    return {
                        label: $optgroup.attr("label"),
                        items: self.gleanOptionsData($optgroup.children()),
                    }
                })
            } else {
                return this.gleanOptionsData($elements)
            }
        },

        gleanOptionsData: function ($options) {
            return $.map($options.get(), function (option) {
                var $option = $(option)
                return {
                    text: $option.text(),
                    value: $option.attr("value"),
                    selected: $option.is(":selected"),
                }
            })
        },

        destroy: function () {
            $document.off(this.ens)
            this.fastsearch.destroy()
            this.$input.off(this.ens).detach().insertAfter(this.$el)
            this.$el.off(this.ens).remove()

            this.$input.data() && delete this.$input.data().fastselect
        },
    })

    function OptionsCollection(options) {
        this.init(options)
    }

    $.extend(OptionsCollection.prototype, {
        defaults: {
            loadOnce: false,
            url: null,
            parseData: null,
            multipleValues: false,
            matcher: function (text, query) {
                return text.toLowerCase().indexOf(query.toLowerCase()) > -1
            },
        },

        init: function (options) {
            this.options = $.extend({}, this.defaults, options)
            this.selectedValues = {}
        },

        fetch: function (fetchParams, onDone) {
            var self = this,
                afterFetch = function () {
                    self.applySelectedValues(onDone)
                }

            if (this.options.loadOnce) {
                this.fetchDeferred =
                    this.fetchDeferred || this.load(fetchParams)
                this.fetchDeferred.done(afterFetch)
            } else {
                this.load(fetchParams, afterFetch)
            }
        },

        reset: function (models) {
            this.models = this.options.parseData
                ? this.options.parseData(models)
                : models
            this.applySelectedValues()
        },

        applySelectedValues: function (onDone) {
            this.each(function (option) {
                if (this.options.multipleValues && option.selected) {
                    this.selectedValues[option.value] = true
                } else {
                    option.selected = !!this.selectedValues[option.value]
                }
            })

            onDone && onDone.call(this)
        },

        load: function (params, onDone) {
            var self = this,
                options = this.options

            return $.get(options.url, params, function (data) {
                self.models = options.parseData ? options.parseData(data) : data

                onDone && onDone.call(self)
            })
        },

        setSelected: function (option) {
            if (!this.options.multipleValues) {
                this.selectedValues = {}
            }

            this.selectedValues[option.value] = true
            this.applySelectedValues()
        },

        removeSelected: function (option) {
            var model = this.findWhere(function (model) {
                return option.value === model.value
            })

            model && (model.selected = false)

            delete this.selectedValues[option.value]

            return model
        },

        isSelected: function (value) {
            return !!this.selectedValues[value]
        },

        hasSelectedValues: function () {
            return this.getValues().length > 0
        },

        each: function (iterator) {
            var self = this

            this.models &&
                $.each(this.models, function (i, option) {
                    option.items
                        ? $.each(option.items, function (i, nestedOption) {
                              iterator.call(self, nestedOption)
                          })
                        : iterator.call(self, option)
                })
        },

        where: function (predicate) {
            var temp = []

            this.each(function (option) {
                predicate(option) && temp.push(option)
            })

            return temp
        },

        findWhere: function (predicate) {
            var models = this.where(predicate)

            return models.length ? models[0] : undefined
        },

        filter: function (query) {
            var self = this

            function checkItem(item) {
                return self.options.matcher(item.text, query) ? item : null
            }

            if (!query || query.length === 0) {
                return this.models
            }

            return $.map(this.models, function (item) {
                if (item.items) {
                    var filteredItems = $.map(item.items, checkItem)

                    return filteredItems.length
                        ? {
                              label: item.label,
                              items: filteredItems,
                          }
                        : null
                } else {
                    return checkItem(item)
                }
            })
        },

        getValues: function () {
            return $.map(this.selectedValues, function (prop, key) {
                return prop ? key : null
            })
        },
    })

    Fastselect.defaults = {
        elementClass: "fstElement",
        singleModeClass: "fstSingleMode",
        noneSelectedClass: "fstNoneSelected",
        multipleModeClass: "fstMultipleMode",
        queryInputClass: "fstQueryInput",
        queryInputExpandedClass: "fstQueryInputExpanded",
        fakeInputClass: "fstFakeInput",
        controlsClass: "fstControls",
        toggleButtonClass: "fstToggleBtn",
        activeClass: "fstActive",
        itemSelectedClass: "fstSelected",
        choiceItemClass: "fstChoiceItem",
        choiceRemoveClass: "fstChoiceRemove",
        userOptionClass: "fstUserOption",

        resultsContClass: "fstResults",
        resultsOpenedClass: "fstResultsOpened",
        resultsFlippedClass: "fstResultsFilpped",
        groupClass: "fstGroup",
        itemClass: "fstResultItem",
        groupTitleClass: "fstGroupTitle",
        loadingClass: "fstLoading",
        noResultsClass: "fstNoResults",
        focusedItemClass: "fstFocused",

        matcher: null,

        url: null,
        loadOnce: false,
        apiParam: "query",
        initialValue: null,
        clearQueryOnSelect: true,
        minQueryLength: 1,
        focusFirstItem: false,
        flipOnBottom: true,
        typeTimeout: 150,
        userOptionAllowed: false,
        valueDelimiter: ",",
        maxItems: null,

        parseData: null,
        onItemSelect: null,
        onItemCreate: null,
        onMaxItemsReached: null,

        placeholder: "Choose option",
        searchPlaceholder: "Search options",
        noResultsText: "No results",
        userOptionPrefix: "Add ",
    }

    $.Fastselect = Fastselect
    $.Fastselect.OptionsCollection = OptionsCollection

    $.fn.fastselect = function (options) {
        return this.each(function () {
            if (!$.data(this, "fastselect")) {
                $.data(this, "fastselect", new Fastselect(this, options))
            }
        })
    }

    return $
})
