From bc9553a82e3e97b40b0e1e5060e6a232af19abfa Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Sun, 8 Mar 2020 17:58:06 -0700 Subject: [PATCH 01/88] fix: Compare Options with getOptionKey instead of label + reduce (#1012) --- src/components/Select.vue | 90 ++++++++------------- src/mixins/typeAheadPointer.js | 14 ++-- src/utility/sortAndStringify.js | 15 ++++ tests/unit/Labels.spec.js | 37 +++++++++ tests/unit/OptionComparator.spec.js | 31 +++++++ tests/unit/OptionKey.spec.js | 4 +- tests/unit/Selecting.spec.js | 15 +++- tests/unit/Tagging.spec.js | 14 +++- tests/unit/utility/sortAndStringify.spec.js | 14 ++++ 9 files changed, 162 insertions(+), 72 deletions(-) create mode 100644 src/utility/sortAndStringify.js create mode 100644 tests/unit/OptionComparator.spec.js create mode 100644 tests/unit/utility/sortAndStringify.spec.js diff --git a/src/components/Select.vue b/src/components/Select.vue index ded68c5..89235cc 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -84,6 +84,7 @@ import ajax from '../mixins/ajax' import childComponents from './childComponents'; import appendToBody from '../directives/appendToBody'; + import sortAndStringify from '../utility/sortAndStringify' import uniqueId from '../utility/uniqueId'; /** @@ -281,12 +282,16 @@ }, /** - * Callback to get an option key. If {option} - * is an object and has an {id}, returns {option.id} - * by default, otherwise tries to serialize {option} - * to JSON. + * Generate a unique identifier for each option. If `option` + * is an object and `option.hasOwnProperty('id')` exists, + * `option.id` is used by default, otherwise the option + * will be serialized to JSON. * - * The key must be unique for an option. + * If you are supplying a lot of options, you should + * provide your own keys, as JSON.stringify can be + * slow with lots of objects. + * + * The result of this function *must* be unique. * * @type {Function} * @param {Object || String} option @@ -294,22 +299,21 @@ */ getOptionKey: { type: Function, - default(option) { - if (typeof option === 'object' && option.id) { - return option.id - } else { - try { - return JSON.stringify(option) - } catch(e) { - return console.warn( - `[vue-select warn]: Could not stringify option ` + - `to generate unique key. Please provide'getOptionKey' prop ` + - `to return a unique key for each option.\n` + - 'https://vue-select.org/api/props.html#getoptionkey' - ); - } + default (option) { + if (typeof option !== 'object') { + return option; } - } + + try { + return option.hasOwnProperty('id') ? option.id : sortAndStringify(option); + } catch (e) { + const warning = `[vue-select warn]: Could not stringify this option ` + + `to generate unique key. Please provide'getOptionKey' prop ` + + `to return a unique key for each option.\n` + + 'https://vue-select.org/api/props.html#getoptionkey'; + return console.warn(warning, option, e); + } + }, }, /** @@ -393,7 +397,7 @@ * @return {Boolean} */ filter: { - "type": Function, + type: Function, default(options, search) { return options.filter((option) => { let label = this.getOptionLabel(option) @@ -643,7 +647,6 @@ select(option) { if (!this.isOptionSelected(option)) { if (this.taggable && !this.optionExists(option)) { - option = this.createOption(option); this.$emit('option:created', option); } if (this.multiple) { @@ -746,38 +749,18 @@ * @return {Boolean} True when selected | False otherwise */ isOptionSelected(option) { - return this.selectedValue.some(value => { - return this.optionComparator(value, option) - }) + return this.selectedValue.some(value => this.optionComparator(value, option)) }, /** * Determine if two option objects are matching. * - * @param value {Object} - * @param option {Object} + * @param a {Object} + * @param b {Object} * @returns {boolean} */ - optionComparator(value, option) { - if (typeof value !== 'object' && typeof option !== 'object') { - // Comparing primitives - if (value === option) { - return true - } - } else { - // Comparing objects - if (value === this.reduce(option)) { - return true - } - if ((this.getOptionLabel(value) === this.getOptionLabel(option)) || (this.getOptionLabel(value) === option)) { - return true - } - if (this.reduce(value) === this.reduce(option)) { - return true - } - } - - return false; + optionComparator(a, b) { + return this.getOptionKey(a) === this.getOptionKey(b); }, /** @@ -825,14 +808,7 @@ * @return {boolean} */ optionExists(option) { - return this.optionList.some(opt => { - if (typeof opt === 'object' && this.getOptionLabel(opt) === option) { - return true - } else if (opt === option) { - return true - } - return false - }) + return this.optionList.some(_option => this.optionComparator(_option, option)) }, /** @@ -1138,7 +1114,7 @@ } let options = this.search.length ? this.filter(optionList, this.search, this) : optionList; - if (this.taggable && this.search.length && !this.optionExists(this.search)) { + if (this.taggable && this.search.length && !this.optionExists(this.createOption(this.search))) { options.unshift(this.search) } return options @@ -1158,7 +1134,7 @@ */ showClearButton() { return !this.multiple && this.clearable && !this.open && !this.isValueEmpty - } + }, }, } diff --git a/src/mixins/typeAheadPointer.js b/src/mixins/typeAheadPointer.js index 4eb2636..331cfec 100644 --- a/src/mixins/typeAheadPointer.js +++ b/src/mixins/typeAheadPointer.js @@ -56,15 +56,15 @@ export default { * Optionally clear the search input on selection. * @return {void} */ - typeAheadSelect() { - if( this.filteredOptions[ this.typeAheadPointer ] ) { - this.select( this.filteredOptions[ this.typeAheadPointer ] ); - } else if (this.taggable && this.search.length){ - this.select(this.search) + typeAheadSelect () { + if (!this.taggable && this.filteredOptions[this.typeAheadPointer]) { + this.select(this.filteredOptions[this.typeAheadPointer]); + } else if (this.taggable && this.search.length) { + this.select(this.createOption(this.search)); } - if( this.clearSearchOnSelect ) { - this.search = ""; + if (this.clearSearchOnSelect) { + this.search = ''; } }, } diff --git a/src/utility/sortAndStringify.js b/src/utility/sortAndStringify.js new file mode 100644 index 0000000..14a619d --- /dev/null +++ b/src/utility/sortAndStringify.js @@ -0,0 +1,15 @@ +/** + * @param sortable {object} + * @return {string} + */ +function sortAndStringify(sortable) { + const ordered = {}; + + Object.keys(sortable).sort().forEach(key => { + ordered[key] = sortable[key]; + }); + + return JSON.stringify(ordered); +} + +export default sortAndStringify; diff --git a/tests/unit/Labels.spec.js b/tests/unit/Labels.spec.js index 5f161d9..1cbe8ef 100755 --- a/tests/unit/Labels.spec.js +++ b/tests/unit/Labels.spec.js @@ -41,4 +41,41 @@ describe("Labels", () => { Select.vm.$data._value = "one"; expect(Select.vm.searchPlaceholder).not.toBeDefined(); }); + + describe('getOptionLabel', () => { + it('will return undefined if the option lacks the label key', () => { + const getOptionLabel = VueSelect.props.getOptionLabel.default.bind({ label: 'label' }); + expect(getOptionLabel({name: 'vue'})).toEqual(undefined); + }); + + it('will return a string value for a valid key', () => { + const getOptionLabel = VueSelect.props.getOptionLabel.default.bind({ label: 'label' }); + expect(getOptionLabel({label: 'vue'})).toEqual('vue'); + }); + + /** + * this test fails because of a bug where Vue executes the default contents + * of a slot, even if it is implemented by the consumer. + * @see https://github.com/vuejs/vue/issues/10224 + * @see https://github.com/vuejs/vue/pull/10229 + */ + xit('will not call getOptionLabel if both scoped option slots are used and a filter is provided', () => { + const spy = spyOn(VueSelect.props.getOptionLabel, 'default'); + const Select = shallowMount(VueSelect, { + propsData: { + options: [{name: 'one'}], + filter: () => {}, + }, + scopedSlots: { + 'option': '{{ props.name }}', + 'selected-option': '{{ props.name }}', + }, + }); + + Select.vm.select({name: 'one'}); + + expect(spy).toHaveBeenCalledTimes(0); + expect(Select.find('.selected').exists()).toBeTruthy(); + }); + }); }); diff --git a/tests/unit/OptionComparator.spec.js b/tests/unit/OptionComparator.spec.js new file mode 100644 index 0000000..0e4da48 --- /dev/null +++ b/tests/unit/OptionComparator.spec.js @@ -0,0 +1,31 @@ +import Select from '../../src/components/Select'; + +describe('Comparing Options', () => { + + const comparator = Select.methods.optionComparator.bind({ + getOptionKey: Select.props.getOptionKey.default, + }); + + it('can compare numbers', () => { + expect(comparator(1, 2)).toBeFalsy(); + expect(comparator(1, 1)).toBeTruthy(); + }); + + it('can compare strings', () => { + expect(comparator('one', 'one')).toBeTruthy(); + expect(comparator('one', 'two')).toBeFalsy(); + }); + + it('can compare objects', () => { + // compare ID keys + expect(comparator({label: 'halo', id: 1}, {label: 'halo', id: 2})) + .toBeFalsy(); + // compare objects + expect(comparator({label: 'halo', value: 1}, {label: 'halo', value: 1})) + .toBeTruthy(); + // compare objects with different orders + expect(comparator({value: 1, label: 'halo'}, {label: 'halo', value: 1})) + .toBeTruthy(); + }); + +}); diff --git a/tests/unit/OptionKey.spec.js b/tests/unit/OptionKey.spec.js index c1417f0..cbe40b2 100644 --- a/tests/unit/OptionKey.spec.js +++ b/tests/unit/OptionKey.spec.js @@ -5,11 +5,11 @@ describe('Serializing Option Keys', () => { const getOptionKey = Select.props.getOptionKey.default; it('can serialize strings to a key', () => { - expect(getOptionKey('vue')).toBe('"vue"'); + expect(getOptionKey('vue')).toBe('vue'); }); it('can serialize integers to a key', () => { - expect(getOptionKey(1)).toBe('1'); + expect(getOptionKey(1)).toBe(1); }); it('can serialize objects to a key', () => { diff --git a/tests/unit/Selecting.spec.js b/tests/unit/Selecting.spec.js index d4d1489..b7a7290 100755 --- a/tests/unit/Selecting.spec.js +++ b/tests/unit/Selecting.spec.js @@ -1,5 +1,6 @@ import { mount, shallowMount } from "@vue/test-utils"; import VueSelect from "../../src/components/Select.vue"; +import { mountDefault } from '../helpers'; describe("VS - Selecting Values", () => { let defaultProps; @@ -192,10 +193,20 @@ describe("VS - Selecting Values", () => { value: [{ label: "foo", value: "bar" }] } }); - expect(Select.vm.isOptionSelected("foo")).toEqual(true); + expect(Select.vm.isOptionSelected({ label: "foo", value: "bar" })).toEqual(true); }); - describe("change Event", () => { + it('can select two options with the same label', () => { + const options = [{label: 'one', id: 1}, {label: 'one', id: 2}]; + const Select = mountDefault({options, multiple: true}); + + Select.vm.select({label: 'one', id: 1}); + Select.vm.select({label: 'one', id: 2}); + + expect(Select.vm.selectedValue).toEqual(options); + }); + + describe("input Event", () => { it("will trigger the input event when the selection changes", () => { const Select = shallowMount(VueSelect); Select.vm.select("bar"); diff --git a/tests/unit/Tagging.spec.js b/tests/unit/Tagging.spec.js index 46292ff..9e32979 100755 --- a/tests/unit/Tagging.spec.js +++ b/tests/unit/Tagging.spec.js @@ -1,6 +1,8 @@ import { searchSubmit, selectWithProps } from "../helpers"; +import Select from '../../src/components/Select'; describe("When Tagging Is Enabled", () => { + it("can determine if a given option string already exists", () => { const Select = selectWithProps({ taggable: true, options: ["one", "two"] }); expect(Select.vm.optionExists("one")).toEqual(true); @@ -13,8 +15,8 @@ describe("When Tagging Is Enabled", () => { options: [{ label: "one" }, { label: "two" }] }); - expect(Select.vm.optionExists("one")).toEqual(true); - expect(Select.vm.optionExists("three")).toEqual(false); + expect(Select.vm.optionExists({label: "one"})).toEqual(true); + expect(Select.vm.optionExists({label: "three"})).toEqual(false); }); it("can determine if a given option object already exists when using custom labels", () => { @@ -24,8 +26,10 @@ describe("When Tagging Is Enabled", () => { label: "foo" }); - expect(Select.vm.optionExists("one")).toEqual(true); - expect(Select.vm.optionExists("three")).toEqual(false); + const createOption = (text) => Select.vm.createOption(text); + + expect(Select.vm.optionExists(createOption("one"))).toEqual(true); + expect(Select.vm.optionExists(createOption("three"))).toEqual(false); }); it("can add the current search text as the first item in the options list", () => { @@ -228,9 +232,11 @@ describe("When Tagging Is Enabled", () => { multiple: true, options: [{ label: "two" }] }); + const spy = jest.spyOn(Select.vm, 'select'); searchSubmit(Select, "one"); expect(Select.vm.selectedValue).toEqual([{ label: "one" }]); + expect(spy).lastCalledWith({label: 'one'}); expect(Select.vm.search).toEqual(""); searchSubmit(Select, "one"); diff --git a/tests/unit/utility/sortAndStringify.spec.js b/tests/unit/utility/sortAndStringify.spec.js new file mode 100644 index 0000000..414e2a0 --- /dev/null +++ b/tests/unit/utility/sortAndStringify.spec.js @@ -0,0 +1,14 @@ +import sortAndStringify from '../../../src/utility/sortAndStringify'; + +test('it will stringify an object', () => { + expect(sortAndStringify({hello: 'world'})).toEqual('{"hello":"world"}'); +}); + +test('it will sort attributes alphabetically', () => { + expect(sortAndStringify({b: 'b', a: 'a'})).toEqual('{"a":"a","b":"b"}'); +}); + +test('comparing two objects with unsorted keys', () => { + expect(sortAndStringify({b: 'b', a: 'a'})) + .toEqual(sortAndStringify({a: 'a', b: 'b'})) +}); From 531949447cfac37930321144c043f0fded7c25c7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 9 Mar 2020 00:59:13 +0000 Subject: [PATCH 02/88] =?UTF-8?q?chore(=F0=9F=9A=80):=203.7.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20ccf91..83d0a95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "3.7.0", + "version": "3.7.1", "description": "Everything you wish the HTML element could do, wrapped up into a lightweight, extensible Vue component.", "author": "Jeff Sagal ", "homepage": "https://vue-select.org", From b2f388bc89d8834f0b1d7d96cc8ca77c97460d94 Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Mon, 9 Mar 2020 21:56:37 -0700 Subject: [PATCH 05/88] feat: header, footer, list-header, list-footer slots (#1085) --- docs/.vuepress/components/Paginated.vue | 49 +++++++++++++++++++ docs/.vuepress/config.js | 1 + docs/api/slots.md | 63 +++++++++++++++++++++++++ docs/guide/pagination.md | 18 +++++++ src/components/Select.vue | 16 ++++++- tests/unit/Slots.spec.js | 47 ++++++++++++++++++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 docs/.vuepress/components/Paginated.vue create mode 100644 docs/guide/pagination.md diff --git a/docs/.vuepress/components/Paginated.vue b/docs/.vuepress/components/Paginated.vue new file mode 100644 index 0000000..65fc0b8 --- /dev/null +++ b/docs/.vuepress/components/Paginated.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 2ee84ca..da73379 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -119,6 +119,7 @@ module.exports = { children: [ ['guide/validation', 'Validation'], ['guide/selectable', 'Limiting Selections'], + ['guide/pagination', 'Pagination'], ['guide/vuex', 'Vuex'], ['guide/ajax', 'AJAX'], ['guide/loops', 'Using in Loops'], diff --git a/docs/api/slots.md b/docs/api/slots.md index 68e74a2..453a97f 100644 --- a/docs/api/slots.md +++ b/docs/api/slots.md @@ -3,6 +3,39 @@ Vue Select leverages scoped slots to allow for total customization of the presen Slots can be used to change the look and feel of the UI, or to simply swap out text. ::: +## Wrapper + +### `header` + +Displayed at the top of the component, above `.vs__dropdown-toggle`. + +- `search {string}` - the current search query +- `loading {boolean}` - is the component loading +- `searching {boolean}` - is the component searching +- `filteredOptions {array}` - options filtered by the search text +- `deselect {function}` - function to deselect an option + +```html + +``` + +### `footer` + +Displayed at the bottom of the component, below `.vs__dropdown-toggle`. + +When implementing this slot, you'll likely need to use `appendToBody` to position the dropdown. +Otherwise content in this slot will affect it's positioning. + +- `search {string}` - the current search query +- `loading {boolean}` - is the component loading +- `searching {boolean}` - is the component searching +- `filteredOptions {array}` - options filtered by the search text +- `deselect {function}` - function to deselect an option + +```html + +``` + ## Selected Option(s) ### `selected-option` @@ -91,8 +124,38 @@ attributes : { ## Dropdown +### `list-header` + +Displayed as the first item in the dropdown. No content by default. Parent element is the `
    `, +so this slot should contain a root `
  • `. + +- `search {string}` - the current search query +- `loading {boolean}` - is the component loading +- `searching {boolean}` - is the component searching +- `filteredOptions {array}` - options filtered by the search text + +```html + +``` + +### `list-footer` + +Displayed as the last item in the dropdown. No content by default. Parent element is the `
      `, +so this slot should contain a root `
    • `. + +- `search {string}` - the current search query +- `loading {boolean}` - is the component loading +- `searching {boolean}` - is the component searching +- `filteredOptions {array}` - options filtered by the search text + +```html + +``` + ### `option` +The current option within the dropdown, contained within `
    • `. + - `option {Object}` - The currently iterated option from `filteredOptions` ```html diff --git a/docs/guide/pagination.md b/docs/guide/pagination.md new file mode 100644 index 0000000..15042dd --- /dev/null +++ b/docs/guide/pagination.md @@ -0,0 +1,18 @@ +::: tip +Pagination is supported using slots available with Vue Select 3.8 and above. +::: + +Pagination can be a super helpful tool when working with large sets of data. If you have 1,000 +options, the component is going to render 1,000 DOM nodes. That's a lot of nodes to insert/remove, +and chances are your user is only interested in a few of them anyways. + +To implement pagination with Vue Select, you can take advantage of the `list-footer` slot. It +appears below all other options in the drop down list. + +To make pagination work properly with filtering, you'll have to handle it yourself in the parent. +You can use the `filterable` boolean to turn off Vue Select's filtering, and then hook into the +`search` event to use the current search query in the parent component. + + + +<<< @/.vuepress/components/Paginated.vue diff --git a/src/components/Select.vue b/src/components/Select.vue index 3527b77..cdf2509 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -4,6 +4,7 @@ @@ -1001,6 +1004,12 @@ * @returns {Object} */ scope () { + const listSlot = { + search: this.search, + loading: this.loading, + searching: this.searching, + filteredOptions: this.filteredOptions + }; return { search: { attributes: { @@ -1032,6 +1041,7 @@ }, noOptions: { search: this.search, + loading: this.loading, searching: this.searching, }, openIndicator: { @@ -1041,6 +1051,10 @@ 'class': 'vs__open-indicator', }, }, + listHeader: listSlot, + listFooter: listSlot, + header: { ...listSlot, deselect: this.deselect }, + footer: { ...listSlot, deselect: this.deselect } }; }, diff --git a/tests/unit/Slots.spec.js b/tests/unit/Slots.spec.js index 4e886d5..955829b 100644 --- a/tests/unit/Slots.spec.js +++ b/tests/unit/Slots.spec.js @@ -68,8 +68,55 @@ describe('Scoped Slots', () => { await Select.vm.$nextTick(); expect(noOptions).toHaveBeenCalledWith({ + loading: false, search: 'something not there', searching: true, }) }); + + test('header slot props', async () => { + const header = jest.fn(); + const Select = mountDefault({}, { + scopedSlots: {header: header}, + }); + await Select.vm.$nextTick(); + expect(Object.keys(header.mock.calls[0][0])).toEqual([ + 'search', 'loading', 'searching', 'filteredOptions', 'deselect', + ]); + }); + + test('footer slot props', async () => { + const footer = jest.fn(); + const Select = mountDefault({}, { + scopedSlots: {footer: footer}, + }); + await Select.vm.$nextTick(); + expect(Object.keys(footer.mock.calls[0][0])).toEqual([ + 'search', 'loading', 'searching', 'filteredOptions', 'deselect', + ]); + }); + + test('list-header slot props', async () => { + const header = jest.fn(); + const Select = mountDefault({}, { + scopedSlots: {'list-header': header}, + }); + Select.vm.open = true; + await Select.vm.$nextTick(); + expect(Object.keys(header.mock.calls[0][0])).toEqual([ + 'search', 'loading', 'searching', 'filteredOptions', + ]); + }); + + test('list-footer slot props', async () => { + const footer = jest.fn(); + const Select = mountDefault({}, { + scopedSlots: {'list-footer': footer}, + }); + Select.vm.open = true; + await Select.vm.$nextTick(); + expect(Object.keys(footer.mock.calls[0][0])).toEqual([ + 'search', 'loading', 'searching', 'filteredOptions', + ]); + }); }); From d71d592bb36cec0a260262cc4aae1a072e359a44 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 10 Mar 2020 04:57:45 +0000 Subject: [PATCH 06/88] =?UTF-8?q?chore(=F0=9F=9A=80):=203.8.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 934ef4c..3ae58b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "3.7.2", + "version": "3.8.0", "description": "Everything you wish the HTML element could do, wrapped up into a lightweight, extensible Vue component.", "author": "Jeff Sagal ", "homepage": "https://vue-select.org", From a820f06d61e7100402b95fad5d2ed1c012cbae80 Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Wed, 11 Mar 2020 09:16:07 -0700 Subject: [PATCH 09/88] docs: overhaul slot docs (#1099) * docs: sort slots alphabetically --- docs/.vuepress/components/SlotFooter.vue | 7 + docs/.vuepress/components/SlotHeader.vue | 7 + docs/.vuepress/components/SlotListFooter.vue | 7 + docs/.vuepress/components/SlotListHeader.vue | 7 + docs/.vuepress/components/SlotNoOptions.vue | 7 + .../components/SlotOpenIndicator.vue | 7 + docs/.vuepress/components/SlotOption.vue | 24 ++ docs/.vuepress/components/SlotSearch.vue | 12 + .../components/SlotSelectedOption.vue | 26 ++ .../SlotSelectedOptionContainer.vue | 23 ++ docs/.vuepress/components/SlotSpinner.vue | 9 + docs/api/slots.md | 271 +++++++++--------- 12 files changed, 275 insertions(+), 132 deletions(-) create mode 100644 docs/.vuepress/components/SlotFooter.vue create mode 100644 docs/.vuepress/components/SlotHeader.vue create mode 100644 docs/.vuepress/components/SlotListFooter.vue create mode 100644 docs/.vuepress/components/SlotListHeader.vue create mode 100644 docs/.vuepress/components/SlotNoOptions.vue create mode 100644 docs/.vuepress/components/SlotOpenIndicator.vue create mode 100644 docs/.vuepress/components/SlotOption.vue create mode 100644 docs/.vuepress/components/SlotSearch.vue create mode 100644 docs/.vuepress/components/SlotSelectedOption.vue create mode 100644 docs/.vuepress/components/SlotSelectedOptionContainer.vue create mode 100644 docs/.vuepress/components/SlotSpinner.vue diff --git a/docs/.vuepress/components/SlotFooter.vue b/docs/.vuepress/components/SlotFooter.vue new file mode 100644 index 0000000..2d95fdc --- /dev/null +++ b/docs/.vuepress/components/SlotFooter.vue @@ -0,0 +1,7 @@ + diff --git a/docs/.vuepress/components/SlotHeader.vue b/docs/.vuepress/components/SlotHeader.vue new file mode 100644 index 0000000..4c1525a --- /dev/null +++ b/docs/.vuepress/components/SlotHeader.vue @@ -0,0 +1,7 @@ + diff --git a/docs/.vuepress/components/SlotListFooter.vue b/docs/.vuepress/components/SlotListFooter.vue new file mode 100644 index 0000000..9ba155b --- /dev/null +++ b/docs/.vuepress/components/SlotListFooter.vue @@ -0,0 +1,7 @@ + diff --git a/docs/.vuepress/components/SlotListHeader.vue b/docs/.vuepress/components/SlotListHeader.vue new file mode 100644 index 0000000..bfb24d3 --- /dev/null +++ b/docs/.vuepress/components/SlotListHeader.vue @@ -0,0 +1,7 @@ + diff --git a/docs/.vuepress/components/SlotNoOptions.vue b/docs/.vuepress/components/SlotNoOptions.vue new file mode 100644 index 0000000..c8e9b5c --- /dev/null +++ b/docs/.vuepress/components/SlotNoOptions.vue @@ -0,0 +1,7 @@ + diff --git a/docs/.vuepress/components/SlotOpenIndicator.vue b/docs/.vuepress/components/SlotOpenIndicator.vue new file mode 100644 index 0000000..302d85a --- /dev/null +++ b/docs/.vuepress/components/SlotOpenIndicator.vue @@ -0,0 +1,7 @@ + diff --git a/docs/.vuepress/components/SlotOption.vue b/docs/.vuepress/components/SlotOption.vue new file mode 100644 index 0000000..e94e435 --- /dev/null +++ b/docs/.vuepress/components/SlotOption.vue @@ -0,0 +1,24 @@ + + + diff --git a/docs/.vuepress/components/SlotSearch.vue b/docs/.vuepress/components/SlotSearch.vue new file mode 100644 index 0000000..d8b611f --- /dev/null +++ b/docs/.vuepress/components/SlotSearch.vue @@ -0,0 +1,12 @@ + diff --git a/docs/.vuepress/components/SlotSelectedOption.vue b/docs/.vuepress/components/SlotSelectedOption.vue new file mode 100644 index 0000000..4db88ed --- /dev/null +++ b/docs/.vuepress/components/SlotSelectedOption.vue @@ -0,0 +1,26 @@ + + + diff --git a/docs/.vuepress/components/SlotSelectedOptionContainer.vue b/docs/.vuepress/components/SlotSelectedOptionContainer.vue new file mode 100644 index 0000000..aa0ba69 --- /dev/null +++ b/docs/.vuepress/components/SlotSelectedOptionContainer.vue @@ -0,0 +1,23 @@ + + + diff --git a/docs/.vuepress/components/SlotSpinner.vue b/docs/.vuepress/components/SlotSpinner.vue new file mode 100644 index 0000000..3e9c083 --- /dev/null +++ b/docs/.vuepress/components/SlotSpinner.vue @@ -0,0 +1,9 @@ + diff --git a/docs/api/slots.md b/docs/api/slots.md index 453a97f..6cbc817 100644 --- a/docs/api/slots.md +++ b/docs/api/slots.md @@ -3,23 +3,22 @@ Vue Select leverages scoped slots to allow for total customization of the presen Slots can be used to change the look and feel of the UI, or to simply swap out text. ::: -## Wrapper + -### `header` +
      -Displayed at the top of the component, above `.vs__dropdown-toggle`. - -- `search {string}` - the current search query -- `loading {boolean}` - is the component loading -- `searching {boolean}` - is the component searching -- `filteredOptions {array}` - options filtered by the search text -- `deselect {function}` - function to deselect an option - -```html - -``` - -### `footer` +## `footer` Displayed at the bottom of the component, below `.vs__dropdown-toggle`. @@ -32,113 +31,23 @@ Otherwise content in this slot will affect it's positioning. - `filteredOptions {array}` - options filtered by the search text - `deselect {function}` - function to deselect an option -```html - -``` + +<<< @/.vuepress/components/SlotFooter.vue -## Selected Option(s) +## `header` -### `selected-option` - -#### Scope: - -- `option {Object}` - A selected option - -```html - - {{ getOptionLabel(option) }} - -``` - -### `selected-option-container` - -#### Scope: - -- `option {Object}` - A selected option -- `deselect {Function}` - Method used to deselect a given option when `multiple` is true -- `disabled {Boolean}` - Determine if the component is disabled -- `multiple {Boolean}` - If the component supports the selection of multiple values - -```html - - - - {{ getOptionLabel(option) }} - - - - -``` - -## Component Actions - -### `spinner` - -#### Scope: - -- `loading {Boolean}` - if the component is in a loading state - -```html - -
      Loading...
      -
      -``` - -### `open-indicator` - -```js -attributes : { - 'ref': 'openIndicator', - 'role': 'presentation', - 'class': 'vs__open-indicator', -} -``` - -```vue - - - -``` - -## Dropdown - -### `list-header` - -Displayed as the first item in the dropdown. No content by default. Parent element is the `
        `, -so this slot should contain a root `
      • `. +Displayed at the top of the component, above `.vs__dropdown-toggle`. - `search {string}` - the current search query - `loading {boolean}` - is the component loading - `searching {boolean}` - is the component searching - `filteredOptions {array}` - options filtered by the search text +- `deselect {function}` - function to deselect an option -```html - -``` + +<<< @/.vuepress/components/SlotHeader.vue -### `list-footer` +## `list-footer` Displayed as the last item in the dropdown. No content by default. Parent element is the `
          `, so this slot should contain a root `
        • `. @@ -148,34 +57,132 @@ so this slot should contain a root `
        • `. - `searching {boolean}` - is the component searching - `filteredOptions {array}` - options filtered by the search text -```html - + +<<< @/.vuepress/components/SlotListFooter.vue + +## `list-header` + +Displayed as the first item in the dropdown. No content by default. Parent element is the `
            `, +so this slot should contain a root `
          • `. + +- `search {string}` - the current search query +- `loading {boolean}` - is the component loading +- `searching {boolean}` - is the component searching +- `filteredOptions {array}` - options filtered by the search text + + +<<< @/.vuepress/components/SlotListHeader.vue + +## `no-options` + +The no options slot is displayed above `list-footer` in the dropdown when +`filteredOptions.length === 0`. + +- `search {string}` - the current search query +- `loading {boolean}` - is the component loading +- `searching {boolean}` - is the component searching + + +<<< @/.vuepress/components/SlotNoOptions.vue + +## `open-indicator` + +The open indicator is the caret icon on the component used to indicate dropdown status. + +```js +attributes: { + 'ref': 'openIndicator', + 'role': 'presentation', + 'class': 'vs__open-indicator', +} ``` -### `option` + +<<< @/.vuepress/components/SlotOpenIndicator.vue + +## `option` The current option within the dropdown, contained within `
          • `. - `option {Object}` - The currently iterated option from `filteredOptions` -```html - - {{ getOptionLabel(option) }} - + +<<< @/.vuepress/components/SlotOption.vue + +## `search` + +The search input has a lot of bindings, but they're grouped into `attributes` and `events`. Most +of the time, you will just be binding those two with `v-on="events"` and `v-bind="attributes"`. + +If you want the default styling, you'll need to add `.vs__search` to the input you provide. + +```js + /** + * Attributes to be bound to a search input. + */ + attributes: { + 'disabled': this.disabled, + 'placeholder': this.searchPlaceholder, + 'tabindex': this.tabindex, + 'readonly': !this.searchable, + 'id': this.inputId, + 'aria-autocomplete': 'list', + 'aria-labelledby': `vs${this.uid}__combobox`, + 'aria-controls': `vs${this.uid}__listbox`, + 'aria-activedescendant': this.typeAheadPointer > -1 + ? `vs${this.uid}__option-${this.typeAheadPointer}` + : '', + 'ref': 'search', + 'type': 'search', + 'autocomplete': this.autocomplete, + 'value': this.search, + }, + /** + * Events that this element should handle. + */ + events: { + 'compositionstart': () => this.isComposing = true, + 'compositionend': () => this.isComposing = false, + 'keydown': this.onSearchKeyDown, + 'blur': this.onSearchBlur, + 'focus': this.onSearchFocus, + 'input': (e) => this.search = e.target.value, + } ``` -### `no-options` + +<<< @/.vuepress/components/SlotSearch.vue{5-6} -The no options slot is displayed in the dropdown when `filteredOptions.length === 0`. +## `selected-option` -- `search {String}` - the current search text -- `searching {Boolean}` - if the component has search text +The text displayed within `selected-option-container`. -```vue - - Sorry, no matching options. - -``` +This slot doesn't exist if `selected-option-container` is implemented. + +- `option {Object}` - A selected option + + +<<< @/.vuepress/components/SlotSelectedOption.vue + +## `selected-option-container` + +This is the root element where `v-for="option in selectedValue"`. Most of the time you'll want to +use `selected-option`, but this container is useful if you want to disable the deselect button, +or have fine grain control over the markup. + +- `option {Object}` - Currently iterated selected option +- `deselect {Function}` - Method used to deselect a given option when `multiple` is true +- `disabled {Boolean}` - Determine if the component is disabled +- `multiple {Boolean}` - If the component supports the selection of multiple values + + +<<< @/.vuepress/components/SlotSelectedOptionContainer.vue + +## `spinner` + +- `loading {Boolean}` - if the component is in a loading state + + +<<< @/.vuepress/components/SlotSpinner.vue + +
      From bb3f28478fc6816e975a3661667a79d4b9ed8c49 Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Thu, 12 Mar 2020 20:44:14 -0700 Subject: [PATCH 10/88] ci: semantic release plugin config (#1102) --- release.config.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/release.config.js b/release.config.js index 8b8883e..8e9fb29 100644 --- a/release.config.js +++ b/release.config.js @@ -6,9 +6,14 @@ module.exports = { "@semantic-release/npm", "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", - ["@semantic-release/github", { - "assets": ["dist/**"] - }], + [ + "@semantic-release/github", + { + assets: ["dist/**"], + successComment: + ":tada: This issue has been resolved in version ${nextRelease.version} :tada:\\n\\nThe release is available on [GitHub release]()\\n\\nPlease consider [sponsoring Vue Select](https://github.com/sponsors/sagalbot), your support is much appreciated! :+1:" + } + ], [ "@semantic-release/git", { From e6d0da6d523465c273466fb61cc1f649ee2c3672 Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Thu, 12 Mar 2020 20:46:08 -0700 Subject: [PATCH 11/88] feat: add open & close events (#1101) --- docs/.vuepress/components/InfiniteScroll.vue | 72 ++++++++++++++++++++ docs/.vuepress/config.js | 1 + docs/guide/infinite-scroll.md | 23 +++++++ src/components/Select.vue | 4 ++ 4 files changed, 100 insertions(+) create mode 100644 docs/.vuepress/components/InfiniteScroll.vue create mode 100644 docs/guide/infinite-scroll.md diff --git a/docs/.vuepress/components/InfiniteScroll.vue b/docs/.vuepress/components/InfiniteScroll.vue new file mode 100644 index 0000000..f3fdd8b --- /dev/null +++ b/docs/.vuepress/components/InfiniteScroll.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index da73379..9cff15a 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -120,6 +120,7 @@ module.exports = { ['guide/validation', 'Validation'], ['guide/selectable', 'Limiting Selections'], ['guide/pagination', 'Pagination'], + ['guide/infinite-scroll', 'Infinite Scroll'], ['guide/vuex', 'Vuex'], ['guide/ajax', 'AJAX'], ['guide/loops', 'Using in Loops'], diff --git a/docs/guide/infinite-scroll.md b/docs/guide/infinite-scroll.md new file mode 100644 index 0000000..2444581 --- /dev/null +++ b/docs/guide/infinite-scroll.md @@ -0,0 +1,23 @@ +Vue Select doesn't ship with first party support for infinite scroll, but it's possible to implement +by hooking into the `open`, `close`, and `search` events, along with the `filterable` prop, and the +`list-footer` slot. + +Let's break down the example below, starting with the `data`. + +- `observer` - when the component is mounted, a new `IntersectionObserver` will be set here +- `limit` - the number of options to display 'per page' +- `search` - since we've disabled Vue Selects filtering, we'll need to filter options ourselves + +When Vue Select opens, the `open` event is emitted and `onOpen` will be called. We wait for +`$nextTick()` so that the `$ref` we need will exist, then begin observing it for intersection. + +The observer is set to call `infiniteScroll` when the `
    • ` is completely visible within the list. +Some fancy destructuring is done here to get the first `ObservedEntry`, and specifically the +`isIntersecting` & `target` properties. If the `
    • ` is intersecting, we increase the `limit`, and +ensure that the scroll position remains where it was before the list size changed. Again, it's +important to wait for `$nextTick` here so that the DOM elements have been inserted before setting +the scroll position. + + + +<<< @/.vuepress/components/InfiniteScroll.vue diff --git a/src/components/Select.vue b/src/components/Select.vue index 1b21f69..4f4c623 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -619,6 +619,10 @@ */ multiple() { this.clearSelection() + }, + + open(isOpen) { + this.$emit(isOpen ? 'open' : 'close'); } }, From ffa625714fa10dfb3079ef86b7250859f2dab101 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 13 Mar 2020 03:47:07 +0000 Subject: [PATCH 12/88] =?UTF-8?q?chore(=F0=9F=9A=80):=203.9.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72685a7..1fcf91c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "3.8.1", + "version": "3.9.0", "description": "Everything you wish the HTML