<commit_before>(function ($, window, document) {
    'use strict';

    window.SOL = {
        configuration : {
            texts : {
                noItemsAvailable: 'No entries found',
                selectAll: 'Select all',
                selectNone: 'Select none',
                quickDelete: '×'
            },
            classes: {
                selectAll: null,
                selectNone: null
            },
            useBracketParameters: false,
            showSelectAll: false,
            showSelection: true,
            showSelectionBelowList: false,
            allowNullSelection: false
        }
    };

    var PLUGIN_NAME = 'optionListCheckbox',
        DEFAULT_DATATYPE = '_sol_provided_',
        EVENTS_KEY = '_sol_events_',
        PLUGIN_KEY = 'plugin_' + PLUGIN_NAME,
        TYPE_OPTION_ITEM = {
            type: 'option',
            value: '',
            selected: false,
            disabled: false,
            label: '',
            tooltip: ''
        },
        TYPE_OPTION_GROUP = {
            type: 'optiongroup',
            label: '',
            tooltip: '',
            disabled: false
        },
        DEFAULT_OPTIONS = {
            data: DEFAULT_DATATYPE,
            maxHeight: null,
            converter: null,
            onRendered: null
        };

    function SearchableOptionList(element, options) {
        this.element = element;
        this.settings = $.extend(true, {}, window.SOL.configuration, DEFAULT_OPTIONS, options);
        this.items = [];
        this.useCheckboxes = element.prop('multiple');

        this.init();
    }

    $.extend(SearchableOptionList.prototype, {
        init : function () {
            var sol = this;
            this.addUiElements();

            if (!this.element.attr('name')) {
                this.addErrorLabel('name Attribute is required for single selection selects');
                return;
            }

            this.fetchData(function () {
                sol.registerEvents();

                // remove the original element since we completely replace it
                sol.element.remove();
                sol.element = undefined;
            });
        },

        addUiElements: function () {
            this.container = $('
');
            this.inputContainer = $('
');
            this.input = $('<input type="text" />');
            this.caret = $('

');
            this.selection = $('
');
            this.noResultsItem = $('

').html(this.settings.texts.noItemsAvailable).hide();
            this.selectionContainer = $('
').append(this.noResultsItem).append(this.selection);

            if (this.settings.maxHeight) {
                this.selectionContainer.css('max-height', this.settings.maxHeight);
            }

            this.inputContainer
                .append(this.input)
                .append(this.caret)
                .append(this.selectionContainer);

            this.container
                .append(this.inputContainer)
                .insertBefore(this.element);

            if (this.settings.showSelection) {
                this.showSelectionContainer = $('
');

                if (this.settings.showSelectionBelowList) {
                    this.showSelectionContainer.insertAfter(this.inputContainer);
                } else {
                    this.showSelectionContainer.insertBefore(this.inputContainer);
                }
            }

            if (this.settings.showSelectAll && this.useCheckboxes) {
                var iocl = this;
                this.deselectAllButton = $('')
                    .on('click', function (e) { e.preventDefault(); iocl.deselectAll(); return false; })
                    .html(this.settings.texts.selectNone);

                if (this.settings.classes.selectNone) {
                    this.deselectAllButton.addClass(this.settings.classes.selectNone);
                }

                this.selectAllButton = $('')
                    .on('click', function (e) { e.preventDefault(); iocl.selectAll(); return false; })
                    .html(this.settings.texts.selectAll);

                if (this.settings.classes.selectAll) {
                    this.selectAllButton.addClass(this.settings.classes.selectAll);
                }

                this.actionButtons = $('
')
                    .append(this.selectAllButton)
                    .append(this.deselectAllButton)
                    .append('

');
                this.selectionContainer.prepend(this.actionButtons);
            }

            // maximum of original <select> width and default <input> width
            this.input.css('width', Math.max(this.input.outerWidth(false), $(this.element).outerWidth(false)));

            if ($.isFunction(this.settings.onRendered)) {
                this.settings.onRendered.call(this);
            }
        },

        fetchData: function (callback) {
            var sol = this,
                doRender = true;

            if (this.settings.data === DEFAULT_DATATYPE) {
                this.items = this.getDataFromSelectElement();
            } else if ($.isFunction(this.settings.data)) {
                this.items = this.settings.data(this);
            } else if ($.isArray(this.settings.data)) {
                this.items = this.settings.data;
            } else if (typeof this.settings.data === 'string') {
                doRender = false;
                $.ajax(this.settings.data, {
                    success: function (actualData) {
                        sol.items = actualData;
                        sol.postProcessDataAndRender();
                        callback();
                    }
                });
            } else {
                this.addErrorLabel('Invalid data type');
                doRender = false;
            }

            if (doRender) {
                this.postProcessDataAndRender();
                callback();
            }
        },

        postProcessDataAndRender: function () {
            if ($.isFunction(this.settings.converter)) {
                this.items = this.settings.converter(this.items);
            }

            this.renderData();
        },

        renderData: function () {
            var sol = this;

            if (!this.items) {
                this.addErrorLabel('Data items not set. Maybe the converter did not return any values');
                return;
            }

            if (this.items.length === 0) {
                this.noResultsItem.show();
                return;
            }

            $.each(this.items, function (index, item) {
                if (item.type === 'option') {
                    sol.renderOption(index, item, sol.selection);
                } else {
                    sol.renderOptionGroup(index, item, sol.selection);
                }
            });
        },

        getDataFromSelectElement: function () {
            var dataArray = [],
                sol = this;

            $.each(this.element.children(), function (index, item) {
                var $item = $(item),
                    tagName = $item.prop('tagName').toLowerCase(),
                    converted;

                if ('option' === tagName) {
                    converted = sol.onHtmlOption(index, $item);
                    if (converted) {
                        converted.element = $item;
                        dataArray.push(converted);
                    }
                } else if ('optgroup' === tagName) {
                    converted = sol.onHtmlOptionGroup(index, $item);
                    if (converted) {
                        dataArray.push(converted);
                    }
                } else {
                    sol.addErrorLabel('Invalid element found in select: ' + tagName + '. Only option and optiongroup are allowed');
                }
            });

            return dataArray;
        },

        applySearch: function () {
            if (!this.items || this.items.length === 0) { return; }

            var searchTerm = this.input.val(),
                searchTermLowerCased = (searchTerm || '').toLowerCase();

            this.selectionContainer.find('.sol-filtered-search').removeClass('sol-filtered-search');
            this.noResultsItem.hide();

            if (searchTerm && searchTerm.trim().length > 0) {
                this.findTerms(this.items, searchTermLowerCased);
            }
        },

        findTerms: function (dataArray, searchTerm) {
            if (!dataArray || !$.isArray(dataArray) || dataArray.length === 0) { return; }

            var sol = this;

            $.each(dataArray, function (index, item) {
                if (item.type === 'option') {
                    var $element = item.displayElement,
                        elementSearchableTerms = (item.label + ' ' + item.tooltip).trim().toLowerCase();

                    if (elementSearchableTerms.indexOf(searchTerm) === -1) {
                        $element.addClass('sol-filtered-search');
                    }
                } else {
                    sol.findTerms(item.children, searchTerm);
                    var amountOfUnfilteredChildren = item.displayElement.find('.sol-option:not(.sol-filtered-search)');

                    if (amountOfUnfilteredChildren.length === 0) {
                        item.displayElement.addClass('sol-filtered-search');
                    }
                }
            });

            if (this.selectionContainer.find('.sol-option:not(.sol-filtered-search)').length === 0) {
                this.noResultsItem.show();
            } else {
                this.noResultsItem.hide();
            }
        },

        registerEvents: function () {
            var sol = this;

            // global events nur einmal
            if (!window[EVENTS_KEY]) {
                $(document).on('click', function (e) {
                    var $closestSolContainer = $(e.target).closest('.sol-container'),
                        $currentItem = $closestSolContainer.first();

                    if (!$closestSolContainer.length) {
                        // clicked outside of any sol-container
                        $('.sol-container.active').removeClass('active')
                            .find('input[type="text"]').val('').trigger('keyup');
                    } else {
                        $('.sol-container.active').not($currentItem).each(function (index, item) {
                            $(item).removeClass('active')
                                .find('input[type="text"]').val('').trigger('keyup');
                        });
                    }
                });
                window[EVENTS_KEY] = new Date().getTime();
            }

            // element events mehrfach
            if (!$.data(this.element, EVENTS_KEY)) {
                this.caret.on('click', function () {
                    sol.container.toggleClass('active');
                });

                this.input
                    .on('focus', function () {
                        sol.container.addClass('active');
                    })
                    .on('keyup', function (e) {
                        var keyCode = e.keyCode;
                        if (keyCode === 27) {
                            sol.input.val('');
                        }

                        if (keyCode === 16 || keyCode === 17 || keyCode === 18 || keyCode === 20) {
                            return;
                        }

                        sol.applySearch();
                    });

                $.data(this.element, EVENTS_KEY, new Date().getTime());
            }
        },

        onHtmlOption: function (index, $element) {
            return $.extend({}, TYPE_OPTION_ITEM, {
                value: $element.attr('value'),
                selected: $element.prop('selected'),
                disabled: $element.prop('disabled'),
                label: $element.html(),
                tooltip: $element.attr('title'),
                element: $element
            });
        },

        onHtmlOptionGroup: function (index, $element) {
            var optionGroup = $.extend({}, TYPE_OPTION_GROUP, {
                label: $element.attr('label'),
                tooltip: $element.attr('title'),
                disabled: $element.prop('disabled')
            }),
                childOptions = $element.children('option'),
                sol = this;
            optionGroup.children = [];

            $.each(childOptions, function (childIndex, child) {
                var $child = $(child),
                    childDataItem = sol.onHtmlOption(childIndex, $child);

                if (childDataItem) {
                    if (optionGroup.disabled) {
                        childDataItem.disabled = true;
                    }

                    childDataItem.parent = optionGroup;
                    optionGroup.children.push(childDataItem);
                }
            });

            return optionGroup;
        },

        renderOption: function (index, item, $container) {
            var $uiInput,
                inputName = this.element.attr('name'),
                sol = this;

            if (this.useCheckboxes) {
                $uiInput = $('<input type="checkbox" class="sol-checkbox" />');

                if (this.settings.useBracketParameters) {
                    inputName += '[]';
                }
            } else {
                $uiInput = $('<input type="radio" class="sol-radio" />')
                    .on('change', function () {
                        sol.selectionContainer.find('input[type="radio"][name="' + inputName + '"]').not($(this)).trigger('sol-deselect');
                    })
                    .on('sol-deselect', function () {
                        var $myDisplayItem = $(this).data('sol-item').displaySelectionItem;
                        if ($myDisplayItem) {
                            $myDisplayItem.remove();
                        }
                    });
            }

            $uiInput
                .prop('checked', item.selected)
                .prop('disabled', item.disabled)
                .attr('name', inputName)
                .data('sol-item', item)
                .val(item.value);

            var $label = $('')
                    .attr('title', item.tooltip)
                    .html(item.label)
                    .prepend($uiInput),

                $entry = $('
')
                    .append($label);

            item.displayElement = $entry;

            if (this.settings.showSelection) {
                $uiInput.on('change', function () {
                    if ($(this).prop('checked')) {
                        sol.addSelectionDisplayItem(item, $uiInput);
                    } else {
                        item.displaySelectionItem.remove();
                    }
                });

                if (item.selected) {
                    this.addSelectionDisplayItem(item, $uiInput);
                }
            }

            $container.append($entry);
        },

        addSelectionDisplayItem: function (item, $uiInput) {
            var $displaySelectionItem = $('
')
                    .html(item.label)
                    .attr('title', item.tooltip)
                    .appendTo(this.showSelectionContainer);

            if (this.useCheckboxes || this.settings.allowNullSelection) {
                $('')
                    .html(this.settings.texts.quickDelete)
                    .on('click', function () {
                        $uiInput.prop('checked', false);
                        $displaySelectionItem.remove();
                    })
                    .prependTo($displaySelectionItem);
            }

            item.displaySelectionItem = $displaySelectionItem;
        },

        renderOptionGroup: function (index, item, $container, attachAndCreateElements) {
            var $groupCaption = $('
')
                    .attr('title', item.tooltip)
                    .html(item.label),
                $groupItem = $('
')
                    .append($groupCaption),
                sol = this;

            if (item.disabled) {
                $groupItem.addClass('disabled');
            }

            if ($.isArray(item.children)) {
                $.each(item.children, function (childIndex, child) {
                    sol.renderOption(childIndex, child, $groupItem, attachAndCreateElements);
                });
            }

            item.displayElement = $groupItem;

            $container.append($groupItem);
        },

        selectAll: function () {
            this.selectionContainer
                .find('input[type="checkbox"]:not([disabled], :checked)')
                .prop('checked', true)
                .trigger('change');
        },

        deselectAll: function () {
            this.selectionContainer
                .find('input[type="checkbox"]:not([disabled]):checked')
                .prop('checked', false)
                .trigger('change');
        },

        getSelection: function () {
            return this.element.find('option:selected');
        },

        addErrorLabel: function (message) {
            if (!this.container) {
                alert('Error: this.container not set yet. This is a bug in the plugin and needs to be fixed');
            } else {
                this.container.append($('
').html(message));
            }
        }
    });

	$.fn.searchableOptionList = function (options) {
        if (this.length === 1) {
            if (!$.data(this, PLUGIN_KEY)) {
                $.data(this, PLUGIN_KEY, new SearchableOptionList($(this), options));
            }

            return $.data(this, PLUGIN_KEY);
        } else {
            return this.each(function () {
                if (!$.data(this, PLUGIN_KEY)) {
                    $.data(this, PLUGIN_KEY, new SearchableOptionList($(this), options));
                }
            });
        }
    };
}(jQuery, window, document));
<commit_msg>Update searchableOptionList.js<commit_after>(function ($, window, document) {
    'use strict';

    window.SOL = {
        configuration : {
            texts : {
                noItemsAvailable: 'No entries found',
                selectAll: 'Select all',
                selectNone: 'Select none',
                quickDelete: '×',
                searchplaceholder: 'Click here to search'
            },
            classes: {
                selectAll: null,
                selectNone: null
            },
            useBracketParameters: false,
            showSelectAll: false,
            showSelection: true,
            showSelectionBelowList: false,
            allowNullSelection: false
        }
    };

    var PLUGIN_NAME = 'optionListCheckbox',
        DEFAULT_DATATYPE = '_sol_provided_',
        EVENTS_KEY = '_sol_events_',
        PLUGIN_KEY = 'plugin_' + PLUGIN_NAME,
        TYPE_OPTION_ITEM = {
            type: 'option',
            value: '',
            selected: false,
            disabled: false,
            label: '',
            tooltip: ''
        },
        TYPE_OPTION_GROUP = {
            type: 'optiongroup',
            label: '',
            tooltip: '',
            disabled: false
        },
        DEFAULT_OPTIONS = {
            data: DEFAULT_DATATYPE,
            maxHeight: null,
            converter: null,
            onRendered: null
        };

    function SearchableOptionList(element, options) {
        this.element = element;
        this.settings = $.extend(true, {}, window.SOL.configuration, DEFAULT_OPTIONS, options);
        this.items = [];
        this.useCheckboxes = element.prop('multiple');

        this.init();
    }

    $.extend(SearchableOptionList.prototype, {
        init : function () {
            var sol = this;
            this.addUiElements();

            if (!this.element.attr('name')) {
                this.addErrorLabel('name Attribute is required for single selection selects');
                return;
            }

            this.fetchData(function () {
                sol.registerEvents();

                // remove the original element since we completely replace it
                sol.element.remove();
                sol.element = undefined;
            });
        },

        addUiElements: function () {
            this.container = $('
');
            this.inputContainer = $('
');
            this.input = $('<input type="text" />');
            this.caret = $('

');
            this.selection = $('
');
            this.noResultsItem = $('

').html(this.settings.texts.noItemsAvailable).hide();
            this.selectionContainer = $('
').append(this.noResultsItem).append(this.selection);

            if (this.settings.maxHeight) {
                this.selectionContainer.css('max-height', this.settings.maxHeight);
            }

            this.input.attr('placeholder', this.settings.texts.searchplaceholder);

            this.inputContainer
                .append(this.input)
                .append(this.caret)
                .append(this.selectionContainer);

            this.container
                .append(this.inputContainer)
                .insertBefore(this.element);

            if (this.settings.showSelection) {
                this.showSelectionContainer = $('
');

                if (this.settings.showSelectionBelowList) {
                    this.showSelectionContainer.insertAfter(this.inputContainer);
                } else {
                    this.showSelectionContainer.insertBefore(this.inputContainer);
                }
            }

            if (this.settings.showSelectAll && this.useCheckboxes) {
                var iocl = this;
                this.deselectAllButton = $('')
                    .on('click', function (e) { e.preventDefault(); iocl.deselectAll(); return false; })
                    .html(this.settings.texts.selectNone);

                if (this.settings.classes.selectNone) {
                    this.deselectAllButton.addClass(this.settings.classes.selectNone);
                }

                this.selectAllButton = $('')
                    .on('click', function (e) { e.preventDefault(); iocl.selectAll(); return false; })
                    .html(this.settings.texts.selectAll);

                if (this.settings.classes.selectAll) {
                    this.selectAllButton.addClass(this.settings.classes.selectAll);
                }

                this.actionButtons = $('
')
                    .append(this.selectAllButton)
                    .append(this.deselectAllButton)
                    .append('

');
                this.selectionContainer.prepend(this.actionButtons);
            }

            // maximum of original <select> width and default <input> width
            this.input.css('width', Math.max(this.input.outerWidth(false), $(this.element).outerWidth(false)));

            if ($.isFunction(this.settings.onRendered)) {
                this.settings.onRendered.call(this);
            }
        },

        fetchData: function (callback) {
            var sol = this,
                doRender = true;

            if (this.settings.data === DEFAULT_DATATYPE) {
                this.items = this.getDataFromSelectElement();
            } else if ($.isFunction(this.settings.data)) {
                this.items = this.settings.data(this);
            } else if ($.isArray(this.settings.data)) {
                this.items = this.settings.data;
            } else if (typeof this.settings.data === 'string') {
                doRender = false;
                $.ajax(this.settings.data, {
                    success: function (actualData) {
                        sol.items = actualData;
                        sol.postProcessDataAndRender();
                        callback();
                    }
                });
            } else {
                this.addErrorLabel('Invalid data type');
                doRender = false;
            }

            if (doRender) {
                this.postProcessDataAndRender();
                callback();
            }
        },

        postProcessDataAndRender: function () {
            if ($.isFunction(this.settings.converter)) {
                this.items = this.settings.converter(this.items);
            }

            this.renderData();
        },

        renderData: function () {
            var sol = this;

            if (!this.items) {
                this.addErrorLabel('Data items not set. Maybe the converter did not return any values');
                return;
            }

            if (this.items.length === 0) {
                this.noResultsItem.show();
                return;
            }

            $.each(this.items, function (index, item) {
                if (item.type === 'option') {
                    sol.renderOption(index, item, sol.selection);
                } else {
                    sol.renderOptionGroup(index, item, sol.selection);
                }
            });
        },

        getDataFromSelectElement: function () {
            var dataArray = [],
                sol = this;

            $.each(this.element.children(), function (index, item) {
                var $item = $(item),
                    tagName = $item.prop('tagName').toLowerCase(),
                    converted;

                if ('option' === tagName) {
                    converted = sol.onHtmlOption(index, $item);
                    if (converted) {
                        converted.element = $item;
                        dataArray.push(converted);
                    }
                } else if ('optgroup' === tagName) {
                    converted = sol.onHtmlOptionGroup(index, $item);
                    if (converted) {
                        dataArray.push(converted);
                    }
                } else {
                    sol.addErrorLabel('Invalid element found in select: ' + tagName + '. Only option and optiongroup are allowed');
                }
            });

            return dataArray;
        },

        applySearch: function () {
            if (!this.items || this.items.length === 0) { return; }

            var searchTerm = this.input.val(),
                searchTermLowerCased = (searchTerm || '').toLowerCase();

            this.selectionContainer.find('.sol-filtered-search').removeClass('sol-filtered-search');
            this.noResultsItem.hide();

            if (searchTerm && searchTerm.trim().length > 0) {
                this.findTerms(this.items, searchTermLowerCased);
            }
        },

        findTerms: function (dataArray, searchTerm) {
            if (!dataArray || !$.isArray(dataArray) || dataArray.length === 0) { return; }

            var sol = this;

            $.each(dataArray, function (index, item) {
                if (item.type === 'option') {
                    var $element = item.displayElement,
                        elementSearchableTerms = (item.label + ' ' + item.tooltip).trim().toLowerCase();

                    if (elementSearchableTerms.indexOf(searchTerm) === -1) {
                        $element.addClass('sol-filtered-search');
                    }
                } else {
                    sol.findTerms(item.children, searchTerm);
                    var amountOfUnfilteredChildren = item.displayElement.find('.sol-option:not(.sol-filtered-search)');

                    if (amountOfUnfilteredChildren.length === 0) {
                        item.displayElement.addClass('sol-filtered-search');
                    }
                }
            });

            if (this.selectionContainer.find('.sol-option:not(.sol-filtered-search)').length === 0) {
                this.noResultsItem.show();
            } else {
                this.noResultsItem.hide();
            }
        },

        registerEvents: function () {
            var sol = this;

            // global events nur einmal
            if (!window[EVENTS_KEY]) {
                $(document).on('click', function (e) {
                    var $closestSolContainer = $(e.target).closest('.sol-container'),
                        $currentItem = $closestSolContainer.first();

                    if (!$closestSolContainer.length) {
                        // clicked outside of any sol-container
                        $('.sol-container.active').removeClass('active')
                            .find('input[type="text"]').val('').trigger('keyup');
                    } else {
                        $('.sol-container.active').not($currentItem).each(function (index, item) {
                            $(item).removeClass('active')
                                .find('input[type="text"]').val('').trigger('keyup');
                        });
                    }
                });
                window[EVENTS_KEY] = new Date().getTime();
            }

            // element events mehrfach
            if (!$.data(this.element, EVENTS_KEY)) {
                this.caret.on('click', function () {
                    sol.container.toggleClass('active');
                });

                this.input
                    .on('focus', function () {
                        sol.container.addClass('active');
                    })
                    .on('keyup', function (e) {
                        var keyCode = e.keyCode;
                        if (keyCode === 27) {
                            sol.input.val('');
                        }

                        if (keyCode === 16 || keyCode === 17 || keyCode === 18 || keyCode === 20) {
                            return;
                        }

                        sol.applySearch();
                    });

                $.data(this.element, EVENTS_KEY, new Date().getTime());
            }
        },

        onHtmlOption: function (index, $element) {
            return $.extend({}, TYPE_OPTION_ITEM, {
                value: $element.attr('value'),
                selected: $element.prop('selected'),
                disabled: $element.prop('disabled'),
                label: $element.html(),
                tooltip: $element.attr('title'),
                element: $element
            });
        },

        onHtmlOptionGroup: function (index, $element) {
            var optionGroup = $.extend({}, TYPE_OPTION_GROUP, {
                label: $element.attr('label'),
                tooltip: $element.attr('title'),
                disabled: $element.prop('disabled')
            }),
                childOptions = $element.children('option'),
                sol = this;
            optionGroup.children = [];

            $.each(childOptions, function (childIndex, child) {
                var $child = $(child),
                    childDataItem = sol.onHtmlOption(childIndex, $child);

                if (childDataItem) {
                    if (optionGroup.disabled) {
                        childDataItem.disabled = true;
                    }

                    childDataItem.parent = optionGroup;
                    optionGroup.children.push(childDataItem);
                }
            });

            return optionGroup;
        },

        renderOption: function (index, item, $container) {
            var $uiInput,
                inputName = this.element.attr('name'),
                sol = this;

            if (this.useCheckboxes) {
                $uiInput = $('<input type="checkbox" class="sol-checkbox" />');

                if (this.settings.useBracketParameters) {
                    inputName += '[]';
                }
            } else {
                $uiInput = $('<input type="radio" class="sol-radio" />')
                    .on('change', function () {
                        sol.selectionContainer.find('input[type="radio"][name="' + inputName + '"]').not($(this)).trigger('sol-deselect');
                    })
                    .on('sol-deselect', function () {
                        var $myDisplayItem = $(this).data('sol-item').displaySelectionItem;
                        if ($myDisplayItem) {
                            $myDisplayItem.remove();
                        }
                    });
            }

            $uiInput
                .prop('checked', item.selected)
                .prop('disabled', item.disabled)
                .attr('name', inputName)
                .data('sol-item', item)
                .val(item.value);

            var $label = $('')
                    .attr('title', item.tooltip)
                    .html(item.label)
                    .prepend($uiInput),

                $entry = $('
')
                    .append($label);

            item.displayElement = $entry;

            if (this.settings.showSelection) {
                $uiInput.on('change', function () {
                    if ($(this).prop('checked')) {
                        sol.addSelectionDisplayItem(item, $uiInput);
                    } else {
                        item.displaySelectionItem.remove();
                    }
                });

                if (item.selected) {
                    this.addSelectionDisplayItem(item, $uiInput);
                }
            }

            $container.append($entry);
        },

        addSelectionDisplayItem: function (item, $uiInput) {
            var $displaySelectionItem = $('
')
                    .html(item.label)
                    .attr('title', item.tooltip)
                    .appendTo(this.showSelectionContainer);

            if (this.useCheckboxes || this.settings.allowNullSelection) {
                $('')
                    .html(this.settings.texts.quickDelete)
                    .on('click', function () {
                        $uiInput.prop('checked', false);
                        $displaySelectionItem.remove();
                    })
                    .prependTo($displaySelectionItem);
            }

            item.displaySelectionItem = $displaySelectionItem;
        },

        renderOptionGroup: function (index, item, $container, attachAndCreateElements) {
            var $groupCaption = $('
')
                    .attr('title', item.tooltip)
                    .html(item.label),
                $groupItem = $('
')
                    .append($groupCaption),
                sol = this;

            if (item.disabled) {
                $groupItem.addClass('disabled');
            }

            if ($.isArray(item.children)) {
                $.each(item.children, function (childIndex, child) {
                    sol.renderOption(childIndex, child, $groupItem, attachAndCreateElements);
                });
            }

            item.displayElement = $groupItem;

            $container.append($groupItem);
        },

        selectAll: function () {
            this.selectionContainer
                .find('input[type="checkbox"]:not([disabled], :checked)')
                .prop('checked', true)
                .trigger('change');
        },

        deselectAll: function () {
            this.selectionContainer
                .find('input[type="checkbox"]:not([disabled]):checked')
                .prop('checked', false)
                .trigger('change');
        },

        getSelection: function () {
            return this.element.find('option:selected');
        },

        addErrorLabel: function (message) {
            if (!this.container) {
                alert('Error: this.container not set yet. This is a bug in the plugin and needs to be fixed');
            } else {
                this.container.append($('
').html(message));
            }
        }
    });

	$.fn.searchableOptionList = function (options) {
        if (this.length === 1) {
            if (!$.data(this, PLUGIN_KEY)) {
                $.data(this, PLUGIN_KEY, new SearchableOptionList($(this), options));
            }

            return $.data(this, PLUGIN_KEY);
        } else {
            return this.each(function () {
                if (!$.data(this, PLUGIN_KEY)) {
                    $.data(this, PLUGIN_KEY, new SearchableOptionList($(this), options));
                }
            });
        }
    };
}(jQuery, window, document));
<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery');

var debounce = require('./lodash/debounce');

var EventDelegator = require('./event-delegator');

var Selectivity = require('./selectivity-base');

/**
 * selectivity Dropdown Constructor.
 *
 * @param options Options object. Should have the following properties:
 *                selectivity - Selectivity instance to show the dropdown for.
 *                showSearchInput - Boolean whether a search input should be shown.
 */
function SelectivityDropdown(options) {

    var selectivity = options.selectivity;

    this.$el = $(selectivity.template('dropdown', {
        dropdownCssClass: selectivity.options.dropdownCssClass,
        searchInputPlaceholder: selectivity.options.searchInputPlaceholder,
        showSearchInput: options.showSearchInput
    }));

    /**
     * jQuery container to add the results to.
     */
    this.$results = this.$('.selectivity-results-container');

    /**
     * Boolean indicating whether more results are available than currently displayed in the
     * dropdown.
     */
    this.hasMore = false;

    /**
     * The currently highlighted result item.
     */
    this.highlightedResult = null;

    /**
     * Boolean whether the load more link is currently highlighted.
     */
    this.loadMoreHighlighted = false;

    /**
     * Options passed to the dropdown constructor.
     */
    this.options = options;

    /**
     * The results displayed in the dropdown.
     */
    this.results = [];

    /**
     * Selectivity instance.
     */
    this.selectivity = selectivity;

    this._closed = false;

    this._closeProxy = this.close.bind(this);
    if (selectivity.options.closeOnSelect !== false) {
        selectivity.$el.on('selectivity-selecting', this._closeProxy);
    }

    this._lastMousePosition = {};

    this.addToDom();
    this.position();
    this.setupCloseHandler();

    this._suppressMouseWheel();

    if (options.showSearchInput) {
        selectivity.initSearchInput(this.$('.selectivity-search-input'));
        selectivity.focus();
    }

    EventDelegator.call(this);

    this.$results.on('scroll touchmove touchend', debounce(this._scrolled.bind(this), 50));

    this.showLoading();

    setTimeout(this.triggerOpen.bind(this), 1);
}

/**
 * Methods.
 */
$.extend(SelectivityDropdown.prototype, EventDelegator.prototype, {

    /**
     * Convenience shortcut for this.$el.find(selector).
     */
    $: function(selector) {

        return this.$el.find(selector);
    },

    /**
     * Adds the dropdown to the DOM.
     */
    addToDom: function() {

        var $next;
        var $anchor = this.selectivity.$el;
        while (($next = $anchor.next('.selectivity-dropdown')).length) {
            $anchor = $next;
        }
        this.$el.insertAfter($anchor);
    },

    /**
     * Closes the dropdown.
     */
    close: function() {

        if (!this._closed) {
            this._closed = true;

            this.$el.remove();

            this.removeCloseHandler();

            this.selectivity.$el.off('selectivity-selecting', this._closeProxy);

            this.triggerClose();
        }
    },

    /**
     * Events map.
     *
     * Follows the same format as Backbone: http://backbonejs.org/#View-delegateEvents
     */
    events: {
        'click .selectivity-load-more': '_loadMoreClicked',
        'click .selectivity-result-item': '_resultClicked',
        'mouseenter .selectivity-load-more': '_loadMoreHovered',
        'mouseenter .selectivity-result-item': '_resultHovered'
    },

    /**
     * Highlights a result item.
     *
     * @param item The item to highlight.
     */
    highlight: function(item) {

        if (this.loadMoreHighlighted) {
            this.$('.selectivity-load-more').removeClass('highlight');
        }

        this.$('.selectivity-result-item').removeClass('highlight')
            .filter('[data-item-id=' + Selectivity.quoteCssAttr(item.id) + ']')
            .addClass('highlight');

        this.highlightedResult = item;
        this.loadMoreHighlighted = false;

        this.selectivity.triggerEvent('selectivity-highlight', { item: item, id: item.id });
    },

    /**
     * Highlights the load more link.
     *
     * @param item The item to highlight.
     */
    highlightLoadMore: function() {

        this.$('.selectivity-result-item').removeClass('highlight');

        this.$('.selectivity-load-more').addClass('highlight');

        this.highlightedResult = null;
        this.loadMoreHighlighted = true;
    },

    /**
     * Loads a follow-up page with results after a search.
     *
     * This method should only be called after a call to search() when the callback has indicated
     * more results are available.
     */
    loadMore: function() {

        this.options.query({
            callback: function(response) {
                if (response && response.results) {
                    this._showResults(
                        Selectivity.processItems(response.results),
                        { add: true, hasMore: !!response.more }
                    );
                } else {
                    throw new Error('callback must be passed a response object');
                }
            }.bind(this),
            error: this._showResults.bind(this, [], { add: true }),
            offset: this.results.length,
            selectivity: this.selectivity,
            term: this.term
        });
    },

    /**
     * Positions the dropdown inside the DOM.
     */
    position: function() {

        var position = this.options.position;
        if (position) {
            position(this.$el, this.selectivity.$el);
        }

        this._scrolled();
    },

    /**
     * Removes the event handler to close the dropdown.
     */
    removeCloseHandler: function() {

        $('body').off('click', this._closeProxy);
    },

    /**
     * Renders an array of result items.
     *
     * @param items Array of result items.
     *
     * @return HTML-formatted string to display the result items.
     */
    renderItems: function(items) {

        var selectivity = this.selectivity;
        return items.map(function(item) {
            var result = selectivity.template(item.id ? 'resultItem' : 'resultLabel', item);
            if (item.children) {
                result += selectivity.template('resultChildren', {
                    childrenHtml: this.renderItems(item.children)
                });
            }
            return result;
        }, this).join('');
    },

    /**
     * Searches for results based on the term given or the term entered in the search input.
     *
     * If an items array has been passed with the options to the Selectivity instance, a local
     * search will be performed among those items. Otherwise, the query function specified in the
     * options will be used to perform the search. If neither is defined, nothing happens.
     *
     * @param term Term to search for.
     */
    search: function(term) {

        var self = this;

        term = term || '';
        self.term = term;

        if (self.options.items) {
            term = Selectivity.transformText(term);
            var matcher = self.selectivity.matcher;
            self._showResults(self.options.items.map(function(item) {
                return matcher(item, term);
            }).filter(function(item) {
                return !!item;
            }), { term: term });
        } else if (self.options.query) {
            self.options.query({
                callback: function(response) {
                    if (response && response.results) {
                        self._showResults(
                            Selectivity.processItems(response.results),
                            { hasMore: !!response.more, term: term }
                        );
                    } else {
                        throw new Error('callback must be passed a response object');
                    }
                },
                error: self.showError.bind(self),
                offset: 0,
                selectivity: self.selectivity,
                term: term
            });
        }
    },

    /**
     * Selects the highlighted item.
     */
    selectHighlight: function() {

        if (this.highlightedResult) {
            this.selectItem(this.highlightedResult.id);
        } else if (this.loadMoreHighlighted) {
            this._loadMoreClicked();
        }
    },

    /**
     * Selects the item with the given ID.
     *
     * @param id ID of the item to select.
     */
    selectItem: function(id) {

        var item = Selectivity.findNestedById(this.results, id);
        if (item && !item.disabled) {
            var options = { id: id, item: item };
            if (this.selectivity.triggerEvent('selectivity-selecting', options)) {
                this.selectivity.triggerEvent('selectivity-selected', options);
            }
        }
    },

    /**
     * Sets up an event handler that will close the dropdown when the Selectivity control loses
     * focus.
     */
    setupCloseHandler: function() {

        $('body').on('click', this._closeProxy);
    },

    /**
     * Shows an error message.
     *
     * @param message Error message to display.
     * @param options Options object. May contain the following property:
     *                escape - Set to false to disable HTML-escaping of the message. Useful if you
     *                         want to set raw HTML as the message, but may open you up to XSS
     *                         attacks if you're not careful with escaping user input.
     */
    showError: function(message, options) {

        options = options || {};

        this.$results.html(this.selectivity.template('error', {
            escape: options.escape !== false,
            message: message
        }));

        this.hasMore = false;
        this.results = [];

        this.highlightedResult = null;
        this.loadMoreHighlighted = false;

        this.position();
    },

    /**
     * Shows a loading indicator in the dropdown.
     */
    showLoading: function() {

        this.$results.html(this.selectivity.template('loading'));

        this.hasMore = false;
        this.results = [];

        this.highlightedResult = null;
        this.loadMoreHighlighted = false;

        this.position();
    },

    /**
     * Shows the results from a search query.
     *
     * @param results Array of result items.
     * @param options Options object. May contain the following properties:
     *                add - True if the results should be added to any already shown results.
     *                hasMore - Boolean whether more results can be fetched using the query()
     *                          function.
     *                term - The search term for which the results are displayed.
     */
    showResults: function(results, options) {

        var resultsHtml = this.renderItems(results);
        if (options.hasMore) {
            resultsHtml += this.selectivity.template('loadMore');
        } else {
            if (!resultsHtml && !options.add) {
                resultsHtml = this.selectivity.template('noResults', { term: options.term });
            }
        }

        if (options.add) {
            this.$('.selectivity-loading').replaceWith(resultsHtml);

            this.results = this.results.concat(results);
        } else {
            this.$results.html(resultsHtml);

            this.results = results;
        }

        this.hasMore = options.hasMore;

        if (!options.add || this.loadMoreHighlighted) {
            this._highlightFirstItem(results);
        }

        this.position();
    },

    /**
     * Triggers the 'selectivity-close' event.
     */
    triggerClose: function() {

        this.selectivity.$el.trigger('selectivity-close');
    },

    /**
     * Triggers the 'selectivity-open' event.
     */
    triggerOpen: function() {

        this.selectivity.$el.trigger('selectivity-open');
    },

    /**
     * @private
     */
    _highlightFirstItem: function(results) {

        function findFirstItem(results) {
            for (var i = 0, length = results.length; i < length; i++) {
                var result = results[i];
                if (result.id) {
                    return result;
                } else if (result.children) {
                    var item = findFirstItem(result.children);
                    if (item) {
                        return item;
                    }
                }
            }
        }

        var firstItem = findFirstItem(results);
        if (firstItem) {
            this.highlight(firstItem);
        } else {
            this.highlightedResult = null;
            this.loadMoreHighlighted = false;
        }
    },

    /**
     * @private
     */
    _loadMoreClicked: function() {

        this.$('.selectivity-load-more').replaceWith(this.selectivity.template('loading'));

        this.loadMore();

        this.selectivity.focus();

        return false;
    },

    /**
     * @private
     */
    _loadMoreHovered: function(event) {

        if (event.screenX === undefined || event.screenX !== this._lastMousePosition.x ||
            event.screenY === undefined || event.screenY !== this._lastMousePosition.y) {
            this.highlightLoadMore();

            this._recordMousePosition(event);
        }
    },

    /**
     * @private
     */
    _recordMousePosition: function(event) {

        this._lastMousePosition = { x: event.screenX, y: event.screenY };
    },

    /**
     * @private
     */
    _resultClicked: function(event) {

        this.selectItem(this.selectivity._getItemId(event));

        return false;
    },

    /**
     * @private
     */
    _resultHovered: function(event) {

        if (event.screenX === undefined || event.screenX !== this._lastMousePosition.x ||
            event.screenY === undefined || event.screenY !== this._lastMousePosition.y) {
            var id = this.selectivity._getItemId(event);
            var item = Selectivity.findNestedById(this.results, id);
            if (item && !item.disabled) {
                this.highlight(item);
            }

            this._recordMousePosition(event);
        }
    },

    /**
     * @private
     */
    _scrolled: function() {

        var $loadMore = this.$('.selectivity-load-more');
        if ($loadMore.length) {
            if ($loadMore[0].offsetTop - this.$results[0].scrollTop < this.$el.height()) {
                this._loadMoreClicked();
            }
        }
    },

    /**
     * @private
     */
    _showResults: function(results, options) {

        this.showResults(this.selectivity.filterResults(results), options);
    },

    /**
     * @private
     */
    _suppressMouseWheel: function() {

        var suppressMouseWheelSelector = this.selectivity.options.suppressMouseWheelSelector;
        if (suppressMouseWheelSelector === null) {
            return;
        }

        var selector = suppressMouseWheelSelector || '.selectivity-results-container';
        this.$el.on('DOMMouseScroll mousewheel', selector, function(event) {

            // Thanks to Troy Alford:
            // http://stackoverflow.com/questions/5802467/prevent-scrolling-of-parent-element

            var $el = $(this),
                scrollTop = this.scrollTop,
                scrollHeight = this.scrollHeight,
                height = $el.height(),
                originalEvent = event.originalEvent,
                delta = (event.type === 'DOMMouseScroll' ? originalEvent.detail * -40
                                                         : originalEvent.wheelDelta),
                up = delta > 0;

            function prevent() {
                event.stopPropagation();
                event.preventDefault();
                event.returnValue = false;
                return false;
            }

            if (scrollHeight > height) {
                if (!up && -delta > scrollHeight - height - scrollTop) {
                    // Scrolling down, but this will take us past the bottom.
                    $el.scrollTop(scrollHeight);
                    return prevent();
                } else if (up && delta > scrollTop) {
                    // Scrolling up, but this will take us past the top.
                    $el.scrollTop(0);
                    return prevent();
                }
            }
        });
    }

});

module.exports = Selectivity.Dropdown = SelectivityDropdown;
<commit_msg>Move dropdown under parent.<commit_after>'use strict';

var $ = require('jquery');

var debounce = require('./lodash/debounce');

var EventDelegator = require('./event-delegator');

var Selectivity = require('./selectivity-base');

/**
 * selectivity Dropdown Constructor.
 *
 * @param options Options object. Should have the following properties:
 *                selectivity - Selectivity instance to show the dropdown for.
 *                showSearchInput - Boolean whether a search input should be shown.
 */
function SelectivityDropdown(options) {

    var selectivity = options.selectivity;

    this.$el = $(selectivity.template('dropdown', {
        dropdownCssClass: selectivity.options.dropdownCssClass,
        searchInputPlaceholder: selectivity.options.searchInputPlaceholder,
        showSearchInput: options.showSearchInput
    }));

    /**
     * jQuery container to add the results to.
     */
    this.$results = this.$('.selectivity-results-container');

    /**
     * Boolean indicating whether more results are available than currently displayed in the
     * dropdown.
     */
    this.hasMore = false;

    /**
     * The currently highlighted result item.
     */
    this.highlightedResult = null;

    /**
     * Boolean whether the load more link is currently highlighted.
     */
    this.loadMoreHighlighted = false;

    /**
     * Options passed to the dropdown constructor.
     */
    this.options = options;

    /**
     * The results displayed in the dropdown.
     */
    this.results = [];

    /**
     * Selectivity instance.
     */
    this.selectivity = selectivity;

    this._closed = false;

    this._closeProxy = this.close.bind(this);
    if (selectivity.options.closeOnSelect !== false) {
        selectivity.$el.on('selectivity-selecting', this._closeProxy);
    }

    this._lastMousePosition = {};

    this.addToDom();
    this.position();
    this.setupCloseHandler();

    this._suppressMouseWheel();

    if (options.showSearchInput) {
        selectivity.initSearchInput(this.$('.selectivity-search-input'));
        selectivity.focus();
    }

    EventDelegator.call(this);

    this.$results.on('scroll touchmove touchend', debounce(this._scrolled.bind(this), 50));

    this.showLoading();

    setTimeout(this.triggerOpen.bind(this), 1);
}

/**
 * Methods.
 */
$.extend(SelectivityDropdown.prototype, EventDelegator.prototype, {

    /**
     * Convenience shortcut for this.$el.find(selector).
     */
    $: function(selector) {

        return this.$el.find(selector);
    },

    /**
     * Adds the dropdown to the DOM.
     */
    addToDom: function() {

        var $next;
        var $anchor = this.selectivity.$el;
        while (($next = $anchor.next('.selectivity-dropdown')).length) {
            $anchor = $next;
        }

        $anchor.append(this.$el);
    },

    /**
     * Closes the dropdown.
     */
    close: function() {

        if (!this._closed) {
            this._closed = true;

            this.$el.remove();

            this.removeCloseHandler();

            this.selectivity.$el.off('selectivity-selecting', this._closeProxy);

            this.triggerClose();
        }
    },

    /**
     * Events map.
     *
     * Follows the same format as Backbone: http://backbonejs.org/#View-delegateEvents
     */
    events: {
        'click .selectivity-load-more': '_loadMoreClicked',
        'click .selectivity-result-item': '_resultClicked',
        'mouseenter .selectivity-load-more': '_loadMoreHovered',
        'mouseenter .selectivity-result-item': '_resultHovered'
    },

    /**
     * Highlights a result item.
     *
     * @param item The item to highlight.
     */
    highlight: function(item) {

        if (this.loadMoreHighlighted) {
            this.$('.selectivity-load-more').removeClass('highlight');
        }

        this.$('.selectivity-result-item').removeClass('highlight')
            .filter('[data-item-id=' + Selectivity.quoteCssAttr(item.id) + ']')
            .addClass('highlight');

        this.highlightedResult = item;
        this.loadMoreHighlighted = false;

        this.selectivity.triggerEvent('selectivity-highlight', { item: item, id: item.id });
    },

    /**
     * Highlights the load more link.
     *
     * @param item The item to highlight.
     */
    highlightLoadMore: function() {

        this.$('.selectivity-result-item').removeClass('highlight');

        this.$('.selectivity-load-more').addClass('highlight');

        this.highlightedResult = null;
        this.loadMoreHighlighted = true;
    },

    /**
     * Loads a follow-up page with results after a search.
     *
     * This method should only be called after a call to search() when the callback has indicated
     * more results are available.
     */
    loadMore: function() {

        this.options.query({
            callback: function(response) {
                if (response && response.results) {
                    this._showResults(
                        Selectivity.processItems(response.results),
                        { add: true, hasMore: !!response.more }
                    );
                } else {
                    throw new Error('callback must be passed a response object');
                }
            }.bind(this),
            error: this._showResults.bind(this, [], { add: true }),
            offset: this.results.length,
            selectivity: this.selectivity,
            term: this.term
        });
    },

    /**
     * Positions the dropdown inside the DOM.
     */
    position: function() {

        var position = this.options.position;
        if (position) {
            position(this.$el, this.selectivity.$el);
        }

        this._scrolled();
    },

    /**
     * Removes the event handler to close the dropdown.
     */
    removeCloseHandler: function() {

        $('body').off('click', this._closeProxy);
    },

    /**
     * Renders an array of result items.
     *
     * @param items Array of result items.
     *
     * @return HTML-formatted string to display the result items.
     */
    renderItems: function(items) {

        var selectivity = this.selectivity;
        return items.map(function(item) {
            var result = selectivity.template(item.id ? 'resultItem' : 'resultLabel', item);
            if (item.children) {
                result += selectivity.template('resultChildren', {
                    childrenHtml: this.renderItems(item.children)
                });
            }
            return result;
        }, this).join('');
    },

    /**
     * Searches for results based on the term given or the term entered in the search input.
     *
     * If an items array has been passed with the options to the Selectivity instance, a local
     * search will be performed among those items. Otherwise, the query function specified in the
     * options will be used to perform the search. If neither is defined, nothing happens.
     *
     * @param term Term to search for.
     */
    search: function(term) {

        var self = this;

        term = term || '';
        self.term = term;

        if (self.options.items) {
            term = Selectivity.transformText(term);
            var matcher = self.selectivity.matcher;
            self._showResults(self.options.items.map(function(item) {
                return matcher(item, term);
            }).filter(function(item) {
                return !!item;
            }), { term: term });
        } else if (self.options.query) {
            self.options.query({
                callback: function(response) {
                    if (response && response.results) {
                        self._showResults(
                            Selectivity.processItems(response.results),
                            { hasMore: !!response.more, term: term }
                        );
                    } else {
                        throw new Error('callback must be passed a response object');
                    }
                },
                error: self.showError.bind(self),
                offset: 0,
                selectivity: self.selectivity,
                term: term
            });
        }
    },

    /**
     * Selects the highlighted item.
     */
    selectHighlight: function() {

        if (this.highlightedResult) {
            this.selectItem(this.highlightedResult.id);
        } else if (this.loadMoreHighlighted) {
            this._loadMoreClicked();
        }
    },

    /**
     * Selects the item with the given ID.
     *
     * @param id ID of the item to select.
     */
    selectItem: function(id) {

        var item = Selectivity.findNestedById(this.results, id);
        if (item && !item.disabled) {
            var options = { id: id, item: item };
            if (this.selectivity.triggerEvent('selectivity-selecting', options)) {
                this.selectivity.triggerEvent('selectivity-selected', options);
            }
        }
    },

    /**
     * Sets up an event handler that will close the dropdown when the Selectivity control loses
     * focus.
     */
    setupCloseHandler: function() {

        $('body').on('click', this._closeProxy);
    },

    /**
     * Shows an error message.
     *
     * @param message Error message to display.
     * @param options Options object. May contain the following property:
     *                escape - Set to false to disable HTML-escaping of the message. Useful if you
     *                         want to set raw HTML as the message, but may open you up to XSS
     *                         attacks if you're not careful with escaping user input.
     */
    showError: function(message, options) {

        options = options || {};

        this.$results.html(this.selectivity.template('error', {
            escape: options.escape !== false,
            message: message
        }));

        this.hasMore = false;
        this.results = [];

        this.highlightedResult = null;
        this.loadMoreHighlighted = false;

        this.position();
    },

    /**
     * Shows a loading indicator in the dropdown.
     */
    showLoading: function() {

        this.$results.html(this.selectivity.template('loading'));

        this.hasMore = false;
        this.results = [];

        this.highlightedResult = null;
        this.loadMoreHighlighted = false;

        this.position();
    },

    /**
     * Shows the results from a search query.
     *
     * @param results Array of result items.
     * @param options Options object. May contain the following properties:
     *                add - True if the results should be added to any already shown results.
     *                hasMore - Boolean whether more results can be fetched using the query()
     *                          function.
     *                term - The search term for which the results are displayed.
     */
    showResults: function(results, options) {

        var resultsHtml = this.renderItems(results);
        if (options.hasMore) {
            resultsHtml += this.selectivity.template('loadMore');
        } else {
            if (!resultsHtml && !options.add) {
                resultsHtml = this.selectivity.template('noResults', { term: options.term });
            }
        }

        if (options.add) {
            this.$('.selectivity-loading').replaceWith(resultsHtml);

            this.results = this.results.concat(results);
        } else {
            this.$results.html(resultsHtml);

            this.results = results;
        }

        this.hasMore = options.hasMore;

        if (!options.add || this.loadMoreHighlighted) {
            this._highlightFirstItem(results);
        }

        this.position();
    },

    /**
     * Triggers the 'selectivity-close' event.
     */
    triggerClose: function() {

        this.selectivity.$el.trigger('selectivity-close');
    },

    /**
     * Triggers the 'selectivity-open' event.
     */
    triggerOpen: function() {

        this.selectivity.$el.trigger('selectivity-open');
    },

    /**
     * @private
     */
    _highlightFirstItem: function(results) {

        function findFirstItem(results) {
            for (var i = 0, length = results.length; i < length; i++) {
                var result = results[i];
                if (result.id) {
                    return result;
                } else if (result.children) {
                    var item = findFirstItem(result.children);
                    if (item) {
                        return item;
                    }
                }
            }
        }

        var firstItem = findFirstItem(results);
        if (firstItem) {
            this.highlight(firstItem);
        } else {
            this.highlightedResult = null;
            this.loadMoreHighlighted = false;
        }
    },

    /**
     * @private
     */
    _loadMoreClicked: function() {

        this.$('.selectivity-load-more').replaceWith(this.selectivity.template('loading'));

        this.loadMore();

        this.selectivity.focus();

        return false;
    },

    /**
     * @private
     */
    _loadMoreHovered: function(event) {

        if (event.screenX === undefined || event.screenX !== this._lastMousePosition.x ||
            event.screenY === undefined || event.screenY !== this._lastMousePosition.y) {
            this.highlightLoadMore();

            this._recordMousePosition(event);
        }
    },

    /**
     * @private
     */
    _recordMousePosition: function(event) {

        this._lastMousePosition = { x: event.screenX, y: event.screenY };
    },

    /**
     * @private
     */
    _resultClicked: function(event) {

        this.selectItem(this.selectivity._getItemId(event));

        return false;
    },

    /**
     * @private
     */
    _resultHovered: function(event) {

        if (event.screenX === undefined || event.screenX !== this._lastMousePosition.x ||
            event.screenY === undefined || event.screenY !== this._lastMousePosition.y) {
            var id = this.selectivity._getItemId(event);
            var item = Selectivity.findNestedById(this.results, id);
            if (item && !item.disabled) {
                this.highlight(item);
            }

            this._recordMousePosition(event);
        }
    },

    /**
     * @private
     */
    _scrolled: function() {

        var $loadMore = this.$('.selectivity-load-more');
        if ($loadMore.length) {
            if ($loadMore[0].offsetTop - this.$results[0].scrollTop < this.$el.height()) {
                this._loadMoreClicked();
            }
        }
    },

    /**
     * @private
     */
    _showResults: function(results, options) {

        this.showResults(this.selectivity.filterResults(results), options);
    },

    /**
     * @private
     */
    _suppressMouseWheel: function() {

        var suppressMouseWheelSelector = this.selectivity.options.suppressMouseWheelSelector;
        if (suppressMouseWheelSelector === null) {
            return;
        }

        var selector = suppressMouseWheelSelector || '.selectivity-results-container';
        this.$el.on('DOMMouseScroll mousewheel', selector, function(event) {

            // Thanks to Troy Alford:
            // http://stackoverflow.com/questions/5802467/prevent-scrolling-of-parent-element

            var $el = $(this),
                scrollTop = this.scrollTop,
                scrollHeight = this.scrollHeight,
                height = $el.height(),
                originalEvent = event.originalEvent,
                delta = (event.type === 'DOMMouseScroll' ? originalEvent.detail * -40
                                                         : originalEvent.wheelDelta),
                up = delta > 0;

            function prevent() {
                event.stopPropagation();
                event.preventDefault();
                event.returnValue = false;
                return false;
            }

            if (scrollHeight > height) {
                if (!up && -delta > scrollHeight - height - scrollTop) {
                    // Scrolling down, but this will take us past the bottom.
                    $el.scrollTop(scrollHeight);
                    return prevent();
                } else if (up && delta > scrollTop) {
                    // Scrolling up, but this will take us past the top.
                    $el.scrollTop(0);
                    return prevent();
                }
            }
        });
    }

});

module.exports = Selectivity.Dropdown = SelectivityDropdown;
<|endoftext|>


<commit_before>var match = require('../lib/match'),
    ranking = require('../lib/ranking'),
    player = require('../lib/player'),
    gameres = require('../lib/gameres'),
    auth = require('basic-auth'),
    debug = require('debug')('wol:leaderboard');

exports.submit = function (req, res, next) {
    res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
    res.header('Expires', '-1');
    res.header('Pragma', 'no-cache');

    /* discontinue if missing request data */
    if (!req.body || !req.params.game) return res.send(400);

    var credentials = auth(req) || {},
        dump = gameres.parse(req.body) || {};

    /* discontinue if missing gameres data */
    if (!dump.client || !dump.client.nick) return res.send(400);

    var _success = function() {
        res.send(202);
        match.process(req.params.game, dump);
    };

    var _error = function() {
        res.writeHead(401, {
            'WWW-Authenticate': 'Basic realm="CnCNet 5 Leaderboard"'
        });

        res.end();
    };

    if (!credentials.name || !credentials.pass) return _error();

    /* dirty hack for td, since they have no accounts */
    if (req.params.game === 'td') return _success();

    player.auth(dump.client.nick, credentials.name, credentials.pass).then(_success, _error);
};

exports.ladder = function (req, res, next) {
    ranking.ladder(req.params.game, (req.params.limit || 150)).then(function(data) {
        if (data && data.length > 0) {
            res.header('Cache-Control', 'public, max-age=60');
        }

        res.send(data);
    });
};

exports.search = function (req, res, next) {
    var _success = function (data) {
        res.send(data);
    };
    var _error = function () {
        res.send(404);
    };

    if (!req.body || !req.body.player) return _error();
    player.search(req.params.game, req.body.player).then(_success, _error);
};

exports.player = function(req, res, next) {
    var _success = function (data) {
        res.header('Cache-Control', 'public, max-age=30');
        res.send(data);
    };
    var _error = function () {
        res.send(404);
    };

    player.stats(req.params.game, req.params.player, req.params.games).then(_success, _error);
};

exports.match = function(req, res) {
    var _success = function (data) {
        res.header('Cache-Control', 'public, max-age=950400');
        res.send(data);
    };
    var _error = function () {
        res.send(404);
    };

    match.information(req.params.game, parseInt(req.params.gameId)).then(_success, _error);
};
<commit_msg>moving above credentials check<commit_after>var match = require('../lib/match'),
    ranking = require('../lib/ranking'),
    player = require('../lib/player'),
    gameres = require('../lib/gameres'),
    auth = require('basic-auth'),
    debug = require('debug')('wol:leaderboard');

exports.submit = function (req, res, next) {
    res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
    res.header('Expires', '-1');
    res.header('Pragma', 'no-cache');

    /* discontinue if missing request data */
    if (!req.body || !req.params.game) return res.send(400);

    var credentials = auth(req) || {},
        dump = gameres.parse(req.body) || {};

    /* discontinue if missing gameres data */
    if (!dump.client || !dump.client.nick) return res.send(400);

    var _success = function() {
        res.send(202);
        match.process(req.params.game, dump);
    };

    var _error = function() {
        res.writeHead(401, {
            'WWW-Authenticate': 'Basic realm="CnCNet 5 Leaderboard"'
        });

        res.end();
    };

    /* dirty hack for td, since they have no accounts */
    if (req.params.game === 'td') return _success();

    if (!credentials.name || !credentials.pass) return _error();
    player.auth(dump.client.nick, credentials.name, credentials.pass).then(_success, _error);
};

exports.ladder = function (req, res, next) {
    ranking.ladder(req.params.game, (req.params.limit || 150)).then(function(data) {
        if (data && data.length > 0) {
            res.header('Cache-Control', 'public, max-age=60');
        }

        res.send(data);
    });
};

exports.search = function (req, res, next) {
    var _success = function (data) {
        res.send(data);
    };
    var _error = function () {
        res.send(404);
    };

    if (!req.body || !req.body.player) return _error();
    player.search(req.params.game, req.body.player).then(_success, _error);
};

exports.player = function(req, res, next) {
    var _success = function (data) {
        res.header('Cache-Control', 'public, max-age=30');
        res.send(data);
    };
    var _error = function () {
        res.send(404);
    };

    player.stats(req.params.game, req.params.player, req.params.games).then(_success, _error);
};

exports.match = function(req, res) {
    var _success = function (data) {
        res.header('Cache-Control', 'public, max-age=950400');
        res.send(data);
    };
    var _error = function () {
        res.send(404);
    };

    match.information(req.params.game, parseInt(req.params.gameId)).then(_success, _error);
};
<|endoftext|>


<commit_before>// @file Manage a group of servers with ServerManager.

'use strict';

// Node included libs.
var child_process = require('child_process');
var path = require('path');

// Third party libs.
var _ = require('lodash');
var debug = require('debug')('cee:childmanager');
var http_proxy = require('http-proxy');
var when = require('when');

// Export the ServerManager constructor.
module.exports = ServerManager;

// Wrap a sub server process for easier communicataion.
function ServerChild(manager, name, indexFile) {
  var self = this;

  this.name = name;
  this.path = indexFile;

  this.port = 0;
  this.proxy = null;
  this.process = child_process.fork(indexFile, {
    cwd: path.dirname(indexFile)
  });

  this._whenOk =
    this._whenMessage(function(data) {return data === 'ok';})
    .then(function() {debug('%s - ok (pid %d)', name, self.process.pid);});
  this._whenPort =
    this._whenMessage(function(data) {
      return typeof data === 'object' && data.name === 'listening-on';
    })
    .then(function(data) {
      debug('%s - received port server is listening on %d', name, data.port);
      self._setPort(data.port);
    });

  this.whenLaunched = when.all([this._whenOk, this._whenPort]);

  // Have a bound version stored that can be easily removed as a listener.
  this._relaunch = manager.launch.bind(manager, name, indexFile);

  // On exit, relaunch this child.
  this.process.on('exit', this._relaunch);
}

// Set the port and create an object to proxy traffic with.
ServerChild.prototype._setPort = function(port) {
  this.port = port;
  this.proxy = http_proxy.createProxyServer({
    target: {
      host: 'localhost',
      port: port
    }
  });
};

// Create a promise to resolve when the child sends a message fitting a given
// test function.
ServerChild.prototype._whenMessage = function(test) {
  var self = this;
  return when.promise(function(resolve, reject) {
    self.process.on('message', function receive(data) {
      if (test(data)) {
        try {
          self.process.removeListener('message', receive);
          resolve(data);
        } catch (e) {
          reject(e);
        }
      }
    });
  });
};

// Kill the child.
ServerChild.prototype.kill = function(signal) {
  var self = this;
  return when.promise(function(resolve) {
    debug('kill %s', self.name);
    var process = self.process;
    process.removeListener('exit', self._relaunch);
    process.on('exit', function() {
      debug('stopped %s (pid %d)', self.name, process.pid);
      resolve();
    });
    process.kill(signal);
  });
};

// Manage sub servers. Launch, auto-relaunch on exit, kill, send messages, and
// proxy web traffic to them by a given name.
function ServerManager() {
  this._children = {};
}

// Launch a sub server at path with a name.
// @param name
// @param path
// @returns {Promise}
ServerManager.prototype.launch = function(name, path) {
  debug('%s - launch', name);

  var child = this._children[name] = new ServerChild(this, name, path);

  return child.whenLaunched;
};

// Kill a sub server with a given name.
// @param name
// @param signal optional. Send a kill signal of the given type. eg. 'SIGINT'
// @returns {Promise} promise that resolves when the server has exited.
ServerManager.prototype.kill = function(name, signal) {
  var self = this;
  if (self._children[name] === undefined) {
    return when.resolve();
  } else {
    var promise = self._children[name].kill(signal);
    self._children[name] = undefined;
    return promise;
  }
};

// Kill all subservers
// @param signal optional
// @returns {Promise} promise that resolves when all sub servers have exited.
ServerManager.prototype.killAll = function(signal) {
  var self = this;
  return when.map(_.pairs(self._children), function(pair) {
    var name = pair[0];
    var child = pair[1];
    return child ? self.kill(name, signal) : when.resolve();
  });
};

// Send a sub server a message.
//
// In the sub server, the message is received with
// `process.on('message', function(data) {})`.
// @param name
// @param {array} args
ServerManager.prototype.send = function(name, args) {
  var child = this._children[name].process;
  child.send.apply(child, args);
};

// Proxy web traffic to a sub server.
ServerManager.prototype.proxyWeb = function(name, args) {
  var child = this._children[name];
  child.whenLaunched.then(function() {
    child.proxy.web.apply(child.proxy, args);
  });
};

// Proxy a web socket to a sub server.
ServerManager.prototype.proxyWs = function(name, args) {
  var child = this._children[name];
  child.whenLaunched.then(function() {
    child.proxy.ws.apply(child.proxy, args);
  });
};
<commit_msg>Improve error message when unable to proxy child.<commit_after>// @file Manage a group of servers with ServerManager.

'use strict';

// Node included libs.
var child_process = require('child_process');
var path = require('path');

// Third party libs.
var _ = require('lodash');
var debug = require('debug')('cee:childmanager');
var http_proxy = require('http-proxy');
var when = require('when');

// Export the ServerManager constructor.
module.exports = ServerManager;

// Wrap a sub server process for easier communicataion.
function ServerChild(manager, name, indexFile) {
  var self = this;

  this.name = name;
  this.path = indexFile;

  this.port = 0;
  this.proxy = null;
  this.process = child_process.fork(indexFile, {
    cwd: path.dirname(indexFile)
  });

  this._whenOk =
    this._whenMessage(function(data) {return data === 'ok';})
    .then(function() {debug('%s - ok (pid %d)', name, self.process.pid);});
  this._whenPort =
    this._whenMessage(function(data) {
      return typeof data === 'object' && data.name === 'listening-on';
    })
    .then(function(data) {
      debug('%s - received port server is listening on %d', name, data.port);
      self._setPort(data.port);
    });

  this.whenLaunched = when.all([this._whenOk, this._whenPort]);

  // Have a bound version stored that can be easily removed as a listener.
  this._relaunch = manager.launch.bind(manager, name, indexFile);

  // On exit, relaunch this child.
  this.process.on('exit', this._relaunch);
}

// Set the port and create an object to proxy traffic with.
ServerChild.prototype._setPort = function(port) {
  this.port = port;
  this.proxy = http_proxy.createProxyServer({
    target: {
      host: 'localhost',
      port: port
    }
  });
};

// Create a promise to resolve when the child sends a message fitting a given
// test function.
ServerChild.prototype._whenMessage = function(test) {
  var self = this;
  return when.promise(function(resolve, reject) {
    self.process.on('message', function receive(data) {
      if (test(data)) {
        try {
          self.process.removeListener('message', receive);
          resolve(data);
        } catch (e) {
          reject(e);
        }
      }
    });
  });
};

// Kill the child.
ServerChild.prototype.kill = function(signal) {
  var self = this;
  return when.promise(function(resolve) {
    debug('kill %s', self.name);
    var process = self.process;
    process.removeListener('exit', self._relaunch);
    process.on('exit', function() {
      debug('stopped %s (pid %d)', self.name, process.pid);
      resolve();
    });
    process.kill(signal);
  });
};

// Manage sub servers. Launch, auto-relaunch on exit, kill, send messages, and
// proxy web traffic to them by a given name.
function ServerManager() {
  this._children = {};
}

// Launch a sub server at path with a name.
// @param name
// @param path
// @returns {Promise}
ServerManager.prototype.launch = function(name, path) {
  debug('%s - launch', name);

  var child = this._children[name] = new ServerChild(this, name, path);

  return child.whenLaunched;
};

// Kill a sub server with a given name.
// @param name
// @param signal optional. Send a kill signal of the given type. eg. 'SIGINT'
// @returns {Promise} promise that resolves when the server has exited.
ServerManager.prototype.kill = function(name, signal) {
  var self = this;
  if (self._children[name] === undefined) {
    return when.resolve();
  } else {
    var promise = self._children[name].kill(signal);
    self._children[name] = undefined;
    return promise;
  }
};

// Kill all subservers
// @param signal optional
// @returns {Promise} promise that resolves when all sub servers have exited.
ServerManager.prototype.killAll = function(signal) {
  var self = this;
  return when.map(_.pairs(self._children), function(pair) {
    var name = pair[0];
    var child = pair[1];
    return child ? self.kill(name, signal) : when.resolve();
  });
};

// Send a sub server a message.
//
// In the sub server, the message is received with
// `process.on('message', function(data) {})`.
// @param name
// @param {array} args
ServerManager.prototype.send = function(name, args) {
  var child = this._children[name].process;
  child.send.apply(child, args);
};

// Proxy web traffic to a sub server.
ServerManager.prototype.proxyWeb = function(name, args) {
  var child = this._children[name];
  if (!child) {
    throw new Error('Cannot proxy web traffic to ' + name + '.');
  }
  child.whenLaunched.then(function() {
    child.proxy.web.apply(child.proxy, args);
  });
};

// Proxy a web socket to a sub server.
ServerManager.prototype.proxyWs = function(name, args) {
  var child = this._children[name];
  if (!child) {
    throw new Error('Cannot proxy web socket connection to ' + name + '.');
  }
  child.whenLaunched.then(function() {
    child.proxy.ws.apply(child.proxy, args);
  });
};
<|endoftext|>