UNPKG

vue-choice

Version:

a vue select/choice component that strives for native-select compatibility

910 lines (800 loc) 25.6 kB
'use strict'; function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function () {}; return { s: F, n: function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function (e) { throw e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function () { it = o[Symbol.iterator](); }, n: function () { var step = it.next(); normalCompletion = step.done; return step; }, e: function (e) { didErr = true; err = e; }, f: function () { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function unwrapExports (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var vueOnClickOutside = createCommonjsModule(function (module, exports) { (function (global, factory) { factory(exports) ; }(commonjsGlobal, (function (exports) { var registeredHandlers = []; var domListener = void 0; function on(el, event, callback) { el.addEventListener(event, callback, false); return { destroy: function destroy() { return el.removeEventListener(event, callback, false); } }; } function dynamicStrategy(el, callback) { var hasMouseOver = false; var enterListener = on(el, 'mouseenter', function () { hasMouseOver = true; }); var leaveListener = on(el, 'mouseleave', function () { hasMouseOver = false; }); return { el: el, check: function check(event) { if (!hasMouseOver) { callback(event); } }, destroy: function destroy() { enterListener.destroy(); leaveListener.destroy(); } }; } function staticStrategy(el, callback) { return { el: el, check: function check(event) { if (!el.contains(event.target)) { callback(event); } }, destroy: function destroy() {} }; } function bind(el, binding) { var callback = binding.value, modifiers = binding.modifiers; // unbind any existing listeners first unbind(el); if (!domListener) { domListener = on(document.documentElement, 'click', function (event) { registeredHandlers.forEach(function (handler) { return handler.check(event); }); }); } setTimeout(function () { registeredHandlers.push(modifiers.static ? staticStrategy(el, callback) : dynamicStrategy(el, callback)); }, 0); } function update(el, binding) { if (binding.value !== binding.oldValue) { bind(el, binding); } } function unbind(el) { var index = registeredHandlers.length - 1; while (index >= 0) { if (registeredHandlers[index].el === el) { registeredHandlers[index].destroy(); registeredHandlers.splice(index, 1); } index -= 1; } if (registeredHandlers.length === 0 && domListener) { domListener.destroy(); domListener = null; } } var directive = { bind: bind, unbind: unbind, update: update }; var mixin = { directives: { 'on-click-outside': directive } }; exports.directive = directive; exports.mixin = mixin; Object.defineProperty(exports, '__esModule', { value: true }); }))); }); var onClickOutside = unwrapExports(vueOnClickOutside); var throttleit = throttle; /** * Returns a new function that, when invoked, invokes `func` at most once per `wait` milliseconds. * * @param {Function} func Function to wrap. * @param {Number} wait Number of milliseconds that must elapse between `func` invocations. * @return {Function} A new function that wraps the `func` function passed in. */ function throttle (func, wait) { var ctx, args, rtn, timeoutID; // caching var last = 0; return function throttled () { ctx = this; args = arguments; var delta = new Date() - last; if (!timeoutID) if (delta >= wait) call(); else timeoutID = setTimeout(call, wait - delta); return rtn; }; function call () { timeoutID = 0; last = +new Date(); rtn = func.apply(ctx, args); ctx = null; args = null; } } var instanceId = 0; function isFunction(obj) { return typeof obj === 'function'; } function isBoolean(obj) { return typeof obj === 'boolean' || _typeof(obj) === 'object' && obj !== null && typeof obj.valueOf() === 'boolean'; } function isString(obj) { return typeof obj === 'string' || obj instanceof String; } function find(collection, test) { var _iterator = _createForOfIteratorHelper(collection), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var item = _step.value; if (test(item)) { return item; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } function findIndex(collection, test) { var index = 0; var _iterator2 = _createForOfIteratorHelper(collection), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var item = _step2.value; if (test(item)) { return index; } index++; } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } return -1; } function includesText(searchIn, str) { return isString(searchIn) ? searchIn.toLowerCase().indexOf(str) !== -1 : false; } var defaultFilter = function defaultFilter(term, choice) { return !term || includesText(choice.label, term) || includesText(choice.value, term) || includesText(choice.text, term); }; var typeFilter = function typeFilter(term, choice) { return choice.label.toLowerCase().indexOf(term) === 0 || choice.text.toLowerCase().indexOf(term) === 0; }; var script = { name: 'SgChoice', mixins: [onClickOutside.mixin], props: { id: { type: String, default: function _default() { return "sg-choice-".concat(instanceId++); } }, texts: { type: Object, default: function _default() { return { searchPlaceholder: 'Suche nach Einträgen', searchLabel: 'Suche', noResult: 'Keine Ergebnisse gefunden 😔', noSelection: 'Keine Auswahl' }; } }, renderer: { type: [Object, String], default: null }, defaultValue: [String, Number], defaultChoice: Object, value: { type: [Number, String], default: null }, choices: { type: Array, required: true }, filter: { type: [Boolean, Function], default: function _default() { return defaultFilter; } }, searchThreshold: { type: Number, default: 5 } }, data: function data() { return { currentValue: null, showFinder: null, currentSearch: '', typedSearch: '', typedTimer: null, size: 0, finderSize: 0, maxFinderSize: null, resizeListener: null, scrollListener: null, finderBelow: true }; }, computed: { selectClasses: function selectClasses() { return { 'sg-choice-open': this.showFinder, 'sg-choice-bottom': this.finderBelow, 'sg-choice-top': !this.finderBelow }; }, availableChoices: function availableChoices() { var term = this.currentSearch.toLowerCase(); var filter = this.filter === false || isFunction(this.filter) ? this.filter : defaultFilter; return isFunction(filter) ? this.choices.filter(function (choice) { return filter(term, choice, defaultFilter); }) : this.choices; }, componentId: function componentId() { return { wrapper: "".concat(this.id, "-wrapper"), search: "".concat(this.id, "-search") }; }, currentChoice: function currentChoice() { var _this = this; return find(this.choices, function (c) { return c.value === _this.currentValue; }) || this.defaultChoice; }, currentChoiceIndex: function currentChoiceIndex() { var _this2 = this; return findIndex(this.choices, function (c) { return c.value === _this2.currentValue; }); }, isDismissable: function isDismissable() { return this.currentChoice && this.defaultValue !== null && this.currentChoice !== this.defaultChoice; }, decoratorStyle: function decoratorStyle() { return { minHeight: "calc(".concat(this.size, "px - 1rem)") }; } }, watch: { currentValue: function currentValue(value) { var _this3 = this; setTimeout(function () { _this3.$emit('input', value); _this3.$emit('choice', _this3.currentChoice); _this3.updateSize(); }, 0); } }, created: function created() { this.currentValue = this.value || null; this.resizeListener = throttleit(this.updateSize, 250); this.scrollListener = throttleit(this.updateFinderPosition, 250); }, mounted: function mounted() { this.updateSize(); this.updateFinderPosition(); window.addEventListener('resize', this.resizeListener); window.addEventListener('scroll', this.scrollListener); }, beforeDestroy: function beforeDestroy() { window.removeEventListener('resize', this.resizeListener); window.removeEventListener('scroll', this.scrollListener); }, methods: { toggleFinder: function toggleFinder() { var _this4 = this; var focus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; var forcedState = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var currentState = this.showFinder; var newState = forcedState === null ? !this.showFinder : forcedState; if (currentState === newState) return; this.showFinder = newState; this.currentSearch = ''; if (focus && newState && this.$refs.search) { setTimeout(function () { return _this4.$refs.search.focus(); }, 0); } else if (focus && newState && this.$refs.choices.firstChild) { setTimeout(function () { return _this4.$refs.choices.firstChild.focus(); }, 0); } else if (focus && !newState && this.$refs.current) { setTimeout(function () { return _this4.$refs.current.focus(); }, 0); } }, openFinder: function openFinder(focus) { this.toggleFinder(isBoolean(focus) ? focus : true, true); }, closeFinder: function closeFinder(focus) { this.toggleFinder(isBoolean(focus) ? focus : true, false); }, maybeSelect: function maybeSelect() { if (this.availableChoices.length === 1) { this.currentValue = this.availableChoices[0].value; this.closeFinder(); } }, typeSelect: function typeSelect(event) { var _this5 = this; if (this.showFinder) return; var cidx = this.currentChoiceIndex; // reset type-search timer clearTimeout(this.typedTimer); this.typedTimer = setTimeout(function () { _this5.typedSearch = ''; }, 500); /** * emulates basic select features like: * * open on enter * * up/down navigating choices without opening select */ switch (event.keyCode) { case 13: // enter this.toggleFinder(); return; case 27: // escape this.dismissChoice(); return; case 38: // up event.preventDefault(); if (cidx > 0) { this.currentValue = this.choices[cidx - 1].value; } return; case 40: // down event.preventDefault(); if (cidx < this.choices.length - 1) { this.currentValue = this.choices[cidx + 1].value; } return; } /** * event.key is supported in modern browsers * and refers to the actual key being pressed * this code assumes that every single character * key value is a valid character in every language * * TODO: it would be nice to distinguish between actual * character classes instead of string length */ if (event.key && event.key.length === 1) { this.typedSearch += event.key; var term = this.typedSearch.toLowerCase(); var choice = this.choices.filter(typeFilter.bind(null, term))[0]; if (choice) { this.currentValue = choice.value; } } }, select: function select(choice) { if (choice) { this.currentValue = choice.value; } this.closeFinder(); }, focusChoices: function focusChoices() { this.$refs.choices.firstChild.focus(); }, nextChoice: function nextChoice(event) { (event.target.nextElementSibling || event.target.parentNode.firstChild).focus(); }, prevChoice: function prevChoice(event) { (event.target.previousElementSibling || event.target.parentNode.lastChild).focus(); }, dismissChoice: function dismissChoice() { this.currentValue = this.defaultValue; }, dismissSelect: function dismissSelect() { this.closeFinder(false); }, updateSize: function updateSize() { this.size = this.$refs.current.clientHeight; }, updateFinderPosition: function updateFinderPosition() { var _this$$refs = this.$refs, current = _this$$refs.current, finder = _this$$refs.finder; if (!finder || !current) return; if (finder.offsetHeight > 0) { this.finderSize = finder.offsetHeight; } var dropdownSize = this.finderSize || 500; var inputBounds = current.getBoundingClientRect(); var distanceFromTop = inputBounds.top; var distanceFromBottom = window.innerHeight - inputBounds.bottom; // if we show the dropdown we don’t want it to sit directly // on the edge of the screen var screenEdgeOffset = 30; var optimalDropdownSize = dropdownSize + screenEdgeOffset; // the finder should be displayed below if we have enough space there, // on the top if the place below is not sufficient but on top // or where ever there’s more space available if (distanceFromBottom > optimalDropdownSize) { this.finderBelow = true; } else if (distanceFromTop > optimalDropdownSize) { this.finderBelow = false; } else { this.finderBelow = distanceFromBottom > distanceFromTop; } // make sure that the finder does not overflow the screen this.maxFinderSize = Math.max(distanceFromBottom, distanceFromTop) - screenEdgeOffset; // this is the initial rendering to calculate the correct finder size if (this.showFinder === null) { this.showFinder = 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. const 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; } let hook; if (moduleIdentifier) { // server build hook = function (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 (context) { style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot)); } : function (context) { style.call(this, createInjector(context)); }; } if (hook) { if (options.functional) { // register for functional component in vue file const originalRender = options.render; options.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context); }; } else { // inject component registration as beforeCreate hook const existing = options.beforeCreate; options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } return script; } /* script */ var __vue_script__ = script; /* template */ var __vue_render__ = function __vue_render__() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c('div', { directives: [{ name: "on-click-outside", rawName: "v-on-click-outside", value: _vm.dismissSelect, expression: "dismissSelect" }], staticClass: "sg-choice", class: _vm.selectClasses, attrs: { "id": _vm.componentId.wrapper }, on: { "keydown": function keydown($event) { if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "esc", 27, $event.key, ["Esc", "Escape"])) { return null; } return _vm.closeFinder($event); } } }, [_c('div', { ref: "current", staticClass: "sg-choice-current", attrs: { "tabindex": "0" }, on: { "click": function click($event) { $event.preventDefault(); return _vm.toggleFinder(); }, "keydown": _vm.typeSelect } }, [_vm._t("current-choice", [_vm.currentChoice ? _c(_vm.renderer, { tag: "component", attrs: { "choice": _vm.currentChoice } }) : _vm._e()], { "choice": _vm.currentChoice }), _vm._v(" "), _vm._t("no-result", [!_vm.currentChoice ? _c('div', { staticClass: "sg-choice-default" }, [_vm._v("\n " + _vm._s(_vm.texts.noSelection) + "\n ")]) : _vm._e()]), _vm._v(" "), _vm.isDismissable ? _c('div', { staticClass: "sg-choice-decorator sg-choice-dismiss", style: _vm.decoratorStyle, on: { "click": function click($event) { $event.preventDefault(); $event.stopPropagation(); return _vm.dismissChoice($event); } } }, [_c('i', [_vm._v("×")])]) : _c('div', { staticClass: "sg-choice-decorator sg-choice-caret", style: _vm.decoratorStyle }, [_c('i', [_vm._v("▼")])])], 2), _vm._v(" "), _c('transition', { attrs: { "name": _vm.finderBelow ? 'fade-down' : 'fade-up' }, on: { "after-enter": _vm.updateFinderPosition } }, [_c('div', { directives: [{ name: "show", rawName: "v-show", value: _vm.showFinder === null ? true : _vm.showFinder, expression: "showFinder === null ? true : showFinder" }], ref: "finder", staticClass: "sg-choice-finder", style: { 'max-height': _vm.maxFinderSize + "px" } }, [_vm.filter && _vm.choices.length > _vm.searchThreshold ? _c('div', { staticClass: "sg-choice-search" }, [_c('label', { staticClass: "sr-only", attrs: { "for": _vm.componentId.search } }, [_vm._v(_vm._s(_vm.texts.searchLabel))]), _vm._v(" "), _c('input', { directives: [{ name: "model", rawName: "v-model", value: _vm.currentSearch, expression: "currentSearch" }], ref: "search", staticClass: "sg-choice-search-input", attrs: { "id": _vm.componentId.search, "type": "search", "placeholder": _vm.texts.searchPlaceholder }, domProps: { "value": _vm.currentSearch }, on: { "keydown": [function ($event) { if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "down", 40, $event.key, ["Down", "ArrowDown"])) { return null; } $event.preventDefault(); return _vm.focusChoices($event); }, function ($event) { if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "enter", 13, $event.key, "Enter")) { return null; } $event.preventDefault(); return _vm.maybeSelect($event); }], "input": function input($event) { if ($event.target.composing) { return; } _vm.currentSearch = $event.target.value; } } }), _vm._v(" "), _c('span', { staticClass: "sg-choice-search-count" }, [_vm._v(_vm._s(_vm.availableChoices.length))])]) : _vm._e(), _vm._v(" "), _vm.availableChoices.length > 0 ? _c('ol', { ref: "choices", staticClass: "sg-choice-choices", on: { "keydown": [function ($event) { if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "up", 38, $event.key, ["Up", "ArrowUp"])) { return null; } $event.preventDefault(); return _vm.prevChoice($event); }, function ($event) { if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "down", 40, $event.key, ["Down", "ArrowDown"])) { return null; } $event.preventDefault(); return _vm.nextChoice($event); }] } }, _vm._l(_vm.availableChoices, function (choice, index) { return _c('li', { key: index, attrs: { "data-value": choice.value, "tabindex": "0" }, on: { "click": function click($event) { return _vm.select(choice); }, "keydown": function keydown($event) { if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "enter", 13, $event.key, "Enter")) { return null; } return _vm.select(choice); } } }, [_vm._t("choice", [_c(_vm.renderer, { tag: "component", attrs: { "choice": choice, "index": index } })], { "choice": choice })], 2); }), 0) : _c('p', { staticClass: "sg-choice-no-results" }, [_vm._v("\n " + _vm._s(_vm.texts.noResult) + "\n ")])])])], 1); }; var __vue_staticRenderFns__ = []; /* style */ var __vue_inject_styles__ = undefined; /* scoped */ var __vue_scope_id__ = undefined; /* module identifier */ var __vue_module_identifier__ = undefined; /* functional template */ var __vue_is_functional_template__ = false; /* style inject */ /* style inject SSR */ /* style inject shadow dom */ var __vue_component__ = normalizeComponent({ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, false, undefined, undefined, undefined); module.exports = __vue_component__; //# sourceMappingURL=vue-choice.cjs.js.map