diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..69272f33 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "allowParens": "avoid" +} diff --git a/build/index.js b/build/index.js index 87e699e6..f8285cf7 100644 --- a/build/index.js +++ b/build/index.js @@ -1,18 +1,20 @@ -const rollup = require('rollup').rollup; -const bundles = require('./bundles'); -const config = require('./rollup.config'); -const fs = require('fs-extra'); +const rollup = require('rollup').rollup +const bundles = require('./bundles') +const config = require('./rollup.config') +const fs = require('fs-extra') const roll = (format, name, conf) => { - rollup( - config(conf.compress, conf.polyfills, conf.autoDefine) - ).then(bundle => bundle - .write({ - format, - name: 'VueSimpleSuggest', - file: 'dist/' + name + '.js' - }) - ); + rollup(config(conf.compress, conf.polyfills, conf.autoDefine)).then( + (bundle) => + bundle.write({ + format, + name: 'VueSimpleSuggest', + file: 'dist/' + name + '.js', + globals: { + vue: 'Vue' + } + }) + ) } if (fs.pathExistsSync('dist')) { @@ -20,5 +22,5 @@ if (fs.pathExistsSync('dist')) { } for (const format in bundles) { - roll(format.replace(/\d+/g, ''), format, bundles[format]); + roll(format.replace(/\d+/g, ''), format, bundles[format]) } diff --git a/build/rollup.config.js b/build/rollup.config.js index 17923337..503f0c32 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -1,11 +1,11 @@ -const vue = require('rollup-plugin-vue2'); -const css = require('rollup-plugin-css-only'); -const babel = require('rollup-plugin-babel'); -const nodeResolve = require('rollup-plugin-node-resolve'); -const commonjs = require('rollup-plugin-commonjs'); -const { uglify } = require('rollup-plugin-uglify'); +const vue = require('rollup-plugin-vue') +const css = require('rollup-plugin-css-only') +const babel = require('rollup-plugin-babel') +const nodeResolve = require('rollup-plugin-node-resolve') +const commonjs = require('rollup-plugin-commonjs') +const { uglify } = require('rollup-plugin-uglify') -module.exports = exports = function( +module.exports = exports = function ( compress = false, polyfills = { arrows: true, @@ -15,7 +15,7 @@ module.exports = exports = function( }, defineInWindow = false ) { - const babelPlugins = []; + const babelPlugins = [] if (polyfills.assign) { babelPlugins.push('transform-object-assign') @@ -31,23 +31,24 @@ module.exports = exports = function( const plugins = [ vue(), - css({ output: 'dist/styles.css' }), + css({ output: 'styles.css' }), babel({ exclude: 'node_modules/**', runtimeHelpers: false, presets: polyfills.arrows ? ['stage-3', 'es2015-rollup'] : [], plugins: babelPlugins }), - nodeResolve({ mainFields: ['browser', 'jsnext:main', 'main'] }), + nodeResolve(), commonjs() - ]; + ] if (compress) { - plugins.push(uglify()); + plugins.push(uglify()) } return { input: defineInWindow ? 'lib/window.js' : 'lib/index.js', - plugins - }; + plugins, + external: ['vue'] + } } diff --git a/dist/cjs.js b/dist/cjs.js index 6b309fad..9516cb67 100644 --- a/dist/cjs.js +++ b/dist/cjs.js @@ -1,5 +1,7 @@ 'use strict'; +var vue = require('vue'); + var defaultControls = { selectionUp: [38], selectionDown: [40], @@ -41,19 +43,52 @@ function hasKeyCodeByCode(arr, keyCode) { } } +var onRE = /^on[^a-z]/; + +function isOn(key) { + return onRE.test(key); +} + +function getPropertyByAttribute(obj, attr) { + return typeof obj !== 'undefined' ? fromPath(obj, attr) : obj; +} + +function display(obj, attribute, isPlainSuggestion) { + if (isPlainSuggestion) { + return obj; + } + + var display = getPropertyByAttribute(obj, attribute); + + if (typeof display === 'undefined') { + display = JSON.stringify(obj); + + if (process && ~process.env.NODE_ENV.indexOf('dev')) { + console.warn('[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.'); + } + } + + return String(display || ''); +} + +var HAS_WINDOW_SUPPORT = typeof window !== 'undefined'; + +var requestAF = HAS_WINDOW_SUPPORT ? window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || +// Fallback, but not a true polyfill +// Only needed for Opera Mini +function (cb) { + return setTimeout(cb, 16); +} : function (cb) { + return setTimeout(cb, 0); +}; + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; -function _await(value, then, direct) { - if (direct) { - return then ? then(value) : value; - }if (!value || !value.then) { - value = Promise.resolve(value); - }return then ? value.then(then) : value; -}function _empty() {}function _awaitIgnored(value, direct) { +function _empty() {}function _awaitIgnored(value, direct) { if (!direct) { return value && value.then ? value.then(_empty) : Promise.resolve(); } @@ -61,12 +96,17 @@ function _await(value, then, direct) { var result = body();if (result && result.then) { return result.then(then); }return then(result); +}function _await(value, then, direct) { + if (direct) { + return then ? then(value) : value; + }if (!value || !value.then) { + value = Promise.resolve(value); + }return then ? value.then(then) : value; }function _invokeIgnored(body) { var result = body();if (result && result.then) { return result.then(_empty); } -} -function _catch(body, recover) { +}function _catch(body, recover) { try { var result = body(); } catch (e) { @@ -82,34 +122,9 @@ function _catch(body, recover) { }if (result && result.then) { return result.then(finalizer, finalizer); }return finalizer(); -}var VueSimpleSuggest = { - render: function render() { - var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('div', { staticClass: "vue-simple-suggest", class: [_vm.styles.vueSimpleSuggest, { designed: !_vm.destyled, focus: _vm.isInFocus }], on: { "keydown": function keydown($event) { - if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "tab", 9, $event.key, "Tab")) { - return null; - }_vm.isTabbed = true; - } } }, [_c('div', { ref: "inputSlot", staticClass: "input-wrapper", class: _vm.styles.inputWrapper, attrs: { "role": "combobox", "aria-haspopup": "listbox", "aria-owns": _vm.listId, "aria-expanded": !!_vm.listShown && !_vm.removeList ? 'true' : 'false' } }, [_vm._t("default", [_c('input', _vm._b({ staticClass: "default-input", class: _vm.styles.defaultInput, domProps: { "value": _vm.text || '' } }, 'input', _vm.$attrs, false))])], 2), _vm._v(" "), _c('transition', { attrs: { "name": "vue-simple-suggest" } }, [!!_vm.listShown && !_vm.removeList ? _c('ul', { staticClass: "suggestions", class: _vm.styles.suggestions, attrs: { "id": _vm.listId, "role": "listbox", "aria-labelledby": _vm.listId } }, [!!this.$scopedSlots['misc-item-above'] ? _c('li', { class: _vm.styles.miscItemAbove }, [_vm._t("misc-item-above", null, { "suggestions": _vm.suggestions, "query": _vm.text })], 2) : _vm._e(), _vm._v(" "), _vm._l(_vm.suggestions, function (suggestion, index) { - return _c('li', { key: _vm.getId(suggestion, index), staticClass: "suggest-item", class: [_vm.styles.suggestItem, { - selected: _vm.isSelected(suggestion), - hover: _vm.isHovered(suggestion) - }], attrs: { "role": "option", "aria-selected": _vm.isHovered(suggestion) || _vm.isSelected(suggestion) ? 'true' : 'false', "id": _vm.getId(suggestion, index) }, on: { "mouseenter": function mouseenter($event) { - return _vm.hover(suggestion, $event.target); - }, "mouseleave": function mouseleave($event) { - return _vm.hover(undefined); - }, "click": function click($event) { - return _vm.suggestionClick(suggestion, $event); - } } }, [_vm._t("suggestion-item", [_c('span', [_vm._v(_vm._s(_vm.displayProperty(suggestion)))])], { "autocomplete": function autocomplete() { - return _vm.autocompleteText(suggestion); - }, "suggestion": suggestion, "query": _vm.text })], 2); - }), _vm._v(" "), !!this.$scopedSlots['misc-item-below'] ? _c('li', { class: _vm.styles.miscItemBelow }, [_vm._t("misc-item-below", null, { "suggestions": _vm.suggestions, "query": _vm.text })], 2) : _vm._e()], 2) : _vm._e()])], 1); - }, - staticRenderFns: [], +}var script = { name: 'vue-simple-suggest', inheritAttrs: false, - model: { - prop: 'value', - event: 'input' - }, props: { styles: { type: Object, @@ -158,10 +173,7 @@ function _catch(body, recover) { default: false }, filter: { - type: Function, - default: function _default(el, value) { - return value ? ~this.displayProperty(el).toLowerCase().indexOf(value.toLowerCase()) : true; - } + type: Function }, debounce: { type: Number, @@ -171,7 +183,8 @@ function _catch(body, recover) { type: Boolean, default: false }, - value: {}, + modelValue: {}, + modelSelect: {}, mode: { type: String, default: 'input', @@ -184,42 +197,64 @@ function _catch(body, recover) { default: false } }, - // Handle run-time mode changes (now working): watch: { mode: { - handler: function handler(current, old) { + handler: function handler() { var _this = this; - this.constructor.options.model.event = current; - // Can be null if the component is root this.$parent && this.$parent.$forceUpdate(); this.$nextTick(function () { - if (current === 'input') { - _this.$emit('input', _this.text); - } else { - _this.$emit('select', _this.selected); - } + _this.$emit('update:modelValue', _this.text); + _this.$emit('update:modelSelect', _this.selected); + // For backward compatibility: + _this.$emit('select', _this.selected); }); }, immediate: true }, - value: { + filter: { + handler: function handler(current) { + var _this2 = this; + + this.filterResult = current != null ? current : function (el, value) { + return value ? ~display(el, _this2.displayAttribute).toLowerCase().indexOf(value.toLowerCase()) : true; + }; + }, + + immediate: true + }, + modelValue: { + handler: function handler(current) { + if (this.mode === 'input') { + if (typeof current !== 'string') { + current = this.displayProperty(current); + } + this.updateTextOutside(current); + } + }, + + immediate: true + }, + modelSelect: { handler: function handler(current) { - if (typeof current !== 'string') { - current = this.displayProperty(current); + if (this.mode === 'select') { + if (typeof current !== 'string') { + current = this.displayProperty(current); + } + this.updateTextOutside(current); } - this.updateTextOutside(current); }, immediate: true } }, // - data: function data() { + data: function data(vm) { return { + filterResult: null, selected: null, hovered: null, suggestions: [], @@ -227,14 +262,14 @@ function _catch(body, recover) { inputElement: null, canSend: true, timeoutInstance: null, - text: this.value, + text: vm.modelValue, isPlainSuggestion: false, isClicking: false, isInFocus: false, isFalseFocus: false, isTabbed: false, controlScheme: {}, - listId: this._uid + '-suggestions' + listId: this.$.uid + '-suggestions' }; }, @@ -242,18 +277,6 @@ function _catch(body, recover) { listIsRequest: function listIsRequest() { return typeof this.list === 'function'; }, - inputIsComponent: function inputIsComponent() { - return this.$slots.default && this.$slots.default.length > 0 && !!this.$slots.default[0].componentInstance; - }, - input: function input() { - return this.inputIsComponent ? this.$slots.default[0].componentInstance : this.inputElement; - }, - on: function on() { - return this.inputIsComponent ? '$on' : 'addEventListener'; - }, - off: function off() { - return this.inputIsComponent ? '$off' : 'removeEventListener'; - }, hoveredIndex: function hoveredIndex() { for (var i = 0; i < this.suggestions.length; i++) { var el = this.suggestions[i]; @@ -264,38 +287,58 @@ function _catch(body, recover) { return -1; }, textLength: function textLength() { - return this.text && this.text.length || this.inputElement.value.length || 0; + return this.text && this.text.length || this.inputElement && this.inputElement.value.length || 0; }, isSelectedUpToDate: function isSelectedUpToDate() { return !!this.selected && this.displayProperty(this.selected) === this.text; + }, + attrsWithoutListeners: function attrsWithoutListeners() { + var _this3 = this; + + var o = {}; + Object.keys(this.$attrs).forEach(function (key) { + return !isOn(key) && (o[key] = _this3.$attrs[key]); + }); + return o; + }, + field: function field() { + return Object.assign({}, this.attrsWithoutListeners, { + onBlur: this.onBlur, + onFocus: this.onFocus, + onInput: this.onInput, + onClick: this.showSuggestions, + onKeydown: this.onKeyDown, + onKeyup: this.onListKeyUp + }); + }, + componentField: function componentField() { + return Object.assign({}, this.attrsWithoutListeners, { + onBlur: this.onBlur, + onFocus: this.onFocus, + 'onUpdate:modelValue': this.onInput, + onClick: this.showSuggestions, + onKeydown: this.onKeyDown, + onKeyup: this.onListKeyUp + }); } }, created: function created() { this.controlScheme = Object.assign({}, defaultControls, this.controls); }, mounted: function mounted() { - try { - var _this3 = this; - - return _await(_this3.$slots.default, function () { + var _this4 = this; - _this3.$nextTick(function () { - _this3.inputElement = _this3.$refs['inputSlot'].querySelector('input'); + this.$nextTick(function () { + return requestAF(function () { + _this4.inputElement = _this4.$refs['inputSlot'].querySelector('input'); - if (_this3.inputElement) { - _this3.setInputAriaAttributes(); - _this3.prepareEventHandlers(true); - } else { - console.error('No input element found'); - } - }); + if (_this4.inputElement) { + _this4.setInputAriaAttributes(); + } else { + console.error('No input element found'); + } }); - } catch (e) { - return Promise.reject(e); - } - }, - beforeDestroy: function beforeDestroy() { - this.prepareEventHandlers(false); + }); }, methods: { @@ -309,46 +352,31 @@ function _catch(body, recover) { return this.isEqual(suggestion, this.hovered); }, setInputAriaAttributes: function setInputAriaAttributes() { - this.inputElement.setAttribute('aria-activedescendant', ''); - this.inputElement.setAttribute('aria-autocomplete', 'list'); - this.inputElement.setAttribute('aria-controls', this.listId); - }, - prepareEventHandlers: function prepareEventHandlers(enable) { - var binder = this[enable ? 'on' : 'off']; - var keyEventsList = { - click: this.showSuggestions, - keydown: this.onKeyDown, - keyup: this.onListKeyUp - }; - var eventsList = Object.assign({ - blur: this.onBlur, - focus: this.onFocus, - input: this.onInput - }, keyEventsList); - - for (var event in eventsList) { - this.input[binder](event, eventsList[event]); - } - - var listenerBinder = enable ? 'addEventListener' : 'removeEventListener'; - - for (var _event in keyEventsList) { - this.inputElement[listenerBinder](_event, keyEventsList[_event]); + if (this.inputElement) { + this.inputElement.setAttribute('aria-activedescendant', ''); + this.inputElement.setAttribute('aria-autocomplete', 'list'); + this.inputElement.setAttribute('aria-controls', this.listId); } }, isScopedSlotEmpty: function isScopedSlotEmpty(slot) { if (slot) { - var vNode = slot(this); - return !(Array.isArray(vNode) || vNode && (vNode.tag || vNode.context || vNode.text || vNode.children)); + var slotContent = slot(this); + // https://github.com/vuejs/core/issues/4733#issuecomment-1024816095 + // https://github.com/vuejs/core/issues/3056#issuecomment-786560172 + return slotContent.some(function (vnode) { + if (vnode.type === Comment) return false; + if (Array.isArray(vnode.children) && !vnode.children.length) return false; + return vnode.type !== Text || typeof vnode.children === 'string' && vnode.children.trim() !== ''; + }); } return true; }, miscSlotsAreEmpty: function miscSlotsAreEmpty() { - var _this4 = this; + var _this5 = this; var slots = ['misc-item-above', 'misc-item-below'].map(function (s) { - return _this4.$scopedSlots[s]; + return _this5.$slots[s]; }); if (slots.every(function (s) { @@ -363,32 +391,15 @@ function _catch(body, recover) { return this.isScopedSlotEmpty.call(this, slot); }, - getPropertyByAttribute: function getPropertyByAttribute(obj, attr) { - return this.isPlainSuggestion ? obj : (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) !== undefined ? fromPath(obj, attr) : obj; - }, - displayProperty: function displayProperty(obj) { - if (this.isPlainSuggestion) { - return obj; - } - - var display = this.getPropertyByAttribute(obj, this.displayAttribute); - - if (typeof display === 'undefined') { - display = JSON.stringify(obj); - - if (process && ~process.env.NODE_ENV.indexOf('dev')) { - console.warn('[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.'); - } - } - - return String(display || ''); + displayProperty: function displayProperty(suggestion) { + return display(suggestion, this.displayAttribute); }, valueProperty: function valueProperty(obj) { if (this.isPlainSuggestion) { return obj; } - var value = this.getPropertyByAttribute(obj, this.valueAttribute); + var value = getPropertyByAttribute(obj, this.valueAttribute); if (typeof value === 'undefined') { console.error('[vue-simple-suggest]: Please, check if you passed \'value-attribute\' (default is \'id\') and \'display-attribute\' (default is \'title\') props correctly.\n Your list objects should always contain a unique identifier.'); @@ -400,17 +411,21 @@ function _catch(body, recover) { this.setText(this.displayProperty(suggestion)); }, setText: function setText(text) { - var _this5 = this; + var _this6 = this; this.$nextTick(function () { - _this5.inputElement.value = text; - _this5.text = text; - _this5.$emit('input', text); + if (_this6.inputElement) { + _this6.inputElement.value = text; + } + _this6.text = text; + _this6.$emit('update:modelValue', text); }); }, select: function select(item) { - if (this.selected !== item || this.nullableSelect && !item) { + if (item && this.selected !== item || this.nullableSelect && !item) { this.selected = item; + this.$emit('update:modelSelect', item); + // For backward compatibility: this.$emit('select', item); if (item) { @@ -421,9 +436,11 @@ function _catch(body, recover) { this.hover(null); }, hover: function hover(item, elem) { - var elemId = !!item ? this.getId(item, this.hoveredIndex) : ''; + var elemId = item ? this.getId(item, this.hoveredIndex) : ''; - this.inputElement.setAttribute('aria-activedescendant', elemId); + if (this.inputElement) { + this.inputElement.setAttribute('aria-activedescendant', elemId); + } if (item && item !== this.hovered) { this.$emit('hover', item, elem); @@ -448,18 +465,18 @@ function _catch(body, recover) { }, showSuggestions: function showSuggestions() { try { - var _this7 = this; + var _this8 = this; - return _invoke(function () { - if (_this7.suggestions.length === 0 && _this7.minLength <= _this7.textLength) { + return _await(_invoke(function () { + if (_this8.suggestions.length === 0 && _this8.minLength <= _this8.textLength) { // try show misc slots while researching - _this7.showList(); - return _awaitIgnored(_this7.research()); + _this8.showList(); + return _awaitIgnored(_this8.research()); } }, function () { - _this7.showList(); - }); + _this8.showList(); + })); } catch (e) { return Promise.reject(e); } @@ -485,7 +502,7 @@ function _catch(body, recover) { item = this.selected || this.suggestions[listEdge]; } else if (hoversBetweenEdges) { item = this.suggestions[this.hoveredIndex + direction]; - } else /* if hovers on edge */{ + } /* if hovers on edge */else { item = this.suggestions[listEdge]; } this.hover(item); @@ -529,7 +546,7 @@ function _catch(body, recover) { } }, suggestionClick: function suggestionClick(suggestion, e) { - var _this8 = this; + var _this9 = this; this.$emit('suggestion-click', suggestion, e); this.select(suggestion); @@ -538,16 +555,17 @@ function _catch(body, recover) { if (this.isClicking) { setTimeout(function () { - _this8.inputElement.focus(); + if (_this9.inputElement) { + _this9.inputElement.focus(); + } /// Ensure, that all needed flags are off before finishing the click. - _this8.isClicking = false; + _this9.isClicking = false; }, 0); } }, onBlur: function onBlur(e) { if (this.isInFocus) { - /// Clicking starts here, because input's blur occurs before the suggestionClick /// and exactly when the user clicks the mouse button or taps the screen. this.isClicking = this.hovered && !this.isTabbed; @@ -561,7 +579,9 @@ function _catch(body, recover) { this.isFalseFocus = true; } } else { - this.inputElement.blur(); + if (this.inputElement) { + this.inputElement.blur(); + } console.error('This should never happen!\n If you encountered this error, please make sure that your input component emits \'focus\' events properly.\n For more info see https://github.com/KazanExpress/vue-simple-suggest#custom-input.\n\n If your \'vue-simple-suggest\' setup does not include a custom input component - please,\n report to https://github.com/KazanExpress/vue-simple-suggest/issues/new'); } @@ -586,7 +606,7 @@ function _catch(body, recover) { var value = !inputEvent.target ? inputEvent : inputEvent.target.value; this.updateTextOutside(value); - this.$emit('input', value); + this.$emit('update:modelValue', value); }, updateTextOutside: function updateTextOutside(value) { if (this.text === value) { @@ -610,69 +630,70 @@ function _catch(body, recover) { }, research: function research() { try { - var _this10 = this; + var _this11 = this; - return _finally(function () { + return _await(_finally(function () { return _catch(function () { return _invokeIgnored(function () { - if (_this10.canSend) { - _this10.canSend = false; + if (_this11.canSend) { + _this11.canSend = false; // @TODO: fix when promises will be cancelable (never :D) - var textBeforeRequest = _this10.text; - return _await(_this10.getSuggestions(_this10.text), function (newList) { - if (textBeforeRequest === _this10.text) { - _this10.$set(_this10, 'suggestions', newList); + var textBeforeRequest = _this11.text; + return _await(_this11.getSuggestions(_this11.text), function (newList) { + if (textBeforeRequest === _this11.text) { + _this11.suggestions = newList; } }); } }); }, function (e) { - _this10.clearSuggestions(); + _this11.clearSuggestions(); throw e; }); }, function () { - _this10.canSend = true; + _this11.canSend = true; - if (_this10.suggestions.length === 0 && _this10.miscSlotsAreEmpty()) { - _this10.hideList(); - } else if (_this10.isInFocus) { - _this10.showList(); + if (_this11.suggestions.length === 0 && _this11.miscSlotsAreEmpty()) { + _this11.hideList(); + } else if (_this11.isInFocus) { + _this11.showList(); } - return _this10.suggestions; - }); + // eslint-disable-next-line no-unsafe-finally + return _this11.suggestions; + })); } catch (e) { return Promise.reject(e); } }, getSuggestions: function getSuggestions(value) { try { - var _this12 = this; + var _this13 = this; value = value || ''; - if (value.length < _this12.minLength) { - return []; + if (value.length < _this13.minLength) { + return _await([]); } - _this12.selected = null; + _this13.selected = null; // Start request if can - if (_this12.listIsRequest) { - _this12.$emit('request-start', value); + if (_this13.listIsRequest) { + _this13.$emit('request-start', value); } var nextIsPlainSuggestion = false; var result = []; - return _finally(function () { + return _await(_finally(function () { return _catch(function () { return _invoke(function () { - if (_this12.listIsRequest) { - return _await(_this12.list(value), function (_this11$list) { - result = _this11$list || []; + if (_this13.listIsRequest) { + return _await(_this13.list(value), function (_this12$list) { + result = _this12$list || []; }); } else { - result = _this12.list; + result = _this13.list; } }, function () { @@ -683,31 +704,32 @@ function _catch(body, recover) { nextIsPlainSuggestion = _typeof(result[0]) !== 'object' && typeof result[0] !== 'undefined' || Array.isArray(result[0]); - if (_this12.filterByQuery) { + if (_this13.filterByQuery) { result = result.filter(function (el) { - return _this12.filter(el, value); + return _this13.filterResult(el, value); }); } - if (_this12.listIsRequest) { - _this12.$emit('request-done', result); + if (_this13.listIsRequest) { + _this13.$emit('request-done', result); } }); }, function (e) { - if (_this12.listIsRequest) { - _this12.$emit('request-failed', e); + if (_this13.listIsRequest) { + _this13.$emit('request-failed', e); } else { throw e; } }); }, function () { - if (_this12.maxSuggestions) { - result.splice(_this12.maxSuggestions); + if (_this13.maxSuggestions) { + result = result.slice(0, _this13.maxSuggestions); } - _this12.isPlainSuggestion = nextIsPlainSuggestion; + _this13.isPlainSuggestion = nextIsPlainSuggestion; + // eslint-disable-next-line no-unsafe-finally return result; - }); + })); } catch (e) { return Promise.reject(e); } @@ -721,4 +743,88 @@ function _catch(body, recover) { } }; -module.exports = VueSimpleSuggest; +var _hoisted_1 = ["aria-owns", "aria-expanded"]; +var _hoisted_2 = ["value"]; +var _hoisted_3 = ["id", "aria-labelledby"]; +var _hoisted_4 = ["onMouseenter", "onClick", "aria-selected", "id"]; + +function render(_ctx, _cache, $props, $setup, $data, $options) { + return vue.openBlock(), vue.createElementBlock("div", { + class: vue.normalizeClass(["vue-simple-suggest", [$props.styles.vueSimpleSuggest, { designed: !$props.destyled, focus: $data.isInFocus }]]), + onKeydown: _cache[1] || (_cache[1] = vue.withKeys(function ($event) { + return $data.isTabbed = true; + }, ["tab"])) + }, [vue.createElementVNode("div", { + class: vue.normalizeClass(["input-wrapper", $props.styles.inputWrapper]), + ref: "inputSlot", + role: "combobox", + "aria-haspopup": "listbox", + "aria-owns": $data.listId, + "aria-expanded": !!$data.listShown && !$props.removeList ? 'true' : 'false' + }, [vue.renderSlot(_ctx.$slots, "default", { + field: $options.field, + componentField: $options.componentField + }, function () { + return [vue.createElementVNode("input", vue.mergeProps({ class: "default-input" }, $options.field, { + value: $data.text || '', + class: $props.styles.defaultInput + }), null, 16 /* FULL_PROPS */, _hoisted_2)]; + })], 10 /* CLASS, PROPS */, _hoisted_1), vue.createVNode(vue.Transition, { name: "vue-simple-suggest" }, { + default: vue.withCtx(function () { + return [!!$data.listShown && !$props.removeList ? (vue.openBlock(), vue.createElementBlock("ul", { + key: 0, + id: $data.listId, + class: vue.normalizeClass(["suggestions", $props.styles.suggestions]), + role: "listbox", + "aria-labelledby": $data.listId + }, [!!_ctx.$slots['misc-item-above'] ? (vue.openBlock(), vue.createElementBlock("li", { + key: 0, + class: vue.normalizeClass($props.styles.miscItemAbove) + }, [vue.renderSlot(_ctx.$slots, "misc-item-above", { + suggestions: $data.suggestions, + query: $data.text + })], 2 /* CLASS */)) : vue.createCommentVNode("v-if", true), (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList($data.suggestions, function (suggestion, index) { + return vue.openBlock(), vue.createElementBlock("li", { + class: vue.normalizeClass(["suggest-item", [$props.styles.suggestItem, { + selected: $options.isSelected(suggestion), + hover: $options.isHovered(suggestion) + }]]), + role: "option", + onMouseenter: function onMouseenter($event) { + return $options.hover(suggestion, $event.target); + }, + onMouseleave: _cache[0] || (_cache[0] = function ($event) { + return $options.hover(null); + }), + onClick: function onClick($event) { + return $options.suggestionClick(suggestion, $event); + }, + "aria-selected": $options.isHovered(suggestion) || $options.isSelected(suggestion) ? 'true' : 'false', + + id: $options.getId(suggestion, index), + key: $options.getId(suggestion, index) + }, [vue.renderSlot(_ctx.$slots, "suggestion-item", { + autocomplete: function autocomplete() { + return $options.autocompleteText(suggestion); + }, + suggestion: suggestion, + query: $data.text + }, function () { + return [vue.createElementVNode("span", null, vue.toDisplayString($options.displayProperty(suggestion)), 1 /* TEXT */)]; + })], 42 /* CLASS, PROPS, HYDRATE_EVENTS */, _hoisted_4); + }), 128 /* KEYED_FRAGMENT */)), !!_ctx.$slots['misc-item-below'] ? (vue.openBlock(), vue.createElementBlock("li", { + key: 1, + class: vue.normalizeClass($props.styles.miscItemBelow) + }, [vue.renderSlot(_ctx.$slots, "misc-item-below", { + suggestions: $data.suggestions, + query: $data.text + })], 2 /* CLASS */)) : vue.createCommentVNode("v-if", true)], 10 /* CLASS, PROPS */, _hoisted_3)) : vue.createCommentVNode("v-if", true)]; + }), + _: 3 /* FORWARDED */ + })], 34 /* CLASS, HYDRATE_EVENTS */); +} + +script.render = render; +script.__file = "lib/vue-simple-suggest.vue"; + +module.exports = script; diff --git a/dist/es6.js b/dist/es6.js index eaa0393c..9331b50e 100644 --- a/dist/es6.js +++ b/dist/es6.js @@ -1,3 +1,5 @@ +import { openBlock, createElementBlock, normalizeClass, withKeys, createElementVNode, renderSlot, mergeProps, createVNode, Transition, withCtx, createCommentVNode, Fragment, renderList, toDisplayString } from 'vue'; + const defaultControls = { selectionUp: [38], selectionDown: [40], @@ -31,12 +33,49 @@ function hasKeyCodeByCode(arr, keyCode) { } } -function _await(value, then, direct) { - if (direct) { - return then ? then(value) : value; - }if (!value || !value.then) { - value = Promise.resolve(value); - }return then ? value.then(then) : value; +const onRE = /^on[^a-z]/; + +function isOn(key) { + return onRE.test(key); +} + +function getPropertyByAttribute(obj, attr) { + return typeof obj !== 'undefined' ? fromPath(obj, attr) : obj; +} + +function display(obj, attribute, isPlainSuggestion) { + if (isPlainSuggestion) { + return obj; + } + + let display = getPropertyByAttribute(obj, attribute); + + if (typeof display === 'undefined') { + display = JSON.stringify(obj); + + if (process && ~process.env.NODE_ENV.indexOf('dev')) { + console.warn('[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.'); + } + } + + return String(display || ''); +} + +const HAS_WINDOW_SUPPORT = typeof window !== 'undefined'; + +const requestAF = HAS_WINDOW_SUPPORT ? window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || ( +// Fallback, but not a true polyfill +// Only needed for Opera Mini +cb => setTimeout(cb, 16)) : cb => setTimeout(cb, 0); + +function _empty() {}function _awaitIgnored(value, direct) { + if (!direct) { + return value && value.then ? value.then(_empty) : Promise.resolve(); + } +}function _invoke(body, then) { + var result = body();if (result && result.then) { + return result.then(then); + }return then(result); }function _async(f) { return function () { for (var args = [], i = 0; i < arguments.length; i++) { @@ -47,20 +86,17 @@ function _await(value, then, direct) { return Promise.reject(e); } }; -}function _empty() {}function _awaitIgnored(value, direct) { - if (!direct) { - return value && value.then ? value.then(_empty) : Promise.resolve(); - } -}function _invoke(body, then) { - var result = body();if (result && result.then) { - return result.then(then); - }return then(result); +}function _await(value, then, direct) { + if (direct) { + return then ? then(value) : value; + }if (!value || !value.then) { + value = Promise.resolve(value); + }return then ? value.then(then) : value; }function _invokeIgnored(body) { var result = body();if (result && result.then) { return result.then(_empty); } -} -function _catch(body, recover) { +}function _catch(body, recover) { try { var result = body(); } catch (e) { @@ -76,34 +112,9 @@ function _catch(body, recover) { }if (result && result.then) { return result.then(finalizer, finalizer); }return finalizer(); -}var VueSimpleSuggest = { - render: function () { - var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('div', { staticClass: "vue-simple-suggest", class: [_vm.styles.vueSimpleSuggest, { designed: !_vm.destyled, focus: _vm.isInFocus }], on: { "keydown": function ($event) { - if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "tab", 9, $event.key, "Tab")) { - return null; - }_vm.isTabbed = true; - } } }, [_c('div', { ref: "inputSlot", staticClass: "input-wrapper", class: _vm.styles.inputWrapper, attrs: { "role": "combobox", "aria-haspopup": "listbox", "aria-owns": _vm.listId, "aria-expanded": !!_vm.listShown && !_vm.removeList ? 'true' : 'false' } }, [_vm._t("default", [_c('input', _vm._b({ staticClass: "default-input", class: _vm.styles.defaultInput, domProps: { "value": _vm.text || '' } }, 'input', _vm.$attrs, false))])], 2), _vm._v(" "), _c('transition', { attrs: { "name": "vue-simple-suggest" } }, [!!_vm.listShown && !_vm.removeList ? _c('ul', { staticClass: "suggestions", class: _vm.styles.suggestions, attrs: { "id": _vm.listId, "role": "listbox", "aria-labelledby": _vm.listId } }, [!!this.$scopedSlots['misc-item-above'] ? _c('li', { class: _vm.styles.miscItemAbove }, [_vm._t("misc-item-above", null, { "suggestions": _vm.suggestions, "query": _vm.text })], 2) : _vm._e(), _vm._v(" "), _vm._l(_vm.suggestions, function (suggestion, index) { - return _c('li', { key: _vm.getId(suggestion, index), staticClass: "suggest-item", class: [_vm.styles.suggestItem, { - selected: _vm.isSelected(suggestion), - hover: _vm.isHovered(suggestion) - }], attrs: { "role": "option", "aria-selected": _vm.isHovered(suggestion) || _vm.isSelected(suggestion) ? 'true' : 'false', "id": _vm.getId(suggestion, index) }, on: { "mouseenter": function ($event) { - return _vm.hover(suggestion, $event.target); - }, "mouseleave": function ($event) { - return _vm.hover(undefined); - }, "click": function ($event) { - return _vm.suggestionClick(suggestion, $event); - } } }, [_vm._t("suggestion-item", [_c('span', [_vm._v(_vm._s(_vm.displayProperty(suggestion)))])], { "autocomplete": function () { - return _vm.autocompleteText(suggestion); - }, "suggestion": suggestion, "query": _vm.text })], 2); - }), _vm._v(" "), !!this.$scopedSlots['misc-item-below'] ? _c('li', { class: _vm.styles.miscItemBelow }, [_vm._t("misc-item-below", null, { "suggestions": _vm.suggestions, "query": _vm.text })], 2) : _vm._e()], 2) : _vm._e()])], 1); - }, - staticRenderFns: [], +}var script = { name: 'vue-simple-suggest', inheritAttrs: false, - model: { - prop: 'value', - event: 'input' - }, props: { styles: { type: Object, @@ -146,10 +157,7 @@ function _catch(body, recover) { default: false }, filter: { - type: Function, - default(el, value) { - return value ? ~this.displayProperty(el).toLowerCase().indexOf(value.toLowerCase()) : true; - } + type: Function }, debounce: { type: Number, @@ -159,7 +167,8 @@ function _catch(body, recover) { type: Boolean, default: false }, - value: {}, + modelValue: {}, + modelSelect: {}, mode: { type: String, default: 'input', @@ -170,38 +179,54 @@ function _catch(body, recover) { default: false } }, - // Handle run-time mode changes (now working): watch: { mode: { - handler(current, old) { - this.constructor.options.model.event = current; - + handler() { // Can be null if the component is root this.$parent && this.$parent.$forceUpdate(); this.$nextTick(() => { - if (current === 'input') { - this.$emit('input', this.text); - } else { - this.$emit('select', this.selected); - } + this.$emit('update:modelValue', this.text); + this.$emit('update:modelSelect', this.selected); // For backward compatibility: + this.$emit('select', this.selected); }); + }, immediate: true + }, + filter: { + handler(current) { + this.filterResult = current != null ? current : (el, value) => { + return value ? ~display(el, this.displayAttribute).toLowerCase().indexOf(value.toLowerCase()) : true; + }; }, immediate: true }, - value: { + modelValue: { handler(current) { - if (typeof current !== 'string') { - current = this.displayProperty(current); + if (this.mode === 'input') { + if (typeof current !== 'string') { + current = this.displayProperty(current); + } + this.updateTextOutside(current); + } + }, + immediate: true + }, + modelSelect: { + handler(current) { + if (this.mode === 'select') { + if (typeof current !== 'string') { + current = this.displayProperty(current); + } + this.updateTextOutside(current); } - this.updateTextOutside(current); }, immediate: true } }, // - data() { + data(vm) { return { + filterResult: null, selected: null, hovered: null, suggestions: [], @@ -209,32 +234,20 @@ function _catch(body, recover) { inputElement: null, canSend: true, timeoutInstance: null, - text: this.value, + text: vm.modelValue, isPlainSuggestion: false, isClicking: false, isInFocus: false, isFalseFocus: false, isTabbed: false, controlScheme: {}, - listId: `${this._uid}-suggestions` + listId: `${this.$.uid}-suggestions` }; }, computed: { listIsRequest() { return typeof this.list === 'function'; }, - inputIsComponent() { - return this.$slots.default && this.$slots.default.length > 0 && !!this.$slots.default[0].componentInstance; - }, - input() { - return this.inputIsComponent ? this.$slots.default[0].componentInstance : this.inputElement; - }, - on() { - return this.inputIsComponent ? '$on' : 'addEventListener'; - }, - off() { - return this.inputIsComponent ? '$off' : 'removeEventListener'; - }, hoveredIndex() { for (let i = 0; i < this.suggestions.length; i++) { const el = this.suggestions[i]; @@ -245,35 +258,50 @@ function _catch(body, recover) { return -1; }, textLength() { - return this.text && this.text.length || this.inputElement.value.length || 0; + return this.text && this.text.length || this.inputElement && this.inputElement.value.length || 0; }, isSelectedUpToDate() { return !!this.selected && this.displayProperty(this.selected) === this.text; + }, + attrsWithoutListeners() { + const o = {}; + Object.keys(this.$attrs).forEach(key => !isOn(key) && (o[key] = this.$attrs[key])); + return o; + }, + field() { + return Object.assign({}, this.attrsWithoutListeners, { + onBlur: this.onBlur, + onFocus: this.onFocus, + onInput: this.onInput, + onClick: this.showSuggestions, + onKeydown: this.onKeyDown, + onKeyup: this.onListKeyUp + }); + }, + componentField() { + return Object.assign({}, this.attrsWithoutListeners, { + onBlur: this.onBlur, + onFocus: this.onFocus, + 'onUpdate:modelValue': this.onInput, + onClick: this.showSuggestions, + onKeydown: this.onKeyDown, + onKeyup: this.onListKeyUp + }); } }, created() { this.controlScheme = Object.assign({}, defaultControls, this.controls); }, - mounted: _async(function () { - const _this = this; - - return _await(_this.$slots.default, function () { - - _this.$nextTick(() => { - _this.inputElement = _this.$refs['inputSlot'].querySelector('input'); - - if (_this.inputElement) { - _this.setInputAriaAttributes(); - _this.prepareEventHandlers(true); - } else { - console.error('No input element found'); - } - }); - }); - }), + mounted() { + this.$nextTick(() => requestAF(() => { + this.inputElement = this.$refs['inputSlot'].querySelector('input'); - beforeDestroy() { - this.prepareEventHandlers(false); + if (this.inputElement) { + this.setInputAriaAttributes(); + } else { + console.error('No input element found'); + } + })); }, methods: { isEqual(suggestion, item) { @@ -286,43 +314,28 @@ function _catch(body, recover) { return this.isEqual(suggestion, this.hovered); }, setInputAriaAttributes() { - this.inputElement.setAttribute('aria-activedescendant', ''); - this.inputElement.setAttribute('aria-autocomplete', 'list'); - this.inputElement.setAttribute('aria-controls', this.listId); - }, - prepareEventHandlers(enable) { - const binder = this[enable ? 'on' : 'off']; - const keyEventsList = { - click: this.showSuggestions, - keydown: this.onKeyDown, - keyup: this.onListKeyUp - }; - const eventsList = Object.assign({ - blur: this.onBlur, - focus: this.onFocus, - input: this.onInput - }, keyEventsList); - - for (const event in eventsList) { - this.input[binder](event, eventsList[event]); - } - - const listenerBinder = enable ? 'addEventListener' : 'removeEventListener'; - - for (const event in keyEventsList) { - this.inputElement[listenerBinder](event, keyEventsList[event]); + if (this.inputElement) { + this.inputElement.setAttribute('aria-activedescendant', ''); + this.inputElement.setAttribute('aria-autocomplete', 'list'); + this.inputElement.setAttribute('aria-controls', this.listId); } }, isScopedSlotEmpty(slot) { if (slot) { - const vNode = slot(this); - return !(Array.isArray(vNode) || vNode && (vNode.tag || vNode.context || vNode.text || vNode.children)); + const slotContent = slot(this); + // https://github.com/vuejs/core/issues/4733#issuecomment-1024816095 + // https://github.com/vuejs/core/issues/3056#issuecomment-786560172 + return slotContent.some(vnode => { + if (vnode.type === Comment) return false; + if (Array.isArray(vnode.children) && !vnode.children.length) return false; + return vnode.type !== Text || typeof vnode.children === 'string' && vnode.children.trim() !== ''; + }); } return true; }, miscSlotsAreEmpty() { - const slots = ['misc-item-above', 'misc-item-below'].map(s => this.$scopedSlots[s]); + const slots = ['misc-item-above', 'misc-item-below'].map(s => this.$slots[s]); if (slots.every(s => !!s)) { return slots.every(this.isScopedSlotEmpty.bind(this)); @@ -332,32 +345,15 @@ function _catch(body, recover) { return this.isScopedSlotEmpty.call(this, slot); }, - getPropertyByAttribute(obj, attr) { - return this.isPlainSuggestion ? obj : typeof obj !== undefined ? fromPath(obj, attr) : obj; - }, - displayProperty(obj) { - if (this.isPlainSuggestion) { - return obj; - } - - let display = this.getPropertyByAttribute(obj, this.displayAttribute); - - if (typeof display === 'undefined') { - display = JSON.stringify(obj); - - if (process && ~process.env.NODE_ENV.indexOf('dev')) { - console.warn('[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.'); - } - } - - return String(display || ''); + displayProperty(suggestion) { + return display(suggestion, this.displayAttribute); }, valueProperty(obj) { if (this.isPlainSuggestion) { return obj; } - const value = this.getPropertyByAttribute(obj, this.valueAttribute); + const value = getPropertyByAttribute(obj, this.valueAttribute); if (typeof value === 'undefined') { console.error(`[vue-simple-suggest]: Please, check if you passed 'value-attribute' (default is 'id') and 'display-attribute' (default is 'title') props correctly. @@ -372,14 +368,18 @@ function _catch(body, recover) { }, setText(text) { this.$nextTick(() => { - this.inputElement.value = text; + if (this.inputElement) { + this.inputElement.value = text; + } this.text = text; - this.$emit('input', text); + this.$emit('update:modelValue', text); }); }, select(item) { - if (this.selected !== item || this.nullableSelect && !item) { + if (item && this.selected !== item || this.nullableSelect && !item) { this.selected = item; + this.$emit('update:modelSelect', item); + // For backward compatibility: this.$emit('select', item); if (item) { @@ -390,9 +390,11 @@ function _catch(body, recover) { this.hover(null); }, hover(item, elem) { - const elemId = !!item ? this.getId(item, this.hoveredIndex) : ''; + const elemId = item ? this.getId(item, this.hoveredIndex) : ''; - this.inputElement.setAttribute('aria-activedescendant', elemId); + if (this.inputElement) { + this.inputElement.setAttribute('aria-activedescendant', elemId); + } if (item && item !== this.hovered) { this.$emit('hover', item, elem); @@ -416,17 +418,17 @@ function _catch(body, recover) { } }, showSuggestions: _async(function () { - const _this2 = this; + const _this = this; return _invoke(function () { - if (_this2.suggestions.length === 0 && _this2.minLength <= _this2.textLength) { + if (_this.suggestions.length === 0 && _this.minLength <= _this.textLength) { // try show misc slots while researching - _this2.showList(); - return _awaitIgnored(_this2.research()); + _this.showList(); + return _awaitIgnored(_this.research()); } }, function () { - _this2.showList(); + _this.showList(); }); }), @@ -451,7 +453,7 @@ function _catch(body, recover) { item = this.selected || this.suggestions[listEdge]; } else if (hoversBetweenEdges) { item = this.suggestions[this.hoveredIndex + direction]; - } else /* if hovers on edge */{ + } /* if hovers on edge */else { item = this.suggestions[listEdge]; } this.hover(item); @@ -502,7 +504,9 @@ function _catch(body, recover) { if (this.isClicking) { setTimeout(() => { - this.inputElement.focus(); + if (this.inputElement) { + this.inputElement.focus(); + } /// Ensure, that all needed flags are off before finishing the click. this.isClicking = false; @@ -511,7 +515,6 @@ function _catch(body, recover) { }, onBlur(e) { if (this.isInFocus) { - /// Clicking starts here, because input's blur occurs before the suggestionClick /// and exactly when the user clicks the mouse button or taps the screen. this.isClicking = this.hovered && !this.isTabbed; @@ -525,7 +528,9 @@ function _catch(body, recover) { this.isFalseFocus = true; } } else { - this.inputElement.blur(); + if (this.inputElement) { + this.inputElement.blur(); + } console.error(`This should never happen! If you encountered this error, please make sure that your input component emits 'focus' events properly. For more info see https://github.com/KazanExpress/vue-simple-suggest#custom-input. @@ -555,7 +560,7 @@ function _catch(body, recover) { const value = !inputEvent.target ? inputEvent : inputEvent.target.value; this.updateTextOutside(value); - this.$emit('input', value); + this.$emit('update:modelValue', value); }, updateTextOutside(value) { if (this.text === value) { @@ -578,52 +583,53 @@ function _catch(body, recover) { } }, research: _async(function () { - const _this3 = this; + const _this2 = this; return _finally(function () { return _catch(function () { return _invokeIgnored(function () { - if (_this3.canSend) { - _this3.canSend = false; + if (_this2.canSend) { + _this2.canSend = false; // @TODO: fix when promises will be cancelable (never :D) - let textBeforeRequest = _this3.text; - return _await(_this3.getSuggestions(_this3.text), function (newList) { - if (textBeforeRequest === _this3.text) { - _this3.$set(_this3, 'suggestions', newList); + let textBeforeRequest = _this2.text; + return _await(_this2.getSuggestions(_this2.text), function (newList) { + if (textBeforeRequest === _this2.text) { + _this2.suggestions = newList; } }); } }); }, function (e) { - _this3.clearSuggestions(); + _this2.clearSuggestions(); throw e; }); }, function () { - _this3.canSend = true; + _this2.canSend = true; - if (_this3.suggestions.length === 0 && _this3.miscSlotsAreEmpty()) { - _this3.hideList(); - } else if (_this3.isInFocus) { - _this3.showList(); + if (_this2.suggestions.length === 0 && _this2.miscSlotsAreEmpty()) { + _this2.hideList(); + } else if (_this2.isInFocus) { + _this2.showList(); } - return _this3.suggestions; + // eslint-disable-next-line no-unsafe-finally + return _this2.suggestions; }); }), getSuggestions: _async(function (value) { - const _this4 = this; + const _this3 = this; value = value || ''; - if (value.length < _this4.minLength) { + if (value.length < _this3.minLength) { return []; } - _this4.selected = null; + _this3.selected = null; // Start request if can - if (_this4.listIsRequest) { - _this4.$emit('request-start', value); + if (_this3.listIsRequest) { + _this3.$emit('request-start', value); } let nextIsPlainSuggestion = false; @@ -631,12 +637,12 @@ function _catch(body, recover) { return _finally(function () { return _catch(function () { return _invoke(function () { - if (_this4.listIsRequest) { - return _await(_this4.list(value), function (_this4$list) { - result = _this4$list || []; + if (_this3.listIsRequest) { + return _await(_this3.list(value), function (_this3$list) { + result = _this3$list || []; }); } else { - result = _this4.list; + result = _this3.list; } }, function () { @@ -647,27 +653,28 @@ function _catch(body, recover) { nextIsPlainSuggestion = typeof result[0] !== 'object' && typeof result[0] !== 'undefined' || Array.isArray(result[0]); - if (_this4.filterByQuery) { - result = result.filter(el => _this4.filter(el, value)); + if (_this3.filterByQuery) { + result = result.filter(el => _this3.filterResult(el, value)); } - if (_this4.listIsRequest) { - _this4.$emit('request-done', result); + if (_this3.listIsRequest) { + _this3.$emit('request-done', result); } }); }, function (e) { - if (_this4.listIsRequest) { - _this4.$emit('request-failed', e); + if (_this3.listIsRequest) { + _this3.$emit('request-failed', e); } else { throw e; } }); }, function () { - if (_this4.maxSuggestions) { - result.splice(_this4.maxSuggestions); + if (_this3.maxSuggestions) { + result = result.slice(0, _this3.maxSuggestions); } - _this4.isPlainSuggestion = nextIsPlainSuggestion; + _this3.isPlainSuggestion = nextIsPlainSuggestion; + // eslint-disable-next-line no-unsafe-finally return result; }); }), @@ -681,4 +688,72 @@ function _catch(body, recover) { } }; -export default VueSimpleSuggest; +const _hoisted_1 = ["aria-owns", "aria-expanded"]; +const _hoisted_2 = ["value"]; +const _hoisted_3 = ["id", "aria-labelledby"]; +const _hoisted_4 = ["onMouseenter", "onClick", "aria-selected", "id"]; + +function render(_ctx, _cache, $props, $setup, $data, $options) { + return openBlock(), createElementBlock("div", { + class: normalizeClass(["vue-simple-suggest", [$props.styles.vueSimpleSuggest, { designed: !$props.destyled, focus: $data.isInFocus }]]), + onKeydown: _cache[1] || (_cache[1] = withKeys($event => $data.isTabbed = true, ["tab"])) + }, [createElementVNode("div", { + class: normalizeClass(["input-wrapper", $props.styles.inputWrapper]), + ref: "inputSlot", + role: "combobox", + "aria-haspopup": "listbox", + "aria-owns": $data.listId, + "aria-expanded": !!$data.listShown && !$props.removeList ? 'true' : 'false' + }, [renderSlot(_ctx.$slots, "default", { + field: $options.field, + componentField: $options.componentField + }, () => [createElementVNode("input", mergeProps({ class: "default-input" }, $options.field, { + value: $data.text || '', + class: $props.styles.defaultInput + }), null, 16 /* FULL_PROPS */, _hoisted_2)])], 10 /* CLASS, PROPS */, _hoisted_1), createVNode(Transition, { name: "vue-simple-suggest" }, { + default: withCtx(() => [!!$data.listShown && !$props.removeList ? (openBlock(), createElementBlock("ul", { + key: 0, + id: $data.listId, + class: normalizeClass(["suggestions", $props.styles.suggestions]), + role: "listbox", + "aria-labelledby": $data.listId + }, [!!_ctx.$slots['misc-item-above'] ? (openBlock(), createElementBlock("li", { + key: 0, + class: normalizeClass($props.styles.miscItemAbove) + }, [renderSlot(_ctx.$slots, "misc-item-above", { + suggestions: $data.suggestions, + query: $data.text + })], 2 /* CLASS */)) : createCommentVNode("v-if", true), (openBlock(true), createElementBlock(Fragment, null, renderList($data.suggestions, (suggestion, index) => { + return openBlock(), createElementBlock("li", { + class: normalizeClass(["suggest-item", [$props.styles.suggestItem, { + selected: $options.isSelected(suggestion), + hover: $options.isHovered(suggestion) + }]]), + role: "option", + onMouseenter: $event => $options.hover(suggestion, $event.target), + onMouseleave: _cache[0] || (_cache[0] = $event => $options.hover(null)), + onClick: $event => $options.suggestionClick(suggestion, $event), + "aria-selected": $options.isHovered(suggestion) || $options.isSelected(suggestion) ? 'true' : 'false', + + id: $options.getId(suggestion, index), + key: $options.getId(suggestion, index) + }, [renderSlot(_ctx.$slots, "suggestion-item", { + autocomplete: () => $options.autocompleteText(suggestion), + suggestion: suggestion, + query: $data.text + }, () => [createElementVNode("span", null, toDisplayString($options.displayProperty(suggestion)), 1 /* TEXT */)])], 42 /* CLASS, PROPS, HYDRATE_EVENTS */, _hoisted_4); + }), 128 /* KEYED_FRAGMENT */)), !!_ctx.$slots['misc-item-below'] ? (openBlock(), createElementBlock("li", { + key: 1, + class: normalizeClass($props.styles.miscItemBelow) + }, [renderSlot(_ctx.$slots, "misc-item-below", { + suggestions: $data.suggestions, + query: $data.text + })], 2 /* CLASS */)) : createCommentVNode("v-if", true)], 10 /* CLASS, PROPS */, _hoisted_3)) : createCommentVNode("v-if", true)]), + _: 3 /* FORWARDED */ + })], 34 /* CLASS, HYDRATE_EVENTS */); +} + +script.render = render; +script.__file = "lib/vue-simple-suggest.vue"; + +export default script; diff --git a/dist/es7.js b/dist/es7.js index 8704185b..637cef1b 100644 --- a/dist/es7.js +++ b/dist/es7.js @@ -1,3 +1,5 @@ +import { openBlock, createElementBlock, normalizeClass, withKeys, createElementVNode, renderSlot, mergeProps, createVNode, Transition, withCtx, createCommentVNode, Fragment, renderList, toDisplayString } from 'vue'; + const defaultControls = { selectionUp: [38], selectionDown: [40], @@ -31,34 +33,44 @@ function hasKeyCodeByCode(arr, keyCode) { } } -var VueSimpleSuggest = { - render: function () { - var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('div', { staticClass: "vue-simple-suggest", class: [_vm.styles.vueSimpleSuggest, { designed: !_vm.destyled, focus: _vm.isInFocus }], on: { "keydown": function ($event) { - if (!$event.type.indexOf('key') && _vm._k($event.keyCode, "tab", 9, $event.key, "Tab")) { - return null; - }_vm.isTabbed = true; - } } }, [_c('div', { ref: "inputSlot", staticClass: "input-wrapper", class: _vm.styles.inputWrapper, attrs: { "role": "combobox", "aria-haspopup": "listbox", "aria-owns": _vm.listId, "aria-expanded": !!_vm.listShown && !_vm.removeList ? 'true' : 'false' } }, [_vm._t("default", [_c('input', _vm._b({ staticClass: "default-input", class: _vm.styles.defaultInput, domProps: { "value": _vm.text || '' } }, 'input', _vm.$attrs, false))])], 2), _vm._v(" "), _c('transition', { attrs: { "name": "vue-simple-suggest" } }, [!!_vm.listShown && !_vm.removeList ? _c('ul', { staticClass: "suggestions", class: _vm.styles.suggestions, attrs: { "id": _vm.listId, "role": "listbox", "aria-labelledby": _vm.listId } }, [!!this.$scopedSlots['misc-item-above'] ? _c('li', { class: _vm.styles.miscItemAbove }, [_vm._t("misc-item-above", null, { "suggestions": _vm.suggestions, "query": _vm.text })], 2) : _vm._e(), _vm._v(" "), _vm._l(_vm.suggestions, function (suggestion, index) { - return _c('li', { key: _vm.getId(suggestion, index), staticClass: "suggest-item", class: [_vm.styles.suggestItem, { - selected: _vm.isSelected(suggestion), - hover: _vm.isHovered(suggestion) - }], attrs: { "role": "option", "aria-selected": _vm.isHovered(suggestion) || _vm.isSelected(suggestion) ? 'true' : 'false', "id": _vm.getId(suggestion, index) }, on: { "mouseenter": function ($event) { - return _vm.hover(suggestion, $event.target); - }, "mouseleave": function ($event) { - return _vm.hover(undefined); - }, "click": function ($event) { - return _vm.suggestionClick(suggestion, $event); - } } }, [_vm._t("suggestion-item", [_c('span', [_vm._v(_vm._s(_vm.displayProperty(suggestion)))])], { "autocomplete": function () { - return _vm.autocompleteText(suggestion); - }, "suggestion": suggestion, "query": _vm.text })], 2); - }), _vm._v(" "), !!this.$scopedSlots['misc-item-below'] ? _c('li', { class: _vm.styles.miscItemBelow }, [_vm._t("misc-item-below", null, { "suggestions": _vm.suggestions, "query": _vm.text })], 2) : _vm._e()], 2) : _vm._e()])], 1); - }, - staticRenderFns: [], +const onRE = /^on[^a-z]/; + +function isOn(key) { + return onRE.test(key); +} + +function getPropertyByAttribute(obj, attr) { + return typeof obj !== 'undefined' ? fromPath(obj, attr) : obj; +} + +function display(obj, attribute, isPlainSuggestion) { + if (isPlainSuggestion) { + return obj; + } + + let display = getPropertyByAttribute(obj, attribute); + + if (typeof display === 'undefined') { + display = JSON.stringify(obj); + + if (process && ~process.env.NODE_ENV.indexOf('dev')) { + console.warn('[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.'); + } + } + + return String(display || ''); +} + +const HAS_WINDOW_SUPPORT = typeof window !== 'undefined'; + +const requestAF = HAS_WINDOW_SUPPORT ? window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || ( +// Fallback, but not a true polyfill +// Only needed for Opera Mini +cb => setTimeout(cb, 16)) : cb => setTimeout(cb, 0); + +var script = { name: 'vue-simple-suggest', inheritAttrs: false, - model: { - prop: 'value', - event: 'input' - }, props: { styles: { type: Object, @@ -101,10 +113,7 @@ var VueSimpleSuggest = { default: false }, filter: { - type: Function, - default(el, value) { - return value ? ~this.displayProperty(el).toLowerCase().indexOf(value.toLowerCase()) : true; - } + type: Function }, debounce: { type: Number, @@ -114,7 +123,8 @@ var VueSimpleSuggest = { type: Boolean, default: false }, - value: {}, + modelValue: {}, + modelSelect: {}, mode: { type: String, default: 'input', @@ -125,38 +135,56 @@ var VueSimpleSuggest = { default: false } }, - // Handle run-time mode changes (now working): watch: { mode: { - handler(current, old) { - this.constructor.options.model.event = current; - + handler() { // Can be null if the component is root this.$parent && this.$parent.$forceUpdate(); this.$nextTick(() => { - if (current === 'input') { - this.$emit('input', this.text); - } else { - this.$emit('select', this.selected); - } + this.$emit('update:modelValue', this.text); + this.$emit('update:modelSelect', this.selected); + // For backward compatibility: + this.$emit('select', this.selected); }); }, immediate: true }, - value: { + filter: { handler(current) { - if (typeof current !== 'string') { - current = this.displayProperty(current); + this.filterResult = current != null ? current : (el, value) => { + return value ? ~display(el, this.displayAttribute).toLowerCase().indexOf(value.toLowerCase()) : true; + }; + }, + immediate: true + }, + modelValue: { + handler(current) { + if (this.mode === 'input') { + if (typeof current !== 'string') { + current = this.displayProperty(current); + } + this.updateTextOutside(current); + } + }, + immediate: true + }, + modelSelect: { + handler(current) { + if (this.mode === 'select') { + if (typeof current !== 'string') { + current = this.displayProperty(current); + } + this.updateTextOutside(current); } - this.updateTextOutside(current); }, immediate: true } }, // - data() { + data(vm) { return { + filterResult: null, selected: null, hovered: null, suggestions: [], @@ -164,32 +192,20 @@ var VueSimpleSuggest = { inputElement: null, canSend: true, timeoutInstance: null, - text: this.value, + text: vm.modelValue, isPlainSuggestion: false, isClicking: false, isInFocus: false, isFalseFocus: false, isTabbed: false, controlScheme: {}, - listId: `${this._uid}-suggestions` + listId: `${this.$.uid}-suggestions` }; }, computed: { listIsRequest() { return typeof this.list === 'function'; }, - inputIsComponent() { - return this.$slots.default && this.$slots.default.length > 0 && !!this.$slots.default[0].componentInstance; - }, - input() { - return this.inputIsComponent ? this.$slots.default[0].componentInstance : this.inputElement; - }, - on() { - return this.inputIsComponent ? '$on' : 'addEventListener'; - }, - off() { - return this.inputIsComponent ? '$off' : 'removeEventListener'; - }, hoveredIndex() { for (let i = 0; i < this.suggestions.length; i++) { const el = this.suggestions[i]; @@ -200,31 +216,50 @@ var VueSimpleSuggest = { return -1; }, textLength() { - return this.text && this.text.length || this.inputElement.value.length || 0; + return this.text && this.text.length || this.inputElement && this.inputElement.value.length || 0; }, isSelectedUpToDate() { return !!this.selected && this.displayProperty(this.selected) === this.text; + }, + attrsWithoutListeners() { + const o = {}; + Object.keys(this.$attrs).forEach(key => !isOn(key) && (o[key] = this.$attrs[key])); + return o; + }, + field() { + return Object.assign({}, this.attrsWithoutListeners, { + onBlur: this.onBlur, + onFocus: this.onFocus, + onInput: this.onInput, + onClick: this.showSuggestions, + onKeydown: this.onKeyDown, + onKeyup: this.onListKeyUp + }); + }, + componentField() { + return Object.assign({}, this.attrsWithoutListeners, { + onBlur: this.onBlur, + onFocus: this.onFocus, + 'onUpdate:modelValue': this.onInput, + onClick: this.showSuggestions, + onKeydown: this.onKeyDown, + onKeyup: this.onListKeyUp + }); } }, created() { this.controlScheme = Object.assign({}, defaultControls, this.controls); }, - async mounted() { - await this.$slots.default; - - this.$nextTick(() => { + mounted() { + this.$nextTick(() => requestAF(() => { this.inputElement = this.$refs['inputSlot'].querySelector('input'); if (this.inputElement) { this.setInputAriaAttributes(); - this.prepareEventHandlers(true); } else { console.error('No input element found'); } - }); - }, - beforeDestroy() { - this.prepareEventHandlers(false); + })); }, methods: { isEqual(suggestion, item) { @@ -237,43 +272,28 @@ var VueSimpleSuggest = { return this.isEqual(suggestion, this.hovered); }, setInputAriaAttributes() { - this.inputElement.setAttribute('aria-activedescendant', ''); - this.inputElement.setAttribute('aria-autocomplete', 'list'); - this.inputElement.setAttribute('aria-controls', this.listId); - }, - prepareEventHandlers(enable) { - const binder = this[enable ? 'on' : 'off']; - const keyEventsList = { - click: this.showSuggestions, - keydown: this.onKeyDown, - keyup: this.onListKeyUp - }; - const eventsList = Object.assign({ - blur: this.onBlur, - focus: this.onFocus, - input: this.onInput - }, keyEventsList); - - for (const event in eventsList) { - this.input[binder](event, eventsList[event]); - } - - const listenerBinder = enable ? 'addEventListener' : 'removeEventListener'; - - for (const event in keyEventsList) { - this.inputElement[listenerBinder](event, keyEventsList[event]); + if (this.inputElement) { + this.inputElement.setAttribute('aria-activedescendant', ''); + this.inputElement.setAttribute('aria-autocomplete', 'list'); + this.inputElement.setAttribute('aria-controls', this.listId); } }, isScopedSlotEmpty(slot) { if (slot) { - const vNode = slot(this); - return !(Array.isArray(vNode) || vNode && (vNode.tag || vNode.context || vNode.text || vNode.children)); + const slotContent = slot(this); + // https://github.com/vuejs/core/issues/4733#issuecomment-1024816095 + // https://github.com/vuejs/core/issues/3056#issuecomment-786560172 + return slotContent.some(vnode => { + if (vnode.type === Comment) return false; + if (Array.isArray(vnode.children) && !vnode.children.length) return false; + return vnode.type !== Text || typeof vnode.children === 'string' && vnode.children.trim() !== ''; + }); } return true; }, miscSlotsAreEmpty() { - const slots = ['misc-item-above', 'misc-item-below'].map(s => this.$scopedSlots[s]); + const slots = ['misc-item-above', 'misc-item-below'].map(s => this.$slots[s]); if (slots.every(s => !!s)) { return slots.every(this.isScopedSlotEmpty.bind(this)); @@ -283,32 +303,15 @@ var VueSimpleSuggest = { return this.isScopedSlotEmpty.call(this, slot); }, - getPropertyByAttribute(obj, attr) { - return this.isPlainSuggestion ? obj : typeof obj !== undefined ? fromPath(obj, attr) : obj; - }, - displayProperty(obj) { - if (this.isPlainSuggestion) { - return obj; - } - - let display = this.getPropertyByAttribute(obj, this.displayAttribute); - - if (typeof display === 'undefined') { - display = JSON.stringify(obj); - - if (process && ~process.env.NODE_ENV.indexOf('dev')) { - console.warn('[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.'); - } - } - - return String(display || ''); + displayProperty(suggestion) { + return display(suggestion, this.displayAttribute); }, valueProperty(obj) { if (this.isPlainSuggestion) { return obj; } - const value = this.getPropertyByAttribute(obj, this.valueAttribute); + const value = getPropertyByAttribute(obj, this.valueAttribute); if (typeof value === 'undefined') { console.error(`[vue-simple-suggest]: Please, check if you passed 'value-attribute' (default is 'id') and 'display-attribute' (default is 'title') props correctly. @@ -323,14 +326,18 @@ var VueSimpleSuggest = { }, setText(text) { this.$nextTick(() => { - this.inputElement.value = text; + if (this.inputElement) { + this.inputElement.value = text; + } this.text = text; - this.$emit('input', text); + this.$emit('update:modelValue', text); }); }, select(item) { - if (this.selected !== item || this.nullableSelect && !item) { + if (item && this.selected !== item || this.nullableSelect && !item) { this.selected = item; + this.$emit('update:modelSelect', item); + // For backward compatibility: this.$emit('select', item); if (item) { @@ -341,9 +348,11 @@ var VueSimpleSuggest = { this.hover(null); }, hover(item, elem) { - const elemId = !!item ? this.getId(item, this.hoveredIndex) : ''; + const elemId = item ? this.getId(item, this.hoveredIndex) : ''; - this.inputElement.setAttribute('aria-activedescendant', elemId); + if (this.inputElement) { + this.inputElement.setAttribute('aria-activedescendant', elemId); + } if (item && item !== this.hovered) { this.$emit('hover', item, elem); @@ -396,7 +405,7 @@ var VueSimpleSuggest = { item = this.selected || this.suggestions[listEdge]; } else if (hoversBetweenEdges) { item = this.suggestions[this.hoveredIndex + direction]; - } else /* if hovers on edge */{ + } /* if hovers on edge */else { item = this.suggestions[listEdge]; } this.hover(item); @@ -447,7 +456,9 @@ var VueSimpleSuggest = { if (this.isClicking) { setTimeout(() => { - this.inputElement.focus(); + if (this.inputElement) { + this.inputElement.focus(); + } /// Ensure, that all needed flags are off before finishing the click. this.isClicking = false; @@ -456,7 +467,6 @@ var VueSimpleSuggest = { }, onBlur(e) { if (this.isInFocus) { - /// Clicking starts here, because input's blur occurs before the suggestionClick /// and exactly when the user clicks the mouse button or taps the screen. this.isClicking = this.hovered && !this.isTabbed; @@ -470,7 +480,9 @@ var VueSimpleSuggest = { this.isFalseFocus = true; } } else { - this.inputElement.blur(); + if (this.inputElement) { + this.inputElement.blur(); + } console.error(`This should never happen! If you encountered this error, please make sure that your input component emits 'focus' events properly. For more info see https://github.com/KazanExpress/vue-simple-suggest#custom-input. @@ -500,7 +512,7 @@ var VueSimpleSuggest = { const value = !inputEvent.target ? inputEvent : inputEvent.target.value; this.updateTextOutside(value); - this.$emit('input', value); + this.$emit('update:modelValue', value); }, updateTextOutside(value) { if (this.text === value) { @@ -531,7 +543,7 @@ var VueSimpleSuggest = { let newList = await this.getSuggestions(this.text); if (textBeforeRequest === this.text) { - this.$set(this, 'suggestions', newList); + this.suggestions = newList; } } } catch (e) { @@ -546,6 +558,7 @@ var VueSimpleSuggest = { this.showList(); } + // eslint-disable-next-line no-unsafe-finally return this.suggestions; } }, @@ -580,7 +593,7 @@ var VueSimpleSuggest = { nextIsPlainSuggestion = typeof result[0] !== 'object' && typeof result[0] !== 'undefined' || Array.isArray(result[0]); if (this.filterByQuery) { - result = result.filter(el => this.filter(el, value)); + result = result.filter(el => this.filterResult(el, value)); } if (this.listIsRequest) { @@ -594,10 +607,11 @@ var VueSimpleSuggest = { } } finally { if (this.maxSuggestions) { - result.splice(this.maxSuggestions); + result = result.slice(0, this.maxSuggestions); } this.isPlainSuggestion = nextIsPlainSuggestion; + // eslint-disable-next-line no-unsafe-finally return result; } }, @@ -610,4 +624,72 @@ var VueSimpleSuggest = { } }; -export default VueSimpleSuggest; +const _hoisted_1 = ["aria-owns", "aria-expanded"]; +const _hoisted_2 = ["value"]; +const _hoisted_3 = ["id", "aria-labelledby"]; +const _hoisted_4 = ["onMouseenter", "onClick", "aria-selected", "id"]; + +function render(_ctx, _cache, $props, $setup, $data, $options) { + return openBlock(), createElementBlock("div", { + class: normalizeClass(["vue-simple-suggest", [$props.styles.vueSimpleSuggest, { designed: !$props.destyled, focus: $data.isInFocus }]]), + onKeydown: _cache[1] || (_cache[1] = withKeys($event => $data.isTabbed = true, ["tab"])) + }, [createElementVNode("div", { + class: normalizeClass(["input-wrapper", $props.styles.inputWrapper]), + ref: "inputSlot", + role: "combobox", + "aria-haspopup": "listbox", + "aria-owns": $data.listId, + "aria-expanded": !!$data.listShown && !$props.removeList ? 'true' : 'false' + }, [renderSlot(_ctx.$slots, "default", { + field: $options.field, + componentField: $options.componentField + }, () => [createElementVNode("input", mergeProps({ class: "default-input" }, $options.field, { + value: $data.text || '', + class: $props.styles.defaultInput + }), null, 16 /* FULL_PROPS */, _hoisted_2)])], 10 /* CLASS, PROPS */, _hoisted_1), createVNode(Transition, { name: "vue-simple-suggest" }, { + default: withCtx(() => [!!$data.listShown && !$props.removeList ? (openBlock(), createElementBlock("ul", { + key: 0, + id: $data.listId, + class: normalizeClass(["suggestions", $props.styles.suggestions]), + role: "listbox", + "aria-labelledby": $data.listId + }, [!!_ctx.$slots['misc-item-above'] ? (openBlock(), createElementBlock("li", { + key: 0, + class: normalizeClass($props.styles.miscItemAbove) + }, [renderSlot(_ctx.$slots, "misc-item-above", { + suggestions: $data.suggestions, + query: $data.text + })], 2 /* CLASS */)) : createCommentVNode("v-if", true), (openBlock(true), createElementBlock(Fragment, null, renderList($data.suggestions, (suggestion, index) => { + return openBlock(), createElementBlock("li", { + class: normalizeClass(["suggest-item", [$props.styles.suggestItem, { + selected: $options.isSelected(suggestion), + hover: $options.isHovered(suggestion) + }]]), + role: "option", + onMouseenter: $event => $options.hover(suggestion, $event.target), + onMouseleave: _cache[0] || (_cache[0] = $event => $options.hover(null)), + onClick: $event => $options.suggestionClick(suggestion, $event), + "aria-selected": $options.isHovered(suggestion) || $options.isSelected(suggestion) ? 'true' : 'false', + + id: $options.getId(suggestion, index), + key: $options.getId(suggestion, index) + }, [renderSlot(_ctx.$slots, "suggestion-item", { + autocomplete: () => $options.autocompleteText(suggestion), + suggestion: suggestion, + query: $data.text + }, () => [createElementVNode("span", null, toDisplayString($options.displayProperty(suggestion)), 1 /* TEXT */)])], 42 /* CLASS, PROPS, HYDRATE_EVENTS */, _hoisted_4); + }), 128 /* KEYED_FRAGMENT */)), !!_ctx.$slots['misc-item-below'] ? (openBlock(), createElementBlock("li", { + key: 1, + class: normalizeClass($props.styles.miscItemBelow) + }, [renderSlot(_ctx.$slots, "misc-item-below", { + suggestions: $data.suggestions, + query: $data.text + })], 2 /* CLASS */)) : createCommentVNode("v-if", true)], 10 /* CLASS, PROPS */, _hoisted_3)) : createCommentVNode("v-if", true)]), + _: 3 /* FORWARDED */ + })], 34 /* CLASS, HYDRATE_EVENTS */); +} + +script.render = render; +script.__file = "lib/vue-simple-suggest.vue"; + +export default script; diff --git a/dist/iife.js b/dist/iife.js index 08138ed3..4b2844fd 100644 --- a/dist/iife.js +++ b/dist/iife.js @@ -1 +1 @@ -var VueSimpleSuggest=function(){"use strict";var t={selectionUp:[38],selectionDown:[40],select:[13],hideList:[27],showList:[40],autocomplete:[32,13]},e={input:String,select:Object};function r(t,e){return i(t,e.keyCode)}function i(t,e){if(t.length<=0)return!1;function s(t){return t.some(function(t){return t===e})}return Array.isArray(t[0])?t.some(function(t){return s(t)}):s(t)}var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},u=Object.assign||function(t){for(var e=1;e=this.minLength&&(0=this.minLength&&(0 ul { list-style: none; margin: 0; padding: 0; } - .vue-simple-suggest.designed { position: relative; } - -.vue-simple-suggest.designed, .vue-simple-suggest.designed * { +.vue-simple-suggest.designed, +.vue-simple-suggest.designed * { box-sizing: border-box; } - .vue-simple-suggest.designed .input-wrapper input { display: block; width: 100%; @@ -22,15 +19,13 @@ border-radius: 3px; color: black; background: white; - outline:none; - transition: all .1s; - transition-delay: .05s + outline: none; + transition: all 0.1s; + transition-delay: 0.05s; } - .vue-simple-suggest.designed.focus .input-wrapper input { border: 1px solid #aaa; } - .vue-simple-suggest.designed .suggestions { position: absolute; left: 0; @@ -43,23 +38,20 @@ opacity: 1; z-index: 1000; } - .vue-simple-suggest.designed .suggestions .suggest-item { cursor: pointer; user-select: none; } - .vue-simple-suggest.designed .suggestions .suggest-item, .vue-simple-suggest.designed .suggestions .misc-item { padding: 5px 10px; } - .vue-simple-suggest.designed .suggestions .suggest-item.hover { - background-color: #2874D5 !important; + background-color: #2874d5 !important; color: #fff !important; } - .vue-simple-suggest.designed .suggestions .suggest-item.selected { - background-color: #2832D5; + background-color: #2832d5; color: #fff; } + diff --git a/dist/umd.js b/dist/umd.js index b2388c04..8366d788 100644 --- a/dist/umd.js +++ b/dist/umd.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).VueSimpleSuggest=e()}(this,function(){"use strict";var t={selectionUp:[38],selectionDown:[40],select:[13],hideList:[27],showList:[40],autocomplete:[32,13]},e={input:String,select:Object};function r(t,e){return i(t,e.keyCode)}function i(t,e){if(t.length<=0)return!1;function s(t){return t.some(function(t){return t===e})}return Array.isArray(t[0])?t.some(function(t){return s(t)}):s(t)}var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function o(t,e,s){return s?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}function u(){}function l(t,e){var s=t();return s&&s.then?s.then(e):e(s)}function c(t,e){try{var s=t()}catch(t){return e(t)}return s&&s.then?s.then(void 0,e):s}function h(t,e){try{var s=t()}catch(t){return e()}return s&&s.then?s.then(e,e):e()}var s={render:function(){var s=this,t=s.$createElement,i=s._self._c||t;return i("div",{staticClass:"vue-simple-suggest",class:[s.styles.vueSimpleSuggest,{designed:!s.destyled,focus:s.isInFocus}],on:{keydown:function(t){if(!t.type.indexOf("key")&&s._k(t.keyCode,"tab",9,t.key,"Tab"))return null;s.isTabbed=!0}}},[i("div",{ref:"inputSlot",staticClass:"input-wrapper",class:s.styles.inputWrapper,attrs:{role:"combobox","aria-haspopup":"listbox","aria-owns":s.listId,"aria-expanded":s.listShown&&!s.removeList?"true":"false"}},[s._t("default",[i("input",s._b({staticClass:"default-input",class:s.styles.defaultInput,domProps:{value:s.text||""}},"input",s.$attrs,!1))])],2),s._v(" "),i("transition",{attrs:{name:"vue-simple-suggest"}},[s.listShown&&!s.removeList?i("ul",{staticClass:"suggestions",class:s.styles.suggestions,attrs:{id:s.listId,role:"listbox","aria-labelledby":s.listId}},[this.$scopedSlots["misc-item-above"]?i("li",{class:s.styles.miscItemAbove},[s._t("misc-item-above",null,{suggestions:s.suggestions,query:s.text})],2):s._e(),s._v(" "),s._l(s.suggestions,function(e,t){return i("li",{key:s.getId(e,t),staticClass:"suggest-item",class:[s.styles.suggestItem,{selected:s.isSelected(e),hover:s.isHovered(e)}],attrs:{role:"option","aria-selected":s.isHovered(e)||s.isSelected(e)?"true":"false",id:s.getId(e,t)},on:{mouseenter:function(t){return s.hover(e,t.target)},mouseleave:function(){return s.hover(void 0)},click:function(t){return s.suggestionClick(e,t)}}},[s._t("suggestion-item",[i("span",[s._v(s._s(s.displayProperty(e)))])],{autocomplete:function(){return s.autocompleteText(e)},suggestion:e,query:s.text})],2)}),s._v(" "),this.$scopedSlots["misc-item-below"]?i("li",{class:s.styles.miscItemBelow},[s._t("misc-item-below",null,{suggestions:s.suggestions,query:s.text})],2):s._e()],2):s._e()])],1)},staticRenderFns:[],name:"vue-simple-suggest",inheritAttrs:!1,model:{prop:"value",event:"input"},props:{styles:{type:Object,default:function(){return{}}},controls:{type:Object,default:function(){return t}},minLength:{type:Number,default:1},maxSuggestions:{type:Number,default:10},displayAttribute:{type:String,default:"title"},valueAttribute:{type:String,default:"id"},list:{type:[Function,Array],default:function(){return[]}},removeList:{type:Boolean,default:!1},destyled:{type:Boolean,default:!1},filterByQuery:{type:Boolean,default:!1},filter:{type:Function,default:function(t,e){return!e||~this.displayProperty(t).toLowerCase().indexOf(e.toLowerCase())}},debounce:{type:Number,default:0},nullableSelect:{type:Boolean,default:!1},value:{},mode:{type:String,default:"input",validator:function(t){return!!~Object.keys(e).indexOf(t.toLowerCase())}},preventHide:{type:Boolean,default:!1}},watch:{mode:{handler:function(t){var e=this;this.constructor.options.model.event=t,this.$parent&&this.$parent.$forceUpdate(),this.$nextTick(function(){"input"===t?e.$emit("input",e.text):e.$emit("select",e.selected)})},immediate:!0},value:{handler:function(t){"string"!=typeof t&&(t=this.displayProperty(t)),this.updateTextOutside(t)},immediate:!0}},data:function(){return{selected:null,hovered:null,suggestions:[],listShown:!1,inputElement:null,canSend:!0,timeoutInstance:null,text:this.value,isPlainSuggestion:!1,isClicking:!1,isInFocus:!1,isFalseFocus:!1,isTabbed:!1,controlScheme:{},listId:this._uid+"-suggestions"}},computed:{listIsRequest:function(){return"function"==typeof this.list},inputIsComponent:function(){return this.$slots.default&&0=this.minLength&&(0=this.minLength&&(0 -
-
-

v-model mode: - - - - -

- - - +
+

+ v-model mode: + + + +

+ + + + - - - diff --git a/example/src/main.js b/example/src/main.js index 385fcfe8..43d27f62 100644 --- a/example/src/main.js +++ b/example/src/main.js @@ -1,7 +1,5 @@ -import Vue from 'vue' +import { createApp } from 'vue' import App from './App.vue' -new Vue({ - el: '#app', - render: h => h(App) -}) +const app = createApp(App) +app.mount('#app') diff --git a/example/webpack.config.js b/example/webpack.config.js index 412b78a3..a7dd50a8 100644 --- a/example/webpack.config.js +++ b/example/webpack.config.js @@ -2,18 +2,19 @@ var path = require('path') var webpack = require('webpack') var HtmlWebPackPlugin = require('html-webpack-plugin') var CopyWebpackPlugin = require('copy-webpack-plugin') -var VueLoaderPlugin = require('vue-loader/lib/plugin') +var { VueLoaderPlugin } = require('vue-loader') module.exports = { - entry: ['whatwg-fetch/fetch.js', 'core-js/es/promise', path.resolve(__dirname, './src/main.js')], + entry: [ + 'whatwg-fetch/fetch.js', + 'core-js/es/promise', + path.resolve(__dirname, './src/main.js') + ], module: { rules: [ { test: /\.css$/, - use: [ - 'vue-style-loader', - 'css-loader' - ], + use: ['vue-style-loader', 'css-loader'] }, { test: /\.vue$/, @@ -28,7 +29,7 @@ module.exports = { test: /\.html$/, use: [ { - loader: "html-loader", + loader: 'html-loader', options: { minimize: true } } ] @@ -44,32 +45,44 @@ module.exports = { }, resolve: { alias: { - 'vue$': 'vue/dist/vue.esm.js', + vue$: 'vue/dist/vue.esm-bundler.js', 'vue-simple-suggest': path.resolve(__dirname, '../') }, - extensions: ['*', '.js', '.vue', '.json'] + extensions: ['.*', '.js', '.vue', '.json'] }, devServer: { - contentBase: __dirname, + static: __dirname, historyApiFallback: true, - noInfo: false, - overlay: true + client: { + overlay: true + } }, performance: { hints: false }, plugins: [ new VueLoaderPlugin(), - new CopyWebpackPlugin([ - { from: path.resolve(__dirname, '../assets'), to: path.resolve(__dirname, 'src/assets') }, - { from: path.resolve(__dirname, '../assets'), to: path.resolve(__dirname, '../docs/assets') }, - { from: path.resolve(__dirname, 'src/assets'), to: path.resolve(__dirname, '../docs/assets') } - ]), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(__dirname, '../assets'), + to: path.resolve(__dirname, 'src/assets') + }, + { + from: path.resolve(__dirname, '../assets'), + to: path.resolve(__dirname, '../docs/assets') + }, + { + from: path.resolve(__dirname, 'src/assets'), + to: path.resolve(__dirname, '../docs/assets') + } + ] + }), new HtmlWebPackPlugin({ template: path.resolve(__dirname, './src/index.ejs') }) ], output: { - path: path.resolve(__dirname, "../docs") + path: path.resolve(__dirname, '../docs') } } diff --git a/lib/misc.js b/lib/misc.js index eeb64d10..3bb8a0fd 100644 --- a/lib/misc.js +++ b/lib/misc.js @@ -9,7 +9,7 @@ export const defaultControls = { export const modes = { input: String, - select: Object, + select: Object } export function fromPath(obj, path) { @@ -23,10 +23,53 @@ export function hasKeyCode(arr, event) { export function hasKeyCodeByCode(arr, keyCode) { if (arr.length <= 0) return false - const has = arr => arr.some(code => code === keyCode) + const has = (arr) => arr.some((code) => code === keyCode) if (Array.isArray(arr[0])) { - return arr.some(array => has(array)) + return arr.some((array) => has(array)) } else { return has(arr) } } + +const onRE = /^on[^a-z]/ + +export function isOn(key) { + return onRE.test(key) +} + +export function getPropertyByAttribute(obj, attr) { + return typeof obj !== 'undefined' ? fromPath(obj, attr) : obj +} + +export function display(obj, attribute, isPlainSuggestion) { + if (isPlainSuggestion) { + return obj + } + + let display = getPropertyByAttribute(obj, attribute) + + if (typeof display === 'undefined') { + display = JSON.stringify(obj) + + if (typeof process !== 'undefined' && ~process.env.NODE_ENV.indexOf('dev')) { + console.warn( + '[vue-simple-suggest]: Please, provide `display-attribute` as a key or a dotted path for a property from your object.' + ) + } + } + + return String(display || '') +} + +const HAS_WINDOW_SUPPORT = typeof window !== 'undefined' + +export const requestAF = HAS_WINDOW_SUPPORT + ? window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + window.oRequestAnimationFrame || + // Fallback, but not a true polyfill + // Only needed for Opera Mini + ((cb) => setTimeout(cb, 16)) + : (cb) => setTimeout(cb, 0) diff --git a/lib/vue-simple-suggest.vue b/lib/vue-simple-suggest.vue index ac3b4bbf..0895cc83 100644 --- a/lib/vue-simple-suggest.vue +++ b/lib/vue-simple-suggest.vue @@ -1,55 +1,80 @@