├── .gitignore
├── src
├── images
│ └── dynamic-select.png
├── index.js
└── DynamicSelect.vue
├── LICENSE
├── package.json
├── README.md
└── dist
├── vue-dynamic-select.esm.js
├── vue-dynamic-select.min.js
└── vue-dynamic-select.umd.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/src/images/dynamic-select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/silasmontgomery/vue-dynamic-select/HEAD/src/images/dynamic-select.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import DynamicSelect from './DynamicSelect.vue'
2 |
3 | // Declare install function executed by Vue.use()
4 | export function install(Vue) {
5 | if (install.installed) return;
6 | install.installed = true;
7 | Vue.component('DynamicSelect', DynamicSelect);
8 | }
9 |
10 | // Create module definition for Vue.use()
11 | const plugin = {
12 | install,
13 | };
14 |
15 | // Auto-install when vue is found (eg. in browser via
169 |
170 |
224 |
--------------------------------------------------------------------------------
/dist/vue-dynamic-select.esm.js:
--------------------------------------------------------------------------------
1 | //
2 | //
3 | //
4 | //
5 | //
6 | //
7 | //
8 | //
9 | //
10 | //
11 | //
12 | //
13 | //
14 | //
15 |
16 | var script = {
17 | props: {
18 | placeholder: {
19 | type: String,
20 | default: 'search',
21 | required: false
22 | },
23 | options: {
24 | type: Array,
25 | default: function() {
26 | return []
27 | },
28 | required: true
29 | },
30 | optionValue: {
31 | type: String,
32 | default: 'id',
33 | required: true
34 | },
35 | optionText: {
36 | type: String,
37 | default: 'name',
38 | required: true
39 | },
40 | value: {
41 | type: Object,
42 | default: function() {
43 | return null
44 | },
45 | required: false
46 | }
47 | },
48 | data: function() {
49 | return {
50 | hasFocus: false,
51 | search: null,
52 | selectedOption: this.value,
53 | selectedResult: 0
54 | };
55 | },
56 | mounted: function mounted() {
57 | // Add onclick method to body to hide result list when component loses focus
58 | window.addEventListener("click", this.loseFocus);
59 | },
60 | destroyed: function destroyed() {
61 | window.removeEventListener("click", this.loseFocus);
62 | },
63 | computed: {
64 | results: function() {
65 | var this$1 = this;
66 |
67 | // Filter items on search text (if not empty, case insensitive) and when item isn't already selected (else return all items not selected)
68 | return this.search ? this.options.filter(function (i) { return String(i[this$1.optionText]).toLowerCase().indexOf(this$1.search.toLowerCase()) > -1; }) : this.options;
69 | },
70 | showResultList: function() {
71 | return this.hasFocus && this.results.length > 0;
72 | },
73 | showPlaceholder: function() {
74 | return !this.hasFocus && !this.selectedOption;
75 | }
76 | },
77 | watch: {
78 | hasFocus: function(hasFocus) {
79 | // Clear the search box when component loses focus
80 | window.removeEventListener("keydown", this.stopScroll);
81 | if(hasFocus) {
82 | window.addEventListener("keydown", this.stopScroll);
83 | this.$refs.search.focus();
84 | } else {
85 | this.search = null;
86 | this.selectedResult = 0;
87 | this.$refs.search.blur();
88 | }
89 | },
90 | value: function() {
91 | var this$1 = this;
92 |
93 | // Load selected option on prop value change
94 | this.selectedOption = this.options.find( function (option) {
95 | return this$1.value && option[this$1.optionValue] == this$1.value[this$1.optionValue];
96 | });
97 | },
98 | selectedOption: function() {
99 | // Provide selected item to parent
100 | this.$emit('input', this.selectedOption);
101 | },
102 | search: function() {
103 | // Provide search text to parent (for ajax fetching, etc)
104 | this.$emit('search', this.search);
105 | }
106 | },
107 | methods: {
108 | selectOption: function(option) {
109 | this.selectedOption = option;
110 | this.hasFocus = false;
111 | },
112 | removeOption: function(event) {
113 | // Remove selected option if user hits backspace on empty search field
114 | if(event.keyCode === 8 && (this.search == null || this.search == '')) {
115 | this.selectedOption = null;
116 | this.hasFocus = false;
117 | event.preventDefault();
118 | }
119 | },
120 | moveToResults: function(event) {
121 | // Move down to first result if user presses down arrow (from search field)
122 | if(event.keyCode === 40) {
123 | if(this.$refs.result.length > 0) {
124 | this.$refs.resultList.children.item(0).focus();
125 | }
126 | }
127 | },
128 | navigateResults: function(option, event) {
129 | // Add option to selected items on enter key
130 | if(event.keyCode === 13) {
131 | this.selectOption(option);
132 | // Move up or down items in result list with up or down arrow keys
133 | } else if(event.keyCode === 40 || event.keyCode === 38) {
134 | if(event.keyCode === 40) {
135 | this.selectedResult++;
136 | } else if(event.keyCode === 38) {
137 | this.selectedResult--;
138 | }
139 | var next = this.$refs.resultList.children.item(this.selectedResult);
140 | if(next) {
141 | next.focus();
142 | } else {
143 | this.selectedResult = 0;
144 | this.$refs.search.focus();
145 | }
146 | }
147 | },
148 | highlight: function(value) {
149 | // Highlights the part of each result that matches the search text
150 | if(this.search) {
151 | var matchPos = String(value).toLowerCase().indexOf(this.search.toLowerCase());
152 | if(matchPos > -1) {
153 | var matchStr = String(value).substr(matchPos, this.search.length);
154 | value = String(value).replace(matchStr, ''+matchStr+'');
155 | }
156 | }
157 |
158 | return value;
159 | },
160 | stopScroll: function(event) {
161 | if(event.keyCode === 40 || event.keyCode === 38) {
162 | event.preventDefault();
163 | }
164 | },
165 | loseFocus: function(event) {
166 | if(!this.$el.contains(event.target)) {
167 | this.hasFocus = false;
168 | }
169 | }
170 | }
171 | }
172 |
173 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
174 | /* server only */
175 | , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
176 | if (typeof shadowMode !== 'boolean') {
177 | createInjectorSSR = createInjector;
178 | createInjector = shadowMode;
179 | shadowMode = false;
180 | } // Vue.extend constructor export interop.
181 |
182 |
183 | var options = typeof script === 'function' ? script.options : script; // render functions
184 |
185 | if (template && template.render) {
186 | options.render = template.render;
187 | options.staticRenderFns = template.staticRenderFns;
188 | options._compiled = true; // functional template
189 |
190 | if (isFunctionalTemplate) {
191 | options.functional = true;
192 | }
193 | } // scopedId
194 |
195 |
196 | if (scopeId) {
197 | options._scopeId = scopeId;
198 | }
199 |
200 | var hook;
201 |
202 | if (moduleIdentifier) {
203 | // server build
204 | hook = function hook(context) {
205 | // 2.3 injection
206 | context = context || // cached call
207 | this.$vnode && this.$vnode.ssrContext || // stateful
208 | this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional
209 | // 2.2 with runInNewContext: true
210 |
211 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
212 | context = __VUE_SSR_CONTEXT__;
213 | } // inject component styles
214 |
215 |
216 | if (style) {
217 | style.call(this, createInjectorSSR(context));
218 | } // register component module identifier for async chunk inference
219 |
220 |
221 | if (context && context._registeredComponents) {
222 | context._registeredComponents.add(moduleIdentifier);
223 | }
224 | }; // used by ssr in case component is cached and beforeCreate
225 | // never gets called
226 |
227 |
228 | options._ssrRegister = hook;
229 | } else if (style) {
230 | hook = shadowMode ? function () {
231 | style.call(this, createInjectorShadow(this.$root.$options.shadowRoot));
232 | } : function (context) {
233 | style.call(this, createInjector(context));
234 | };
235 | }
236 |
237 | if (hook) {
238 | if (options.functional) {
239 | // register for functional component in vue file
240 | var originalRender = options.render;
241 |
242 | options.render = function renderWithStyleInjection(h, context) {
243 | hook.call(context);
244 | return originalRender(h, context);
245 | };
246 | } else {
247 | // inject component registration as beforeCreate hook
248 | var existing = options.beforeCreate;
249 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
250 | }
251 | }
252 |
253 | return script;
254 | }
255 |
256 | var normalizeComponent_1 = normalizeComponent;
257 |
258 | var isOldIE = typeof navigator !== 'undefined' && /msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());
259 | function createInjector(context) {
260 | return function (id, style) {
261 | return addStyle(id, style);
262 | };
263 | }
264 | var HEAD = document.head || document.getElementsByTagName('head')[0];
265 | var styles = {};
266 |
267 | function addStyle(id, css) {
268 | var group = isOldIE ? css.media || 'default' : id;
269 | var style = styles[group] || (styles[group] = {
270 | ids: new Set(),
271 | styles: []
272 | });
273 |
274 | if (!style.ids.has(id)) {
275 | style.ids.add(id);
276 | var code = css.source;
277 |
278 | if (css.map) {
279 | // https://developer.chrome.com/devtools/docs/javascript-debugging
280 | // this makes source maps inside style tags work properly in Chrome
281 | code += '\n/*# sourceURL=' + css.map.sources[0] + ' */'; // http://stackoverflow.com/a/26603875
282 |
283 | code += '\n/*# sourceMappingURL=data:application/json;base64,' + btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + ' */';
284 | }
285 |
286 | if (!style.element) {
287 | style.element = document.createElement('style');
288 | style.element.type = 'text/css';
289 | if (css.media) { style.element.setAttribute('media', css.media); }
290 | HEAD.appendChild(style.element);
291 | }
292 |
293 | if ('styleSheet' in style.element) {
294 | style.styles.push(code);
295 | style.element.styleSheet.cssText = style.styles.filter(Boolean).join('\n');
296 | } else {
297 | var index = style.ids.size - 1;
298 | var textNode = document.createTextNode(code);
299 | var nodes = style.element.childNodes;
300 | if (nodes[index]) { style.element.removeChild(nodes[index]); }
301 | if (nodes.length) { style.element.insertBefore(textNode, nodes[index]); }else { style.element.appendChild(textNode); }
302 | }
303 | }
304 | }
305 |
306 | var browser = createInjector;
307 |
308 | /* script */
309 | var __vue_script__ = script;
310 |
311 | /* template */
312 | var __vue_render__ = function() {
313 | var _vm = this;
314 | var _h = _vm.$createElement;
315 | var _c = _vm._self._c || _h;
316 | return _c("div", [
317 | _c(
318 | "div",
319 | {
320 | staticClass: "vue-dynamic-select",
321 | attrs: { tabindex: "0" },
322 | on: {
323 | focusin: function($event) {
324 | _vm.hasFocus = true;
325 | }
326 | }
327 | },
328 | [
329 | _vm.showPlaceholder
330 | ? _c("div", {
331 | staticClass: "placeholder",
332 | domProps: { textContent: _vm._s(_vm.placeholder) }
333 | })
334 | : _vm._e(),
335 | _vm._v(" "),
336 | _vm.selectedOption && !_vm.hasFocus
337 | ? _c("div", {
338 | staticClass: "selected-option",
339 | domProps: {
340 | textContent: _vm._s(_vm.selectedOption[_vm.optionText])
341 | }
342 | })
343 | : _vm._e(),
344 | _vm._v(" "),
345 | _c("input", {
346 | directives: [
347 | {
348 | name: "model",
349 | rawName: "v-model",
350 | value: _vm.search,
351 | expression: "search"
352 | }
353 | ],
354 | ref: "search",
355 | staticClass: "search",
356 | attrs: { autocomplete: "off" },
357 | domProps: { value: _vm.search },
358 | on: {
359 | focus: function($event) {
360 | _vm.hasFocus = true;
361 | },
362 | keyup: _vm.moveToResults,
363 | keydown: _vm.removeOption,
364 | input: function($event) {
365 | if ($event.target.composing) {
366 | return
367 | }
368 | _vm.search = $event.target.value;
369 | }
370 | }
371 | }),
372 | _vm._v(" "),
373 | _c("i", { staticClass: "dropdown" }),
374 | _vm._v(" "),
375 | _vm.showResultList
376 | ? _c(
377 | "div",
378 | { ref: "resultList", staticClass: "result-list" },
379 | _vm._l(_vm.results, function(result) {
380 | return _c("div", {
381 | key: result[_vm.optionValue],
382 | ref: "result",
383 | refInFor: true,
384 | staticClass: "result",
385 | attrs: { tabindex: "0" },
386 | domProps: {
387 | innerHTML: _vm._s(_vm.highlight(result[_vm.optionText]))
388 | },
389 | on: {
390 | click: function($event) {
391 | return _vm.selectOption(result)
392 | },
393 | keyup: function($event) {
394 | $event.preventDefault();
395 | return _vm.navigateResults(result, $event)
396 | }
397 | }
398 | })
399 | }),
400 | 0
401 | )
402 | : _vm._e()
403 | ]
404 | )
405 | ])
406 | };
407 | var __vue_staticRenderFns__ = [];
408 | __vue_render__._withStripped = true;
409 |
410 | /* style */
411 | var __vue_inject_styles__ = function (inject) {
412 | if (!inject) { return }
413 | 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":["\n \n\n\n\n\n\n"]}, media: undefined });
414 |
415 | };
416 | /* scoped */
417 | var __vue_scope_id__ = "data-v-24d1f392";
418 | /* module identifier */
419 | var __vue_module_identifier__ = undefined;
420 | /* functional template */
421 | var __vue_is_functional_template__ = false;
422 | /* style inject SSR */
423 |
424 |
425 |
426 | var DynamicSelect = normalizeComponent_1(
427 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
428 | __vue_inject_styles__,
429 | __vue_script__,
430 | __vue_scope_id__,
431 | __vue_is_functional_template__,
432 | __vue_module_identifier__,
433 | browser,
434 | undefined
435 | )
436 |
437 | // Declare install function executed by Vue.use()
438 | function install(Vue) {
439 | if (install.installed) { return; }
440 | install.installed = true;
441 | Vue.component('DynamicSelect', DynamicSelect);
442 | }
443 |
444 | // Create module definition for Vue.use()
445 | var plugin = {
446 | install: install,
447 | };
448 |
449 | // Auto-install when vue is found (eg. in browser via \n\n\n"]}, media: undefined });
417 |
418 | };
419 | /* scoped */
420 | var __vue_scope_id__ = "data-v-24d1f392";
421 | /* module identifier */
422 | var __vue_module_identifier__ = undefined;
423 | /* functional template */
424 | var __vue_is_functional_template__ = false;
425 | /* style inject SSR */
426 |
427 |
428 |
429 | var DynamicSelect = normalizeComponent_1(
430 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
431 | __vue_inject_styles__,
432 | __vue_script__,
433 | __vue_scope_id__,
434 | __vue_is_functional_template__,
435 | __vue_module_identifier__,
436 | browser,
437 | undefined
438 | )
439 |
440 | // Declare install function executed by Vue.use()
441 | function install(Vue) {
442 | if (install.installed) { return; }
443 | install.installed = true;
444 | Vue.component('DynamicSelect', DynamicSelect);
445 | }
446 |
447 | // Create module definition for Vue.use()
448 | var plugin = {
449 | install: install,
450 | };
451 |
452 | // Auto-install when vue is found (eg. in browser via \n\n\n"]}, media: undefined });
420 |
421 | };
422 | /* scoped */
423 | var __vue_scope_id__ = "data-v-24d1f392";
424 | /* module identifier */
425 | var __vue_module_identifier__ = undefined;
426 | /* functional template */
427 | var __vue_is_functional_template__ = false;
428 | /* style inject SSR */
429 |
430 |
431 |
432 | var DynamicSelect = normalizeComponent_1(
433 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
434 | __vue_inject_styles__,
435 | __vue_script__,
436 | __vue_scope_id__,
437 | __vue_is_functional_template__,
438 | __vue_module_identifier__,
439 | browser,
440 | undefined
441 | )
442 |
443 | // Declare install function executed by Vue.use()
444 | function install(Vue) {
445 | if (install.installed) { return; }
446 | install.installed = true;
447 | Vue.component('DynamicSelect', DynamicSelect);
448 | }
449 |
450 | // Create module definition for Vue.use()
451 | var plugin = {
452 | install: install,
453 | };
454 |
455 | // Auto-install when vue is found (eg. in browser via