vue-choice
Version:
a vue select/choice component that strives for native-select compatibility
910 lines (800 loc) • 25.6 kB
JavaScript
'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