UNPKG

mobius1-selectr

Version:

A lightweight, dependency-free, mobile-friendly javascript select box replacement.

1,517 lines (1,270 loc) 76.3 kB
/*! * Selectr 2.4.13 * http://mobius.ovh/docs/selectr * * Released under the MIT license */ (function(root, factory) { var plugin = "Selectr"; if (typeof define === "function" && define.amd) { define([], factory); } else if (typeof exports === "object") { module.exports = factory(plugin); } else { root[plugin] = factory(plugin); } }(this, function(plugin) { 'use strict'; /** * Event Emitter */ var Events = function() {}; /** * Event Prototype * @type {Object} */ Events.prototype = { /** * Add custom event listener * @param {String} event Event type * @param {Function} func Callback * @return {Void} */ on: function(event, func) { this._events = this._events || {}; this._events[event] = this._events[event] || []; this._events[event].push(func); }, /** * Remove custom event listener * @param {String} event Event type * @param {Function} func Callback * @return {Void} */ off: function(event, func) { this._events = this._events || {}; if (event in this._events === false) return; this._events[event].splice(this._events[event].indexOf(func), 1); }, /** * Fire a custom event * @param {String} event Event type * @return {Void} */ emit: 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)); } } }; /** * Event mixin * @param {Object} obj * @return {Object} */ Events.mixin = function(obj) { var props = ['on', 'off', 'emit']; for (var i = 0; i < props.length; i++) { if (typeof obj === 'function') { obj.prototype[props[i]] = Events.prototype[props[i]]; } else { obj[props[i]] = Events.prototype[props[i]]; } } return obj; }; /** * Helpers * @type {Object} */ var util = { escapeRegExp: function(str) { // source from lodash 3.0.0 var _reRegExpChar = /[\\^$.*+?()[\]{}|]/g; var _reHasRegExpChar = new RegExp(_reRegExpChar.source); return (str && _reHasRegExpChar.test(str)) ? str.replace(_reRegExpChar, '\\$&') : str; }, extend: function(src, props) { for (var prop in props) { if (props.hasOwnProperty(prop)) { var val = props[prop]; if (val && Object.prototype.toString.call(val) === "[object Object]") { src[prop] = src[prop] || {}; util.extend(src[prop], val); } else { src[prop] = val; } } } return src; }, each: function(a, b, c) { if ("[object Object]" === Object.prototype.toString.call(a)) { for (var d in a) { if (Object.prototype.hasOwnProperty.call(a, d)) { b.call(c, d, a[d], a); } } } else { for (var e = 0, f = a.length; e < f; e++) { b.call(c, e, a[e], a); } } }, createElement: function(e, a) { var d = document, el = d.createElement(e); if (a && "[object Object]" === Object.prototype.toString.call(a)) { var i; for (i in a) if (i in el) el[i] = a[i]; else if ("html" === i) el.innerHTML = a[i]; else el.setAttribute(i, a[i]); } return el; }, hasClass: function(a, b) { if (a) return a.classList ? a.classList.contains(b) : !!a.className && !!a.className.match(new RegExp("(\\s|^)" + b + "(\\s|$)")); }, addClass: function(a, b) { if (!util.hasClass(a, b)) { if (a.classList) { a.classList.add(b); } else { a.className = a.className.trim() + " " + b; } } }, removeClass: function(a, b) { if (util.hasClass(a, b)) { if (a.classList) { a.classList.remove(b); } else { a.className = a.className.replace(new RegExp("(^|\\s)" + b.split(" ").join("|") + "(\\s|$)", "gi"), " "); } } }, closest: function(el, fn) { return el && el !== document.body && (fn(el) ? el : util.closest(el.parentNode, fn)); }, isInt: function(val) { return typeof val === 'number' && isFinite(val) && Math.floor(val) === val; }, debounce: function(a, b, c) { var d; return function() { var e = this, f = arguments, g = function() { d = null; if (!c) a.apply(e, f); }, h = c && !d; clearTimeout(d); d = setTimeout(g, b); if (h) { a.apply(e, f); } }; }, rect: function(el, abs) { var w = window; var r = el.getBoundingClientRect(); var x = abs ? w.pageXOffset : 0; var y = abs ? w.pageYOffset : 0; return { bottom: r.bottom + y, height: r.height, left: r.left + x, right: r.right + x, top: r.top + y, width: r.width }; }, includes: function(a, b) { return a.indexOf(b) > -1; }, startsWith: function(a, b) { return a.substr( 0, b.length ) === b; }, truncate: function(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } }; function isset(obj, prop) { return obj.hasOwnProperty(prop) && (obj[prop] === true || obj[prop].length); } /** * Append an item to the list * @param {Object} item * @param {Object} custom * @return {Void} */ function appendItem(item, parent, custom) { if (item.parentNode) { if (!item.parentNode.parentNode) { parent.appendChild(item.parentNode); } } else { parent.appendChild(item); } util.removeClass(item, "excluded"); if (!custom) { // remove any <span> highlighting, without xss item.textContent = item.textContent; } } /** * Render the item list * @return {Void} */ var render = function() { if (this.items.length) { var f = document.createDocumentFragment(); if (this.config.pagination) { var pages = this.pages.slice(0, this.pageIndex); util.each(pages, function(i, items) { util.each(items, function(j, item) { appendItem(item, f, this.customOption); }, this); }, this); } else { util.each(this.items, function(i, item) { appendItem(item, f, this.customOption); }, this); } // highlight first selected option if any; first option otherwise if (f.childElementCount) { util.removeClass(this.items[this.navIndex], "active"); this.navIndex = ( f.querySelector(".selectr-option.selected") || f.querySelector(".selectr-option") ).idx; util.addClass(this.items[this.navIndex], "active"); } this.tree.appendChild(f); } }; /** * Dismiss / close the dropdown * @param {obj} e * @return {void} */ var dismiss = function(e) { var target = e.target; if (!this.container.contains(target) && (this.opened || util.hasClass(this.container, "notice"))) { this.close(); } }; /** * Build a list item from the HTMLOptionElement * @param {int} i HTMLOptionElement index * @param {HTMLOptionElement} option * @param {bool} group Has parent optgroup * @return {void} */ var createItem = function(option, data) { data = data || option; var elementData = { class: "selectr-option", role: "treeitem", "aria-selected": false }; if(this.customOption){ elementData.html = this.config.renderOption(data); // asume xss prevention in custom render function } else{ elementData.textContent = option.textContent; // treat all as plain text } var opt = util.createElement("li",elementData); opt.idx = option.idx; this.items.push(opt); if (option.defaultSelected) { this.defaultSelected.push(option.idx); } if (option.disabled) { opt.disabled = true; util.addClass(opt, "disabled"); } return opt; }; /** * Build the container * @return {Void} */ var build = function() { this.requiresPagination = this.config.pagination && this.config.pagination > 0; // Set width if (isset(this.config, "width")) { if (util.isInt(this.config.width)) { this.width = this.config.width + "px"; } else { if (this.config.width === "auto") { this.width = "100%"; } else if (util.includes(this.config.width, "%")) { this.width = this.config.width; } } } this.container = util.createElement("div", { class: "selectr-container" }); // Custom className if (this.config.customClass) { util.addClass(this.container, this.config.customClass); } // Mobile device if (this.mobileDevice) { util.addClass(this.container, "selectr-mobile"); } else { util.addClass(this.container, "selectr-desktop"); } // Hide the HTMLSelectElement and prevent focus this.el.tabIndex = -1; // Native dropdown if (this.config.nativeDropdown || this.mobileDevice) { util.addClass(this.el, "selectr-visible"); } else { util.addClass(this.el, "selectr-hidden"); } this.selected = util.createElement("div", { class: "selectr-selected", disabled: this.disabled, tabIndex: 0, "aria-expanded": false }); this.label = util.createElement(this.el.multiple ? "ul" : "span", { class: "selectr-label" }); var dropdown = util.createElement("div", { class: "selectr-options-container" }); this.tree = util.createElement("ul", { class: "selectr-options", role: "tree", "aria-hidden": true, "aria-expanded": false }); this.notice = util.createElement("div", { class: "selectr-notice" }); this.el.setAttribute("aria-hidden", true); if (this.disabled) { this.el.disabled = true; } if (this.el.multiple) { util.addClass(this.label, "selectr-tags"); util.addClass(this.container, "multiple"); // Collection of tags this.tags = []; // Collection of selected values // #93 defaultSelected = false did not work as expected this.selectedValues = (this.config.defaultSelected) ? this.getSelectedProperties('value') : []; // Collection of selected indexes this.selectedIndexes = this.getSelectedProperties('idx'); } else { // #93 defaultSelected = false did not work as expected // these values were undefined this.selectedValue = null; this.selectedIndex = -1; } this.selected.appendChild(this.label); if (this.config.clearable) { this.selectClear = util.createElement("button", { class: "selectr-clear", type: "button" }); this.container.appendChild(this.selectClear); util.addClass(this.container, "clearable"); } if (this.config.taggable) { var li = util.createElement('li', { class: 'input-tag' }); this.input = util.createElement("input", { class: "selectr-tag-input", placeholder: this.config.tagPlaceholder, tagIndex: 0, autocomplete: "off", autocorrect: "off", autocapitalize: "off", spellcheck: "false", role: "textbox", type: "search" }); li.appendChild(this.input); this.label.appendChild(li); util.addClass(this.container, "taggable"); this.tagSeperators = [","]; if (this.config.tagSeperators) { this.tagSeperators = this.tagSeperators.concat(this.config.tagSeperators); var _aTempEscapedSeperators = []; for(var _nTagSeperatorStepCount = 0; _nTagSeperatorStepCount < this.tagSeperators.length; _nTagSeperatorStepCount++){ _aTempEscapedSeperators.push(util.escapeRegExp(this.tagSeperators[_nTagSeperatorStepCount])); } this.tagSeperatorsRegex = new RegExp(_aTempEscapedSeperators.join('|'),'i'); } else { this.tagSeperatorsRegex = new RegExp(',','i'); } } if (this.config.searchable) { this.input = util.createElement("input", { class: "selectr-input", tagIndex: -1, autocomplete: "off", autocorrect: "off", autocapitalize: "off", spellcheck: "false", role: "textbox", type: "search", placeholder: this.config.messages.searchPlaceholder }); this.inputClear = util.createElement("button", { class: "selectr-input-clear", type: "button" }); this.inputContainer = util.createElement("div", { class: "selectr-input-container" }); this.inputContainer.appendChild(this.input); this.inputContainer.appendChild(this.inputClear); dropdown.appendChild(this.inputContainer); } dropdown.appendChild(this.notice); dropdown.appendChild(this.tree); // List of items for the dropdown this.items = []; // Establish options this.options = []; // Check for options in the element if (this.el.options.length) { this.options = [].slice.call(this.el.options); } // Element may have optgroups so // iterate element.children instead of element.options var group = false, j = 0; if (this.el.children.length) { util.each(this.el.children, function(i, element) { if (element.nodeName === "OPTGROUP") { group = util.createElement("ul", { class: "selectr-optgroup", role: "group", html: "<li class='selectr-optgroup--label'>" + element.label + "</li>" }); util.each(element.children, function(x, el) { el.idx = j; group.appendChild(createItem.call(this, el, group)); j++; }, this); } else { element.idx = j; createItem.call(this, element); j++; } }, this); } // Options defined by the data option if (this.config.data && Array.isArray(this.config.data)) { this.data = []; var optgroup = false, option; group = false; j = 0; util.each(this.config.data, function(i, opt) { // Check for group options if (isset(opt, "children")) { optgroup = util.createElement("optgroup", { label: opt.text }); group = util.createElement("ul", { class: "selectr-optgroup", role: "group", html: "<li class='selectr-optgroup--label'>" + opt.text + "</li>" }); util.each(opt.children, function(x, data) { option = new Option(data.text, data.value, false, data.hasOwnProperty("selected") && data.selected === true); option.disabled = isset(data, "disabled"); this.options.push(option); optgroup.appendChild(option); option.idx = j; group.appendChild(createItem.call(this, option, data)); this.data[j] = data; j++; }, this); this.el.appendChild(optgroup); } else { option = new Option(opt.text, opt.value, false, opt.hasOwnProperty("selected") && opt.selected === true); option.disabled = isset(opt, "disabled"); this.options.push(option); option.idx = j; createItem.call(this, option, opt); this.data[j] = opt; j++; } }, this); } this.setSelected(true); var first; this.navIndex = 0; for (var i = 0; i < this.items.length; i++) { first = this.items[i]; if (!util.hasClass(first, "disabled")) { util.addClass(first, "active"); this.navIndex = i; break; } } // Check for pagination / infinite scroll if (this.requiresPagination) { this.pageIndex = 1; // Create the pages this.paginate(); } this.container.appendChild(this.selected); this.container.appendChild(dropdown); this.placeEl = util.createElement("div", { class: "selectr-placeholder" }); // Set the placeholder this.setPlaceholder(); this.selected.appendChild(this.placeEl); // Disable if required if (this.disabled) { this.disable(); } this.el.parentNode.insertBefore(this.container, this.el); this.container.appendChild(this.el); }; /** * Navigate through the dropdown * @param {obj} e * @return {void} */ var navigate = function(e) { e = e || window.event; // Filter out the keys we don"t want if (!this.items.length || !this.opened || !util.includes([13, 38, 40], e.which)) { this.navigating = false; return; } e.preventDefault(); if (e.which === 13) { if ( this.noResults || (this.config.taggable && this.input.value.length > 0) ) { return false; } return this.change(this.navIndex); } var direction, prevEl = this.items[this.navIndex]; var lastIndex = this.navIndex; switch (e.which) { case 38: direction = 0; if (this.navIndex > 0) { this.navIndex--; } break; case 40: direction = 1; if (this.navIndex < this.items.length - 1) { this.navIndex++; } } this.navigating = true; // Instead of wasting memory holding a copy of this.items // with disabled / excluded options omitted, skip them instead while (util.hasClass(this.items[this.navIndex], "disabled") || util.hasClass(this.items[this.navIndex], "excluded")) { if (this.navIndex > 0 && this.navIndex < this.items.length -1) { if (direction) { this.navIndex++; } else { this.navIndex--; } } else { this.navIndex = lastIndex; break; } if (this.searching) { if (this.navIndex > this.tree.lastElementChild.idx) { this.navIndex = this.tree.lastElementChild.idx; break; } else if (this.navIndex < this.tree.firstElementChild.idx) { this.navIndex = this.tree.firstElementChild.idx; break; } } } // Autoscroll the dropdown during navigation var r = util.rect(this.items[this.navIndex]); if (!direction) { if (this.navIndex === 0) { this.tree.scrollTop = 0; } else if (r.top - this.optsRect.top < 0) { this.tree.scrollTop = this.tree.scrollTop + (r.top - this.optsRect.top); } } else { if (this.navIndex === 0) { this.tree.scrollTop = 0; } else if ((r.top + r.height) > (this.optsRect.top + this.optsRect.height)) { this.tree.scrollTop = this.tree.scrollTop + ((r.top + r.height) - (this.optsRect.top + this.optsRect.height)); } // Load another page if needed if (this.navIndex === this.tree.childElementCount - 1 && this.requiresPagination) { load.call(this); } } if (prevEl) { util.removeClass(prevEl, "active"); } util.addClass(this.items[this.navIndex], "active"); }; /** * Add a tag * @param {HTMLElement} item */ var addTag = function(item) { var that = this, r; var docFrag = document.createDocumentFragment(); var option = this.options[item.idx]; var data = this.data ? this.data[item.idx] : option; var elementData = { class: "selectr-tag" }; if (this.customSelected){ elementData.html = this.config.renderSelection(data); // asume xss prevention in custom render function } else { elementData.textContent = option.textContent; } var tag = util.createElement("li", elementData); var btn = util.createElement("button", { class: "selectr-tag-remove", type: "button" }); tag.appendChild(btn); // Set property to check against later tag.idx = item.idx; tag.tag = option.value; this.tags.push(tag); if (this.config.sortSelected) { var tags = this.tags.slice(); // Deal with values that contain numbers r = function(val, arr) { val.replace(/(\d+)|(\D+)/g, function(that, $1, $2) { arr.push([$1 || Infinity, $2 || ""]); }); }; tags.sort(function(a, b) { var x = [], y = [], ac, bc; if (that.config.sortSelected === true) { ac = a.tag; bc = b.tag; } else if (that.config.sortSelected === 'text') { ac = a.textContent; bc = b.textContent; } r(ac, x); r(bc, y); while (x.length && y.length) { var ax = x.shift(); var by = y.shift(); var nn = (ax[0] - by[0]) || ax[1].localeCompare(by[1]); if (nn) return nn; } return x.length - y.length; }); util.each(tags, function(i, tg) { docFrag.appendChild(tg); }); this.label.innerHTML = ""; } else { docFrag.appendChild(tag); } if (this.config.taggable) { this.label.insertBefore(docFrag, this.input.parentNode); } else { this.label.appendChild(docFrag); } }; /** * Remove a tag * @param {HTMLElement} item * @return {void} */ var removeTag = function(item) { var tag = false; util.each(this.tags, function(i, t) { if (t.idx === item.idx) { tag = t; } }, this); if (tag) { this.label.removeChild(tag); this.tags.splice(this.tags.indexOf(tag), 1); } }; /** * Load the next page of items * @return {void} */ var load = function() { var tree = this.tree; var scrollTop = tree.scrollTop; var scrollHeight = tree.scrollHeight; var offsetHeight = tree.offsetHeight; var atBottom = scrollTop >= (scrollHeight - offsetHeight); if ((atBottom && this.pageIndex < this.pages.length)) { var f = document.createDocumentFragment(); util.each(this.pages[this.pageIndex], function(i, item) { appendItem(item, f, this.customOption); }, this); tree.appendChild(f); this.pageIndex++; this.emit("selectr.paginate", { items: this.items.length, total: this.data.length, page: this.pageIndex, pages: this.pages.length }); } }; /** * Clear a search * @return {void} */ var clearSearch = function() { if (this.config.searchable || this.config.taggable) { this.input.value = null; this.searching = false; if (this.config.searchable) { util.removeClass(this.inputContainer, "active"); } if (util.hasClass(this.container, "notice")) { util.removeClass(this.container, "notice"); util.addClass(this.container, "open"); this.input.focus(); } util.each(this.items, function(i, item) { // Items that didn't match need the class // removing to make them visible again util.removeClass(item, "excluded"); // Remove the span element for underlining matched items if (!this.customOption) { // without xss item.textContent = item.textContent; } }, this); } }; /** * Query matching for searches. * Wraps matching text in a span.selectr-match. * * @param {string} query * @param {HTMLOptionElement} option element * @return {bool} true if matched; false otherwise */ var match = function(query, option) { var text = option.textContent; var RX = new RegExp( query, "ig" ); var result = RX.exec( text ); if (result) { // #102 stop xss option.innerHTML = ""; var span = document.createElement( "span" ); span.classList.add( "selectr-match" ); span.textContent = result[0]; option.appendChild( document.createTextNode( text.substring( 0, result.index ) ) ); option.appendChild( span ); option.appendChild( document.createTextNode( text.substring( RX.lastIndex ) ) ); return true; } return false; }; // Main Lib var Selectr = function(el, config) { if (!el) { throw new Error("You must supply either a HTMLSelectElement or a CSS3 selector string."); } this.el = el; // CSS3 selector string if (typeof el === "string") { this.el = document.querySelector(el); } if (this.el === null) { throw new Error("The element you passed to Selectr can not be found."); } if (this.el.nodeName.toLowerCase() !== "select") { throw new Error("The element you passed to Selectr is not a HTMLSelectElement."); } this.render(config); }; /** * Render the instance * @param {object} config * @return {void} */ Selectr.prototype.render = function(config) { if (this.rendered) return; /** * Default configuration options * @type {Object} */ var defaultConfig = { /** * Emulates browser behaviour by selecting the first option by default * @type {Boolean} */ defaultSelected: true, /** * Sets the width of the container * @type {String} */ width: "auto", /** * Enables/ disables the container * @type {Boolean} */ disabled: false, /** * Enables/ disables logic for mobile * @type {Boolean} */ disabledMobile: false, /** * Enables / disables the search function * @type {Boolean} */ searchable: true, /** * Enable disable the clear button * @type {Boolean} */ clearable: false, /** * Sort the tags / multiselect options * @type {Boolean} */ sortSelected: false, /** * Allow deselecting of select-one options * @type {Boolean} */ allowDeselect: false, /** * Close the dropdown when scrolling (@AlexanderReiswich, #11) * @type {Boolean} */ closeOnScroll: false, /** * Allow the use of the native dropdown (@jonnyscholes, #14) * @type {Boolean} */ nativeDropdown: false, /** * Allow the use of native typing behavior for toggling, searching, selecting * @type {boolean} */ nativeKeyboard: false, /** * Set the main placeholder * @type {String} */ placeholder: "Select an option...", /** * Allow the tagging feature * @type {Boolean} */ taggable: false, /** * Set the tag input placeholder (@labikmartin, #21, #22) * @type {String} */ tagPlaceholder: "Enter a tag...", messages: { noResults: "No results.", noOptions: "No options available.", maxSelections: "A maximum of {max} items can be selected.", tagDuplicate: "That tag is already in use.", searchPlaceholder: "Search options..." } }; // add instance reference (#87) this.el.selectr = this; // Merge defaults with user set config this.config = util.extend(defaultConfig, config); // Store type this.originalType = this.el.type; // Store tabIndex this.originalIndex = this.el.tabIndex; // Store defaultSelected options for form reset this.defaultSelected = []; // Store the original option count this.originalOptionCount = this.el.options.length; if (this.config.multiple || this.config.taggable) { this.el.multiple = true; } // Disabled? this.disabled = isset(this.config, "disabled"); this.opened = false; if (this.config.taggable) { this.config.searchable = false; } this.navigating = false; this.mobileDevice = false; if (!this.config.disabledMobile && /Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(navigator.userAgent)) { this.mobileDevice = true; } this.customOption = this.config.hasOwnProperty("renderOption") && typeof this.config.renderOption === "function"; this.customSelected = this.config.hasOwnProperty("renderSelection") && typeof this.config.renderSelection === "function"; this.supportsEventPassiveOption = this.detectEventPassiveOption(); // Enable event emitter Events.mixin(this); build.call(this); this.bindEvents(); this.update(); this.optsRect = util.rect(this.tree); this.rendered = true; // Fixes macOS Safari bug #28 if (!this.el.multiple) { this.el.selectedIndex = this.selectedIndex; } var that = this; setTimeout(function() { that.emit("selectr.init"); }, 20); }; Selectr.prototype.getSelected = function () { var selected = this.el.querySelectorAll('option:checked'); return selected; }; Selectr.prototype.getSelectedProperties = function (prop) { var selected = this.getSelected(); var values = [].slice.call(selected) .map(function(option) { return option[prop]; }) .filter(function(i) { return i!==null && i!==undefined; }); return values; }; /** * Feature detection: addEventListener passive option * https://dom.spec.whatwg.org/#dom-addeventlisteneroptions-passive * https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md */ Selectr.prototype.detectEventPassiveOption = function () { var supportsPassiveOption = false; try { var opts = Object.defineProperty({}, 'passive', { get: function() { supportsPassiveOption = true; } }); window.addEventListener('test', null, opts); } catch (e) {} return supportsPassiveOption; }; /** * Attach the required event listeners */ Selectr.prototype.bindEvents = function() { var that = this; this.events = {}; this.events.dismiss = dismiss.bind(this); this.events.navigate = navigate.bind(this); this.events.reset = this.reset.bind(this); if (this.config.nativeDropdown || this.mobileDevice) { this.container.addEventListener("touchstart", function(e) { if (e.changedTouches[0].target === that.el) { that.toggle(); } }, this.supportsEventPassiveOption ? { passive: true } : false); this.container.addEventListener("click", function(e) { if (e.target === that.el) { that.toggle(); } }); var getChangedOptions = function(last, current) { var added=[], removed=last.slice(0); var idx; for (var i=0; i<current.length; i++) { idx = removed.indexOf(current[i]); if (idx > -1) removed.splice(idx, 1); else added.push(current[i]); } return [added, removed]; }; // Listen for the change on the native select // and update accordingly this.el.addEventListener("change", function(e) { if (e.__selfTriggered) { return; } if (that.el.multiple) { var indexes = that.getSelectedProperties('idx'); var changes = getChangedOptions(that.selectedIndexes, indexes); util.each(changes[0], function(i, idx) { that.select(idx); }, that); util.each(changes[1], function(i, idx) { that.deselect(idx); }, that); } else { if (that.el.selectedIndex > -1) { that.select(that.el.selectedIndex); } } }); } // Open the dropdown with Enter key if focused if ( this.config.nativeDropdown ) { this.container.addEventListener("keydown", function(e) { if (e.key === "Enter" && that.selected === document.activeElement) { // show native dropdown that.toggle(); // focus on it setTimeout(function() { that.el.focus(); }, 200); } }); } // Non-native dropdown this.selected.addEventListener("click", function(e) { if (!that.disabled) { that.toggle(); } e.preventDefault(); }); if ( this.config.nativeKeyboard ) { var typing = ''; var typingTimeout = null; this.selected.addEventListener("keydown", function (e) { // Do nothing if disabled, not focused, or modifier keys are pressed if ( that.disabled || that.selected !== document.activeElement || (e.altKey || e.ctrlKey || e.metaKey) ) { return; } // Open the dropdown on [enter], [ ], [↓], and [↑] keys if ( e.key === " " || (! that.opened && ["Enter", "ArrowUp", "ArrowDown"].indexOf(e.key) > -1) ) { that.toggle(); e.preventDefault(); e.stopPropagation(); return; } // Type to search if multiple; type to select otherwise // make sure e.key is a single, printable character // .length check is a short-circut to skip checking keys like "ArrowDown", etc. // prefer "codePoint" methods; they work with the full range of unicode if ( e.key.length <= 2 && String[String.fromCodePoint ? "fromCodePoint" : "fromCharCode"]( e.key[String.codePointAt ? "codePointAt" : "charCodeAt"]( 0 ) ) === e.key ) { if ( that.config.multiple ) { that.open(); if ( that.config.searchable ) { that.input.value = e.key; that.input.focus(); that.search( null, true ); } } else { if ( typingTimeout ) { clearTimeout( typingTimeout ); } typing += e.key; var found = that.search( typing, true ); if ( found && found.length ) { that.clear(); that.setValue( found[0].value ); } setTimeout(function () { typing = ''; }, 1000); } e.preventDefault(); e.stopPropagation(); return; } }); // Close the dropdown on [esc] key this.container.addEventListener("keyup", function (e) { if ( that.opened && e.key === "Escape" ) { that.close(); e.stopPropagation(); // keep focus so we can re-open easily if desired that.selected.focus(); } }); } // Remove tag this.label.addEventListener("click", function(e) { if (util.hasClass(e.target, "selectr-tag-remove")) { that.deselect(e.target.parentNode.idx); } }); // Clear input if (this.selectClear) { this.selectClear.addEventListener("click", this.clear.bind(this)); } // Prevent text selection this.tree.addEventListener("mousedown", function(e) { e.preventDefault(); }); // Select / deselect items this.tree.addEventListener("click", function(e) { var item = util.closest(e.target, function(el) { return el && util.hasClass(el, "selectr-option"); }); if (item) { if (!util.hasClass(item, "disabled")) { if (util.hasClass(item, "selected")) { if (that.el.multiple || !that.el.multiple && that.config.allowDeselect) { that.deselect(item.idx); } } else { that.select(item.idx); } if (that.opened && !that.el.multiple) { that.close(); } } } e.preventDefault(); e.stopPropagation(); }); // Mouseover list items this.tree.addEventListener("mouseover", function(e) { if (util.hasClass(e.target, "selectr-option")) { if (!util.hasClass(e.target, "disabled")) { util.removeClass(that.items[that.navIndex], "active"); util.addClass(e.target, "active"); that.navIndex = [].slice.call(that.items).indexOf(e.target); } } }); // Searchable if (this.config.searchable) { // Show / hide the search input clear button this.input.addEventListener("focus", function(e) { that.searching = true; }); this.input.addEventListener("blur", function(e) { that.searching = false; }); this.input.addEventListener("keyup", function(e) { that.search(); if (!that.config.taggable) { // Show / hide the search input clear button if (this.value.length) { util.addClass(this.parentNode, "active"); } else { util.removeClass(this.parentNode, "active"); } } }); // Clear the search input this.inputClear.addEventListener("click", function(e) { that.input.value = null; clearSearch.call(that); if (!that.tree.childElementCount) { render.call(that); } }); } if (this.config.taggable) { this.input.addEventListener("keyup", function(e) { that.search(); if (that.config.taggable && this.value.length) { var _sVal = this.value.trim(); if (_sVal.length && (e.which === 13 || that.tagSeperatorsRegex.test(_sVal) )) { var _sGrabbedTagValue = _sVal.replace(that.tagSeperatorsRegex, ''); _sGrabbedTagValue = util.escapeRegExp(_sGrabbedTagValue); _sGrabbedTagValue = _sGrabbedTagValue.trim(); var _oOption; if(_sGrabbedTagValue.length){ _oOption = that.add({ value: _sGrabbedTagValue, textContent: _sGrabbedTagValue, selected: true }, true); } if(_oOption){ that.close(); clearSearch.call(that); } else { this.value = ''; that.setMessage(that.config.messages.tagDuplicate); } } } }); } this.update = util.debounce(function() { // Optionally close dropdown on scroll / resize (#11) if (that.opened && that.config.closeOnScroll) { that.close(); } if (that.width) { that.container.style.width = that.width; } that.invert(); }, 50); if (this.requiresPagination) { this.paginateItems = util.debounce(function() { load.call(this); }, 50); this.tree.addEventListener("scroll", this.paginateItems.bind(this)); } // Dismiss when clicking outside the container document.addEventListener("click", this.events.dismiss); window.addEventListener("keydown", this.events.navigate); window.addEventListener("resize", this.update); window.addEventListener("scroll", this.update); // remove event listeners on destroy() this.on('selectr.destroy', function () { document.removeEventListener("click", this.events.dismiss); window.removeEventListener("keydown", this.events.navigate); window.removeEventListener("resize", this.update); window.removeEventListener("scroll", this.update); }); // Listen for form.reset() (@ambrooks, #13) if (this.el.form) { this.el.form.addEventListener("reset", this.events.reset); // remove listener on destroy() this.on('selectr.destroy', function () { this.el.form.removeEventListener("reset", this.events.reset); }); } }; /** * Check for selected options * @param {bool} reset */ Selectr.prototype.setSelected = function(reset) { // Select first option as with a native select-one element - #21, #24 if (!this.config.data && !this.el.multiple && this.el.options.length) { // Browser has selected the first option by default if (this.el.selectedIndex === 0) { if (!this.el.options[0].defaultSelected && !this.config.defaultSelected) { this.el.selectedIndex = -1; } } this.selectedIndex = this.el.selectedIndex; if (this.selectedIndex > -1) { this.select(this.selectedIndex); } } // If we're changing a select-one to select-multiple via the config // and there are no selected options, the first option will be selected by the browser // Let's prevent that here. if (this.config.multiple &&