mirror of
https://github.com/tenrok/vue-select.git
synced 2026-05-17 02:29:37 +03:00
wip - hitting perf issues with devtools open
This commit is contained in:
+53
-2
@@ -1,21 +1,72 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<v-select v-model="selected" v-bind="config" />
|
||||
<!-- <v-select v-model="selected" v-bind="config" />-->
|
||||
|
||||
<v-select v-model="selected" v-bind="config">
|
||||
<template #dropdown>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItem
|
||||
v-for="(option, index) in config.options"
|
||||
:key="option.value"
|
||||
:option="option"
|
||||
:index="index"
|
||||
>
|
||||
{{ option.label }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-select v-model="selected" v-bind="config">
|
||||
<template #dropdown>
|
||||
<DropdownMenu as="div">
|
||||
<div v-for="group in Object.keys(optionsGroupedByLabel)" :key="group">
|
||||
<div>{{ group }}</div>
|
||||
<ul>
|
||||
<DropdownMenuItem
|
||||
as="li"
|
||||
v-for="(option, index) in optionsGroupedByLabel[group]"
|
||||
:key="option.value"
|
||||
:option="option"
|
||||
:index="index"
|
||||
>
|
||||
{{ option.label }}
|
||||
</DropdownMenuItem>
|
||||
</ul>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
</v-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import vSelect from '@/components/Select.vue'
|
||||
import countries from '../docs/.vuepress/data/countryCodes.js'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import DropdownMenuItem from '@/components/DropdownMenuItem.vue'
|
||||
|
||||
export default {
|
||||
components: { vSelect },
|
||||
components: { DropdownMenuItem, DropdownMenu, vSelect },
|
||||
data: () => ({
|
||||
selected: null,
|
||||
config: {
|
||||
options: countries,
|
||||
getOptionKey: ({ value }) => value,
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
optionsGroupedByLabel() {
|
||||
return this.config.options.reduce((acc, option) => {
|
||||
const label = option.label[0]
|
||||
if (!acc[label]) {
|
||||
acc[label] = []
|
||||
}
|
||||
acc[label].push(option)
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { inject, onMounted, ref, watch } from 'vue'
|
||||
import vAppendToBody from '@/directives/appendToBody.js'
|
||||
import { VueSelectInjectionKey } from '@/symbols'
|
||||
import type { VueSelectInjectedProps } from '@/components/Select.vue'
|
||||
import type { InjectedVueSelectContext } from '@/types'
|
||||
|
||||
const context = inject<VueSelectInjectedProps>(VueSelectInjectionKey)
|
||||
const context = inject<InjectedVueSelectContext>(VueSelectInjectionKey)
|
||||
const dropdownMenu = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (dropdownMenu.value) {
|
||||
context?.value.setDropdownMenuEl(dropdownMenu.value)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => context?.value.dropdownOpen,
|
||||
(open) => {
|
||||
if (open) {
|
||||
context?.value.setDropdownMenuEl(dropdownMenu.value)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
withDefaults(defineProps<{ as?: string }>(), { as: 'ul' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
v-if="context.dropdownOpen"
|
||||
<Component
|
||||
:is="as"
|
||||
v-show="context.dropdownOpen"
|
||||
:id="`vs${context.uid}__listbox`"
|
||||
ref="dropdownMenu"
|
||||
:key="`vs${context.uid}__listbox`"
|
||||
@@ -21,11 +40,28 @@ const context = inject<VueSelectInjectedProps>(VueSelectInjectionKey)
|
||||
@mouseup="context.onMouseUp"
|
||||
>
|
||||
<slot></slot>
|
||||
</ul>
|
||||
<ul
|
||||
v-else
|
||||
:id="`vs${context.uid}__listbox`"
|
||||
role="listbox"
|
||||
style="display: none; visibility: hidden"
|
||||
></ul>
|
||||
<!-- TODO: not sure why, but using the previous system of swapping the dropdown el at open causes performance to drop drastically with Vue dev tools open -->
|
||||
<!-- <Component-->
|
||||
<!-- :is="as"-->
|
||||
<!-- v-if="context.dropdownOpen"-->
|
||||
<!-- :id="`vs${context.uid}__listbox`"-->
|
||||
<!-- ref="dropdownMenu"-->
|
||||
<!-- :key="`vs${context.uid}__listbox`"-->
|
||||
<!-- v-append-to-body-->
|
||||
<!-- class="vs__dropdown-menu"-->
|
||||
<!-- role="listbox"-->
|
||||
<!-- tabindex="-1"-->
|
||||
<!-- @mousedown.prevent="context.onMousedown"-->
|
||||
<!-- @mouseup="context.onMouseUp"-->
|
||||
<!-- >-->
|
||||
<!-- <slot></slot>-->
|
||||
<!-- </Component>-->
|
||||
<!-- <Component-->
|
||||
<!-- :is="as"-->
|
||||
<!-- v-else-->
|
||||
<!-- :id="`vs${context.uid}__listbox`"-->
|
||||
<!-- role="listbox"-->
|
||||
<!-- style="display: none; visibility: hidden"-->
|
||||
<!-- ></Component>-->
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { VueSelectInjectionKey } from '@/symbols'
|
||||
import type { InjectedVueSelectContext, VueSelectOption } from '@/types'
|
||||
|
||||
const context = inject<InjectedVueSelectContext>(VueSelectInjectionKey)
|
||||
|
||||
interface Props {
|
||||
as?: string
|
||||
index: number
|
||||
option: VueSelectOption
|
||||
opinionated?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), { as: 'li', opinionated: true })
|
||||
|
||||
const selectableOption = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (selectableOption.value) {
|
||||
context?.value.registerSelectableEl(selectableOption.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (selectableOption.value) {
|
||||
context?.value.unRegisterSelectableEl(selectableOption.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component
|
||||
ref="selectableOption"
|
||||
:is="as"
|
||||
:id="`vs${context.uid}__option-${index}`"
|
||||
role="option"
|
||||
:class="
|
||||
opinionated
|
||||
? [
|
||||
'vs__dropdown-option',
|
||||
{
|
||||
'vs__dropdown-option--deselect':
|
||||
context.isOptionDeselectable(option) &&
|
||||
index === context.typeAheadPointer,
|
||||
'vs__dropdown-option--selected': context.isOptionSelected(option),
|
||||
'vs__dropdown-option--highlight':
|
||||
index === context.typeAheadPointer,
|
||||
'vs__dropdown-option--disabled': !context.selectable(option),
|
||||
},
|
||||
]
|
||||
: ''
|
||||
"
|
||||
:aria-selected="index === context.typeAheadPointer ? true : null"
|
||||
@mouseover="
|
||||
context.selectable(option) ? context.setTypeAheadPointer(index) : null
|
||||
"
|
||||
@click.prevent.stop="
|
||||
context.selectable(option) ? context.select(option) : null
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</Component>
|
||||
</template>
|
||||
+63
-49
@@ -82,36 +82,30 @@
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<transition :name="transition">
|
||||
<DropdownMenu>
|
||||
<li
|
||||
v-for="(option, index) in filteredOptions"
|
||||
:id="`vs${uid}__option-${index}`"
|
||||
:key="getOptionKey(option)"
|
||||
role="option"
|
||||
class="vs__dropdown-option"
|
||||
:class="{
|
||||
'vs__dropdown-option--deselect':
|
||||
isOptionDeselectable(option) && index === typeAheadPointer,
|
||||
'vs__dropdown-option--selected': isOptionSelected(option),
|
||||
'vs__dropdown-option--highlight': index === typeAheadPointer,
|
||||
'vs__dropdown-option--disabled': !selectable(option),
|
||||
}"
|
||||
:aria-selected="index === typeAheadPointer ? true : null"
|
||||
@mouseover="selectable(option) ? (typeAheadPointer = index) : null"
|
||||
@click.prevent.stop="selectable(option) ? select(option) : null"
|
||||
>
|
||||
<slot name="option" v-bind="normalizeOptionForSlot(option)">
|
||||
{{ getOptionLabel(option) }}
|
||||
<slot name="dropdown">
|
||||
<transition :name="transition">
|
||||
<DropdownMenu>
|
||||
<slot name="options">
|
||||
<DropdownMenuItem
|
||||
v-for="(option, index) in filteredOptions"
|
||||
:opinionated="true"
|
||||
:index="index"
|
||||
:option="option"
|
||||
:key="getOptionKey(option)"
|
||||
>
|
||||
<slot name="option" v-bind="normalizeOptionForSlot(option)">
|
||||
{{ getOptionLabel(option) }}
|
||||
</slot>
|
||||
</DropdownMenuItem>
|
||||
<li v-if="filteredOptions.length === 0" class="vs__no-options">
|
||||
<slot name="no-options" v-bind="scope.noOptions">
|
||||
Sorry, no matching options.
|
||||
</slot>
|
||||
</li>
|
||||
</slot>
|
||||
</li>
|
||||
<li v-if="filteredOptions.length === 0" class="vs__no-options">
|
||||
<slot name="no-options" v-bind="scope.noOptions">
|
||||
Sorry, no matching options.
|
||||
</slot>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
</transition>
|
||||
</DropdownMenu>
|
||||
</transition>
|
||||
</slot>
|
||||
<slot name="footer" v-bind="scope.footer" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -123,19 +117,14 @@ import ajax from '@/mixins/ajax.js'
|
||||
import childComponents from '@/components/childComponents.js'
|
||||
import sortAndStringify from '@/utility/sortAndStringify.js'
|
||||
import uniqueId from '@/utility/uniqueId.js'
|
||||
import { computed, ComputedRef, defineComponent } from 'vue'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { VueSelectInjectionKey } from '@/symbols.js'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
|
||||
export interface VueSelectContext {
|
||||
uid: ComputedRef<string>
|
||||
dropdownOpen: ComputedRef<boolean>
|
||||
onMousedown: (e: MouseEvent) => void
|
||||
onMouseup: (e: MouseEvent) => void
|
||||
}
|
||||
import type { VueSelectContext, VueSelectOption } from '@/types'
|
||||
import DropdownMenuItem from '@/components/DropdownMenuItem.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { DropdownMenu, ...childComponents },
|
||||
components: { DropdownMenuItem, DropdownMenu, ...childComponents },
|
||||
|
||||
mixins: [pointerScroll, typeAheadPointer, ajax],
|
||||
|
||||
@@ -309,7 +298,7 @@ export default defineComponent({
|
||||
*/
|
||||
reduce: {
|
||||
type: Function,
|
||||
default: (option) => option,
|
||||
default: (option: VueSelectOption) => option,
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -318,12 +307,10 @@ export default defineComponent({
|
||||
*
|
||||
* @type {Function}
|
||||
* @since 3.3.0
|
||||
* @param {Object|String} option
|
||||
* @return {Boolean}
|
||||
*/
|
||||
selectable: {
|
||||
type: Function,
|
||||
default: (option) => true,
|
||||
default: (option: VueSelectOption): boolean => true,
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -341,7 +328,7 @@ export default defineComponent({
|
||||
*/
|
||||
getOptionLabel: {
|
||||
type: Function,
|
||||
default(option) {
|
||||
default(option: VueSelectOption) {
|
||||
if (typeof option === 'object') {
|
||||
if (!option.hasOwnProperty(this.label)) {
|
||||
return console.warn(
|
||||
@@ -374,7 +361,7 @@ export default defineComponent({
|
||||
*/
|
||||
getOptionKey: {
|
||||
type: Function,
|
||||
default(option) {
|
||||
default(option: VueSelectOption): unknown {
|
||||
if (typeof option !== 'object') {
|
||||
return option
|
||||
}
|
||||
@@ -659,7 +646,7 @@ export default defineComponent({
|
||||
*/
|
||||
dropdownShouldOpen: {
|
||||
type: Function,
|
||||
default({ noDrop, open, mutableLoading }) {
|
||||
default({ noDrop, open, mutableLoading }): boolean {
|
||||
return noDrop ? false : open && !mutableLoading
|
||||
},
|
||||
},
|
||||
@@ -676,12 +663,28 @@ export default defineComponent({
|
||||
|
||||
provide() {
|
||||
return {
|
||||
[VueSelectInjectionKey]: computed(() => {
|
||||
[VueSelectInjectionKey]: computed<VueSelectContext>(() => {
|
||||
return {
|
||||
uid: this.uid,
|
||||
getOptionKey: this.getOptionKey,
|
||||
isOptionDeselectable: this.isOptionDeselectable,
|
||||
isOptionSelected: this.isOptionSelected,
|
||||
typeAheadPointer: this.typeAheadPointer,
|
||||
setTypeAheadPointer: this.setTypeAheadPointer,
|
||||
dropdownOpen: this.dropdownOpen,
|
||||
onMousedown: this.onMousedown,
|
||||
onMouseup: this.onMouseUp,
|
||||
selectable: this.selectable,
|
||||
select: this.select,
|
||||
setDropdownMenuEl: (ref: HTMLElement) => {
|
||||
this.dropdownMenuEl = ref
|
||||
},
|
||||
registerSelectableEl: (ref: HTMLElement) => {
|
||||
this.selectableEls.push(ref)
|
||||
},
|
||||
unRegisterSelectableEl: (ref: HTMLElement) => {
|
||||
this.selectableEls = this.selectableEls.filter((el) => el !== ref)
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -696,6 +699,17 @@ export default defineComponent({
|
||||
// eslint-disable-next-line vue/no-reserved-keys
|
||||
_value: [], // Internal value managed by Vue Select if no `value` prop is passed
|
||||
deselectButtons: [],
|
||||
dropdownMenuEl: null,
|
||||
selectableEls: [],
|
||||
} as {
|
||||
search: string
|
||||
open: boolean
|
||||
isComposing: boolean
|
||||
pushedTags: VueSelectOption[]
|
||||
_value: any[]
|
||||
deselectButtons: any[]
|
||||
dropdownMenuEl: HTMLElement | null
|
||||
selectableEls: HTMLElement[]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -862,7 +876,7 @@ export default defineComponent({
|
||||
* dropdown menu.
|
||||
* @return {Boolean} True if open
|
||||
*/
|
||||
dropdownOpen() {
|
||||
dropdownOpen(): boolean {
|
||||
return this.dropdownShouldOpen(this)
|
||||
},
|
||||
|
||||
@@ -1131,7 +1145,7 @@ export default defineComponent({
|
||||
* @param {Object|String} option
|
||||
* @return {Boolean} True when selected | False otherwise
|
||||
*/
|
||||
isOptionSelected(option) {
|
||||
isOptionSelected(option: VueSelectOption): boolean {
|
||||
return this.selectedValue.some((value) =>
|
||||
this.optionComparator(value, option)
|
||||
)
|
||||
@@ -1140,7 +1154,7 @@ export default defineComponent({
|
||||
/**
|
||||
* Can the current option be removed via the dropdown?
|
||||
*/
|
||||
isOptionDeselectable(option) {
|
||||
isOptionDeselectable(option: VueSelectOption) {
|
||||
return this.isOptionSelected(option) && this.deselectFromDropdown
|
||||
},
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
setTypeAheadPointer(index) {
|
||||
this.typeAheadPointer = index
|
||||
},
|
||||
|
||||
/**
|
||||
* Adjust the scroll position of the dropdown list
|
||||
* if the current pointer is outside of the
|
||||
@@ -28,16 +32,16 @@ export default {
|
||||
*/
|
||||
maybeAdjustScroll() {
|
||||
const optionEl =
|
||||
this.$refs.dropdownMenu?.children[this.typeAheadPointer] || false
|
||||
this.dropdownMenuEl?.children[this.typeAheadPointer] || false
|
||||
|
||||
if (optionEl) {
|
||||
const bounds = this.getDropdownViewport()
|
||||
const { top, bottom, height } = optionEl.getBoundingClientRect()
|
||||
|
||||
if (top < bounds.top) {
|
||||
return (this.$refs.dropdownMenu.scrollTop = optionEl.offsetTop)
|
||||
return (this.dropdownMenuEl.scrollTop = optionEl.offsetTop)
|
||||
} else if (bottom > bounds.bottom) {
|
||||
return (this.$refs.dropdownMenu.scrollTop =
|
||||
return (this.dropdownMenuEl.scrollTop =
|
||||
optionEl.offsetTop - (bounds.height - height))
|
||||
}
|
||||
}
|
||||
@@ -48,8 +52,8 @@ export default {
|
||||
* @returns {{top: (string|*|number), bottom: *}}
|
||||
*/
|
||||
getDropdownViewport() {
|
||||
return this.$refs.dropdownMenu
|
||||
? this.$refs.dropdownMenu.getBoundingClientRect()
|
||||
return this.dropdownMenuEl
|
||||
? this.dropdownMenuEl.getBoundingClientRect()
|
||||
: {
|
||||
height: 0,
|
||||
top: 0,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ComputedRef } from 'vue'
|
||||
|
||||
export interface VueSelectContext {
|
||||
uid: string | number | undefined
|
||||
dropdownOpen: boolean
|
||||
typeAheadPointer: number
|
||||
setTypeAheadPointer: (index: number) => void
|
||||
isOptionDeselectable: (option: VueSelectOption) => boolean
|
||||
isOptionSelected: (option: VueSelectOption) => boolean
|
||||
onMousedown: (e: MouseEvent) => void
|
||||
onMouseup: (e: MouseEvent) => void
|
||||
select: (option: VueSelectOption) => void
|
||||
setDropdownMenuEl: (el: HTMLElement | null) => void
|
||||
registerSelectableEl: (el: HTMLElement | null) => void
|
||||
unRegisterSelectableEl: (el: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
export type InjectedVueSelectContext = ComputedRef<VueSelectContext>
|
||||
|
||||
export type VueSelectOption = unknown
|
||||
@@ -5,7 +5,7 @@ let idCount = 0
|
||||
* Thanks lodash!
|
||||
* @return {number}
|
||||
*/
|
||||
function uniqueId() {
|
||||
function uniqueId(): number {
|
||||
return ++idCount
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user