2
0
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:
Jeff Sagal
2022-11-30 16:22:54 -08:00
parent c32b08dd13
commit c2d69c3517
7 changed files with 258 additions and 69 deletions
+53 -2
View File
@@ -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>
+48 -12
View File
@@ -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>
+64
View File
@@ -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
View File
@@ -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
},
+9 -5
View File
@@ -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,
+20
View File
@@ -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
}