UNPKG

es-selectize

Version:

Selectize is a jQuery-based custom <select> UI control. Useful for tagging, contact lists, country selectors, etc.

1,578 lines (1,391 loc) 93 kB
import Sifter from 'es-sifter'; import MicroPlugin from 'es-microplugin'; // src/contrib/microevent.js /** * MicroEvent - to make any js object an event emitter * * - pure javascript - server compatible, browser compatible * - dont rely on the browser doms * - super simple - you get it immediatly, no mistery, no magic involved * * @author Jerome Etienne (https://github.com/jeromeetienne) */ function MicroEvent() {} MicroEvent.prototype = { on: function(event, fct){ this._events = this._events || {}; this._events[event] = this._events[event] || []; this._events[event].push(fct); }, off: function(event, fct){ var n = arguments.length; if (n === 0) return delete this._events; if (n === 1) return delete this._events[event]; this._events = this._events || {}; if (event in this._events === false) return; this._events[event].splice(this._events[event].indexOf(fct), 1); }, trigger: function(event /* , args... */){ this._events = this._events || {}; if (event in this._events === false) return; for (var i = 0; i < this._events[event].length; i++){ this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); } } }; /** * Mixin will delegate all MicroEvent.js function in the destination object. * * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent * * @param {object} the object which will support MicroEvent */ MicroEvent.mixin = function(destObject){ var props = ['on', 'off', 'trigger']; for (var i = 0; i < props.length; i++){ destObject.prototype[props[i]] = MicroEvent.prototype[props[i]]; } }; // src/contrib/highlight.js /** * highlight v3 | MIT license | Johann Burkard <jb@eaio.com> * Highlights arbitrary terms in a node. * * - Modified by Marshal <beatgates@gmail.com> 2011-6-24 (added regex) * - Modified by Brian Reavis <brian@thirdroute.com> 2012-8-27 (cleanup) */ function highlight($element, pattern) { if (typeof pattern === 'string' && !pattern.length) return; var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern; var highlight = function(node) { var skip = 0; if (node.nodeType === 3) { var pos = node.data.search(regex); if (pos >= 0 && node.data.length > 0) { // var match = node.data.match(regex); var spannode = document.createElement('span'); spannode.className = 'highlight'; var middlebit = node.splitText(pos); // var endbit = middlebit.splitText(match[0].length); var middleclone = middlebit.cloneNode(true); spannode.appendChild(middleclone); middlebit.parentNode.replaceChild(spannode, middlebit); skip = 1; } } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { for (var i = 0; i < node.childNodes.length; ++i) { i += highlight(node.childNodes[i]); } } return skip; }; return $element.each(function() { highlight(this); }); } /** * removeHighlight fn copied from highlight v5 and * edited to remove with() and pass js strict mode */ function init$1() { $.fn.removeHighlight = function() { return this.find('span.highlight').each(function() { // this.parentNode.firstChild.nodeName; var parent = this.parentNode; parent.replaceChild(this.firstChild, this); parent.normalize(); }).end(); }; } // src/defaults.js var defaults = { options: [], optgroups: [], plugins: [], delimiter: ',', splitOn: null, // regexp or string for splitting up values from a paste command persist: true, diacritics: true, create: false, createOnBlur: false, createFilter: null, highlight: true, openOnFocus: true, maxOptions: 1000, maxItems: null, hideSelected: null, addPrecedence: false, selectOnTab: false, preload: false, allowEmptyOption: false, closeAfterSelect: false, scrollDuration: 60, loadThrottle: 300, loadingClass: 'loading', dataAttr: 'data-data', optgroupField: 'optgroup', valueField: 'value', labelField: 'text', optgroupLabelField: 'label', optgroupValueField: 'value', lockOptgroupOrder: false, sortField: '$order', searchField: ['text'], searchConjunction: 'and', mode: null, wrapperClass: 'selectize-control', inputClass: 'selectize-input', dropdownClass: 'selectize-dropdown', dropdownContentClass: 'selectize-dropdown-content', dropdownParent: null, copyClassesToDropdown: true, /* load : null, // function(query, callback) { ... } score : null, // function(search) { ... } onInitialize : null, // function() { ... } onChange : null, // function(value) { ... } onItemAdd : null, // function(value, $item) { ... } onItemRemove : null, // function(value) { ... } onClear : null, // function() { ... } onOptionAdd : null, // function(value, data) { ... } onOptionRemove : null, // function(value) { ... } onOptionClear : null, // function() { ... } onOptionGroupAdd : null, // function(id, data) { ... } onOptionGroupRemove : null, // function(id) { ... } onOptionGroupClear : null, // function() { ... } onDropdownOpen : null, // function($dropdown) { ... } onDropdownClose : null, // function($dropdown) { ... } onType : null, // function(str) { ... } onDelete : null, // function(values) { ... } */ render: { /* item: null, optgroup: null, optgroup_header: null, option: null, option_create: null */ } }; // src/consts.js var IS_MAC = /Mac/.test(navigator.userAgent); var KEY_A = 65; var KEY_RETURN = 13; var KEY_ESC = 27; var KEY_LEFT = 37; var KEY_UP = 38; var KEY_P = 80; var KEY_RIGHT = 39; var KEY_DOWN = 40; var KEY_N = 78; var KEY_BACKSPACE = 8; var KEY_DELETE = 46; var KEY_SHIFT = 16; var KEY_CMD = IS_MAC ? 91 : 17; var KEY_CTRL = IS_MAC ? 18 : 17; var KEY_TAB = 9; var TAG_SELECT = 1; var TAG_INPUT = 2; // for now, android support in general is too spotty to support validity var SUPPORTS_VALIDITY_API = !/android/i.test(window.navigator.userAgent) && !!document.createElement('input').validity; // src/utils.js /** * Determines if the provided value has been defined. * * @param {mixed} object * @returns {boolean} */ function isSet(object) { return typeof object !== 'undefined'; } /** * Converts a scalar to its best string representation * for hash keys and HTML attribute values. * * Transformations: * 'str' -> 'str' * null -> '' * undefined -> '' * true -> '1' * false -> '0' * 0 -> '0' * 1 -> '1' * * @param {string} value * @returns {string|null} */ function hashKey(value) { if (typeof value === 'undefined' || value === null) return null; if (typeof value === 'boolean') return value ? '1' : '0'; return value + ''; } /** * Escapes a string for use within HTML. * * @param {string} str * @returns {string} */ function escapeHtml(str) { return (str + '') .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;'); } /** * Escapes "$" characters in replacement strings. * * @param {string} str * @returns {string} */ /** * Wraps `fn` so that it can only be invoked once. * * @param {function} fn * @returns {function} */ function once(fn) { var called = false; return function() { if (called) return; called = true; fn.apply(this, arguments); }; } /** * Wraps `fn` so that it can only be called once * every `delay` milliseconds (invoked on the falling edge). * * @param {function} fn * @param {int} delay * @returns {function} */ function debounce(fn, delay) { var timeout; return function() { var self = this; var args = arguments; window.clearTimeout(timeout); timeout = window.setTimeout(function() { fn.apply(self, args); }, delay); }; } /** * Debounce all fired events types listed in `types` * while executing the provided `fn`. * * @param {object} self * @param {array} types * @param {function} fn */ function debounceEvents(self, types, fn) { var type; var trigger = self.trigger; var eventArgs = {}; // override trigger method self.trigger = function() { var type = arguments[0]; if (types.indexOf(type) !== -1) { eventArgs[type] = arguments; } else { return trigger.apply(self, arguments); } }; // invoke provided function fn.apply(self, []); self.trigger = trigger; // trigger queued events for (type in eventArgs) { if (eventArgs.hasOwnProperty(type)) { trigger.apply(self, eventArgs[type]); } } } /** * A workaround for http://bugs.jquery.com/ticket/6696 * * @param {object} $parent - Parent element to listen on. * @param {string} event - Event name. * @param {string} selector - Descendant selector to filter by. * @param {function} fn - Event handler. */ function watchChildEvent($parent, event, selector, fn) { $parent.on(event, selector, function(e) { var child = e.target; while (child && child.parentNode !== $parent[0]) { child = child.parentNode; } e.currentTarget = child; return fn.apply(this, [e]); }); } /** * Determines the current selection within a text input control. * Returns an object containing: * - start * - length * * @param {object} input * @returns {object} */ function getSelection(input) { var result = {}; if ('selectionStart' in input) { result.start = input.selectionStart; result.length = input.selectionEnd - result.start; } else if (document.selection) { input.focus(); var sel = document.selection.createRange(); var selLen = document.selection.createRange().text.length; sel.moveStart('character', -input.value.length); result.start = sel.text.length - selLen; result.length = selLen; } return result; } /** * Copies CSS properties from one element to another. * * @param {object} $from * @param {object} $to * @param {array} properties */ function transferStyles($from, $to, properties) { var i, n, styles = {}; if (properties) { for (i = 0, n = properties.length; i < n; i++) { styles[properties[i]] = $from.css(properties[i]); } } else { styles = $from.css(); } $to.css(styles); } /** * Measures the width of a string within a * parent element (in pixels). * * @param {string} str * @param {object} $parent * @returns {int} */ function measureString(str, $parent) { if (!str) { return 0; } var $test = $('<test>').css({ position: 'absolute', top: -99999, left: -99999, width: 'auto', padding: 0, whiteSpace: 'pre' }).text(str).appendTo('body'); transferStyles($parent, $test, [ 'letterSpacing', 'fontSize', 'fontFamily', 'fontWeight', 'textTransform' ]); var width = $test.width(); $test.remove(); return width; } /** * Sets up an input to grow horizontally as the user * types. If the value is changed manually, you can * trigger the "update" handler to resize: * * $input.trigger('update'); * * @param {object} $input */ function autoGrow($input) { var currentWidth = null; var update = function(e, options) { var value, keyCode, printable, placeholder, width; var shift, character, selection; e = e || window.event || {}; options = options || {}; if (e.metaKey || e.altKey) return; if (!options.force && $input.data('grow') === false) return; value = $input.val(); if (e.type && e.type.toLowerCase() === 'keydown') { keyCode = e.keyCode; printable = ( (keyCode >= 97 && keyCode <= 122) || // a-z (keyCode >= 65 && keyCode <= 90) || // A-Z (keyCode >= 48 && keyCode <= 57) || // 0-9 keyCode === 32 // space ); if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) { selection = getSelection($input[0]); if (selection.length) { value = value.substring(0, selection.start) + value.substring(selection.start + selection.length); } else if (keyCode === KEY_BACKSPACE && selection.start) { value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1); } else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') { value = value.substring(0, selection.start) + value.substring(selection.start + 1); } } else if (printable) { shift = e.shiftKey; character = String.fromCharCode(e.keyCode); if (shift) character = character.toUpperCase(); else character = character.toLowerCase(); value += character; } } placeholder = $input.attr('placeholder'); if (!value && placeholder) { value = placeholder; } width = measureString(value, $input) + 4; if (width !== currentWidth) { currentWidth = width; $input.width(width); $input.triggerHandler('resize'); } }; $input.on('keydown keyup update blur', update); update(); } function domToString(d) { var tmp = document.createElement('div'); tmp.appendChild(d.cloneNode(true)); return tmp.innerHTML; } // src/selectize.js var inited = false; function Selectize($input, settings) { if (!inited) { init$$1(); inited = true; } var i, n, dir, input, self = this; input = $input[0]; input.selectize = self; // detect rtl environment var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null); dir = computedStyle ? computedStyle.getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction; dir = dir || $input.parents('[dir]:first').attr('dir') || ''; // setup default state $.extend(self, { order : 0, settings : settings, $input : $input, tabIndex : $input.attr('tabindex') || '', tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT, rtl : /rtl/i.test(dir), eventNS : '.selectize' + (++Selectize.count), highlightedValue : null, isOpen : false, isDisabled : false, isRequired : $input.is('[required]'), isInvalid : false, isLocked : false, isFocused : false, isInputHidden : false, isSetup : false, isShiftDown : false, isCmdDown : false, isCtrlDown : false, ignoreFocus : false, ignoreBlur : false, ignoreHover : false, hasOptions : false, currentResults : null, lastValue : '', caretPos : 0, loading : 0, loadedSearches : {}, $activeOption : null, $activeItems : [], optgroups : {}, options : {}, userOptions : {}, items : [], renderCache : {}, onSearchChange : settings.loadThrottle === null ? self.onSearchChange : debounce(self.onSearchChange, settings.loadThrottle) }); // search system self.sifter = new Sifter(this.options, {diacritics: settings.diacritics}); // build options table if (self.settings.options) { for (i = 0, n = self.settings.options.length; i < n; i++) { self.registerOption(self.settings.options[i]); } delete self.settings.options; } // build optgroup table if (self.settings.optgroups) { for (i = 0, n = self.settings.optgroups.length; i < n; i++) { self.registerOptionGroup(self.settings.optgroups[i]); } delete self.settings.optgroups; } // option-dependent defaults self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi'); if (typeof self.settings.hideSelected !== 'boolean') { self.settings.hideSelected = self.settings.mode === 'multi'; } self.initializePlugins(self.settings.plugins); self.setupCallbacks(); self.setupTemplates(); self.setup(); } function init$$1() { // initialize highlight init$1(); // defaults // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Selectize.defaults = defaults; Selectize.count = 0; // mixins // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MicroEvent.mixin(Selectize); MicroPlugin.mixin(Selectize); // methods // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $.extend(Selectize.prototype, { /** * Creates all elements and sets up event bindings. */ setup: function() { var self = this; var settings = self.settings; var eventNS = self.eventNS; var $window = $(window); var $document = $(document); var $input = self.$input; var $wrapper; var $control; var $controlInput; var $dropdown; var $dropdownContent; var $dropdownParent; var inputMode; var classes; var classesPlugins; var inputId; inputMode = self.settings.mode; classes = $input.attr('class') || ''; $wrapper = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode); $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper); $controlInput = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', $input.is(':disabled') ? '-1' : self.tabIndex); $dropdownParent = $(settings.dropdownParent || $wrapper); $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(inputMode).hide().appendTo($dropdownParent); $dropdownContent = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown); if(inputId = $input.attr('id')) { $controlInput.attr('id', inputId + '-selectized'); $('label[for="'+inputId+'"]').attr('for', inputId + '-selectized'); } if(self.settings.copyClassesToDropdown) { $dropdown.addClass(classes); } $wrapper.css({ width: $input[0].style.width }); if (self.plugins.names.length) { classesPlugins = 'plugin-' + self.plugins.names.join(' plugin-'); $wrapper.addClass(classesPlugins); $dropdown.addClass(classesPlugins); } if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) { $input.attr('multiple', 'multiple'); } if (self.settings.placeholder) { $controlInput.attr('placeholder', settings.placeholder); } // if splitOn was not passed in, construct it from the delimiter to allow pasting universally if (!self.settings.splitOn && self.settings.delimiter) { var delimiterEscaped = self.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); self.settings.splitOn = new RegExp('\\s*' + delimiterEscaped + '+\\s*'); } if ($input.attr('autocorrect')) { $controlInput.attr('autocorrect', $input.attr('autocorrect')); } if ($input.attr('autocapitalize')) { $controlInput.attr('autocapitalize', $input.attr('autocapitalize')); } self.$wrapper = $wrapper; self.$control = $control; self.$controlInput = $controlInput; self.$dropdown = $dropdown; self.$dropdownContent = $dropdownContent; $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); }); $dropdown.on('mousedown click', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); }); watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); }); autoGrow($controlInput); $control.on({ mousedown : function() { return self.onMouseDown.apply(self, arguments); }, click : function() { return self.onClick.apply(self, arguments); } }); $controlInput.on({ mousedown : function(e) { e.stopPropagation(); }, keydown : function() { return self.onKeyDown.apply(self, arguments); }, keyup : function() { return self.onKeyUp.apply(self, arguments); }, keypress : function() { return self.onKeyPress.apply(self, arguments); }, resize : function() { self.positionDropdown.apply(self, []); }, blur : function() { return self.onBlur.apply(self, arguments); }, focus : function() { self.ignoreBlur = false; return self.onFocus.apply(self, arguments); }, paste : function() { return self.onPaste.apply(self, arguments); } }); $document.on('keydown' + eventNS, function(e) { self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey']; self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey']; self.isShiftDown = e.shiftKey; }); $document.on('keyup' + eventNS, function(e) { if (e.keyCode === KEY_CTRL) self.isCtrlDown = false; if (e.keyCode === KEY_SHIFT) self.isShiftDown = false; if (e.keyCode === KEY_CMD) self.isCmdDown = false; }); $document.on('mousedown' + eventNS, function(e) { if (self.isFocused) { // prevent events on the dropdown scrollbar from causing the control to blur if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) { return false; } // blur on click outside if (!self.$control.has(e.target).length && e.target !== self.$control[0]) { self.blur(e.target); } } }); $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() { if (self.isOpen) { self.positionDropdown.apply(self, arguments); } }); $window.on('mousemove' + eventNS, function() { self.ignoreHover = false; }); // store original children and tab index so that they can be // restored when the destroy() method is called. this.revertSettings = { $children : $input.children().detach(), tabindex : $input.attr('tabindex') }; $input.attr('tabindex', -1).hide().after(self.$wrapper); if ($.isArray(settings.items)) { self.setValue(settings.items); delete settings.items; } // feature detect for the validation API if (SUPPORTS_VALIDITY_API) { $input.on('invalid' + eventNS, function(e) { e.preventDefault(); self.isInvalid = true; self.refreshState(); }); } self.updateOriginalInput(); self.refreshItems(); self.refreshState(); self.updatePlaceholder(); self.isSetup = true; if ($input.is(':disabled')) { self.disable(); } self.on('change', this.onChange); $input.data('selectize', self); $input.addClass('selectized'); self.trigger('initialize'); // preload options if (settings.preload === true) { self.onSearchChange(''); } }, /** * Sets up default rendering functions. */ setupTemplates: function() { var self = this; var fieldLabel = self.settings.labelField; var fieldOptgroup = self.settings.optgroupLabelField; var templates = { 'optgroup': function(data) { return '<div class="optgroup">' + data.html + '</div>'; }, 'optgroupHeader': function(data, escape) { return '<div class="optgroup-header">' + escape(data[fieldOptgroup]) + '</div>'; }, 'option': function(data, escape) { return '<div class="option">' + escape(data[fieldLabel]) + '</div>'; }, 'item': function(data, escape) { return '<div class="item">' + escape(data[fieldLabel]) + '</div>'; }, 'optionCreate': function(data, escape) { return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>'; } }; self.settings.render = $.extend({}, templates, self.settings.render); }, /** * Maps fired events to callbacks provided * in the settings used when creating the control. */ setupCallbacks: function() { var key, fn, callbacks = { 'initialize' : 'onInitialize', 'change' : 'onChange', 'itemAdd' : 'onItemAdd', 'itemRemove' : 'onItemRemove', 'clear' : 'onClear', 'optionAdd' : 'onOptionAdd', 'optionRemove' : 'onOptionRemove', 'optionClear' : 'onOptionClear', 'optgroupAdd' : 'onOptionGroupAdd', 'optgroupRemove' : 'onOptionGroupRemove', 'optgroupClear' : 'onOptionGroupClear', 'dropdownOpen' : 'onDropdownOpen', 'dropdownClose' : 'onDropdownClose', 'type' : 'onType', 'load' : 'onLoad', 'focus' : 'onFocus', 'blur' : 'onBlur' }; for (key in callbacks) { if (callbacks.hasOwnProperty(key)) { fn = this.settings[callbacks[key]]; if (fn) this.on(key, fn); } } }, /** * Triggered when the main control element * has a click event. * * @param {object} e * @return {boolean} */ onClick: function(e) { var self = this; // necessary for mobile webkit devices (manual focus triggering // is ignored unless invoked within a click event) if (!self.isFocused) { self.focus(); e.preventDefault(); } }, /** * Triggered when the main control element * has a mouse down event. * * @param {object} e * @return {boolean} */ onMouseDown: function(e) { var self = this; var defaultPrevented = e.isDefaultPrevented(); if (self.isFocused) { // retain focus by preventing native handling. if the // event target is the input it should not be modified. // otherwise, text selection within the input won't work. if (e.target !== self.$controlInput[0]) { if (self.settings.mode === 'single') { // toggle dropdown if (self.isOpen) self.close(); else self.open(); } else if (!defaultPrevented) { self.setActiveItem(null); } return false } } else { // give control focus if (!defaultPrevented) { window.setTimeout(function() { self.focus(); }, 0); } } }, /** * Triggered when the value of the control has been changed. * This should propagate the event to the original DOM * input / select element. */ onChange: function() { this.$input.trigger('change'); }, /** * Triggered on <input> paste. * * @param {object} e * @returns {boolean} */ onPaste: function(e) { var self = this; if (self.isFull() || self.isInputHidden || self.isLocked) { e.preventDefault(); return; } // If a regex or string is included, this will split the pasted // input and create Items for each separate value if (self.settings.splitOn) { // Wait for pasted text to be recognized in value setTimeout(function() { var pastedText = self.$controlInput.val(); if(!pastedText.match(self.settings.splitOn)){ return } var splitInput = $.trim(pastedText).split(self.settings.splitOn); for (var i = 0, n = splitInput.length; i < n; i++) { self.createItem(splitInput[i]); } }, 0); } }, /** * Triggered on <input> keypress. * * @param {object} e * @returns {boolean} */ onKeyPress: function(e) { if (this.isLocked) return e && e.preventDefault(); var character = String.fromCharCode(e.keyCode || e.which); if (this.settings.create && this.settings.mode === 'multi' && character === this.settings.delimiter) { this.createItem(); e.preventDefault(); return false; } }, /** * Triggered on <input> keydown. * * @param {object} e * @returns {boolean} */ onKeyDown: function(e) { var self = this; if (self.isLocked) { if (e.keyCode !== KEY_TAB) { e.preventDefault(); } return; } switch (e.keyCode) { case KEY_A: if (self.isCmdDown) { self.selectAll(); return; } break; case KEY_ESC: if (self.isOpen) { e.preventDefault(); e.stopPropagation(); self.close(); } return; case KEY_N: if (!e.ctrlKey || e.altKey) break; case KEY_DOWN: if (!self.isOpen && self.hasOptions) { self.open(); } else if (self.$activeOption) { self.ignoreHover = true; var $next = self.getAdjacentOption(self.$activeOption, 1); if ($next.length) self.setActiveOption($next, true, true); } e.preventDefault(); return; case KEY_P: if (!e.ctrlKey || e.altKey) break; case KEY_UP: if (self.$activeOption) { self.ignoreHover = true; var $prev = self.getAdjacentOption(self.$activeOption, -1); if ($prev.length) self.setActiveOption($prev, true, true); } e.preventDefault(); return; case KEY_RETURN: if (self.isOpen && self.$activeOption) { self.onOptionSelect({currentTarget: self.$activeOption}); e.preventDefault(); } return; case KEY_LEFT: self.advanceSelection(-1, e); return; case KEY_RIGHT: self.advanceSelection(1, e); return; case KEY_TAB: if (self.settings.selectOnTab && self.isOpen && self.$activeOption) { self.onOptionSelect({currentTarget: self.$activeOption}); // Default behaviour is to jump to the next field, we only want this // if the current field doesn't accept any more entries if (!self.isFull()) { e.preventDefault(); } } if (self.settings.create && self.createItem()) { e.preventDefault(); } return; case KEY_BACKSPACE: case KEY_DELETE: self.deleteSelection(e); return; } if ((self.isFull() || self.isInputHidden) && !(IS_MAC ? e.metaKey : e.ctrlKey)) { e.preventDefault(); return; } }, /** * Triggered on <input> keyup. * * @param {object} e * @returns {boolean} */ onKeyUp: function(e) { var self = this; if (self.isLocked) return e && e.preventDefault(); var value = self.$controlInput.val() || ''; if (self.lastValue !== value) { self.lastValue = value; self.onSearchChange(value); self.refreshOptions(); self.trigger('type', value); } }, /** * Invokes the user-provide option provider / loader. * * Note: this function is debounced in the Selectize * constructor (by `settings.loadThrottle` milliseconds) * * @param {string} value */ onSearchChange: function(value) { var self = this; var fn = self.settings.load; if (!fn) return; if (self.loadedSearches.hasOwnProperty(value)) return; self.loadedSearches[value] = true; self.load(function(callback) { fn.apply(self, [value, callback]); }); }, /** * Triggered on <input> focus. * * @param {object} e (optional) * @returns {boolean} */ onFocus: function(e) { var self = this; var wasFocused = self.isFocused; if (self.isDisabled) { self.blur(); if (e) e.preventDefault(); return false; } if (self.ignoreFocus) return; self.isFocused = true; if (self.settings.preload === 'focus') self.onSearchChange(''); if (!wasFocused) self.trigger('focus'); if (!self.$activeItems.length) { self.showInput(); self.setActiveItem(null); self.refreshOptions(!!self.settings.openOnFocus); } self.refreshState(); }, /** * Triggered on <input> blur. * * @param {object} e * @param {Element} dest */ onBlur: function(e, dest) { var self = this; if (!self.isFocused) return; self.isFocused = false; if (self.ignoreFocus) { return; } else if (!self.ignoreBlur && document.activeElement === self.$dropdownContent[0]) { // necessary to prevent IE closing the dropdown when the scrollbar is clicked self.ignoreBlur = true; self.onFocus(e); return; } var deactivate = function() { self.close(); self.setTextboxValue(''); self.setActiveItem(null); self.setActiveOption(null); self.setCaret(self.items.length); self.refreshState(); // IE11 bug: element still marked as active if (dest && dest.focus) dest.focus(); self.ignoreFocus = false; self.trigger('blur'); }; self.ignoreFocus = true; if (self.settings.create && self.settings.createOnBlur) { self.createItem(null, false, deactivate); } else { deactivate(); } }, /** * Triggered when the user rolls over * an option in the autocomplete dropdown menu. * * @param {object} e * @returns {boolean} */ onOptionHover: function(e) { if (this.ignoreHover) return; this.setActiveOption(e.currentTarget, false); }, /** * Triggered when the user clicks on an option * in the autocomplete dropdown menu. * * @param {object} e * @returns {boolean} */ onOptionSelect: function(e) { var value, $target, self = this; if (e.preventDefault) { e.preventDefault(); e.stopPropagation(); } $target = $(e.currentTarget); if ($target.hasClass('create')) { self.createItem(null, function() { if (self.settings.closeAfterSelect) { self.close(); } }); } else { value = $target.attr('data-value'); if (typeof value !== 'undefined') { self.lastQuery = null; self.setTextboxValue(''); self.addItem(value); if (self.settings.closeAfterSelect) { self.close(); } else if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) { self.setActiveOption(self.getOption(value)); } } } }, /** * Triggered when the user clicks on an item * that has been selected. * * @param {object} e * @returns {boolean} */ onItemSelect: function(e) { var self = this; if (self.isLocked) return; if (self.settings.mode === 'multi') { e.preventDefault(); self.setActiveItem(e.currentTarget, e); } }, /** * Invokes the provided method that provides * results to a callback---which are then added * as options to the control. * * @param {function} fn */ load: function(fn) { var self = this; var $wrapper = self.$wrapper.addClass(self.settings.loadingClass); self.loading++; fn.apply(self, [function(results) { self.loading = Math.max(self.loading - 1, 0); if (results && results.length) { self.addOption(results); self.refreshOptions(self.isFocused && !self.isInputHidden); } if (!self.loading) { $wrapper.removeClass(self.settings.loadingClass); } self.trigger('load', results); }]); }, /** * Sets the input field of the control to the specified value. * * @param {string} value */ setTextboxValue: function(value) { var $input = this.$controlInput; var changed = $input.val() !== value; if (changed) { $input.val(value).triggerHandler('update'); this.lastValue = value; } }, /** * Returns the value of the control. If multiple items * can be selected (e.g. <select multiple>), this returns * an array. If only one item can be selected, this * returns a string. * * @returns {mixed} */ getValue: function() { if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) { return this.items; } else { return this.items.join(this.settings.delimiter); } }, /** * Resets the selected items to the given value. * * @param {mixed} value */ setValue: function(value, silent) { var events = silent ? [] : ['change']; debounceEvents(this, events, function() { this.clear(silent); this.addItems(value, silent); }); }, /** * Sets the selected item. * * @param {object} $item * @param {object} e (optional) */ setActiveItem: function($item, e) { var self = this; var eventName; var i, idx, begin, end, item, swap; var $last; if (self.settings.mode === 'single') return; $item = $($item); // clear the active selection if (!$item.length) { $(self.$activeItems).removeClass('active'); self.$activeItems = []; if (self.isFocused) { self.showInput(); } return; } // modify selection eventName = e && e.type.toLowerCase(); if (eventName === 'mousedown' && self.isShiftDown && self.$activeItems.length) { $last = self.$control.children('.active:last'); begin = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$last[0]]); end = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$item[0]]); if (begin > end) { swap = begin; begin = end; end = swap; } for (i = begin; i <= end; i++) { item = self.$control[0].childNodes[i]; if (self.$activeItems.indexOf(item) === -1) { $(item).addClass('active'); self.$activeItems.push(item); } } e.preventDefault(); } else if ((eventName === 'mousedown' && self.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) { if ($item.hasClass('active')) { idx = self.$activeItems.indexOf($item[0]); self.$activeItems.splice(idx, 1); $item.removeClass('active'); } else { self.$activeItems.push($item.addClass('active')[0]); } } else { $(self.$activeItems).removeClass('active'); self.$activeItems = [$item.addClass('active')[0]]; } // ensure control has focus self.hideInput(); if (!this.isFocused) { self.focus(); } }, /** * Sets the selected item in the dropdown menu * of available options. * * @param {object} $object * @param {boolean} scroll * @param {boolean} animate */ setActiveOption: function($option, scroll, animate) { var heightMenu, heightItem, y; var scrollTop, scrollBottom; var self = this; if (self.$activeOption) self.$activeOption.removeClass('active'); self.$activeOption = null; $option = $($option); if (!$option.length) return; self.$activeOption = $option.addClass('active'); if (scroll || !isSet(scroll)) { heightMenu = self.$dropdownContent.height(); heightItem = self.$activeOption.outerHeight(true); scroll = self.$dropdownContent.scrollTop() || 0; y = self.$activeOption.offset().top - self.$dropdownContent.offset().top + scroll; scrollTop = y; scrollBottom = y - heightMenu + heightItem; if (y + heightItem > heightMenu + scroll) { self.$dropdownContent.stop().animate({scrollTop: scrollBottom}, animate ? self.settings.scrollDuration : 0); } else if (y < scroll) { self.$dropdownContent.stop().animate({scrollTop: scrollTop}, animate ? self.settings.scrollDuration : 0); } } }, /** * Selects all items (CTRL + A). */ selectAll: function() { var self = this; if (self.settings.mode === 'single') return; self.$activeItems = Array.prototype.slice.apply(self.$control.children(':not(input)').addClass('active')); if (self.$activeItems.length) { self.hideInput(); self.close(); } self.focus(); }, /** * Hides the input element out of view, while * retaining its focus. */ hideInput: function() { var self = this; self.setTextboxValue(''); self.$controlInput.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000}); self.isInputHidden = true; }, /** * Restores input visibility. */ showInput: function() { this.$controlInput.css({opacity: 1, position: 'relative', left: 0}); this.isInputHidden = false; }, /** * Gives the control focus. */ focus: function() { var self = this; if (self.isDisabled) return; self.ignoreFocus = true; self.$controlInput[0].focus(); window.setTimeout(function() { self.ignoreFocus = false; self.onFocus(); }, 0); }, /** * Forces the control out of focus. * * @param {Element} dest */ blur: function(dest) { this.$controlInput[0].blur(); this.onBlur(null, dest); }, /** * Returns a function that scores an object * to show how good of a match it is to the * provided query. * * @param {string} query * @param {object} options * @return {function} */ getScoreFunction: function(query) { return this.sifter.getScoreFunction(query, this.getSearchOptions()); }, /** * Returns search options for sifter (the system * for scoring and sorting results). * * @see https://github.com/brianreavis/sifter.js * @return {object} */ getSearchOptions: function() { var settings = this.settings; var sort = settings.sortField; if (typeof sort === 'string') { sort = [{field: sort}]; } return { fields : settings.searchField, conjunction : settings.searchConjunction, sort : sort }; }, /** * Searches through available options and returns * a sorted array of matches. * * Returns an object containing: * * - query {string} * - tokens {array} * - total {int} * - items {array} * * @param {string} query * @returns {object} */ search: function(query) { var i, result, calculateScore; var self = this; var settings = self.settings; var options = this.getSearchOptions(); // validate user-provided result scoring function if (settings.score) { calculateScore = self.settings.score.apply(this, [query]); if (typeof calculateScore !== 'function') { throw new Error('Selectize "score" setting must be a function that returns a function'); } } // perform search if (query !== self.lastQuery) { self.lastQuery = query; result = self.sifter.search(query, $.extend(options, {score: calculateScore})); self.currentResults = result; } else { result = $.extend(true, {}, self.currentResults); } // filter out selected items if (settings.hideSelected) { for (i = result.items.length - 1; i >= 0; i--) { if (self.items.indexOf(hashKey(result.items[i].id)) !== -1) { result.items.splice(i, 1); } } } return result; }, /** * Refreshes the list of available options shown * in the autocomplete dropdown menu. * * @param {boolean} triggerDropdown */ refreshOptions: function(triggerDropdown) { var i, j, k, n, groups, groupsOrder, option, optionHtml, optgroup, optgroups, html, htmlChildren, hasCreateOption; var $active, $activeBefore, $create; if (typeof triggerDropdown === 'undefined') { triggerDropd