From fe51fec6b83c3b1f89e1d6fa03ed9944dc1ba1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20BORDIER?= Date: Sun, 8 Mar 2020 21:31:08 +0100 Subject: [PATCH 01/79] feat: calculated positioning (#1049) Adds `appendToBody` and `calculatePosition` props. https://vue-select.org/guide/positioning.html Co-authored-by: Jeff --- .../components/PositionedWithPopper.vue | 77 ++++++++++++++++++ docs/.vuepress/config.js | 1 + docs/api/props.md | 46 ++++++++++- docs/guide/positioning.md | 33 ++++++++ docs/package.json | 1 + docs/yarn.lock | 5 ++ src/components/Select.vue | 78 ++++++++++++++----- src/directives/appendToBody.js | 21 +++++ tests/unit/Dropdown.spec.js | 2 +- 9 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 docs/.vuepress/components/PositionedWithPopper.vue create mode 100644 docs/guide/positioning.md create mode 100644 src/directives/appendToBody.js diff --git a/docs/.vuepress/components/PositionedWithPopper.vue b/docs/.vuepress/components/PositionedWithPopper.vue new file mode 100644 index 0000000..d7580bf --- /dev/null +++ b/docs/.vuepress/components/PositionedWithPopper.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 2bed2c8..2ee84ca 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -129,6 +129,7 @@ module.exports = { collapsable: false, children: [ ['guide/keydown', 'Keydown Events'], + ['guide/positioning', 'Dropdown Position'] ], }, { diff --git a/docs/api/props.md b/docs/api/props.md index e54126f..d4d200d 100644 --- a/docs/api/props.md +++ b/docs/api/props.md @@ -1,3 +1,18 @@ +## appendToBody + +Append the dropdown element to the end of the body +and size/position it dynamically. Use it if you have +overflow or z-index issues. + +See [Dropdown Position](../guide/positioning.md) for more details. + +```js +appendToBody: { + type: Boolean, + default: false +}, +``` + ## value Contains the currently selected value. Very similar to a @@ -109,6 +124,30 @@ transition: { }, ``` +## calculatePosition + +When `appendToBody` is true, this function is responsible for positioning the drop down list. + +See [Dropdown Position](../guide/positioning.md) for more details. + +```js +calculatePosition: { + type: Function, + /** + * @param dropdownList {HTMLUListElement} + * @param component {Vue} current instance of vue select + * @param width {string} calculated width in pixels of the dropdown menu + * @param top {string} absolute position top value in pixels relative to the document + * @param left {string} absolute position left value in pixels relative to the document + */ + default(dropdownList, component, {width, top, left}) { + dropdownList.style.top = top; + dropdownList.style.left = left; + dropdownList.style.width = width; + } +} +``` + ## clearSearchOnSelect Enables/disables clearing the search text when an option is selected. @@ -353,10 +392,10 @@ createOption: { ## resetOnOptionsChange When false, updating the options will not reset the selected value. - -Since `v3.4+` the prop accepts either a `boolean` or `function` that returns a `boolean`. -If defined as a function, it will receive the params listed below. +Since `v3.4+` the prop accepts either a `boolean` or `function` that returns a `boolean`. + +If defined as a function, it will receive the params listed below. ```js /** @@ -412,3 +451,4 @@ selectOnTab: { type: Boolean, default: false } +``` diff --git a/docs/guide/positioning.md b/docs/guide/positioning.md new file mode 100644 index 0000000..ad5a4b9 --- /dev/null +++ b/docs/guide/positioning.md @@ -0,0 +1,33 @@ +## Default + +With the default CSS, Vue Select uses absolute positioning to render the dropdown menu. The root +`.v-select` container (the components `$el`) is used as the `relative` parent for the dropdown. The +dropdown will be displayed below the `$el` regardless of the available space. + +This works for most cases, but you might run into issues placing into a modal or near the bottom of +the viewport. If you need more fine grain control, you can use calculated positioning. + +## Calculated + +If you want more control over how the dropdown is rendered, or if you're running into z-index issues, +you may use the `appendToBody` boolean prop. When enabled, Vue Select will append the dropdown to +the document, outside of the `.v-select` container, and position it with Javscript. + +When `appendToBody` is true, the positioning will be handled by the `calculatePosition` prop. This +function is responsible for setting top/left absolute positioning values for the dropdown. The +default implementation places the dropdown in the same position that it would normally appear. + +## Popper.js Integration + +[Popper.js](https://popper.js.org/) is an awesome, 3kb utility for calculating positions of just +about any DOM element relative to another. + +By using the `appendToBody` and `calculatePosition` props, we're able to integrate directly with +popper to calculate positioning for us. + + + +Check out the [Popper Docs](https://popper.js.org/docs/v2/modifiers/) to see the full `modifiers` +API being used below. + +<<< @/.vuepress/components/PositionedWithPopper.vue{25-59} diff --git a/docs/package.json b/docs/package.json index 1fcd52b..79ca247 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,6 +8,7 @@ "build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build" }, "devDependencies": { + "@popperjs/core": "^2.1.0", "@vuepress/plugin-active-header-links": "^1.0.0-alpha.47", "@vuepress/plugin-google-analytics": "^1.0.0-alpha.47", "@vuepress/plugin-nprogress": "^1.0.0-alpha.47", diff --git a/docs/yarn.lock b/docs/yarn.lock index 8548f5b..ce6091c 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -715,6 +715,11 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@popperjs/core@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0" + integrity sha512-ntN5t5spqhQv28cLfmmt1dYabsudzR5A7PU15gr/gzcT/gzqAOnYFQPaLPFraDa7ZCJG2eJ1JsO7pgXbYXGIrw== + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" diff --git a/src/components/Select.vue b/src/components/Select.vue index 5d58d4c..ded68c5 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -53,28 +53,27 @@ -
    - +
      +
    • + + {{ getOptionLabel(option) }} + +
    • +
    • + Sorry, no matching options. +
    +
      @@ -84,6 +83,7 @@ import typeAheadPointer from '../mixins/typeAheadPointer' import ajax from '../mixins/ajax' import childComponents from './childComponents'; + import appendToBody from '../directives/appendToBody'; import uniqueId from '../utility/uniqueId'; /** @@ -94,6 +94,8 @@ mixins: [pointerScroll, typeAheadPointer, ajax], + directives: {appendToBody}, + props: { /** * Contains the currently selected value. Very similar to a @@ -517,6 +519,40 @@ * @return {Object} */ default: (map, vm) => map, + }, + + /** + * Append the dropdown element to the end of the body + * and size/position it dynamically. Use it if you have + * overflow or z-index issues. + * @type {Boolean} + */ + appendToBody: { + type: Boolean, + default: false + }, + + /** + * When `appendToBody` is true, this function + * is responsible for positioning the drop + * down list. + * @since v3.7.0 + * @see http://vue-select.org/guide/positioning.html + */ + calculatePosition: { + type: Function, + /** + * @param dropdownList {HTMLUListElement} + * @param component {Vue} current instance of vue select + * @param width {string} calculated width in pixels of the dropdown menu + * @param top {string} absolute position top value in pixels relative to the document + * @param left {string} absolute position left value in pixels relative to the document + */ + default(dropdownList, component, {width, top, left}) { + dropdownList.style.top = top; + dropdownList.style.left = left; + dropdownList.style.width = width; + } } }, diff --git a/src/directives/appendToBody.js b/src/directives/appendToBody.js new file mode 100644 index 0000000..ec5f6ec --- /dev/null +++ b/src/directives/appendToBody.js @@ -0,0 +1,21 @@ +export default { + inserted (el, bindings, {context}) { + if (context.appendToBody) { + const {height, top, left} = context.$refs.toggle.getBoundingClientRect(); + + context.calculatePosition(el, context, { + width: context.$refs.toggle.clientWidth + 'px', + top: (window.scrollY + top + height) + 'px', + left: (window.scrollX + left) + 'px', + }); + + document.body.appendChild(el); + } + }, + + unbind (el, bindings, vnode) { + if (vnode.context.appendToBody && el.parentNode) { + el.parentNode.removeChild(el); + } + }, +} diff --git a/tests/unit/Dropdown.spec.js b/tests/unit/Dropdown.spec.js index 6777410..bd39649 100755 --- a/tests/unit/Dropdown.spec.js +++ b/tests/unit/Dropdown.spec.js @@ -137,7 +137,7 @@ describe("Toggling Dropdown", () => { expect(Select.vm.open).toEqual(true); await Select.vm.$nextTick(); - expect(Select.find('.vs__dropdown-menu').element.style['display']).toEqual('none'); + expect(Select.contains('.vs__dropdown-menu')).toBeFalsy(); expect(Select.contains('.vs__dropdown-option')).toBeFalsy(); expect(Select.contains('.vs__no-options')).toBeFalsy(); expect(Select.vm.stateClasses['vs--open']).toBeFalsy(); From ca2c36c25b765efd1529b4faa68d6a18a3aa40bb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 8 Mar 2020 20:32:15 +0000 Subject: [PATCH 02/79] =?UTF-8?q?chore(=F0=9F=9A=80):=203.7.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 75f34f3..20ccf91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "3.6.0", + "version": "3.7.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 f2479434e4223e7fb5b23a6b43a41ad23b1f7202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ga=C5=82ka?= Date: Tue, 10 Mar 2020 03:08:57 +0100 Subject: [PATCH 05/79] fix: memory leak when positioning with popper (#1094) * fix: memory leak when positioning with popper * docs: update calculate position docs Co-authored-by: Jeff --- docs/.vuepress/components/PositionedWithPopper.vue | 10 ++++++++-- docs/api/props.md | 4 ++++ src/components/Select.vue | 11 ++++++++--- src/directives/appendToBody.js | 13 +++++++++---- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/.vuepress/components/PositionedWithPopper.vue b/docs/.vuepress/components/PositionedWithPopper.vue index d7580bf..0c69a23 100644 --- a/docs/.vuepress/components/PositionedWithPopper.vue +++ b/docs/.vuepress/components/PositionedWithPopper.vue @@ -22,7 +22,7 @@ import { createPopper } from '@popperjs/core'; export default { data: () => ({countries, placement: 'top'}), methods: { - withPopper (dropdownList, component, {width},) { + withPopper (dropdownList, component, {width}) { /** * We need to explicitly define the dropdown width since * it is usually inherited from the parent with CSS. @@ -39,7 +39,7 @@ export default { * wrapper so that we can set some styles for when the dropdown is placed * above. */ - createPopper(component.$refs.toggle, dropdownList, { + const popper = createPopper(component.$refs.toggle, dropdownList, { placement: this.placement, modifiers: [ { @@ -56,6 +56,12 @@ export default { }, }] }); + + /** + * To prevent memory leaks Popper needs to be destroyed. + * If you return function, it will be called just before dropdown is removed from DOM. + */ + return () => popper.destroy(); } } }; diff --git a/docs/api/props.md b/docs/api/props.md index d4d200d..0f157f9 100644 --- a/docs/api/props.md +++ b/docs/api/props.md @@ -128,6 +128,9 @@ transition: { When `appendToBody` is true, this function is responsible for positioning the drop down list. +If a function is returned from `calculatePosition`, it will be called when the drop down list +is removed from the DOM. This allows for any garbage collection you may need to do. + See [Dropdown Position](../guide/positioning.md) for more details. ```js @@ -139,6 +142,7 @@ calculatePosition: { * @param width {string} calculated width in pixels of the dropdown menu * @param top {string} absolute position top value in pixels relative to the document * @param left {string} absolute position left value in pixels relative to the document + * @return {function|void} */ default(dropdownList, component, {width, top, left}) { dropdownList.style.top = top; diff --git a/src/components/Select.vue b/src/components/Select.vue index 89235cc..3527b77 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -537,9 +537,13 @@ }, /** - * When `appendToBody` is true, this function - * is responsible for positioning the drop - * down list. + * When `appendToBody` is true, this function is responsible for + * positioning the drop down list. + * + * If a function is returned from `calculatePosition`, it will + * be called when the drop down list is removed from the DOM. + * This allows for any garbage collection you may need to do. + * * @since v3.7.0 * @see http://vue-select.org/guide/positioning.html */ @@ -551,6 +555,7 @@ * @param width {string} calculated width in pixels of the dropdown menu * @param top {string} absolute position top value in pixels relative to the document * @param left {string} absolute position left value in pixels relative to the document + * @return {function|void} */ default(dropdownList, component, {width, top, left}) { dropdownList.style.top = top; diff --git a/src/directives/appendToBody.js b/src/directives/appendToBody.js index ec5f6ec..9296c9d 100644 --- a/src/directives/appendToBody.js +++ b/src/directives/appendToBody.js @@ -3,7 +3,7 @@ export default { if (context.appendToBody) { const {height, top, left} = context.$refs.toggle.getBoundingClientRect(); - context.calculatePosition(el, context, { + el.unbindPosition = context.calculatePosition(el, context, { width: context.$refs.toggle.clientWidth + 'px', top: (window.scrollY + top + height) + 'px', left: (window.scrollX + left) + 'px', @@ -13,9 +13,14 @@ export default { } }, - unbind (el, bindings, vnode) { - if (vnode.context.appendToBody && el.parentNode) { - el.parentNode.removeChild(el); + unbind (el, bindings, {context}) { + if (context.appendToBody) { + if (el.unbindPosition && typeof el.unbindPosition === 'function') { + el.unbindPosition(); + } + if (el.parentNode) { + el.parentNode.removeChild(el); + } } }, } From 3c546346f7971336937ddd1bcafa0dc80ee02412 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 10 Mar 2020 02:10:04 +0000 Subject: [PATCH 06/79] =?UTF-8?q?chore(=F0=9F=9A=80):=203.7.2?= 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 83d0a95..934ef4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "3.7.1", + "version": "3.7.2", "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 518e1919f82367c23a5ff2661b6fada4ad028e69 Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Tue, 10 Mar 2020 21:10:58 -0700 Subject: [PATCH 09/79] fix(reduce): reduce + taggable bug (#1091) Resolves #1089 Resolves #993 --- src/components/Select.vue | 32 +++++++++++++++++++++++--------- tests/unit/Reduce.spec.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/components/Select.vue b/src/components/Select.vue index cdf2509..1b21f69 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -629,7 +629,7 @@ this.setInternalValueFromOptions(this.value) } - this.$on('option:created', this.maybePushTag) + this.$on('option:created', this.pushTag) }, methods: { @@ -662,7 +662,6 @@ } this.updateValue(option); } - this.onAfterSelect(option) }, @@ -772,7 +771,7 @@ }, /** - * Finds an option from this.options + * Finds an option from the options * where a reduced value matches * the passed in value. * @@ -780,7 +779,24 @@ * @returns {*} */ findOptionFromReducedValue (value) { - return this.options.find(option => JSON.stringify(this.reduce(option)) === JSON.stringify(value)) || value; + const predicate = option => JSON.stringify(this.reduce(option)) === JSON.stringify(value); + + const matches = [ + ...this.options, + ...this.pushedTags, + ].filter(predicate); + + if (matches.length === 1) { + return matches[0]; + } + + /** + * This second loop is needed to cover an edge case where `taggable` + `reduce` + * were used in conjunction with a `create-option` that doesn't create a + * unique reduced value. + * @see https://github.com/sagalbot/vue-select/issues/1089#issuecomment-597238735 + */ + return matches.find(match => this.optionComparator(match, this.$data._value)) || value; }, /** @@ -836,10 +852,8 @@ * @param {Object || String} option * @return {void} */ - maybePushTag(option) { - if (this.pushTags) { - this.pushedTags.push(option) - } + pushTag (option) { + this.pushedTags.push(option); }, /** @@ -986,7 +1000,7 @@ * @return {Array} */ optionList () { - return this.options.concat(this.pushedTags); + return this.options.concat(this.pushTags ? this.pushedTags : []); }, /** diff --git a/tests/unit/Reduce.spec.js b/tests/unit/Reduce.spec.js index 371e144..020f7c0 100755 --- a/tests/unit/Reduce.spec.js +++ b/tests/unit/Reduce.spec.js @@ -226,4 +226,33 @@ describe("When reduce prop is defined", () => { expect(Select.vm.selectedValue).toEqual([optionToChangeTo]); }); + + describe('Reducing Tags', () => { + it('tracks values that have been created by the user', async () => { + const Parent = mount({ + data: () => ({selected: null, options: []}), + template: ` + + `, + components: {'v-select': VueSelect}, + }); + const Select = Parent.vm.$children[0]; + + // When + Select.search = 'hello'; + Select.typeAheadSelect(); + await Select.$nextTick(); + + // Then + expect(Select.selectedValue).toEqual([{label: 'hello', value: -1}]); + expect(Select.$refs.selectedOptions.textContent.trim()).toEqual('hello'); + expect(Parent.vm.selected).toEqual(-1); + }); + }); }); From 1ec5d0dd8a50b7d6422d93819d741253075ef497 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 11 Mar 2020 04:12:10 +0000 Subject: [PATCH 10/79] =?UTF-8?q?chore(=F0=9F=9A=80):=203.8.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 3ae58b6..72685a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-select", - "version": "3.8.0", + "version": "3.8.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 85519c039e3a00975c17a34583942a67c7efd52a Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Thu, 12 Mar 2020 20:53:01 -0700 Subject: [PATCH 15/79] Update release.config.js (#1103) --- release.config.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/release.config.js b/release.config.js index 8e9fb29..0c8e17d 100644 --- a/release.config.js +++ b/release.config.js @@ -9,9 +9,7 @@ module.exports = { [ "@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:" + assets: ["dist/**"] } ], [ From e9ea2d99f319b5fd18bebce3a95cdfc36a14f675 Mon Sep 17 00:00:00 2001 From: Jeff Sagal Date: Fri, 13 Mar 2020 09:19:28 -0700 Subject: [PATCH 16/79] docs(filtering): Add Filtering Docs (#1017) docs(infinite-scroll): improve example (#1017) --- .../components/ClearButtonOverride.vue | 3 +- docs/.vuepress/components/FuseFilter.vue | 31 +++++++++++++++++++ docs/.vuepress/components/InfiniteScroll.vue | 20 +++++------- docs/.vuepress/components/Sandbox.vue | 5 --- docs/.vuepress/config.js | 3 +- docs/guide/filtering.md | 19 ++++++++++++ docs/guide/infinite-scroll.md | 4 +-- 7 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 docs/.vuepress/components/FuseFilter.vue create mode 100644 docs/guide/filtering.md diff --git a/docs/.vuepress/components/ClearButtonOverride.vue b/docs/.vuepress/components/ClearButtonOverride.vue index 837ea68..c533883 100644 --- a/docs/.vuepress/components/ClearButtonOverride.vue +++ b/docs/.vuepress/components/ClearButtonOverride.vue @@ -1,9 +1,10 @@ diff --git a/docs/.vuepress/components/FuseFilter.vue b/docs/.vuepress/components/FuseFilter.vue new file mode 100644 index 0000000..a73e277 --- /dev/null +++ b/docs/.vuepress/components/FuseFilter.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/docs/.vuepress/components/InfiniteScroll.vue b/docs/.vuepress/components/InfiniteScroll.vue index f3fdd8b..677d8fb 100644 --- a/docs/.vuepress/components/InfiniteScroll.vue +++ b/docs/.vuepress/components/InfiniteScroll.vue @@ -5,7 +5,6 @@ @open="onOpen" @close="onClose" @search="query => search = query" - ref="select" >