From f2dec06d4e3e937abd2cae71d093732683c88857 Mon Sep 17 00:00:00 2001 From: Mobius1 Date: Wed, 20 Sep 2017 13:04:15 +0100 Subject: [PATCH] v2.3.9, fixes #32 --- bower.json | 2 +- dist/selectr.min.css | 2 +- dist/selectr.min.js | 4 +- package.json | 2 +- src/selectr.css | 2 +- src/selectr.js | 4178 +++++++++++++++++++++--------------------- 6 files changed, 2118 insertions(+), 2072 deletions(-) diff --git a/bower.json b/bower.json index 762d872..9bcdcb9 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "mobius1-selectr", - "version": "2.3.8", + "version": "2.3.9", "ignore": [ ".gitattributes", "README.md" diff --git a/dist/selectr.min.css b/dist/selectr.min.css index ad618c8..1761fe3 100644 --- a/dist/selectr.min.css +++ b/dist/selectr.min.css @@ -1,5 +1,5 @@ /*! - * Selectr 2.3.8 + * Selectr 2.3.9 * http://mobius.ovh/docs/selectr * * Released under the MIT license diff --git a/dist/selectr.min.js b/dist/selectr.min.js index 180c37d..bc69fa0 100644 --- a/dist/selectr.min.js +++ b/dist/selectr.min.js @@ -1,7 +1,7 @@ /*! - * Selectr 2.3.8 + * Selectr 2.3.9 * http://mobius.ovh/docs/selectr * * Released under the MIT license */ -!function(e,t){"function"==typeof define&&define.amd?define([],t("Selectr")):"object"==typeof exports?module.exports=t("Selectr"):e.Selectr=t("Selectr")}(this,function(e){"use strict";function t(e,t){return e.hasOwnProperty(t)&&(!0===e[t]||e[t].length)}function i(e,t,i){e.parentNode?e.parentNode.parentNode||t.appendChild(e.parentNode):t.appendChild(e),a.removeClass(e,"excluded"),i||(e.innerHTML=e.textContent)}var s={defaultSelected:!0,width:"auto",disabled:!1,searchable:!0,clearable:!1,sortSelected:!1,allowDeselect:!1,closeOnScroll:!1,nativeDropdown:!1,placeholder:"Select an option...",taggable:!1,tagPlaceholder:"Enter a tag..."},n=function(){};n.prototype={on:function(e,t){this._events=this._events||{},this._events[e]=this._events[e]||[],this._events[e].push(t)},off:function(e,t){this._events=this._events||{},e in this._events!=!1&&this._events[e].splice(this._events[e].indexOf(t),1)},emit:function(e){if(this._events=this._events||{},e in this._events!=!1)for(var t=0;t-1},truncate:function(e){for(;e.firstChild;)e.removeChild(e.firstChild)}},l=function(){if(this.items.length){var e=document.createDocumentFragment();if(this.config.pagination){var t=this.pages.slice(0,this.pageIndex);a.each(t,function(t,s){a.each(s,function(t,s){i(s,e,this.customOption)},this)},this)}else a.each(this.items,function(t,s){i(s,e,this.customOption)},this);e.childElementCount&&(a.removeClass(this.items[this.navIndex],"active"),this.navIndex=e.querySelector(".selectr-option").idx,a.addClass(this.items[this.navIndex],"active")),this.tree.appendChild(e)}},o=function(e){var t=e.target;this.container.contains(t)||!this.opened&&!a.hasClass(this.container,"notice")||this.close()},h=function(e,t){t=t||e;var i=this.customOption?this.config.renderOption(t):e.textContent,s=a.createElement("li",{class:"selectr-option",html:i,role:"treeitem","aria-selected":!1});return s.idx=e.idx,this.items.push(s),e.defaultSelected&&this.defaultSelected.push(e.idx),e.disabled&&(s.disabled=!0,a.addClass(s,"disabled")),s},c=function(){this.requiresPagination=this.config.pagination&&this.config.pagination>0,t(this.config,"width")&&(a.isInt(this.config.width)?this.width=this.config.width+"px":"auto"===this.config.width?this.width="100%":a.includes(this.config.width,"%")&&(this.width=this.config.width)),this.container=a.createElement("div",{class:"selectr-container"}),this.config.customClass&&a.addClass(this.container,this.config.customClass),this.mobileDevice?a.addClass(this.container,"selectr-mobile"):a.addClass(this.container,"selectr-desktop"),this.el.tabIndex=-1,this.config.nativeDropdown||this.mobileDevice?a.addClass(this.el,"selectr-visible"):a.addClass(this.el,"selectr-hidden"),this.selected=a.createElement("div",{class:"selectr-selected",disabled:this.disabled,tabIndex:1,"aria-expanded":!1}),this.label=a.createElement(this.el.multiple?"ul":"span",{class:"selectr-label"});var e=a.createElement("div",{class:"selectr-options-container"});if(this.tree=a.createElement("ul",{class:"selectr-options",role:"tree","aria-hidden":!0,"aria-expanded":!1}),this.notice=a.createElement("div",{class:"selectr-notice"}),this.el.setAttribute("aria-hidden",!0),this.disabled&&(this.el.disabled=!0),this.el.multiple&&(a.addClass(this.label,"selectr-tags"),a.addClass(this.container,"multiple"),this.tags=[],this.selectedValues=[],this.selectedIndexes=[]),this.selected.appendChild(this.label),this.config.clearable&&(this.selectClear=a.createElement("button",{class:"selectr-clear",type:"button"}),this.container.appendChild(this.selectClear),a.addClass(this.container,"clearable")),this.config.taggable){var i=a.createElement("li",{class:"input-tag"});this.input=a.createElement("input",{class:"selectr-tag-input",placeholder:this.config.tagPlaceholder,tagIndex:0,autocomplete:"off",autocorrect:"off",autocapitalize:"off",spellcheck:"false",role:"textbox",type:"search"}),i.appendChild(this.input),this.label.appendChild(i),a.addClass(this.container,"taggable"),this.tagSeperators=[","],this.config.tagSeperators&&(this.tagSeperators=this.tagSeperators.concat(this.config.tagSeperators))}this.config.searchable&&(this.input=a.createElement("input",{class:"selectr-input",tagIndex:-1,autocomplete:"off",autocorrect:"off",autocapitalize:"off",spellcheck:"false",role:"textbox",type:"search"}),this.inputClear=a.createElement("button",{class:"selectr-input-clear",type:"button"}),this.inputContainer=a.createElement("div",{class:"selectr-input-container"}),this.inputContainer.appendChild(this.input),this.inputContainer.appendChild(this.inputClear),e.appendChild(this.inputContainer)),e.appendChild(this.notice),e.appendChild(this.tree),this.items=[],this.options=[],this.el.options.length&&(this.options=[].slice.call(this.el.options));var s=!1,n=0;if(this.el.children.length&&a.each(this.el.children,function(e,t){"OPTGROUP"===t.nodeName?(s=a.createElement("ul",{class:"selectr-optgroup",role:"group",html:"
  • "+t.label+"
  • "}),a.each(t.children,function(e,t){t.idx=n,s.appendChild(h.call(this,t,s)),n++},this)):(t.idx=n,h.call(this,t),n++)},this),this.config.data&&Array.isArray(this.config.data)){this.data=[];var l,o=!1;s=!1,n=0,a.each(this.config.data,function(e,i){t(i,"children")?(o=a.createElement("optgroup",{label:i.text}),s=a.createElement("ul",{class:"selectr-optgroup",role:"group",html:"
  • "+i.text+"
  • "}),a.each(i.children,function(e,i){(l=new Option(i.text,i.value,!1,i.hasOwnProperty("selected")&&!0===i.selected)).disabled=t(i,"disabled"),this.options.push(l),o.appendChild(l),l.idx=n,s.appendChild(h.call(this,l,i)),this.data[n]=i,n++},this)):((l=new Option(i.text,i.value,!1,i.hasOwnProperty("selected")&&!0===i.selected)).disabled=t(i,"disabled"),this.options.push(l),l.idx=n,h.call(this,l,i),this.data[n]=i,n++)},this)}this.setSelected(!0);var c;this.navIndex=0;for(var r=0;r0)&&this.change(this.navIndex);var t,i=this.items[this.navIndex];switch(e.which){case 38:t=0,this.navIndex>0&&this.navIndex--;break;case 40:t=1,this.navIndexthis.tree.lastElementChild.idx){this.navIndex=this.tree.lastElementChild.idx;break}if(this.navIndexthis.optsRect.top+this.optsRect.height&&(this.tree.scrollTop=this.tree.scrollTop+(s.top+s.height-(this.optsRect.top+this.optsRect.height))),this.navIndex===this.tree.childElementCount-1&&this.requiresPagination&&u.call(this)):0===this.navIndex?this.tree.scrollTop=0:s.top-this.optsRect.top<0&&(this.tree.scrollTop=this.tree.scrollTop+(s.top-this.optsRect.top)),i&&a.removeClass(i,"active"),a.addClass(this.items[this.navIndex],"active")}else this.navigating=!1},d=function(e){var t,i=this,s=document.createDocumentFragment(),n=this.options[e.idx],l=this.data?this.data[e.idx]:n,o=this.customSelected?this.config.renderSelection(l):n.textContent,h=a.createElement("li",{class:"selectr-tag",html:o}),c=a.createElement("button",{class:"selectr-tag-remove",type:"button"});if(h.appendChild(c),h.idx=e.idx,h.tag=n.value,this.tags.push(h),this.config.sortSelected){var r=this.tags.slice();t=function(e,t){e.replace(/(\d+)|(\D+)/g,function(e,i,s){t.push([i||1/0,s||""])})},r.sort(function(e,s){var n,a,l=[],o=[];for(!0===i.config.sortSelected?(n=e.tag,a=s.tag):"text"===i.config.sortSelected&&(n=e.textContent,a=s.textContent),t(n,l),t(a,o);l.length&&o.length;){var h=l.shift(),c=o.shift(),r=h[0]-c[0]||h[1].localeCompare(c[1]);if(r)return r}return l.length-o.length}),a.each(r,function(e,t){s.appendChild(t)}),this.label.innerHTML=""}else s.appendChild(h);this.config.taggable?this.label.insertBefore(s,this.input.parentNode):this.label.appendChild(s)},p=function(e){var t=!1;a.each(this.tags,function(i,s){s.idx===e.idx&&(t=s)},this),t&&(this.label.removeChild(t),this.tags.splice(this.tags.indexOf(t),1))},u=function(){var e=this.tree;if(e.scrollTop>=e.scrollHeight-e.offsetHeight&&this.pageIndex"+i[0]+"")},v=function(e,t){if(t=t||{},!e)throw new Error("You must supply either a HTMLSelectElement or a CSS3 selector string.");if(this.el=e,"string"==typeof e&&(this.el=document.querySelector(e)),null===this.el)throw new Error("The element you passed to Selectr can not be found.");if("select"!==this.el.nodeName.toLowerCase())throw new Error("The element you passed to Selectr is not a HTMLSelectElement.");this.render(t)};return v.prototype.render=function(e){if(!this.rendered){this.config=a.extend(s,e),this.originalType=this.el.type,this.originalIndex=this.el.tabIndex,this.defaultSelected=[],this.originalOptionCount=this.el.options.length,(this.config.multiple||this.config.taggable)&&(this.el.multiple=!0),this.disabled=t(this.config,"disabled"),this.opened=!1,this.config.taggable&&(this.config.searchable=!1),this.navigating=!1,this.mobileDevice=!1,/Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(navigator.userAgent)&&(this.mobileDevice=!0),this.customOption=this.config.hasOwnProperty("renderOption")&&"function"==typeof this.config.renderOption,this.customSelected=this.config.hasOwnProperty("renderSelection")&&"function"==typeof this.config.renderSelection,n.mixin(this),c.call(this),this.bindEvents(),this.update(),this.optsRect=a.rect(this.tree),this.rendered=!0,this.el.multiple||(this.el.selectedIndex=this.selectedIndex);var i=this;setTimeout(function(){i.emit("selectr.init")},20)}},v.prototype.bindEvents=function(){var e=this;this.events={},this.events.dismiss=o.bind(this),this.events.navigate=r.bind(this),this.events.reset=this.reset.bind(this),(this.config.nativeDropdown||this.mobileDevice)&&(this.container.addEventListener("touchstart",function(t){t.changedTouches[0].target===e.el&&e.toggle()}),(this.config.nativeDropdown||this.mobileDevice)&&this.container.addEventListener("click",function(t){t.target===e.el&&e.toggle()}),this.el.addEventListener("change",function(t){if(!e.opened)return e.open(),!1;if(e.el.multiple){var i=e.el.querySelectorAll("option:checked"),s=[].slice.call(i).map(function(e){return e.idx});e.clear(),a.each(s,function(t,i){e.select(i)},e)}else e.el.selectedIndex>-1&&e.select(e.el.selectedIndex)})),this.config.nativeDropdown&&this.container.addEventListener("keydown",function(t){"Enter"===t.key&&e.selected===document.activeElement&&(e.toggle(),setTimeout(function(){e.el.focus()},200))}),this.selected.addEventListener("click",function(t){e.disabled||e.toggle(),t.preventDefault()}),this.label.addEventListener("click",function(t){a.hasClass(t.target,"selectr-tag-remove")&&e.deselect(t.target.parentNode.idx)}),this.selectClear&&this.selectClear.addEventListener("click",this.clear.bind(this)),this.tree.addEventListener("mousedown",function(e){e.preventDefault()}),this.tree.addEventListener("click",function(t){var i=a.closest(t.target,function(e){return e&&a.hasClass(e,"selectr-option")});i&&(a.hasClass(i,"disabled")||(a.hasClass(i,"selected")?(e.el.multiple||!e.el.multiple&&e.config.allowDeselect)&&e.deselect(i.idx):e.select(i.idx),e.opened&&!e.el.multiple&&e.close()))}),this.tree.addEventListener("mouseover",function(t){a.hasClass(t.target,"selectr-option")&&(a.hasClass(t.target,"disabled")||(a.removeClass(e.items[e.navIndex],"active"),a.addClass(t.target,"active"),e.navIndex=[].slice.call(e.items).indexOf(t.target)))}),this.config.searchable&&(this.input.addEventListener("focus",function(t){e.searching=!0}),this.input.addEventListener("blur",function(t){e.searching=!1}),this.input.addEventListener("keyup",function(t){e.search(),e.config.taggable||(this.value.length?a.addClass(this.parentNode,"active"):a.removeClass(this.parentNode,"active"))}),this.inputClear.addEventListener("click",function(t){e.input.value=null,f.call(e),e.tree.childElementCount||l.call(e)})),this.config.taggable&&this.input.addEventListener("keyup",function(t){if(e.search(),e.config.taggable&&this.value.length){var i=this.value.trim();(13===t.which||a.includes(e.tagSeperators,t.key))&&(a.each(e.tagSeperators,function(e,t){i=i.replace(t,"")}),e.add({value:i,text:i,selected:!0},!0)?(e.close(),f.call(e)):(this.value="",e.setMessage("That tag is already in use.")))}}),this.update=a.debounce(function(){e.opened&&e.config.closeOnScroll&&e.close(),e.width&&(e.container.style.width=e.width),e.invert()},50),this.requiresPagination&&(this.paginateItems=a.debounce(function(){u.call(this)},50),this.tree.addEventListener("scroll",this.paginateItems.bind(this))),document.addEventListener("click",this.events.dismiss),window.addEventListener("keydown",this.events.navigate),window.addEventListener("resize",this.update),window.addEventListener("scroll",this.update),this.el.form&&this.el.form.addEventListener("reset",this.events.reset)},v.prototype.setSelected=function(e){if(this.config.data||this.el.multiple||!this.el.options.length||(0===this.el.selectedIndex&&(this.el.options[0].defaultSelected||this.config.defaultSelected||(this.el.selectedIndex=-1)),this.selectedIndex=this.el.selectedIndex),this.config.multiple&&"select-one"===this.originalType&&!this.config.data&&this.el.options[0].selected&&!this.el.options[0].defaultSelected&&(this.el.options[0].selected=!1),a.each(this.options,function(e,t){t.selected&&t.defaultSelected&&this.select(t.idx)},this),this.config.selectedValue&&this.setValue(this.config.selectedValue),this.config.data){!this.el.multiple&&this.config.defaultSelected&&this.el.selectedIndex<0&&this.select(0);var i=0;a.each(this.config.data,function(e,s){t(s,"children")?a.each(s.children,function(e,t){t.hasOwnProperty("selected")&&!0===t.selected&&this.select(i),i++},this):(s.hasOwnProperty("selected")&&!0===s.selected&&this.select(i),i++)},this)}},v.prototype.destroy=function(){this.rendered&&(this.emit("selectr.destroy"),"select-one"===this.originalType&&(this.el.multiple=!1),this.config.data&&(this.el.innerHTML=""),a.removeClass(this.el,"selectr-hidden"),this.el.form&&a.off(this.el.form,"reset",this.events.reset),a.off(document,"click",this.events.dismiss),a.off(document,"keydown",this.events.navigate),a.off(window,"resize",this.update),a.off(window,"scroll",this.update),this.container.parentNode.replaceChild(this.el,this.container),this.rendered=!1)},v.prototype.change=function(e){var t=this.items[e],i=this.options[e];i.disabled||(i.selected&&a.hasClass(t,"selected")?this.deselect(e):this.select(e),this.opened&&!this.el.multiple&&this.close())},v.prototype.select=function(e){var t=this.items[e],i=[].slice.call(this.el.options),s=this.options[e];if(this.el.multiple){if(a.includes(this.selectedIndexes,e))return!1;if(this.config.maxSelections&&this.tags.length===this.config.maxSelections)return this.setMessage("A maximum of "+this.config.maxSelections+" items can be selected.",!0),!1;this.selectedValues.push(s.value),this.selectedIndexes.push(e),d.call(this,t)}else{var n=this.data?this.data[e]:s;this.label.innerHTML=this.customSelected?this.config.renderSelection(n):s.textContent,this.selectedValue=s.value,this.selectedIndex=e,a.each(this.options,function(t,i){var s=this.items[t];t!==e&&(s&&a.removeClass(s,"selected"),i.selected=!1,i.removeAttribute("selected"))},this)}a.includes(i,s)||this.el.add(s),t.setAttribute("aria-selected",!0),a.addClass(t,"selected"),a.addClass(this.container,"has-selected"),s.selected=!0,s.setAttribute("selected",""),this.emit("selectr.change",s),this.emit("selectr.select",s)},v.prototype.deselect=function(e,t){var i=this.items[e],s=this.options[e];if(this.el.multiple){var n=this.selectedIndexes.indexOf(e);this.selectedIndexes.splice(n,1);var l=this.selectedValues.indexOf(s.value);this.selectedValues.splice(l,1),p.call(this,i),this.tags.length||a.removeClass(this.container,"has-selected")}else{if(!t&&!this.config.clearable&&!this.config.allowDeselect)return!1;this.label.innerHTML="",this.selectedValue=null,this.el.selectedIndex=this.selectedIndex=-1,a.removeClass(this.container,"has-selected")}this.items[e].setAttribute("aria-selected",!1),a.removeClass(this.items[e],"selected"),s.selected=!1,s.removeAttribute("selected"),this.emit("selectr.change",null),this.emit("selectr.deselect",s)},v.prototype.setValue=function(e){var t=Array.isArray(e);if(t||(e=e.toString().trim()),!this.el.multiple&&t)return!1;a.each(this.options,function(i,s){(t&&a.includes(e.toString(),s.value)||s.value===e)&&this.change(s.idx)},this)},v.prototype.getValue=function(e,t){var i;if(this.el.multiple)e?this.selectedIndexes.length&&((i={}).values=[],a.each(this.selectedIndexes,function(e,t){var s=this.options[t];i.values[e]={value:s.value,text:s.textContent}},this)):i=this.selectedValues.slice();else if(e){var s=this.options[this.selectedIndex];i={value:s.value,text:s.textContent}}else i=this.selectedValue;return e&&t&&(i=JSON.stringify(i)),i},v.prototype.add=function(e,t){if(e){if(this.data=this.data||[],this.items=this.items||[],this.options=this.options||[],Array.isArray(e))a.each(e,function(e,i){this.add(i,t)},this);else if("[object Object]"===Object.prototype.toString.call(e)){if(t){var i=!1;if(a.each(this.options,function(t,s){s.value.toLowerCase()===e.value.toLowerCase()&&(i=!0)}),i)return!1}var s=a.createElement("option",e);return this.data.push(e),this.options.push(s),s.idx=this.options.length>0?this.options.length-1:0,h.call(this,s),e.selected&&this.select(s.idx),s}return this.setPlaceholder(),this.config.pagination&&this.paginate(),!0}},v.prototype.remove=function(e){var t=[];if(Array.isArray(e)?a.each(e,function(i,s){a.isInt(s)?t.push(this.getOptionByIndex(s)):"string"==typeof e&&t.push(this.getOptionByValue(s))},this):a.isInt(e)?t.push(this.getOptionByIndex(e)):"string"==typeof e&&t.push(this.getOptionByValue(e)),t.length){var i;a.each(t,function(e,t){i=t.idx,this.el.remove(t),this.options.splice(i,1);var s=this.items[i].parentNode;s&&s.removeChild(this.items[i]),this.items.splice(i,1),a.each(this.options,function(e,t){t.idx=e,this.items[e].idx=e},this)},this),this.setPlaceholder(),this.config.pagination&&this.paginate()}},v.prototype.removeAll=function(){this.clear(!0),a.each(this.el.options,function(e,t){this.el.remove(t)},this),a.truncate(this.tree),this.items=[],this.options=[],this.data=[],this.navIndex=0,this.requiresPagination&&(this.requiresPagination=!1,this.pageIndex=1,this.pages=[]),this.setPlaceholder()},v.prototype.search=function(e){if(!this.navigating){e=e||this.input.value;var t=document.createDocumentFragment();if(this.removeMessage(),a.truncate(this.tree),e.length>1)if(a.each(this.options,function(s,n){var l=this.items[n.idx];a.includes(n.textContent.toLowerCase(),e.toLowerCase())&&!n.disabled?(i(l,t,this.customOption),a.removeClass(l,"excluded"),this.customOption||(l.innerHTML=g(e,n))):a.addClass(l,"excluded")},this),t.childElementCount){var s=this.items[this.navIndex],n=t.firstElementChild;a.removeClass(s,"active"),this.navIndex=n.idx,a.addClass(n,"active")}else this.config.taggable||this.setMessage("no results.");else l.call(this);this.tree.appendChild(t)}},v.prototype.toggle=function(){this.disabled||(this.opened?this.close():this.open())},v.prototype.open=function(){var e=this;return!!this.options.length&&(this.opened||this.emit("selectr.open"),this.opened=!0,this.mobileDevice||this.config.nativeDropdown?(a.addClass(this.container,"native-open"),void(this.config.data&&a.each(this.options,function(e,t){this.el.add(t)},this))):(a.addClass(this.container,"open"),l.call(this),this.invert(),this.tree.scrollTop=0,a.removeClass(this.container,"notice"),this.selected.setAttribute("aria-expanded",!0),this.tree.setAttribute("aria-hidden",!1),this.tree.setAttribute("aria-expanded",!0),void(this.config.searchable&&!this.config.taggable&&setTimeout(function(){e.input.focus(),e.input.tabIndex=0},10))))},v.prototype.close=function(){if(this.opened&&this.emit("selectr.close"),this.opened=!1,this.mobileDevice||this.config.nativeDropdown)a.removeClass(this.container,"native-open");else{var e=a.hasClass(this.container,"notice");this.config.searchable&&!e&&(this.input.blur(),this.input.tabIndex=-1,this.searching=!1),e&&(a.removeClass(this.container,"notice"),this.notice.textContent=""),a.removeClass(this.container,"open"),a.removeClass(this.container,"native-open"),this.selected.setAttribute("aria-expanded",!1),this.tree.setAttribute("aria-hidden",!0),this.tree.setAttribute("aria-expanded",!1),a.truncate(this.tree),f.call(this)}},v.prototype.enable=function(){this.disabled=!1,this.el.disabled=!1,this.selected.tabIndex=this.originalIndex,this.el.multiple&&a.each(this.tags,function(e,t){t.lastElementChild.tabIndex=0}),a.removeClass(this.container,"selectr-disabled")},v.prototype.disable=function(e){e||(this.el.disabled=!0),this.selected.tabIndex=-1,this.el.multiple&&a.each(this.tags,function(e,t){t.lastElementChild.tabIndex=-1}),this.disabled=!0,a.addClass(this.container,"selectr-disabled")},v.prototype.reset=function(){this.disabled||(this.clear(),this.setSelected(!0),a.each(this.defaultSelected,function(e,t){this.select(t)},this),this.emit("selectr.reset"))},v.prototype.clear=function(e){if(this.el.multiple){if(this.selectedIndexes.length){var t=this.selectedIndexes.slice();a.each(t,function(e,t){this.deselect(t)},this)}}else this.selectedIndex>-1&&this.deselect(this.selectedIndex,e);this.emit("selectr.clear")},v.prototype.serialise=function(e){var t=[];return a.each(this.options,function(e,i){var s={value:i.value,text:i.textContent};i.selected&&(s.selected=!0),i.disabled&&(s.disabled=!0),t[e]=s}),e?JSON.stringify(t):t},v.prototype.serialize=function(e){return this.serialise(e)},v.prototype.setPlaceholder=function(e){e=e||this.config.placeholder||this.el.getAttribute("placeholder"),this.options.length||(e="No options available"),this.placeEl.innerHTML=e},v.prototype.paginate=function(){if(this.items.length){var e=this;return this.pages=this.items.map(function(t,i){return i%e.config.pagination==0?e.items.slice(i,i+e.config.pagination):null}).filter(function(e){return e}),this.pages}},v.prototype.setMessage=function(e,t){t&&this.close(),a.addClass(this.container,"notice"),this.notice.textContent=e},v.prototype.removeMessage=function(){a.removeClass(this.container,"notice"),this.notice.innerHTML=""},v.prototype.invert=function(){var e=a.rect(this.selected),t=this.tree.parentNode.offsetHeight,i=window.innerHeight;e.top+e.height+t>i?(a.addClass(this.container,"inverted"),this.isInverted=!0):(a.removeClass(this.container,"inverted"),this.isInverted=!1),this.optsRect=a.rect(this.tree)},v.prototype.getOptionByIndex=function(e){return this.options[e]},v.prototype.getOptionByValue=function(e){for(var t=!1,i=0,s=this.options.length;i-1},truncate:function(e){for(;e.firstChild;)e.removeChild(e.firstChild)}},l=function(){if(this.items.length){var e=document.createDocumentFragment();if(this.config.pagination){var t=this.pages.slice(0,this.pageIndex);a.each(t,function(t,s){a.each(s,function(t,s){i(s,e,this.customOption)},this)},this)}else a.each(this.items,function(t,s){i(s,e,this.customOption)},this);e.childElementCount&&(a.removeClass(this.items[this.navIndex],"active"),this.navIndex=e.querySelector(".selectr-option").idx,a.addClass(this.items[this.navIndex],"active")),this.tree.appendChild(e)}},o=function(e){var t=e.target;this.container.contains(t)||!this.opened&&!a.hasClass(this.container,"notice")||this.close()},h=function(e,t){t=t||e;var i=this.customOption?this.config.renderOption(t):e.textContent,s=a.createElement("li",{class:"selectr-option",html:i,role:"treeitem","aria-selected":!1});return s.idx=e.idx,this.items.push(s),e.defaultSelected&&this.defaultSelected.push(e.idx),e.disabled&&(s.disabled=!0,a.addClass(s,"disabled")),s},c=function(){this.requiresPagination=this.config.pagination&&this.config.pagination>0,t(this.config,"width")&&(a.isInt(this.config.width)?this.width=this.config.width+"px":"auto"===this.config.width?this.width="100%":a.includes(this.config.width,"%")&&(this.width=this.config.width)),this.container=a.createElement("div",{class:"selectr-container"}),this.config.customClass&&a.addClass(this.container,this.config.customClass),this.mobileDevice?a.addClass(this.container,"selectr-mobile"):a.addClass(this.container,"selectr-desktop"),this.el.tabIndex=-1,this.config.nativeDropdown||this.mobileDevice?a.addClass(this.el,"selectr-visible"):a.addClass(this.el,"selectr-hidden"),this.selected=a.createElement("div",{class:"selectr-selected",disabled:this.disabled,tabIndex:1,"aria-expanded":!1}),this.label=a.createElement(this.el.multiple?"ul":"span",{class:"selectr-label"});var e=a.createElement("div",{class:"selectr-options-container"});if(this.tree=a.createElement("ul",{class:"selectr-options",role:"tree","aria-hidden":!0,"aria-expanded":!1}),this.notice=a.createElement("div",{class:"selectr-notice"}),this.el.setAttribute("aria-hidden",!0),this.disabled&&(this.el.disabled=!0),this.el.multiple&&(a.addClass(this.label,"selectr-tags"),a.addClass(this.container,"multiple"),this.tags=[],this.selectedValues=[],this.selectedIndexes=[]),this.selected.appendChild(this.label),this.config.clearable&&(this.selectClear=a.createElement("button",{class:"selectr-clear",type:"button"}),this.container.appendChild(this.selectClear),a.addClass(this.container,"clearable")),this.config.taggable){var i=a.createElement("li",{class:"input-tag"});this.input=a.createElement("input",{class:"selectr-tag-input",placeholder:this.config.tagPlaceholder,tagIndex:0,autocomplete:"off",autocorrect:"off",autocapitalize:"off",spellcheck:"false",role:"textbox",type:"search"}),i.appendChild(this.input),this.label.appendChild(i),a.addClass(this.container,"taggable"),this.tagSeperators=[","],this.config.tagSeperators&&(this.tagSeperators=this.tagSeperators.concat(this.config.tagSeperators))}this.config.searchable&&(this.input=a.createElement("input",{class:"selectr-input",tagIndex:-1,autocomplete:"off",autocorrect:"off",autocapitalize:"off",spellcheck:"false",role:"textbox",type:"search"}),this.inputClear=a.createElement("button",{class:"selectr-input-clear",type:"button"}),this.inputContainer=a.createElement("div",{class:"selectr-input-container"}),this.inputContainer.appendChild(this.input),this.inputContainer.appendChild(this.inputClear),e.appendChild(this.inputContainer)),e.appendChild(this.notice),e.appendChild(this.tree),this.items=[],this.options=[],this.el.options.length&&(this.options=[].slice.call(this.el.options));var s=!1,n=0;if(this.el.children.length&&a.each(this.el.children,function(e,t){"OPTGROUP"===t.nodeName?(s=a.createElement("ul",{class:"selectr-optgroup",role:"group",html:"
  • "+t.label+"
  • "}),a.each(t.children,function(e,t){t.idx=n,s.appendChild(h.call(this,t,s)),n++},this)):(t.idx=n,h.call(this,t),n++)},this),this.config.data&&Array.isArray(this.config.data)){this.data=[];var l,o=!1;s=!1,n=0,a.each(this.config.data,function(e,i){t(i,"children")?(o=a.createElement("optgroup",{label:i.text}),s=a.createElement("ul",{class:"selectr-optgroup",role:"group",html:"
  • "+i.text+"
  • "}),a.each(i.children,function(e,i){(l=new Option(i.text,i.value,!1,i.hasOwnProperty("selected")&&!0===i.selected)).disabled=t(i,"disabled"),this.options.push(l),o.appendChild(l),l.idx=n,s.appendChild(h.call(this,l,i)),this.data[n]=i,n++},this)):((l=new Option(i.text,i.value,!1,i.hasOwnProperty("selected")&&!0===i.selected)).disabled=t(i,"disabled"),this.options.push(l),l.idx=n,h.call(this,l,i),this.data[n]=i,n++)},this)}this.setSelected(!0);var c;this.navIndex=0;for(var r=0;r0)&&this.change(this.navIndex);var t,i=this.items[this.navIndex];switch(e.which){case 38:t=0,this.navIndex>0&&this.navIndex--;break;case 40:t=1,this.navIndexthis.tree.lastElementChild.idx){this.navIndex=this.tree.lastElementChild.idx;break}if(this.navIndexthis.optsRect.top+this.optsRect.height&&(this.tree.scrollTop=this.tree.scrollTop+(s.top+s.height-(this.optsRect.top+this.optsRect.height))),this.navIndex===this.tree.childElementCount-1&&this.requiresPagination&&u.call(this)):0===this.navIndex?this.tree.scrollTop=0:s.top-this.optsRect.top<0&&(this.tree.scrollTop=this.tree.scrollTop+(s.top-this.optsRect.top)),i&&a.removeClass(i,"active"),a.addClass(this.items[this.navIndex],"active")}else this.navigating=!1},d=function(e){var t,i=this,s=document.createDocumentFragment(),n=this.options[e.idx],l=this.data?this.data[e.idx]:n,o=this.customSelected?this.config.renderSelection(l):n.textContent,h=a.createElement("li",{class:"selectr-tag",html:o}),c=a.createElement("button",{class:"selectr-tag-remove",type:"button"});if(h.appendChild(c),h.idx=e.idx,h.tag=n.value,this.tags.push(h),this.config.sortSelected){var r=this.tags.slice();t=function(e,t){e.replace(/(\d+)|(\D+)/g,function(e,i,s){t.push([i||1/0,s||""])})},r.sort(function(e,s){var n,a,l=[],o=[];for(!0===i.config.sortSelected?(n=e.tag,a=s.tag):"text"===i.config.sortSelected&&(n=e.textContent,a=s.textContent),t(n,l),t(a,o);l.length&&o.length;){var h=l.shift(),c=o.shift(),r=h[0]-c[0]||h[1].localeCompare(c[1]);if(r)return r}return l.length-o.length}),a.each(r,function(e,t){s.appendChild(t)}),this.label.innerHTML=""}else s.appendChild(h);this.config.taggable?this.label.insertBefore(s,this.input.parentNode):this.label.appendChild(s)},p=function(e){var t=!1;a.each(this.tags,function(i,s){s.idx===e.idx&&(t=s)},this),t&&(this.label.removeChild(t),this.tags.splice(this.tags.indexOf(t),1))},u=function(){var e=this.tree;if(e.scrollTop>=e.scrollHeight-e.offsetHeight&&this.pageIndex"+i[0]+"")},v=function(e,t){if(t=t||{},!e)throw new Error("You must supply either a HTMLSelectElement or a CSS3 selector string.");if(this.el=e,"string"==typeof e&&(this.el=document.querySelector(e)),null===this.el)throw new Error("The element you passed to Selectr can not be found.");if("select"!==this.el.nodeName.toLowerCase())throw new Error("The element you passed to Selectr is not a HTMLSelectElement.");this.render(t)};return v.prototype.render=function(e){if(!this.rendered){this.config=a.extend(s,e),this.originalType=this.el.type,this.originalIndex=this.el.tabIndex,this.defaultSelected=[],this.originalOptionCount=this.el.options.length,(this.config.multiple||this.config.taggable)&&(this.el.multiple=!0),this.disabled=t(this.config,"disabled"),this.opened=!1,this.config.taggable&&(this.config.searchable=!1),this.navigating=!1,this.mobileDevice=!1,/Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(navigator.userAgent)&&(this.mobileDevice=!0),this.customOption=this.config.hasOwnProperty("renderOption")&&"function"==typeof this.config.renderOption,this.customSelected=this.config.hasOwnProperty("renderSelection")&&"function"==typeof this.config.renderSelection,n.mixin(this),c.call(this),this.bindEvents(),this.update(),this.optsRect=a.rect(this.tree),this.rendered=!0,this.el.multiple||(this.el.selectedIndex=this.selectedIndex);var i=this;setTimeout(function(){i.emit("selectr.init")},20)}},v.prototype.bindEvents=function(){var e=this;this.events={},this.events.dismiss=o.bind(this),this.events.navigate=r.bind(this),this.events.reset=this.reset.bind(this),(this.config.nativeDropdown||this.mobileDevice)&&(this.container.addEventListener("touchstart",function(t){t.changedTouches[0].target===e.el&&e.toggle()}),(this.config.nativeDropdown||this.mobileDevice)&&this.container.addEventListener("click",function(t){t.target===e.el&&e.toggle()}),this.el.addEventListener("change",function(t){if(!e.opened)return e.open(),!1;if(e.el.multiple){var i=e.el.querySelectorAll("option:checked"),s=[].slice.call(i).map(function(e){return e.idx});e.clear(),a.each(s,function(t,i){e.select(i)},e)}else e.el.selectedIndex>-1&&e.select(e.el.selectedIndex)})),this.config.nativeDropdown&&this.container.addEventListener("keydown",function(t){"Enter"===t.key&&e.selected===document.activeElement&&(e.toggle(),setTimeout(function(){e.el.focus()},200))}),this.selected.addEventListener("click",function(t){e.disabled||e.toggle(),t.preventDefault()}),this.label.addEventListener("click",function(t){a.hasClass(t.target,"selectr-tag-remove")&&e.deselect(t.target.parentNode.idx)}),this.selectClear&&this.selectClear.addEventListener("click",this.clear.bind(this)),this.tree.addEventListener("mousedown",function(e){e.preventDefault()}),this.tree.addEventListener("click",function(t){var i=a.closest(t.target,function(e){return e&&a.hasClass(e,"selectr-option")});i&&(a.hasClass(i,"disabled")||(a.hasClass(i,"selected")?(e.el.multiple||!e.el.multiple&&e.config.allowDeselect)&&e.deselect(i.idx):e.select(i.idx),e.opened&&!e.el.multiple&&e.close()))}),this.tree.addEventListener("mouseover",function(t){a.hasClass(t.target,"selectr-option")&&(a.hasClass(t.target,"disabled")||(a.removeClass(e.items[e.navIndex],"active"),a.addClass(t.target,"active"),e.navIndex=[].slice.call(e.items).indexOf(t.target)))}),this.config.searchable&&(this.input.addEventListener("focus",function(t){e.searching=!0}),this.input.addEventListener("blur",function(t){e.searching=!1}),this.input.addEventListener("keyup",function(t){e.search(),e.config.taggable||(this.value.length?a.addClass(this.parentNode,"active"):a.removeClass(this.parentNode,"active"))}),this.inputClear.addEventListener("click",function(t){e.input.value=null,f.call(e),e.tree.childElementCount||l.call(e)})),this.config.taggable&&this.input.addEventListener("keyup",function(t){if(e.search(),e.config.taggable&&this.value.length){var i=this.value.trim();(13===t.which||a.includes(e.tagSeperators,t.key))&&(a.each(e.tagSeperators,function(e,t){i=i.replace(t,"")}),e.add({value:i,text:i,selected:!0},!0)?(e.close(),f.call(e)):(this.value="",e.setMessage("That tag is already in use.")))}}),this.update=a.debounce(function(){e.opened&&e.config.closeOnScroll&&e.close(),e.width&&(e.container.style.width=e.width),e.invert()},50),this.requiresPagination&&(this.paginateItems=a.debounce(function(){u.call(this)},50),this.tree.addEventListener("scroll",this.paginateItems.bind(this))),document.addEventListener("click",this.events.dismiss),window.addEventListener("keydown",this.events.navigate),window.addEventListener("resize",this.update),window.addEventListener("scroll",this.update),this.el.form&&this.el.form.addEventListener("reset",this.events.reset)},v.prototype.setSelected=function(e){if(this.config.data||this.el.multiple||!this.el.options.length||(0===this.el.selectedIndex&&(this.el.options[0].defaultSelected||this.config.defaultSelected||(this.el.selectedIndex=-1)),this.selectedIndex=this.el.selectedIndex,this.selectedIndex>-1&&this.select(this.selectedIndex)),this.config.multiple&&"select-one"===this.originalType&&!this.config.data&&this.el.options[0].selected&&!this.el.options[0].defaultSelected&&(this.el.options[0].selected=!1),a.each(this.options,function(e,t){t.selected&&t.defaultSelected&&this.select(t.idx)},this),this.config.selectedValue&&this.setValue(this.config.selectedValue),this.config.data){!this.el.multiple&&this.config.defaultSelected&&this.el.selectedIndex<0&&this.select(0);var i=0;a.each(this.config.data,function(e,s){t(s,"children")?a.each(s.children,function(e,t){t.hasOwnProperty("selected")&&!0===t.selected&&this.select(i),i++},this):(s.hasOwnProperty("selected")&&!0===s.selected&&this.select(i),i++)},this)}},v.prototype.destroy=function(){this.rendered&&(this.emit("selectr.destroy"),"select-one"===this.originalType&&(this.el.multiple=!1),this.config.data&&(this.el.innerHTML=""),a.removeClass(this.el,"selectr-hidden"),this.el.form&&a.off(this.el.form,"reset",this.events.reset),a.off(document,"click",this.events.dismiss),a.off(document,"keydown",this.events.navigate),a.off(window,"resize",this.update),a.off(window,"scroll",this.update),this.container.parentNode.replaceChild(this.el,this.container),this.rendered=!1)},v.prototype.change=function(e){var t=this.items[e],i=this.options[e];i.disabled||(i.selected&&a.hasClass(t,"selected")?this.deselect(e):this.select(e),this.opened&&!this.el.multiple&&this.close())},v.prototype.select=function(e){var t=this.items[e],i=[].slice.call(this.el.options),s=this.options[e];if(this.el.multiple){if(a.includes(this.selectedIndexes,e))return!1;if(this.config.maxSelections&&this.tags.length===this.config.maxSelections)return this.setMessage("A maximum of "+this.config.maxSelections+" items can be selected.",!0),!1;this.selectedValues.push(s.value),this.selectedIndexes.push(e),d.call(this,t)}else{var n=this.data?this.data[e]:s;this.label.innerHTML=this.customSelected?this.config.renderSelection(n):s.textContent,this.selectedValue=s.value,this.selectedIndex=e,a.each(this.options,function(t,i){var s=this.items[t];t!==e&&(s&&a.removeClass(s,"selected"),i.selected=!1,i.removeAttribute("selected"))},this)}a.includes(i,s)||this.el.add(s),t.setAttribute("aria-selected",!0),a.addClass(t,"selected"),a.addClass(this.container,"has-selected"),s.selected=!0,s.setAttribute("selected",""),this.emit("selectr.change",s),this.emit("selectr.select",s)},v.prototype.deselect=function(e,t){var i=this.items[e],s=this.options[e];if(this.el.multiple){var n=this.selectedIndexes.indexOf(e);this.selectedIndexes.splice(n,1);var l=this.selectedValues.indexOf(s.value);this.selectedValues.splice(l,1),p.call(this,i),this.tags.length||a.removeClass(this.container,"has-selected")}else{if(!t&&!this.config.clearable&&!this.config.allowDeselect)return!1;this.label.innerHTML="",this.selectedValue=null,this.el.selectedIndex=this.selectedIndex=-1,a.removeClass(this.container,"has-selected")}this.items[e].setAttribute("aria-selected",!1),a.removeClass(this.items[e],"selected"),s.selected=!1,s.removeAttribute("selected"),this.emit("selectr.change",null),this.emit("selectr.deselect",s)},v.prototype.setValue=function(e){var t=Array.isArray(e);if(t||(e=e.toString().trim()),!this.el.multiple&&t)return!1;a.each(this.options,function(i,s){(t&&a.includes(e.toString(),s.value)||s.value===e)&&this.change(s.idx)},this)},v.prototype.getValue=function(e,t){var i;if(this.el.multiple)e?this.selectedIndexes.length&&((i={}).values=[],a.each(this.selectedIndexes,function(e,t){var s=this.options[t];i.values[e]={value:s.value,text:s.textContent}},this)):i=this.selectedValues.slice();else if(e){var s=this.options[this.selectedIndex];i={value:s.value,text:s.textContent}}else i=this.selectedValue;return e&&t&&(i=JSON.stringify(i)),i},v.prototype.add=function(e,t){if(e){if(this.data=this.data||[],this.items=this.items||[],this.options=this.options||[],Array.isArray(e))a.each(e,function(e,i){this.add(i,t)},this);else if("[object Object]"===Object.prototype.toString.call(e)){if(t){var i=!1;if(a.each(this.options,function(t,s){s.value.toLowerCase()===e.value.toLowerCase()&&(i=!0)}),i)return!1}var s=a.createElement("option",e);return this.data.push(e),this.options.push(s),s.idx=this.options.length>0?this.options.length-1:0,h.call(this,s),e.selected&&this.select(s.idx),s}return this.setPlaceholder(),this.config.pagination&&this.paginate(),!0}},v.prototype.remove=function(e){var t=[];if(Array.isArray(e)?a.each(e,function(i,s){a.isInt(s)?t.push(this.getOptionByIndex(s)):"string"==typeof e&&t.push(this.getOptionByValue(s))},this):a.isInt(e)?t.push(this.getOptionByIndex(e)):"string"==typeof e&&t.push(this.getOptionByValue(e)),t.length){var i;a.each(t,function(e,t){i=t.idx,this.el.remove(t),this.options.splice(i,1);var s=this.items[i].parentNode;s&&s.removeChild(this.items[i]),this.items.splice(i,1),a.each(this.options,function(e,t){t.idx=e,this.items[e].idx=e},this)},this),this.setPlaceholder(),this.config.pagination&&this.paginate()}},v.prototype.removeAll=function(){this.clear(!0),a.each(this.el.options,function(e,t){this.el.remove(t)},this),a.truncate(this.tree),this.items=[],this.options=[],this.data=[],this.navIndex=0,this.requiresPagination&&(this.requiresPagination=!1,this.pageIndex=1,this.pages=[]),this.setPlaceholder()},v.prototype.search=function(e){if(!this.navigating){e=e||this.input.value;var t=document.createDocumentFragment();if(this.removeMessage(),a.truncate(this.tree),e.length>1)if(a.each(this.options,function(s,n){var l=this.items[n.idx];a.includes(n.textContent.toLowerCase(),e.toLowerCase())&&!n.disabled?(i(l,t,this.customOption),a.removeClass(l,"excluded"),this.customOption||(l.innerHTML=g(e,n))):a.addClass(l,"excluded")},this),t.childElementCount){var s=this.items[this.navIndex],n=t.firstElementChild;a.removeClass(s,"active"),this.navIndex=n.idx,a.addClass(n,"active")}else this.config.taggable||this.setMessage("no results.");else l.call(this);this.tree.appendChild(t)}},v.prototype.toggle=function(){this.disabled||(this.opened?this.close():this.open())},v.prototype.open=function(){var e=this;return!!this.options.length&&(this.opened||this.emit("selectr.open"),this.opened=!0,this.mobileDevice||this.config.nativeDropdown?(a.addClass(this.container,"native-open"),void(this.config.data&&a.each(this.options,function(e,t){this.el.add(t)},this))):(a.addClass(this.container,"open"),l.call(this),this.invert(),this.tree.scrollTop=0,a.removeClass(this.container,"notice"),this.selected.setAttribute("aria-expanded",!0),this.tree.setAttribute("aria-hidden",!1),this.tree.setAttribute("aria-expanded",!0),void(this.config.searchable&&!this.config.taggable&&setTimeout(function(){e.input.focus(),e.input.tabIndex=0},10))))},v.prototype.close=function(){if(this.opened&&this.emit("selectr.close"),this.opened=!1,this.mobileDevice||this.config.nativeDropdown)a.removeClass(this.container,"native-open");else{var e=a.hasClass(this.container,"notice");this.config.searchable&&!e&&(this.input.blur(),this.input.tabIndex=-1,this.searching=!1),e&&(a.removeClass(this.container,"notice"),this.notice.textContent=""),a.removeClass(this.container,"open"),a.removeClass(this.container,"native-open"),this.selected.setAttribute("aria-expanded",!1),this.tree.setAttribute("aria-hidden",!0),this.tree.setAttribute("aria-expanded",!1),a.truncate(this.tree),f.call(this)}},v.prototype.enable=function(){this.disabled=!1,this.el.disabled=!1,this.selected.tabIndex=this.originalIndex,this.el.multiple&&a.each(this.tags,function(e,t){t.lastElementChild.tabIndex=0}),a.removeClass(this.container,"selectr-disabled")},v.prototype.disable=function(e){e||(this.el.disabled=!0),this.selected.tabIndex=-1,this.el.multiple&&a.each(this.tags,function(e,t){t.lastElementChild.tabIndex=-1}),this.disabled=!0,a.addClass(this.container,"selectr-disabled")},v.prototype.reset=function(){this.disabled||(this.clear(),this.setSelected(!0),a.each(this.defaultSelected,function(e,t){this.select(t)},this),this.emit("selectr.reset"))},v.prototype.clear=function(e){if(this.el.multiple){if(this.selectedIndexes.length){var t=this.selectedIndexes.slice();a.each(t,function(e,t){this.deselect(t)},this)}}else this.selectedIndex>-1&&this.deselect(this.selectedIndex,e);this.emit("selectr.clear")},v.prototype.serialise=function(e){var t=[];return a.each(this.options,function(e,i){var s={value:i.value,text:i.textContent};i.selected&&(s.selected=!0),i.disabled&&(s.disabled=!0),t[e]=s}),e?JSON.stringify(t):t},v.prototype.serialize=function(e){return this.serialise(e)},v.prototype.setPlaceholder=function(e){e=e||this.config.placeholder||this.el.getAttribute("placeholder"),this.options.length||(e="No options available"),this.placeEl.innerHTML=e},v.prototype.paginate=function(){if(this.items.length){var e=this;return this.pages=this.items.map(function(t,i){return i%e.config.pagination==0?e.items.slice(i,i+e.config.pagination):null}).filter(function(e){return e}),this.pages}},v.prototype.setMessage=function(e,t){t&&this.close(),a.addClass(this.container,"notice"),this.notice.textContent=e},v.prototype.removeMessage=function(){a.removeClass(this.container,"notice"),this.notice.innerHTML=""},v.prototype.invert=function(){var e=a.rect(this.selected),t=this.tree.parentNode.offsetHeight,i=window.innerHeight;e.top+e.height+t>i?(a.addClass(this.container,"inverted"),this.isInverted=!0):(a.removeClass(this.container,"inverted"),this.isInverted=!1),this.optsRect=a.rect(this.tree)},v.prototype.getOptionByIndex=function(e){return this.options[e]},v.prototype.getOptionByValue=function(e){for(var t=!1,i=0,s=this.options.length;i -1; - }, - 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 ) { - item.innerHTML = 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); - } - - if ( f.childElementCount ) { - util.removeClass(this.items[this.navIndex], "active"); - this.navIndex = 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 content = this.customOption ? this.config.renderOption(data) : option.textContent; - var opt = util.createElement("li", { - class: "selectr-option", - html: content, - role: "treeitem", - "aria-selected": false - }); - - 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: 1, // enable tabIndex (#9) - "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 - this.selectedValues = []; - - // Collection of selected indexes - this.selectedIndexes = []; - } - - 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); - } - } - - 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" - }); - 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: "
  • "+element.label+"
  • " - }); - - 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: "
  • "+opt.text+"
  • " - }); - - 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); - } 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); +(function(root, factory) { + var plugin = "Selectr"; + + if (typeof define === "function" && define.amd) { + define([], factory(plugin)); + } else if (typeof exports === "object") { + module.exports = factory(plugin); + } else { + root[plugin] = factory(plugin); + } +}(this, function(plugin) { + 'use strict'; + + /** + * 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 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, + + /** + * 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..." + }; + + /** + * 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 = { + extend: function(src, props) { + props = props || {}; + var p; + for (p in src) { + if (src.hasOwnProperty(p)) { + if (!props.hasOwnProperty(p)) { + props[p] = src[p]; + } + } + } + return props; + }, + 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 if ("text" === i) { + var t = d.createTextNode(a[i]); + el.appendChild(t); + } 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; + }, + 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) { + item.innerHTML = 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); + } + + if (f.childElementCount) { + util.removeClass(this.items[this.navIndex], "active"); + this.navIndex = 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 content = this.customOption ? this.config.renderOption(data) : option.textContent; + var opt = util.createElement("li", { + class: "selectr-option", + html: content, + role: "treeitem", + "aria-selected": false + }); + + 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: 1, // enable tabIndex (#9) + "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 + this.selectedValues = []; + + // Collection of selected indexes + this.selectedIndexes = []; + } + + 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); + } + } + + 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" + }); + 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: "
  • " + element.label + "
  • " + }); + + 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: "
  • " + opt.text + "
  • " + }); + + 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); + } 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.config.taggable && this.input.value.length > 0) { + return false; + } + + return this.change(this.navIndex); + } + + var direction, prevEl = this.items[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 (direction) { + this.navIndex++; + } else { + this.navIndex--; + } + + 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 content = this.customSelected ? this.config.renderSelection(data) : option.textContent; + + var tag = util.createElement("li", { + class: "selectr-tag", + html: content + }); + 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) { + item.innerHTML = item.textContent; + } + }, this); + } + }; + + /** + * Query matching for searches + * @param {string} query + * @param {HTMLOptionElement} option + * @return {bool} + */ + var match = function(query, option) { + var result = new RegExp(query, "i").exec(option.textContent); + if (result) { + return option.textContent.replace(result[0], "" + result[0] + ""); + } + return false; + }; + + // Main Lib + var Selectr = function(el, config) { + + config = 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; + + // 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 (/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"; + + // Enable event emitter + Events.mixin(this); + + build.call(this); + + this.bindEvents(); - this.data[j] = opt; + this.update(); - j++; - } - }, this); - } + this.optsRect = util.rect(this.tree); - this.setSelected(true); + this.rendered = true; - var first; - this.navIndex = 0; - for ( var i = 0; i < this.items.length; i++ ) { - first = this.items[i]; + // Fixes macOS Safari bug #28 + if (!this.el.multiple) { + this.el.selectedIndex = this.selectedIndex; + } - if ( !util.hasClass(first, "disabled") ) { + var that = this; + setTimeout(function() { + that.emit("selectr.init"); + }, 20); + }; - 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 ) { + /** + * Attach the required event listeners + */ + Selectr.prototype.bindEvents = function() { - if ( this.config.taggable && this.input.value.length > 0 ) { - return false; - } + var that = this; - return this.change(this.navIndex); - } + this.events = {}; - var direction, prevEl = this.items[this.navIndex]; + this.events.dismiss = dismiss.bind(this); + this.events.navigate = navigate.bind(this); + this.events.reset = this.reset.bind(this); - 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 ( direction ) { - this.navIndex++; - } else { - this.navIndex--; - } - - 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 content = this.customSelected ? this.config.renderSelection(data) : option.textContent; - - var tag = util.createElement("li", { - class: "selectr-tag", - html: content - }); - 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 ) { - item.innerHTML = item.textContent; - } - }, this); - } - }; - - /** - * Query matching for searches - * @param {string} query - * @param {HTMLOptionElement} option - * @return {bool} - */ - var match = function(query, option) { - var result = new RegExp(query, "i").exec(option.textContent); - if ( result ) { - return option.textContent.replace(result[0], ""+result[0]+""); - } - return false; - }; - - // Main Lib - var Selectr = function(el, config) { - - config = 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; - - // 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 (/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"; + if (this.config.nativeDropdown || this.mobileDevice) { + + this.container.addEventListener("touchstart", function(e) { + if (e.changedTouches[0].target === that.el) { + that.toggle(); + } + }); + + if (this.config.nativeDropdown || this.mobileDevice) { + this.container.addEventListener("click", function(e) { + if (e.target === that.el) { + that.toggle(); + } + }); + } + + // Listen for the change on the native select + // and update accordingly + this.el.addEventListener("change", function(e) { + + // Tapping on iPhone causes options to be selected instead of opening the dropdown + if (!that.opened) { + that.open(); + return false; + } + + if (that.el.multiple) { + var selected = that.el.querySelectorAll('option:checked'); + var values = [].slice.call(selected).map(function(option) { + return option.idx; + }); + + that.clear(); + + util.each(values, function(i, idx) { + that.select(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 the native + that.toggle(); + + // Focus on the native multiselect + setTimeout(function() { + that.el.focus(); + }, 200); + } + }); + } + + // Non-native dropdown + this.selected.addEventListener("click", function(e) { + + if (!that.disabled) { + that.toggle(); + } + + e.preventDefault(); + }); + + // 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(); + } + } + } + }); + + // 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 val = this.value.trim(); + + if (e.which === 13 || util.includes(that.tagSeperators, e.key)) { + + util.each(that.tagSeperators, function(i, k) { + val = val.replace(k, ''); + }); + + var option = that.add({ + value: val, + text: val, + selected: true + }, true); + + if (!option) { + this.value = ''; + that.setMessage('That tag is already in use.'); + } else { + that.close(); + clearSearch.call(that); + } + } + } + }); + } + + 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); + + // Listen for form.reset() (@ambrooks, #13) + if (this.el.form) { + this.el.form.addEventListener("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 && this.originalType === "select-one" && !this.config.data) { + if (this.el.options[0].selected && !this.el.options[0].defaultSelected) { + this.el.options[0].selected = false; + } + } + + util.each(this.options, function(i, option) { + if (option.selected && option.defaultSelected) { + this.select(option.idx); + } + }, this); + + if (this.config.selectedValue) { + this.setValue(this.config.selectedValue); + } + + if (this.config.data) { + + + if (!this.el.multiple && this.config.defaultSelected && this.el.selectedIndex < 0) { + this.select(0); + } + + var j = 0; + util.each(this.config.data, function(i, opt) { + // Check for group options + if (isset(opt, "children")) { + util.each(opt.children, function(x, item) { + if (item.hasOwnProperty("selected") && item.selected === true) { + this.select(j); + } + j++; + }, this); + } else { + if (opt.hasOwnProperty("selected") && opt.selected === true) { + this.select(j); + } + j++; + } + }, this); + } + }; + + /** + * Destroy the instance + * @return {void} + */ + Selectr.prototype.destroy = function() { + + if (!this.rendered) return; + + this.emit("selectr.destroy"); + + // Revert to select-single if programtically set to multiple + if (this.originalType === 'select-one') { + this.el.multiple = false; + } + + if (this.config.data) { + this.el.innerHTML = ""; + } + + // Remove the className from select element + util.removeClass(this.el, 'selectr-hidden'); + + // Remove reset listener from parent form + if (this.el.form) { + util.off(this.el.form, "reset", this.events.reset); + } + + // Remove event listeners attached to doc and win + util.off(document, "click", this.events.dismiss); + util.off(document, "keydown", this.events.navigate); + util.off(window, "resize", this.update); + util.off(window, "scroll", this.update); + + // Replace the container with the original select element + this.container.parentNode.replaceChild(this.el, this.container); + + this.rendered = false; + }; + + /** + * Change an options state + * @param {Number} index + * @return {void} + */ + Selectr.prototype.change = function(index) { + var item = this.items[index], + option = this.options[index]; + + if (option.disabled) { + return; + } + + if (option.selected && util.hasClass(item, "selected")) { + this.deselect(index); + } else { + this.select(index); + } + + if (this.opened && !this.el.multiple) { + this.close(); + } + }; + + /** + * Select an option + * @param {Number} index + * @return {void} + */ + Selectr.prototype.select = function(index) { + + var item = this.items[index], + options = [].slice.call(this.el.options), + option = this.options[index]; + + if (this.el.multiple) { + if (util.includes(this.selectedIndexes, index)) { + return false; + } + + if (this.config.maxSelections && this.tags.length === this.config.maxSelections) { + this.setMessage("A maximum of " + this.config.maxSelections + " items can be selected.", true); + return false; + } + + this.selectedValues.push(option.value); + this.selectedIndexes.push(index); + + addTag.call(this, item); + } else { + var data = this.data ? this.data[index] : option; + this.label.innerHTML = this.customSelected ? this.config.renderSelection(data) : option.textContent; + + this.selectedValue = option.value; + this.selectedIndex = index; + + util.each(this.options, function(i, o) { + var opt = this.items[i]; - // Enable event emitter - Events.mixin(this); - - build.call(this); + if (i !== index) { + if (opt) { + util.removeClass(opt, "selected"); + } + o.selected = false; + o.removeAttribute("selected"); + } + }, this); + } + + if (!util.includes(options, option)) { + this.el.add(option); + } + + item.setAttribute("aria-selected", true); + + util.addClass(item, "selected"); + util.addClass(this.container, "has-selected"); + + option.selected = true; + option.setAttribute("selected", ""); + + this.emit("selectr.change", option); + + this.emit("selectr.select", option); + }; + + /** + * Deselect an option + * @param {Number} index + * @return {void} + */ + Selectr.prototype.deselect = function(index, force) { + var item = this.items[index], + option = this.options[index]; + + if (this.el.multiple) { + var selIndex = this.selectedIndexes.indexOf(index); + this.selectedIndexes.splice(selIndex, 1); + + var valIndex = this.selectedValues.indexOf(option.value); + this.selectedValues.splice(valIndex, 1); + + removeTag.call(this, item); + + if (!this.tags.length) { + util.removeClass(this.container, "has-selected"); + } + } else { + + if (!force && !this.config.clearable && !this.config.allowDeselect) { + return false; + } + + this.label.innerHTML = ""; + this.selectedValue = null; + + this.el.selectedIndex = this.selectedIndex = -1; + + util.removeClass(this.container, "has-selected"); + } + + + this.items[index].setAttribute("aria-selected", false); + + util.removeClass(this.items[index], "selected"); + + option.selected = false; + + option.removeAttribute("selected"); + + this.emit("selectr.change", null); + + this.emit("selectr.deselect", option); + }; + + /** + * Programmatically set selected values + * @param {String|Array} value - A string or an array of strings + */ + Selectr.prototype.setValue = function(value) { + var isArray = Array.isArray(value); + + if (!isArray) { + value = value.toString().trim(); + } + + // Can't pass array to select-one + if (!this.el.multiple && isArray) { + return false; + } + + util.each(this.options, function(i, option) { + if (isArray && util.includes(value.toString(), option.value) || option.value === value) { + this.change(option.idx); + } + }, this); + }; + + /** + * Set the selected value(s) + * @param {bool} toObject Return only the raw values or an object + * @param {bool} toJson Return the object as a JSON string + * @return {mixed} Array or String + */ + Selectr.prototype.getValue = function(toObject, toJson) { + var value; + + if (this.el.multiple) { + if (toObject) { + if (this.selectedIndexes.length) { + value = {}; + value.values = []; + util.each(this.selectedIndexes, function(i, index) { + var option = this.options[index]; + value.values[i] = { + value: option.value, + text: option.textContent + }; + }, this); + } + } else { + value = this.selectedValues.slice(); + } + } else { + if (toObject) { + var option = this.options[this.selectedIndex]; + value = { + value: option.value, + text: option.textContent + }; + } else { + value = this.selectedValue; + } + } + + if (toObject && toJson) { + value = JSON.stringify(value); + } + + return value; + }; + + /** + * Add a new option or options + * @param {object} data + */ + Selectr.prototype.add = function(data, checkDuplicate) { + if (data) { + + this.data = this.data || []; + this.items = this.items || []; + this.options = this.options || []; + + if (Array.isArray(data)) { + // We have an array on items + util.each(data, function(i, obj) { + this.add(obj, checkDuplicate); + }, this); + } + // User passed a single object to the method + // or Selectr passed an object from an array + else if ("[object Object]" === Object.prototype.toString.call(data)) { + + if (checkDuplicate) { + var dupe = false; + + util.each(this.options, function(i, option) { + if (option.value.toLowerCase() === data.value.toLowerCase()) { + dupe = true; + } + }); + + if (dupe) { + return false; + } + } + + var option = util.createElement('option', data); + + this.data.push(data); + + // Add the new option to the list + this.options.push(option); + + // Add the index for later use + option.idx = this.options.length > 0 ? this.options.length - 1 : 0; + + // Create a new item + createItem.call(this, option); + + // Select the item if required + if (data.selected) { + this.select(option.idx); + } + + return option; + } + + // We may have had an empty select so update + // the placeholder to reflect the changes. + this.setPlaceholder(); + + // Recount the pages + if (this.config.pagination) { + this.paginate(); + } + + return true; + } + }; + + /** + * Remove an option or options + * @param {Mixed} o Array, integer (index) or string (value) + * @return {Void} + */ + Selectr.prototype.remove = function(o) { + var options = []; + if (Array.isArray(o)) { + util.each(o, function(i, opt) { + if (util.isInt(opt)) { + options.push(this.getOptionByIndex(opt)); + } else if (typeof o === "string") { + options.push(this.getOptionByValue(opt)); + } + }, this); + + } else if (util.isInt(o)) { + options.push(this.getOptionByIndex(o)); + } else if (typeof o === "string") { + options.push(this.getOptionByValue(o)); + } + + if (options.length) { + var index; + util.each(options, function(i, option) { + index = option.idx; + + // Remove the HTMLOptionElement + this.el.remove(option); + + // Remove the reference from the option array + this.options.splice(index, 1); + + // If the item has a parentNode (group element) it needs to be removed + // otherwise the render function will still append it to the dropdown + var parentNode = this.items[index].parentNode; + + if (parentNode) { + parentNode.removeChild(this.items[index]); + } + + // Remove reference from the items array + this.items.splice(index, 1); + + // Reset the indexes + util.each(this.options, function(i, opt) { + opt.idx = i; + this.items[i].idx = i; + }, this); + }, this); + + // We may have had an empty select now so update + // the placeholder to reflect the changes. + this.setPlaceholder(); + + // Recount the pages + if (this.config.pagination) { + this.paginate(); + } + } + }; + + /** + * Remove all options + */ + Selectr.prototype.removeAll = function() { + + // Clear any selected options + this.clear(true); + + // Remove the HTMLOptionElements + util.each(this.el.options, function(i, option) { + this.el.remove(option); + }, this); + + // Empty the dropdown + util.truncate(this.tree); + + // Reset variables + this.items = []; + this.options = []; + this.data = []; + + this.navIndex = 0; + + if (this.requiresPagination) { + this.requiresPagination = false; + + this.pageIndex = 1; + this.pages = []; + } + + // Update the placeholder + this.setPlaceholder(); + }; + + /** + * Perform a search + * @param {string} query The query string + */ + Selectr.prototype.search = function(string) { + + if (this.navigating) return; + + string = string || this.input.value; + + var f = document.createDocumentFragment(); + + // Remove message + this.removeMessage(); - this.bindEvents(); + // Clear the dropdown + util.truncate(this.tree); - this.update(); + if (string.length > 1) { + // Check the options for the matching string + util.each(this.options, function(i, option) { + var item = this.items[option.idx]; + var includes = util.includes(option.textContent.toLowerCase(), string.toLowerCase()); - this.optsRect = util.rect(this.tree); + if (includes && !option.disabled) { - this.rendered = true; + appendItem(item, f, this.customOption); - // Fixes macOS Safari bug #28 - if ( !this.el.multiple ) { - this.el.selectedIndex = this.selectedIndex; - } + util.removeClass(item, "excluded"); - var that = this; - setTimeout(function() { - that.emit("selectr.init"); - }, 20); - }; + // Underline the matching results + if (!this.customOption) { + item.innerHTML = match(string, option); + } + } else { + util.addClass(item, "excluded"); + } + }, this); - /** - * Attach the required event listeners - */ - Selectr.prototype.bindEvents = function() { - var that = this; + if (!f.childElementCount) { + if (!this.config.taggable) { + this.setMessage("no results."); + } + } else { + // Highlight top result (@binary-koan #26) + var prevEl = this.items[this.navIndex]; + var firstEl = f.firstElementChild; - this.events = {}; + util.removeClass(prevEl, "active"); - this.events.dismiss = dismiss.bind(this); - this.events.navigate = navigate.bind(this); - this.events.reset = this.reset.bind(this); + this.navIndex = firstEl.idx; - if ( this.config.nativeDropdown || this.mobileDevice ) { + util.addClass(firstEl, "active"); + } - this.container.addEventListener("touchstart", function(e) { - if ( e.changedTouches[0].target === that.el ) { - that.toggle(); - } - }); + } else { + render.call(this); + } - if ( this.config.nativeDropdown || this.mobileDevice ) { - this.container.addEventListener("click", function(e) { - if ( e.target === that.el ) { - that.toggle(); - } - }); - } + this.tree.appendChild(f); + }; - // Listen for the change on the native select - // and update accordingly - this.el.addEventListener("change", function(e) { - - // Tapping on iPhone causes options to be selected instead of opening the dropdown - if ( !that.opened ) { - that.open(); - return false; - } - - if ( that.el.multiple ) { - var selected = that.el.querySelectorAll('option:checked'); - var values = [].slice.call(selected).map(function(option) { return option.idx; }); - - that.clear(); - - util.each(values, function(i, idx) { - that.select(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 the native - that.toggle(); - - // Focus on the native multiselect - setTimeout(function() { - that.el.focus(); - }, 200); - } - }); - } - - // Non-native dropdown - this.selected.addEventListener("click", function(e) { - - if (!that.disabled) { - that.toggle(); - } - - e.preventDefault(); - }); - - // 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(); - } - } - } - }); - - // 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 val = this.value.trim(); - - if ( e.which === 13 || util.includes(that.tagSeperators, e.key) ) { - - util.each(that.tagSeperators, function(i,k) { - val = val.replace(k, ''); - }); - - var option = that.add({ - value: val, - text: val, - selected: true - }, true); - - if ( !option ) { - this.value = ''; - that.setMessage('That tag is already in use.'); - } else { - that.close(); - clearSearch.call(that); - } - } - } - }); - } - - 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); - - // Listen for form.reset() (@ambrooks, #13) - if ( this.el.form ) { - this.el.form.addEventListener("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 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 && this.originalType === "select-one" && !this.config.data ) { - if ( this.el.options[0].selected && !this.el.options[0].defaultSelected ) { - this.el.options[0].selected = false; - } - } - - util.each(this.options, function(i, option) { - if ( option.selected && option.defaultSelected ) { - this.select(option.idx); - } - }, this); - - if ( this.config.selectedValue ) { - this.setValue(this.config.selectedValue); - } - - if ( this.config.data ) { - - - if ( !this.el.multiple && this.config.defaultSelected && this.el.selectedIndex < 0 ) { - this.select(0); - } - - var j = 0; - util.each(this.config.data, function(i, opt) { - // Check for group options - if ( isset(opt, "children") ) { - util.each(opt.children, function(x, item) { - if ( item.hasOwnProperty("selected") && item.selected === true ) { - this.select(j); - } - j++; - }, this); - } else { - if ( opt.hasOwnProperty("selected") && opt.selected === true ) { - this.select(j); - } - j++; - } - }, this); - } - }; - - /** - * Destroy the instance - * @return {void} - */ - Selectr.prototype.destroy = function() { - - if ( !this.rendered ) return; - - this.emit("selectr.destroy"); - - // Revert to select-single if programtically set to multiple - if ( this.originalType === 'select-one' ) { - this.el.multiple = false; - } - - if ( this.config.data ) { - this.el.innerHTML = ""; - } - - // Remove the className from select element - util.removeClass(this.el, 'selectr-hidden'); - - // Remove reset listener from parent form - if ( this.el.form ) { - util.off(this.el.form, "reset", this.events.reset); - } - - // Remove event listeners attached to doc and win - util.off(document, "click", this.events.dismiss); - util.off(document, "keydown", this.events.navigate); - util.off(window, "resize", this.update); - util.off(window, "scroll", this.update); - - // Replace the container with the original select element - this.container.parentNode.replaceChild(this.el, this.container); - - this.rendered = false; - }; - - /** - * Change an options state - * @param {Number} index - * @return {void} - */ - Selectr.prototype.change = function(index) { - var item = this.items[index], - option = this.options[index]; - - if (option.disabled) { - return; - } - - if (option.selected && util.hasClass(item, "selected")) { - this.deselect(index); - } else { - this.select(index); - } - - if (this.opened && !this.el.multiple) { - this.close(); - } - }; - - /** - * Select an option - * @param {Number} index - * @return {void} - */ - Selectr.prototype.select = function(index) { - - var item = this.items[index], - options = [].slice.call(this.el.options), - option = this.options[index]; - - if ( this.el.multiple ) { - if (util.includes(this.selectedIndexes, index) ) { - return false; - } - - if ( this.config.maxSelections && this.tags.length === this.config.maxSelections ) { - this.setMessage("A maximum of " + this.config.maxSelections + " items can be selected.", true); - return false; - } - - this.selectedValues.push(option.value); - this.selectedIndexes.push(index); - - addTag.call(this, item); - } else { - var data = this.data ? this.data[index] : option; - this.label.innerHTML = this.customSelected ? this.config.renderSelection(data) : option.textContent; - - this.selectedValue = option.value; - this.selectedIndex = index; - - util.each(this.options, function(i, o) { - var opt = this.items[i]; + /** + * Toggle the dropdown + * @return {void} + */ + Selectr.prototype.toggle = function() { + if (!this.disabled) { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } + }; - if ( i !== index ) { - if ( opt ) { - util.removeClass(opt, "selected"); - } - o.selected = false; - o.removeAttribute("selected"); - } - }, this); - } - - if ( !util.includes(options, option) ) { - this.el.add(option); - } - - item.setAttribute( "aria-selected", true ); - - util.addClass(item, "selected"); - util.addClass(this.container, "has-selected"); - - option.selected = true; - option.setAttribute("selected", ""); - - this.emit("selectr.change", option); - - this.emit("selectr.select", option); - }; - - /** - * Deselect an option - * @param {Number} index - * @return {void} - */ - Selectr.prototype.deselect = function(index, force) { - var item = this.items[index], - option = this.options[index]; - - if ( this.el.multiple ) { - var selIndex = this.selectedIndexes.indexOf(index); - this.selectedIndexes.splice(selIndex, 1); - - var valIndex = this.selectedValues.indexOf(option.value); - this.selectedValues.splice(valIndex, 1); - - removeTag.call(this, item); - - if ( !this.tags.length ) { - util.removeClass(this.container, "has-selected"); - } - } else { - - if ( !force && !this.config.clearable && !this.config.allowDeselect ) { - return false; - } - - this.label.innerHTML = ""; - this.selectedValue = null; - - this.el.selectedIndex = this.selectedIndex = -1; - - util.removeClass(this.container, "has-selected"); - } - - - this.items[index].setAttribute( "aria-selected", false ); - - util.removeClass(this.items[index], "selected"); - - option.selected = false; - - option.removeAttribute("selected"); - - this.emit("selectr.change", null); - - this.emit("selectr.deselect", option); - }; - - /** - * Programmatically set selected values - * @param {String|Array} value - A string or an array of strings - */ - Selectr.prototype.setValue = function(value) { - var isArray = Array.isArray(value); - - if (!isArray) { - value = value.toString().trim(); - } - - // Can't pass array to select-one - if ( !this.el.multiple && isArray ) { - return false; - } - - util.each(this.options, function(i, option) { - if (isArray && util.includes(value.toString(), option.value) || option.value === value) { - this.change(option.idx); - } - }, this); - }; - - /** - * Set the selected value(s) - * @param {bool} toObject Return only the raw values or an object - * @param {bool} toJson Return the object as a JSON string - * @return {mixed} Array or String - */ - Selectr.prototype.getValue = function(toObject, toJson) { - var value; - - if ( this.el.multiple ) { - if ( toObject) { - if ( this.selectedIndexes.length ) { - value = {}; - value.values = []; - util.each(this.selectedIndexes, function(i,index) { - var option = this.options[index]; - value.values[i] = { - value: option.value, - text: option.textContent - }; - }, this); - } - } else { - value = this.selectedValues.slice(); - } - } else { - if ( toObject) { - var option = this.options[this.selectedIndex]; - value = { - value: option.value, - text: option.textContent - }; - } else { - value = this.selectedValue; - } - } - - if ( toObject && toJson) { - value = JSON.stringify(value); - } - - return value; - }; - - /** - * Add a new option or options - * @param {object} data - */ - Selectr.prototype.add = function(data, checkDuplicate) { - if ( data ) { - - this.data = this.data || []; - this.items = this.items || []; - this.options = this.options || []; - - if ( Array.isArray(data) ) { - // We have an array on items - util.each(data, function(i, obj) { - this.add(obj, checkDuplicate); - }, this); - } - // User passed a single object to the method - // or Selectr passed an object from an array - else if ( "[object Object]" === Object.prototype.toString.call(data) ) { - - if ( checkDuplicate ) { - var dupe = false; - - util.each(this.options, function(i, option) { - if ( option.value.toLowerCase() === data.value.toLowerCase() ) { - dupe = true; - } - }); - - if ( dupe ) { - return false; - } - } - - var option = util.createElement('option', data); - - this.data.push(data); - - // Add the new option to the list - this.options.push(option); - - // Add the index for later use - option.idx = this.options.length > 0 ? this.options.length - 1 : 0; - - // Create a new item - createItem.call(this, option); - - // Select the item if required - if ( data.selected ) { - this.select(option.idx); - } - - return option; - } - - // We may have had an empty select so update - // the placeholder to reflect the changes. - this.setPlaceholder(); - - // Recount the pages - if ( this.config.pagination ) { - this.paginate(); - } - - return true; - } - }; - - /** - * Remove an option or options - * @param {Mixed} o Array, integer (index) or string (value) - * @return {Void} - */ - Selectr.prototype.remove = function(o) { - var options = []; - if ( Array.isArray(o) ) { - util.each(o, function(i, opt) { - if ( util.isInt(opt) ) { - options.push(this.getOptionByIndex(opt)); - } else if ( typeof o === "string" ) { - options.push(this.getOptionByValue(opt)); - } - }, this); - - } else if ( util.isInt(o) ) { - options.push(this.getOptionByIndex(o)); - } else if ( typeof o === "string" ) { - options.push(this.getOptionByValue(o)); - } - - if ( options.length ) { - var index; - util.each(options, function(i, option) { - index = option.idx; - - // Remove the HTMLOptionElement - this.el.remove(option); - - // Remove the reference from the option array - this.options.splice(index, 1); - - // If the item has a parentNode (group element) it needs to be removed - // otherwise the render function will still append it to the dropdown - var parentNode = this.items[index].parentNode; - - if ( parentNode ) { - parentNode.removeChild(this.items[index]); - } - - // Remove reference from the items array - this.items.splice(index, 1); - - // Reset the indexes - util.each(this.options, function(i, opt) { - opt.idx = i; - this.items[i].idx = i; - }, this); - }, this); - - // We may have had an empty select now so update - // the placeholder to reflect the changes. - this.setPlaceholder(); - - // Recount the pages - if ( this.config.pagination ) { - this.paginate(); - } - } - }; - - /** - * Remove all options - */ - Selectr.prototype.removeAll = function() { - - // Clear any selected options - this.clear(true); - - // Remove the HTMLOptionElements - util.each(this.el.options, function(i, option) { - this.el.remove(option); - }, this); - - // Empty the dropdown - util.truncate(this.tree); - - // Reset variables - this.items = []; - this.options = []; - this.data = []; - - this.navIndex = 0; - - if ( this.requiresPagination ) { - this.requiresPagination = false; - - this.pageIndex = 1; - this.pages = []; - } - - // Update the placeholder - this.setPlaceholder(); - }; - - /** - * Perform a search - * @param {string} query The query string - */ - Selectr.prototype.search = function(string) { - - if ( this.navigating ) return; - - string = string || this.input.value; - - var f = document.createDocumentFragment(); - - // Remove message - this.removeMessage(); + /** + * Open the dropdown + * @return {void} + */ + Selectr.prototype.open = function() { - // Clear the dropdown - util.truncate(this.tree); + var that = this; - if ( string.length > 1 ) { - // Check the options for the matching string - util.each(this.options, function(i, option) { - var item = this.items[option.idx]; - var includes = util.includes(option.textContent.toLowerCase(), string.toLowerCase()); + if (!this.options.length) { + return false; + } - if ( includes && !option.disabled ) { + if (!this.opened) { + this.emit("selectr.open"); + } - appendItem(item, f, this.customOption); + this.opened = true; - util.removeClass(item, "excluded"); + if (this.mobileDevice || this.config.nativeDropdown) { + util.addClass(this.container, "native-open"); - // Underline the matching results - if ( !this.customOption ) { - item.innerHTML = match(string, option); - } - } else { - util.addClass(item, "excluded"); - } - }, this); + if (this.config.data) { + // Dump the options into the select + // otherwise the native dropdown will be empty + util.each(this.options, function(i, option) { + this.el.add(option); + }, this); + } + return; + } - if ( !f.childElementCount ) { - if ( !this.config.taggable ) { - this.setMessage("no results."); - } - } else { - // Highlight top result (@binary-koan #26) - var prevEl = this.items[this.navIndex]; - var firstEl = f.firstElementChild; + util.addClass(this.container, "open"); - util.removeClass(prevEl, "active"); + render.call(this); + + this.invert(); - this.navIndex = firstEl.idx; + this.tree.scrollTop = 0; + + util.removeClass(this.container, "notice"); - util.addClass(firstEl, "active"); - } + this.selected.setAttribute("aria-expanded", true); - } else { - render.call(this); - } + this.tree.setAttribute("aria-hidden", false); + this.tree.setAttribute("aria-expanded", true); - this.tree.appendChild(f); - }; + if (this.config.searchable && !this.config.taggable) { + setTimeout(function() { + that.input.focus(); + // Allow tab focus + that.input.tabIndex = 0; + }, 10); + } + }; + + /** + * Close the dropdown + * @return {void} + */ + Selectr.prototype.close = function() { - /** - * Toggle the dropdown - * @return {void} - */ - Selectr.prototype.toggle = function() { - if (!this.disabled) { - if (this.opened) { - this.close(); - } else { - this.open(); - } - } - }; - - /** - * Open the dropdown - * @return {void} - */ - Selectr.prototype.open = function() { - - var that = this; - - if ( !this.options.length ) { - return false; - } - - if ( !this.opened ) { - this.emit("selectr.open"); - } - - this.opened = true; - - if (this.mobileDevice || this.config.nativeDropdown) { - util.addClass(this.container, "native-open"); - - if ( this.config.data ) { - // Dump the options into the select - // otherwise the native dropdown will be empty - util.each(this.options, function(i, option) { - this.el.add(option); - }, this); - } - - return; - } - - util.addClass(this.container, "open"); - - render.call(this); - - this.invert(); - - this.tree.scrollTop = 0; - - util.removeClass(this.container, "notice"); - - this.selected.setAttribute( "aria-expanded", true ); - - this.tree.setAttribute( "aria-hidden", false ); - this.tree.setAttribute( "aria-expanded", true ); - - if (this.config.searchable && !this.config.taggable) { - setTimeout(function() { - that.input.focus(); - // Allow tab focus - that.input.tabIndex = 0; - }, 10); - } - }; - - /** - * Close the dropdown - * @return {void} - */ - Selectr.prototype.close = function() { - - if ( this.opened ) { - this.emit("selectr.close"); - } - - this.opened = false; - - if (this.mobileDevice || this.config.nativeDropdown) { - util.removeClass(this.container, "native-open"); - return; - } - - var notice = util.hasClass(this.container, "notice"); - - if (this.config.searchable && !notice) { - this.input.blur(); - // Disable tab focus - this.input.tabIndex = -1; - this.searching = false; - } - - if (notice) { - util.removeClass(this.container, "notice"); - this.notice.textContent = ""; - } - - util.removeClass(this.container, "open"); - util.removeClass(this.container, "native-open"); - - this.selected.setAttribute( "aria-expanded", false ); - - this.tree.setAttribute( "aria-hidden", true ); - this.tree.setAttribute( "aria-expanded", false ); - - util.truncate(this.tree); - clearSearch.call(this); - }; - - - /** - * Enable the element - * @return {void} - */ - Selectr.prototype.enable = function() { - this.disabled = false; - this.el.disabled = false; - - this.selected.tabIndex = this.originalIndex; - - if ( this.el.multiple ) { - util.each(this.tags, function(i,t) { - t.lastElementChild.tabIndex = 0; - }); - } - - util.removeClass(this.container, "selectr-disabled"); - }; - - /** - * Disable the element - * @param {boolean} container Disable the container only (allow value submit with form) - * @return {void} - */ - Selectr.prototype.disable = function(container) { - if ( !container ) { - this.el.disabled = true; - } - - this.selected.tabIndex = -1; - - if ( this.el.multiple ) { - util.each(this.tags, function(i,t) { - t.lastElementChild.tabIndex = -1; - }); - } - - this.disabled = true; - util.addClass(this.container, "selectr-disabled"); - }; - - - /** - * Reset to initial state - * @return {void} - */ - Selectr.prototype.reset = function() { - if ( !this.disabled ) { - this.clear(); - - this.setSelected(true); - - util.each(this.defaultSelected, function(i,idx) { - this.select(idx); - }, this); - - this.emit("selectr.reset"); - } - }; - - /** - * Clear all selections - * @return {void} - */ - Selectr.prototype.clear = function(force) { - - if ( this.el.multiple ) { - // Loop over the selectedIndexes so we don't have to loop over all the options - // which can be costly if there are a lot of them - - if ( this.selectedIndexes.length ) { - // Copy the array or we'll get an error - var indexes = this.selectedIndexes.slice(); - - util.each(indexes, function(i, idx) { - this.deselect(idx); - }, this); - } - } else { - if ( this.selectedIndex > -1 ) { - this.deselect(this.selectedIndex, force); - } - } - - this.emit("selectr.clear"); - }; - - /** - * Return serialised data - * @param {boolean} toJson - * @return {mixed} Returns either an object or JSON string - */ - Selectr.prototype.serialise = function(toJson) { - var data = []; - util.each(this.options, function(i, option) { - var obj = { - value: option.value, - text: option.textContent - }; - - if ( option.selected ) { - obj.selected = true; - } - if ( option.disabled ) { - obj.disabled = true; - } - data[i] = obj; - }); - - return toJson ? JSON.stringify(data) : data; - }; - - /** - * Localised version of serialise() method - */ - Selectr.prototype.serialize = function(toJson) { - return this.serialise(toJson); - }; - - /** - * Sets the placeholder - * @param {String} placeholder - */ - Selectr.prototype.setPlaceholder = function(placeholder) { - // Set the placeholder - placeholder = placeholder || this.config.placeholder || this.el.getAttribute("placeholder"); - - if ( !this.options.length ) { - placeholder = "No options available"; - } - - this.placeEl.innerHTML = placeholder; - }; - - /** - * Paginate the option list - * @return {Array} - */ - Selectr.prototype.paginate = function() { - if ( this.items.length ) { - var that = this; - - this.pages = this.items.map( function(v, i) { - return i % that.config.pagination === 0 ? that.items.slice(i, i+that.config.pagination) : null; - }).filter(function(pages){ return pages; }); - - return this.pages; - } - }; - - /** - * Display a message - * @param {String} message The message - */ - Selectr.prototype.setMessage = function(message, close) { - if ( close ) { - this.close(); - } - util.addClass(this.container, "notice"); - this.notice.textContent = message; - }; - - /** - * Dismiss the current message - */ - Selectr.prototype.removeMessage = function() { - util.removeClass(this.container, "notice"); - this.notice.innerHTML = ""; - }; - - /** - * Keep the dropdown within the window - * @return {void} - */ - Selectr.prototype.invert = function() { - var rt = util.rect(this.selected), oh = this.tree.parentNode.offsetHeight, - wh = window.innerHeight, doInvert = rt.top + rt.height + oh > wh; - - if (doInvert) { - util.addClass(this.container, "inverted"); - this.isInverted = true; - } else { - util.removeClass(this.container, "inverted"); - this.isInverted = false; - } - - this.optsRect = util.rect(this.tree); - }; - - /** - * Get an option via it's index - * @param {Integer} index The index of the HTMLOptionElement required - * @return {HTMLOptionElement} - */ - Selectr.prototype.getOptionByIndex = function(index) { - return this.options[index]; - }; - - /** - * Get an option via it's value - * @param {String} value The value of the HTMLOptionElement required - * @return {HTMLOptionElement} - */ - Selectr.prototype.getOptionByValue = function(value) { - var option = false; - - for ( var i = 0, l = this.options.length; i < l; i++ ) { - if ( this.options[i].value.trim() === value.toString().trim() ) { - option = this.options[i]; - break; - } - } - - return option; - }; - - return Selectr; + if (this.opened) { + this.emit("selectr.close"); + } + + this.opened = false; + + if (this.mobileDevice || this.config.nativeDropdown) { + util.removeClass(this.container, "native-open"); + return; + } + + var notice = util.hasClass(this.container, "notice"); + + if (this.config.searchable && !notice) { + this.input.blur(); + // Disable tab focus + this.input.tabIndex = -1; + this.searching = false; + } + + if (notice) { + util.removeClass(this.container, "notice"); + this.notice.textContent = ""; + } + + util.removeClass(this.container, "open"); + util.removeClass(this.container, "native-open"); + + this.selected.setAttribute("aria-expanded", false); + + this.tree.setAttribute("aria-hidden", true); + this.tree.setAttribute("aria-expanded", false); + + util.truncate(this.tree); + clearSearch.call(this); + }; + + + /** + * Enable the element + * @return {void} + */ + Selectr.prototype.enable = function() { + this.disabled = false; + this.el.disabled = false; + + this.selected.tabIndex = this.originalIndex; + + if (this.el.multiple) { + util.each(this.tags, function(i, t) { + t.lastElementChild.tabIndex = 0; + }); + } + + util.removeClass(this.container, "selectr-disabled"); + }; + + /** + * Disable the element + * @param {boolean} container Disable the container only (allow value submit with form) + * @return {void} + */ + Selectr.prototype.disable = function(container) { + if (!container) { + this.el.disabled = true; + } + + this.selected.tabIndex = -1; + + if (this.el.multiple) { + util.each(this.tags, function(i, t) { + t.lastElementChild.tabIndex = -1; + }); + } + + this.disabled = true; + util.addClass(this.container, "selectr-disabled"); + }; + + + /** + * Reset to initial state + * @return {void} + */ + Selectr.prototype.reset = function() { + if (!this.disabled) { + this.clear(); + + this.setSelected(true); + + util.each(this.defaultSelected, function(i, idx) { + this.select(idx); + }, this); + + this.emit("selectr.reset"); + } + }; + + /** + * Clear all selections + * @return {void} + */ + Selectr.prototype.clear = function(force) { + + if (this.el.multiple) { + // Loop over the selectedIndexes so we don't have to loop over all the options + // which can be costly if there are a lot of them + + if (this.selectedIndexes.length) { + // Copy the array or we'll get an error + var indexes = this.selectedIndexes.slice(); + + util.each(indexes, function(i, idx) { + this.deselect(idx); + }, this); + } + } else { + if (this.selectedIndex > -1) { + this.deselect(this.selectedIndex, force); + } + } + + this.emit("selectr.clear"); + }; + + /** + * Return serialised data + * @param {boolean} toJson + * @return {mixed} Returns either an object or JSON string + */ + Selectr.prototype.serialise = function(toJson) { + var data = []; + util.each(this.options, function(i, option) { + var obj = { + value: option.value, + text: option.textContent + }; + + if (option.selected) { + obj.selected = true; + } + if (option.disabled) { + obj.disabled = true; + } + data[i] = obj; + }); + + return toJson ? JSON.stringify(data) : data; + }; + + /** + * Localised version of serialise() method + */ + Selectr.prototype.serialize = function(toJson) { + return this.serialise(toJson); + }; + + /** + * Sets the placeholder + * @param {String} placeholder + */ + Selectr.prototype.setPlaceholder = function(placeholder) { + // Set the placeholder + placeholder = placeholder || this.config.placeholder || this.el.getAttribute("placeholder"); + + if (!this.options.length) { + placeholder = "No options available"; + } + + this.placeEl.innerHTML = placeholder; + }; + + /** + * Paginate the option list + * @return {Array} + */ + Selectr.prototype.paginate = function() { + if (this.items.length) { + var that = this; + + this.pages = this.items.map(function(v, i) { + return i % that.config.pagination === 0 ? that.items.slice(i, i + that.config.pagination) : null; + }).filter(function(pages) { + return pages; + }); + + return this.pages; + } + }; + + /** + * Display a message + * @param {String} message The message + */ + Selectr.prototype.setMessage = function(message, close) { + if (close) { + this.close(); + } + util.addClass(this.container, "notice"); + this.notice.textContent = message; + }; + + /** + * Dismiss the current message + */ + Selectr.prototype.removeMessage = function() { + util.removeClass(this.container, "notice"); + this.notice.innerHTML = ""; + }; + + /** + * Keep the dropdown within the window + * @return {void} + */ + Selectr.prototype.invert = function() { + var rt = util.rect(this.selected), + oh = this.tree.parentNode.offsetHeight, + wh = window.innerHeight, + doInvert = rt.top + rt.height + oh > wh; + + if (doInvert) { + util.addClass(this.container, "inverted"); + this.isInverted = true; + } else { + util.removeClass(this.container, "inverted"); + this.isInverted = false; + } + + this.optsRect = util.rect(this.tree); + }; + + /** + * Get an option via it's index + * @param {Integer} index The index of the HTMLOptionElement required + * @return {HTMLOptionElement} + */ + Selectr.prototype.getOptionByIndex = function(index) { + return this.options[index]; + }; + + /** + * Get an option via it's value + * @param {String} value The value of the HTMLOptionElement required + * @return {HTMLOptionElement} + */ + Selectr.prototype.getOptionByValue = function(value) { + var option = false; + + for (var i = 0, l = this.options.length; i < l; i++) { + if (this.options[i].value.trim() === value.toString().trim()) { + option = this.options[i]; + break; + } + } + + return option; + }; + + return Selectr; })); \ No newline at end of file