/** * selectize.js (v0.7.0) * Copyright (c) 2013 Brian Reavis & contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this * file except in compliance with the License. You may obtain a copy of the License at: * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. * * @author Brian Reavis */ /*jshint curly:false */ /*jshint browser:true */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define(['sifter','microplugin'], factory); } else { root.Selectize = factory(root.Sifter, root.MicroPlugin); } }(this, function(Sifter, MicroPlugin) { 'use strict'; var highlight = function($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); }); }; var MicroEvent = function() {}; 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]]; } }; var IS_MAC = /Mac/.test(navigator.userAgent); var KEY_A = 65; var KEY_COMMA = 188; var KEY_RETURN = 13; var KEY_ESC = 27; var KEY_LEFT = 37; var KEY_UP = 38; var KEY_RIGHT = 39; var KEY_DOWN = 40; 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; var isset = function(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} */ var hash_key = function(value) { if (typeof value === 'undefined' || value === null) return ''; if (typeof value === 'boolean') return value ? '1' : '0'; return value + ''; }; /** * Escapes a string for use within HTML. * * @param {string} str * @returns {string} */ var escape_html = function(str) { return (str + '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }; /** * Escapes quotation marks with backslashes. Useful * for escaping values for use in CSS attribute selectors. * * @param {string} str * @return {string} */ var escape_quotes = function(str) { return str.replace(/(['"])/g, '\\$1'); }; var hook = {}; /** * Wraps `method` on `self` so that `fn` * is invoked before the original method. * * @param {object} self * @param {string} method * @param {function} fn */ hook.before = function(self, method, fn) { var original = self[method]; self[method] = function() { fn.apply(self, arguments); return original.apply(self, arguments); }; }; /** * Wraps `method` on `self` so that `fn` * is invoked after the original method. * * @param {object} self * @param {string} method * @param {function} fn */ hook.after = function(self, method, fn) { var original = self[method]; self[method] = function() { var result = original.apply(self, arguments); fn.apply(self, arguments); return result; }; }; /** * Builds a hash table out of an array of * objects, using the specified `key` within * each object. * * @param {string} key * @param {mixed} objects */ var build_hash_table = function(key, objects) { if (!$.isArray(objects)) return objects; var i, n, table = {}; for (i = 0, n = objects.length; i < n; i++) { if (objects[i].hasOwnProperty(key)) { table[objects[i][key]] = objects[i]; } } return table; }; /** * Wraps `fn` so that it can only be invoked once. * * @param {function} fn * @returns {function} */ var once = function(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} */ var debounce = function(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 */ var debounce_events = function(self, types, fn) { var type; var trigger = self.trigger; var event_args = {}; // override trigger method self.trigger = function() { var type = arguments[0]; if (types.indexOf(type) !== -1) { event_args[type] = arguments; } else { return trigger.apply(self, arguments); } }; // invoke provided function fn.apply(self, []); self.trigger = trigger; // trigger queued events for (type in event_args) { if (event_args.hasOwnProperty(type)) { trigger.apply(self, event_args[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. */ var watchChildEvent = function($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} */ var getSelection = function(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 */ var transferStyles = function($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} */ var measureString = function(str, $parent) { var $test = $('').css({ position: 'absolute', top: -99999, left: -99999, width: 'auto', padding: 0, whiteSpace: 'nowrap' }).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 */ var autoGrow = function($input) { var update = function(e) { var value, keyCode, printable, placeholder, width; var shift, character, selection; e = e || window.event || {}; if (e.metaKey || e.altKey) return; if ($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.length && placeholder.length) { value = placeholder; } width = measureString(value, $input) + 4; if (width !== $input.width()) { $input.width(width); $input.triggerHandler('resize'); } }; $input.on('keydown keyup update blur', update); update(); }; var Selectize = function($input, settings) { var key, i, n, self = this; $input[0].selectize = self; // setup default state $.extend(self, { settings : settings, $input : $input, tagType : $input[0].tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT, eventNS : '.selectize' + (++Selectize.count), highlightedValue : null, isOpen : false, isDisabled : false, isLocked : false, isFocused : false, isInputFocused : false, isInputHidden : false, isSetup : false, isShiftDown : false, isCmdDown : false, isCtrlDown : false, ignoreFocus : false, ignoreHover : false, hasOptions : false, currentResults : null, lastValue : '', caretPos : 0, loading : 0, loadedSearches : {}, $activeOption : null, $activeItems : [], optgroups : {}, options : {}, userOptions : {}, items : [], renderCache : {}, onSearchChange : debounce(self.onSearchChange, settings.loadThrottle) }); // search system self.sifter = new Sifter(this.options, {diacritics: settings.diacritics}); // build options table $.extend(self.options, build_hash_table(settings.valueField, settings.options)); delete self.settings.options; // build optgroup table $.extend(self.optgroups, build_hash_table(settings.optgroupValueField, settings.optgroups)); 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.setup(); }; // 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 $wrapper; var $control; var $control_input; var $dropdown; var $dropdown_content; var $dropdown_parent; var inputMode; var timeout_blur; var timeout_focus; var tab_index; var classes; var classes_plugins; inputMode = self.settings.mode; tab_index = self.$input.attr('tabindex') || ''; classes = self.$input.attr('class') || ''; $wrapper = $('
').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode); $control = $('
').addClass(settings.inputClass).addClass('items').appendTo($wrapper); $control_input = $('').appendTo($control).attr('tabindex', tab_index); $dropdown_parent = $(settings.dropdownParent || $wrapper); $dropdown = $('
').addClass(settings.dropdownClass).addClass(classes).addClass(inputMode).hide().appendTo($dropdown_parent); $dropdown_content = $('
').addClass(settings.dropdownContentClass).appendTo($dropdown); $wrapper.css({ width: self.$input[0].style.width, display: self.$input.css('display') }); if (self.plugins.names.length) { classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-'); $wrapper.addClass(classes_plugins); $dropdown.addClass(classes_plugins); } if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) { self.$input.attr('multiple', 'multiple'); } if (self.settings.placeholder) { $control_input.attr('placeholder', settings.placeholder); } self.$wrapper = $wrapper; self.$control = $control; self.$control_input = $control_input; self.$dropdown = $dropdown; self.$dropdown_content = $dropdown_content; $control.on('mousedown', function(e) { if (!e.isDefaultPrevented()) { window.setTimeout(function() { self.focus(true); }, 0); } }); // necessary for mobile webkit devices (manual focus triggering // is ignored unless invoked within a click event) $control.on('click', function(e) { if (!self.isInputFocused) { self.focus(true); } }); $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); }); $dropdown.on('mousedown', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); }); watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); }); autoGrow($control_input); $control_input.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() { return self.onFocus.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]) { var ignoreFocus = self.ignoreFocus; self.ignoreFocus = true; window.setTimeout(function() { self.ignoreFocus = ignoreFocus; self.focus(false); }, 0); return; } // blur on click outside if (!self.$control.has(e.target).length && e.target !== self.$control[0]) { self.blur(); } } }); $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() { if (self.isOpen) { self.positionDropdown.apply(self, arguments); } }); $window.on('mousemove' + eventNS, function() { self.ignoreHover = false; }); self.$input.attr('tabindex',-1).hide().after(self.$wrapper); if ($.isArray(settings.items)) { self.setValue(settings.items); delete settings.items; } self.updateOriginalInput(); self.refreshItems(); self.refreshClasses(); self.updatePlaceholder(); self.isSetup = true; if (self.$input.is(':disabled')) { self.disable(); } self.trigger('initialize'); // preload options if (settings.preload) { self.onSearchChange(''); } }, /** * Maps fired events to callbacks provided * in the settings used when creating the control. */ setupCallbacks: function() { var key, fn, callbacks = { 'initialize' : 'onInitialize', 'change' : 'onChange', 'item_add' : 'onItemAdd', 'item_remove' : 'onItemRemove', 'clear' : 'onClear', 'option_add' : 'onOptionAdd', 'option_remove' : 'onOptionRemove', 'option_clear' : 'onOptionClear', 'dropdown_open' : 'onDropdownOpen', 'dropdown_close' : 'onDropdownClose', 'type' : 'onType' }; for (key in callbacks) { if (callbacks.hasOwnProperty(key)) { fn = this.settings[callbacks[key]]; if (fn) this.on(key, fn); } } }, /** * Triggers a callback defined in the user-provided settings. * Events: onItemAdd, onOptionAdd, etc * * @param {string} event */ triggerCallback: function(event) { var args; if (typeof this.settings[event] === 'function') { args = Array.prototype.slice.apply(arguments, [1]); this.settings[event].apply(this, args); } }, /** * Triggered on 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 && character === this.settings.delimiter) { this.createItem(); e.preventDefault(); return false; } }, /** * Triggered on keydown. * * @param {object} e * @returns {boolean} */ onKeyDown: function(e) { var isInput = e.target === this.$control_input[0]; 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: self.blur(); return; 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_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.$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.create && $.trim(self.$control_input.val()).length) { self.createItem(); e.preventDefault(); } return; case KEY_BACKSPACE: case KEY_DELETE: self.deleteSelection(e); return; } if (self.isFull() || self.isInputHidden) { e.preventDefault(); return; } }, /** * Triggered on keyup. * * @param {object} e * @returns {boolean} */ onKeyUp: function(e) { var self = this; if (self.isLocked) return e && e.preventDefault(); var value = self.$control_input.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.loadDelay` 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 focus. * * @param {object} e (optional) * @returns {boolean} */ onFocus: function(e) { var self = this; self.isInputFocused = true; self.isFocused = true; if (self.isDisabled) { self.blur(); e.preventDefault(); return false; } if (self.ignoreFocus) return; if (self.settings.preload === 'focus') self.onSearchChange(''); self.showInput(); self.setActiveItem(null); self.refreshOptions(!!self.settings.openOnFocus); self.refreshClasses(); }, /** * Triggered on blur. * * @param {object} e * @returns {boolean} */ onBlur: function(e) { var self = this; self.isInputFocused = false; if (self.ignoreFocus) return; self.close(); self.setTextboxValue(''); self.setActiveItem(null); self.setActiveOption(null); self.setCaret(self.items.length); self.isFocused = false; self.refreshClasses(); }, /** * 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, $option, self = this; e.preventDefault && e.preventDefault(); e.stopPropagation && e.stopPropagation(); self.focus(false); $target = $(e.currentTarget); if ($target.hasClass('create')) { self.createItem(); } else { value = $target.attr('data-value'); if (value) { self.setTextboxValue(''); self.addItem(value); 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.settings.mode === 'multi') { e.preventDefault(); self.setActiveItem(e.currentTarget, e); self.focus(false); self.hideInput(); } }, /** * 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('loading'); self.loading++; fn.apply(self, [function(results) { self.loading = Math.max(self.loading - 1, 0); if (results && results.length) { self.addOption(results); self.refreshOptions(false); if (self.isInputFocused) self.open(); } if (!self.loading) { $wrapper.removeClass('loading'); } self.trigger('load', results); }]); }, /** * Sets the input field of the control to the specified value. * * @param {string} value */ setTextboxValue: function(value) { this.$control_input.val(value).triggerHandler('update'); this.lastValue = value; }, /** * Returns the value of the control. If multiple items * can be selected (e.g. or * element to reflect the current state. */ updateOriginalInput: function() { var i, n, options, self = this; if (self.$input[0].tagName.toLowerCase() === 'select') { options = []; for (i = 0, n = self.items.length; i < n; i++) { options.push(''); } if (!options.length && !this.$input.attr('multiple')) { options.push(''); } self.$input.html(options.join('')); } else { self.$input.val(self.getValue()); } self.$input.trigger('change'); if (self.isSetup) { self.trigger('change', self.$input.val()); } }, /** * Shows/hide the input placeholder depending * on if there items in the list already. */ updatePlaceholder: function() { if (!this.settings.placeholder) return; var $input = this.$control_input; if (this.items.length) { $input.removeAttr('placeholder'); } else { $input.attr('placeholder', this.settings.placeholder); } $input.triggerHandler('update'); }, /** * Shows the autocomplete dropdown containing * the available options. */ open: function() { var self = this; if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return; self.focus(true); self.isOpen = true; self.refreshClasses(); self.$dropdown.css({visibility: 'hidden', display: 'block'}); self.positionDropdown(); self.$dropdown.css({visibility: 'visible'}); self.trigger('dropdown_open', this.$dropdown); }, /** * Closes the autocomplete dropdown menu. */ close: function() { var self = this; if (!self.isOpen) return; self.$dropdown.hide(); self.setActiveOption(null); self.isOpen = false; self.refreshClasses(); self.trigger('dropdown_close', self.$dropdown); }, /** * Calculates and applies the appropriate * position of the dropdown. */ positionDropdown: function() { var $control = this.$control; var offset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position(); offset.top += $control.outerHeight(true); this.$dropdown.css({ width : $control.outerWidth(), top : offset.top, left : offset.left }); }, /** * Resets / clears all selected items * from the control. */ clear: function() { var self = this; if (!self.items.length) return; self.$control.children(':not(input)').remove(); self.items = []; self.setCaret(0); self.updatePlaceholder(); self.updateOriginalInput(); self.refreshClasses(); self.showInput(); self.trigger('clear'); }, /** * A helper method for inserting an element * at the current caret position. * * @param {object} $el */ insertAtCaret: function($el) { var caret = Math.min(this.caretPos, this.items.length); if (caret === 0) { this.$control.prepend($el); } else { $(this.$control[0].childNodes[caret]).before($el); } this.setCaret(caret + 1); }, /** * Removes the current selected item(s). * * @param {object} e (optional) * @returns {boolean} */ deleteSelection: function(e) { var i, n, direction, selection, values, caret, option_select, $option_select, $tail; var self = this; direction = (e && e.keyCode === KEY_BACKSPACE) ? -1 : 1; selection = getSelection(self.$control_input[0]); if (self.$activeOption && !self.settings.hideSelected) { option_select = self.getAdjacentOption(self.$activeOption, -1).attr('data-value'); } // determine items that will be removed values = []; if (self.$activeItems.length) { $tail = self.$control.children('.active:' + (direction > 0 ? 'last' : 'first')); caret = self.$control.children(':not(input)').index($tail); if (direction > 0) { caret++; } for (i = 0, n = self.$activeItems.length; i < n; i++) { values.push($(self.$activeItems[i]).attr('data-value')); } if (e) { e.preventDefault(); e.stopPropagation(); } } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) { if (direction < 0 && selection.start === 0 && selection.length === 0) { values.push(self.items[self.caretPos - 1]); } else if (direction > 0 && selection.start === self.$control_input.val().length) { values.push(self.items[self.caretPos]); } } // allow the callback to abort if (!values.length || (typeof self.settings.onDelete === 'function' && self.settings.onDelete(values) === false)) { return false; } // perform removal if (typeof caret !== 'undefined') { self.setCaret(caret); } while (values.length) { self.removeItem(values.pop()); } self.showInput(); self.refreshOptions(true); // select previous option if (option_select) { $option_select = self.getOption(option_select); if ($option_select.length) { self.setActiveOption($option_select); } } return true; }, /** * Selects the previous / next item (depending * on the `direction` argument). * * > 0 - right * < 0 - left * * @param {int} direction * @param {object} e (optional) */ advanceSelection: function(direction, e) { var tail, selection, idx, valueLength, cursorAtEdge, $tail; var self = this; if (direction === 0) return; tail = direction > 0 ? 'last' : 'first'; selection = getSelection(self.$control_input[0]); if (self.isInputFocused && !self.isInputHidden) { valueLength = self.$control_input.val().length; cursorAtEdge = direction < 0 ? selection.start === 0 && selection.length === 0 : selection.start === valueLength; if (cursorAtEdge && !valueLength) { self.advanceCaret(direction, e); } } else { $tail = self.$control.children('.active:' + tail); if ($tail.length) { idx = self.$control.children(':not(input)').index($tail); self.setActiveItem(null); self.setCaret(direction > 0 ? idx + 1 : idx); self.showInput(); } } }, /** * Moves the caret left / right. * * @param {int} direction * @param {object} e (optional) */ advanceCaret: function(direction, e) { if (direction === 0) return; var self = this; var fn = direction > 0 ? 'next' : 'prev'; if (self.isShiftDown) { var $adj = self.$control_input[fn](); if ($adj.length) { self.hideInput(); self.setActiveItem($adj); e && e.preventDefault(); } } else { self.setCaret(self.caretPos + direction); } }, /** * Moves the caret to the specified index. * * @param {int} i */ setCaret: function(i) { var self = this; if (self.settings.mode === 'single') { i = self.items.length; } else { i = Math.max(0, Math.min(self.items.length, i)); } // the input must be moved by leaving it in place and moving the // siblings, due to the fact that focus cannot be restored once lost // on mobile webkit devices var j, n, fn, $children, $child; $children = self.$control.children(':not(input)'); for (j = 0, n = $children.length; j < n; j++) { $child = $($children[j]).detach(); if (j < i) { self.$control_input.before($child); } else { self.$control.append($child); } } self.caretPos = i; }, /** * Disables user input on the control. Used while * items are being asynchronously created. */ lock: function() { this.close(); this.isLocked = true; this.refreshClasses(); }, /** * Re-enables user input on the control. */ unlock: function() { this.isLocked = false; this.refreshClasses(); }, /** * Disables user input on the control completely. * While disabled, it cannot receive focus. */ disable: function() { var self = this; self.$input.prop('disabled', true); self.isDisabled = true; self.lock(); }, /** * Enables the control so that it can respond * to focus and user input. */ enable: function() { var self = this; self.$input.prop('disabled', false); self.isDisabled = false; self.unlock(); }, /** * Completely destroys the control and * unbinds all event listeners so that it can * be garbage collected. */ destroy: function() { var self = this; var eventNS = self.eventNS; self.trigger('destroy'); self.off(); self.$wrapper.remove(); self.$dropdown.remove(); self.$input.show(); $(window).off(eventNS); $(document).off(eventNS); $(document.body).off(eventNS); delete self.$input[0].selectize; }, /** * A helper method for rendering "item" and * "option" templates, given the data. * * @param {string} templateName * @param {object} data * @returns {string} */ render: function(templateName, data) { var value, id, label; var html = ''; var cache = false; var self = this; var regex_tag = /^[\t ]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i; if (templateName === 'option' || templateName === 'item') { value = hash_key(data[self.settings.valueField]); cache = !!value; } // pull markup from cache if it exists if (cache) { if (!isset(self.renderCache[templateName])) { self.renderCache[templateName] = {}; } if (self.renderCache[templateName].hasOwnProperty(value)) { return self.renderCache[templateName][value]; } } // render markup if (self.settings.render && typeof self.settings.render[templateName] === 'function') { html = self.settings.render[templateName].apply(this, [data, escape_html]); } else { label = data[self.settings.labelField]; switch (templateName) { case 'optgroup': html = '
' + data.html + "
"; break; case 'optgroup_header': label = data[self.settings.optgroupLabelField]; html = '
' + escape_html(label) + '
'; break; case 'option': html = '
' + escape_html(label) + '
'; break; case 'item': html = '
' + escape_html(label) + '
'; break; case 'option_create': html = '
Add ' + escape_html(data.input) + '
'; break; } } // add mandatory attributes if (templateName === 'option' || templateName === 'option_create') { html = html.replace(regex_tag, '<$1 data-selectable'); } if (templateName === 'optgroup') { id = data[self.settings.optgroupValueField] || ''; html = html.replace(regex_tag, '<$1 data-group="' + escape_html(id) + '"'); } if (templateName === 'option' || templateName === 'item') { html = html.replace(regex_tag, '<$1 data-value="' + escape_html(value || '') + '"'); } // update cache if (cache) { self.renderCache[templateName][value] = html; } return html; } }); Selectize.count = 0; Selectize.defaults = { plugins: [], delimiter: ',', persist: true, diacritics: true, create: false, highlight: true, openOnFocus: true, maxOptions: 1000, maxItems: null, hideSelected: null, preload: false, scrollDuration: 60, loadThrottle: 300, dataAttr: 'data-data', optgroupField: 'optgroup', sortField: null, sortDirection: 'asc', valueField: 'value', labelField: 'text', optgroupLabelField: 'label', optgroupValueField: 'value', optgroupOrder: null, searchField: ['text'], mode: null, wrapperClass: 'selectize-control', inputClass: 'selectize-input', dropdownClass: 'selectize-dropdown', dropdownContentClass: 'selectize-dropdown-content', dropdownParent: null, /* 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() { ... } 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 */ } }; $.fn.selectize = function(settings) { settings = settings || {}; var defaults = $.fn.selectize.defaults; var dataAttr = settings.dataAttr || defaults.dataAttr; /** * Initializes selectize from a element. * * @param {object} $input * @param {object} settings */ var init_textbox = function($input, settings_element) { var i, n, values, value = $.trim($input.val() || ''); if (!value.length) return; values = value.split(settings.delimiter || defaults.delimiter); for (i = 0, n = values.length; i < n; i++) { settings_element.options[values[i]] = { 'text' : values[i], 'value' : values[i] }; } settings_element.items = values; }; /** * Initializes selectize from a