mobius1-selectr
Version:
A lightweight, dependency-free, mobile-friendly javascript select box replacement.
1,517 lines (1,270 loc) • 76.3 kB
JavaScript
/*!
* 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 &&