UNPKG

vue-dynamic-select

Version:

A VueJS plugin that provides a searchable and reactive select list component with no dependencies.

469 lines (422 loc) 26.4 kB
var DynamicSelect = (function (exports) { 'use strict'; // // // // // // // // // // // // // // var script = { props: { placeholder: { type: String, default: 'search', required: false }, options: { type: Array, default: function() { return [] }, required: true }, optionValue: { type: String, default: 'id', required: true }, optionText: { type: String, default: 'name', required: true }, value: { type: Object, default: function() { return null }, required: false } }, data: function() { return { hasFocus: false, search: null, selectedOption: this.value, selectedResult: 0 }; }, mounted: function mounted() { // Add onclick method to body to hide result list when component loses focus window.addEventListener("click", this.loseFocus); }, destroyed: function destroyed() { window.removeEventListener("click", this.loseFocus); }, computed: { results: function() { var this$1 = this; // Filter items on search text (if not empty, case insensitive) and when item isn't already selected (else return all items not selected) return this.search ? this.options.filter(function (i) { return String(i[this$1.optionText]).toLowerCase().indexOf(this$1.search.toLowerCase()) > -1; }) : this.options; }, showResultList: function() { return this.hasFocus && this.results.length > 0; }, showPlaceholder: function() { return !this.hasFocus && !this.selectedOption; } }, watch: { hasFocus: function(hasFocus) { // Clear the search box when component loses focus window.removeEventListener("keydown", this.stopScroll); if(hasFocus) { window.addEventListener("keydown", this.stopScroll); this.$refs.search.focus(); } else { this.search = null; this.selectedResult = 0; this.$refs.search.blur(); } }, value: function() { var this$1 = this; // Load selected option on prop value change this.selectedOption = this.options.find( function (option) { return this$1.value && option[this$1.optionValue] == this$1.value[this$1.optionValue]; }); }, selectedOption: function() { // Provide selected item to parent this.$emit('input', this.selectedOption); }, search: function() { // Provide search text to parent (for ajax fetching, etc) this.$emit('search', this.search); } }, methods: { selectOption: function(option) { this.selectedOption = option; this.hasFocus = false; }, removeOption: function(event) { // Remove selected option if user hits backspace on empty search field if(event.keyCode === 8 && (this.search == null || this.search == '')) { this.selectedOption = null; this.hasFocus = false; event.preventDefault(); } }, moveToResults: function(event) { // Move down to first result if user presses down arrow (from search field) if(event.keyCode === 40) { if(this.$refs.result.length > 0) { this.$refs.resultList.children.item(0).focus(); } } }, navigateResults: function(option, event) { // Add option to selected items on enter key if(event.keyCode === 13) { this.selectOption(option); // Move up or down items in result list with up or down arrow keys } else if(event.keyCode === 40 || event.keyCode === 38) { if(event.keyCode === 40) { this.selectedResult++; } else if(event.keyCode === 38) { this.selectedResult--; } var next = this.$refs.resultList.children.item(this.selectedResult); if(next) { next.focus(); } else { this.selectedResult = 0; this.$refs.search.focus(); } } }, highlight: function(value) { // Highlights the part of each result that matches the search text if(this.search) { var matchPos = String(value).toLowerCase().indexOf(this.search.toLowerCase()); if(matchPos > -1) { var matchStr = String(value).substr(matchPos, this.search.length); value = String(value).replace(matchStr, '<span style="font-weight: bold; background-color: #efefef;">'+matchStr+'</span>'); } } return value; }, stopScroll: function(event) { if(event.keyCode === 40 || event.keyCode === 38) { event.preventDefault(); } }, loseFocus: function(event) { if(!this.$el.contains(event.target)) { this.hasFocus = false; } } } } function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */ , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { if (typeof shadowMode !== 'boolean') { createInjectorSSR = createInjector; createInjector = shadowMode; shadowMode = false; } // Vue.extend constructor export interop. var options = typeof script === 'function' ? script.options : script; // render functions if (template && template.render) { options.render = template.render; options.staticRenderFns = template.staticRenderFns; options._compiled = true; // functional template if (isFunctionalTemplate) { options.functional = true; } } // scopedId if (scopeId) { options._scopeId = scopeId; } var hook; if (moduleIdentifier) { // server build hook = function hook(context) { // 2.3 injection context = context || // cached call this.$vnode && this.$vnode.ssrContext || // stateful this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional // 2.2 with runInNewContext: true if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__; } // inject component styles if (style) { style.call(this, createInjectorSSR(context)); } // register component module identifier for async chunk inference if (context && context._registeredComponents) { context._registeredComponents.add(moduleIdentifier); } }; // used by ssr in case component is cached and beforeCreate // never gets called options._ssrRegister = hook; } else if (style) { hook = shadowMode ? function () { style.call(this, createInjectorShadow(this.$root.$options.shadowRoot)); } : function (context) { style.call(this, createInjector(context)); }; } if (hook) { if (options.functional) { // register for functional component in vue file var originalRender = options.render; options.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context); }; } else { // inject component registration as beforeCreate hook var existing = options.beforeCreate; options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } return script; } var normalizeComponent_1 = normalizeComponent; var isOldIE = typeof navigator !== 'undefined' && /msie [6-9]\\b/.test(navigator.userAgent.toLowerCase()); function createInjector(context) { return function (id, style) { return addStyle(id, style); }; } var HEAD = document.head || document.getElementsByTagName('head')[0]; var styles = {}; function addStyle(id, css) { var group = isOldIE ? css.media || 'default' : id; var style = styles[group] || (styles[group] = { ids: new Set(), styles: [] }); if (!style.ids.has(id)) { style.ids.add(id); var code = css.source; if (css.map) { // https://developer.chrome.com/devtools/docs/javascript-debugging // this makes source maps inside style tags work properly in Chrome code += '\n/*# sourceURL=' + css.map.sources[0] + ' */'; // http://stackoverflow.com/a/26603875 code += '\n/*# sourceMappingURL=data:application/json;base64,' + btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + ' */'; } if (!style.element) { style.element = document.createElement('style'); style.element.type = 'text/css'; if (css.media) { style.element.setAttribute('media', css.media); } HEAD.appendChild(style.element); } if ('styleSheet' in style.element) { style.styles.push(code); style.element.styleSheet.cssText = style.styles.filter(Boolean).join('\n'); } else { var index = style.ids.size - 1; var textNode = document.createTextNode(code); var nodes = style.element.childNodes; if (nodes[index]) { style.element.removeChild(nodes[index]); } if (nodes.length) { style.element.insertBefore(textNode, nodes[index]); }else { style.element.appendChild(textNode); } } } } var browser = createInjector; /* script */ var __vue_script__ = script; /* template */ var __vue_render__ = function() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c("div", [ _c( "div", { staticClass: "vue-dynamic-select", attrs: { tabindex: "0" }, on: { focusin: function($event) { _vm.hasFocus = true; } } }, [ _vm.showPlaceholder ? _c("div", { staticClass: "placeholder", domProps: { textContent: _vm._s(_vm.placeholder) } }) : _vm._e(), _vm._v(" "), _vm.selectedOption && !_vm.hasFocus ? _c("div", { staticClass: "selected-option", domProps: { textContent: _vm._s(_vm.selectedOption[_vm.optionText]) } }) : _vm._e(), _vm._v(" "), _c("input", { directives: [ { name: "model", rawName: "v-model", value: _vm.search, expression: "search" } ], ref: "search", staticClass: "search", attrs: { autocomplete: "off" }, domProps: { value: _vm.search }, on: { focus: function($event) { _vm.hasFocus = true; }, keyup: _vm.moveToResults, keydown: _vm.removeOption, input: function($event) { if ($event.target.composing) { return } _vm.search = $event.target.value; } } }), _vm._v(" "), _c("i", { staticClass: "dropdown" }), _vm._v(" "), _vm.showResultList ? _c( "div", { ref: "resultList", staticClass: "result-list" }, _vm._l(_vm.results, function(result) { return _c("div", { key: result[_vm.optionValue], ref: "result", refInFor: true, staticClass: "result", attrs: { tabindex: "0" }, domProps: { innerHTML: _vm._s(_vm.highlight(result[_vm.optionText])) }, on: { click: function($event) { return _vm.selectOption(result) }, keyup: function($event) { $event.preventDefault(); return _vm.navigateResults(result, $event) } } }) }), 0 ) : _vm._e() ] ) ]) }; var __vue_staticRenderFns__ = []; __vue_render__._withStripped = true; /* style */ var __vue_inject_styles__ = function (inject) { if (!inject) { return } inject("data-v-24d1f392_0", { source: "\n.vue-dynamic-select[data-v-24d1f392] {\n border: 1px solid #ced4da; \n position: relative;\n padding: .375em .5em;\n border-radius: .25em;\n cursor: text;\n display: block;\n}\n.vue-dynamic-select i.dropdown[data-v-24d1f392] {\n width: 0; \n height: 0; \n border-left: 4px solid transparent; \n border-right: 4px solid transparent; \n border-top: 4px solid; \n float: right; \n top: .75em; \n opacity: .8; \n cursor: pointer;\n}\n.vue-dynamic-select .placeholder[data-v-24d1f392] {\n display: inline-block;\n color: #ccc;\n}\n.vue-dynamic-select .result-list[data-v-24d1f392] {\n border: 1px solid #ced4da; \n margin: calc(.375em - 1px) calc(-.5em - 1px);\n width: calc(100% + 2px);\n min-width: calc(100% + 2px);\n border-radius: 0 0 .25em .25em;\n cursor: pointer;\n position: absolute;\n z-index: 10;\n background-color: #fff;\n}\n.vue-dynamic-select .result-list .result[data-v-24d1f392] {\n padding: .375em .75em;\n color: #333;\n}\n.vue-dynamic-select .result-list .result[data-v-24d1f392]:hover, .vue-dynamic-select .result-list .result[data-v-24d1f392]:focus {\n background-color: #efefef;\n outline: none;\n}\n.vue-dynamic-select .selected-option[data-v-24d1f392] {\n display: inline-block;\n}\n.vue-dynamic-select .search[data-v-24d1f392] {\n border: none;\n width: 50px;\n}\n.vue-dynamic-select .search[data-v-24d1f392]:focus {\n outline: none;\n}\n", map: {"version":3,"sources":["/home/smontgomery/Projects/vue-dynamic-select/src/DynamicSelect.vue"],"names":[],"mappings":";AA0KA;IACA,yBAAA;IACA,kBAAA;IACA,oBAAA;IACA,oBAAA;IACA,YAAA;IACA,cAAA;AACA;AACA;IACA,QAAA;IACA,SAAA;IACA,kCAAA;IACA,mCAAA;IACA,qBAAA;IACA,YAAA;IACA,UAAA;IACA,WAAA;IACA,eAAA;AACA;AACA;IACA,qBAAA;IACA,WAAA;AACA;AACA;IACA,yBAAA;IACA,4CAAA;IACA,uBAAA;IACA,2BAAA;IACA,8BAAA;IACA,eAAA;IACA,kBAAA;IACA,WAAA;IACA,sBAAA;AACA;AACA;IACA,qBAAA;IACA,WAAA;AACA;AACA;IACA,yBAAA;IACA,aAAA;AACA;AACA;IACA,qBAAA;AACA;AACA;IACA,YAAA;IACA,WAAA;AACA;AACA;IACA,aAAA;AACA","file":"DynamicSelect.vue","sourcesContent":["<template>\n <div>\n <div tabindex=\"0\" @focusin=\"hasFocus=true\" class=\"vue-dynamic-select\">\n <div v-if=\"showPlaceholder\" class=\"placeholder\" v-text=\"placeholder\"></div>\n <div class=\"selected-option\" v-text=\"selectedOption[optionText]\" v-if=\"selectedOption && !hasFocus\" />\n <input @focus=\"hasFocus=true\" autocomplete=\"off\" class=\"search\" ref=\"search\" v-model=\"search\" @keyup=\"moveToResults\" @keydown=\"removeOption\" />\n <i class=\"dropdown\" />\n <div v-if=\"showResultList\" ref=\"resultList\" class=\"result-list\">\n <div tabindex=\"0\" ref=\"result\" class=\"result\" v-for=\"result in results\" :key=\"result[optionValue]\" v-html=\"highlight(result[optionText])\" @click=\"selectOption(result)\" @keyup.prevent=\"navigateResults(result, $event)\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n props: {\n placeholder: {\n type: String, \n default: 'search',\n required: false\n },\n options: {\n type: Array, \n default: function() {\n return []\n },\n required: true\n },\n optionValue: {\n type: String, \n default: 'id',\n required: true\n },\n optionText: {\n type: String, \n default: 'name',\n required: true\n },\n value: {\n type: Object,\n default: function() {\n return null\n },\n required: false\n }\n },\n data: function() {\n return {\n hasFocus: false,\n search: null,\n selectedOption: this.value,\n selectedResult: 0\n };\n },\n mounted() {\n // Add onclick method to body to hide result list when component loses focus\n window.addEventListener(\"click\", this.loseFocus)\n },\n destroyed() {\n window.removeEventListener(\"click\", this.loseFocus)\n },\n computed: {\n results: function() {\n // Filter items on search text (if not empty, case insensitive) and when item isn't already selected (else return all items not selected)\n return this.search ? this.options.filter(i => String(i[this.optionText]).toLowerCase().indexOf(this.search.toLowerCase()) > -1) : this.options;\n },\n showResultList: function() {\n return this.hasFocus && this.results.length > 0;\n },\n showPlaceholder: function() {\n return !this.hasFocus && !this.selectedOption;\n }\n },\n watch: {\n hasFocus: function(hasFocus) {\n // Clear the search box when component loses focus\n window.removeEventListener(\"keydown\", this.stopScroll);\n if(hasFocus) {\n window.addEventListener(\"keydown\", this.stopScroll);\n this.$refs.search.focus();\n } else {\n this.search = null;\n this.selectedResult = 0;\n this.$refs.search.blur();\n }\n },\n value: function() {\n // Load selected option on prop value change\n this.selectedOption = this.options.find( option => {\n return this.value && option[this.optionValue] == this.value[this.optionValue];\n })\n },\n selectedOption: function() {\n // Provide selected item to parent\n this.$emit('input', this.selectedOption);\n },\n search: function() {\n // Provide search text to parent (for ajax fetching, etc)\n this.$emit('search', this.search);\n }\n },\n methods: {\n selectOption: function(option) {\n this.selectedOption = option;\n this.hasFocus = false;\n },\n removeOption: function(event) {\n // Remove selected option if user hits backspace on empty search field\n if(event.keyCode === 8 && (this.search == null || this.search == '')) {\n this.selectedOption = null;\n this.hasFocus = false;\n event.preventDefault();\n }\n },\n moveToResults: function(event) {\n // Move down to first result if user presses down arrow (from search field)\n if(event.keyCode === 40) {\n if(this.$refs.result.length > 0) {\n this.$refs.resultList.children.item(0).focus();\n }\n }\n },\n navigateResults: function(option, event) {\n // Add option to selected items on enter key\n if(event.keyCode === 13) {\n this.selectOption(option);\n // Move up or down items in result list with up or down arrow keys\n } else if(event.keyCode === 40 || event.keyCode === 38) {\n if(event.keyCode === 40) {\n this.selectedResult++;\n } else if(event.keyCode === 38) {\n this.selectedResult--;\n }\n let next = this.$refs.resultList.children.item(this.selectedResult);\n if(next) {\n next.focus();\n } else {\n this.selectedResult = 0;\n this.$refs.search.focus();\n }\n }\n },\n highlight: function(value) {\n // Highlights the part of each result that matches the search text\n if(this.search) {\n let matchPos = String(value).toLowerCase().indexOf(this.search.toLowerCase());\n if(matchPos > -1) {\n let matchStr = String(value).substr(matchPos, this.search.length);\n value = String(value).replace(matchStr, '<span style=\"font-weight: bold; background-color: #efefef;\">'+matchStr+'</span>');\n }\n }\n\n return value;\n },\n stopScroll: function(event) {\n if(event.keyCode === 40 || event.keyCode === 38) {\n event.preventDefault();\n }\n },\n loseFocus: function(event) {\n if(!this.$el.contains(event.target)) {\n this.hasFocus = false;\n }\n }\n }\n }\n</script>\n\n<style scoped>\n .vue-dynamic-select {\n border: 1px solid #ced4da; \n position: relative;\n padding: .375em .5em;\n border-radius: .25em;\n cursor: text;\n display: block;\n }\n .vue-dynamic-select i.dropdown {\n width: 0; \n height: 0; \n border-left: 4px solid transparent; \n border-right: 4px solid transparent; \n border-top: 4px solid; \n float: right; \n top: .75em; \n opacity: .8; \n cursor: pointer;\n }\n .vue-dynamic-select .placeholder {\n display: inline-block;\n color: #ccc;\n }\n .vue-dynamic-select .result-list {\n border: 1px solid #ced4da; \n margin: calc(.375em - 1px) calc(-.5em - 1px);\n width: calc(100% + 2px);\n min-width: calc(100% + 2px);\n border-radius: 0 0 .25em .25em;\n cursor: pointer;\n position: absolute;\n z-index: 10;\n background-color: #fff;\n }\n .vue-dynamic-select .result-list .result {\n padding: .375em .75em;\n color: #333;\n }\n .vue-dynamic-select .result-list .result:hover, .vue-dynamic-select .result-list .result:focus {\n background-color: #efefef;\n outline: none;\n }\n .vue-dynamic-select .selected-option {\n display: inline-block;\n }\n .vue-dynamic-select .search {\n border: none;\n width: 50px;\n }\n .vue-dynamic-select .search:focus {\n outline: none;\n }\n</style>\n"]}, media: undefined }); }; /* scoped */ var __vue_scope_id__ = "data-v-24d1f392"; /* module identifier */ var __vue_module_identifier__ = undefined; /* functional template */ var __vue_is_functional_template__ = false; /* style inject SSR */ var DynamicSelect = normalizeComponent_1( { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, browser, undefined ) // Declare install function executed by Vue.use() function install(Vue) { if (install.installed) { return; } install.installed = true; Vue.component('DynamicSelect', DynamicSelect); } // Create module definition for Vue.use() var plugin = { install: install, }; // Auto-install when vue is found (eg. in browser via <script> tag) var GlobalVue = null; if (typeof window !== 'undefined') { GlobalVue = window.Vue; } else if (typeof global !== 'undefined') { GlobalVue = global.Vue; } if (GlobalVue) { GlobalVue.use(plugin); } exports.install = install; exports.default = DynamicSelect; return exports; }({}));