Skip to content

Commit b1a5897

Browse files
authored
perf: table, select and drawer checks (#130)
* fix: remove upload and drawer listeners * perf(select): filter cache * perf(table): map keys
1 parent ec6cb80 commit b1a5897

File tree

5 files changed

+105
-28
lines changed

5 files changed

+105
-28
lines changed

.changeset/mean-hornets-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@indielayer/ui": patch
3+
---
4+
5+
perf: table, select and drawer checks

packages/ui/src/components/drawer/Drawer.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,13 @@ function onEnter(el: Element, done: () => void) {
137137
138138
return
139139
}
140-
el.addEventListener('transitionend', done)
140+
141+
const handler = () => {
142+
el.removeEventListener('transitionend', handler)
143+
done()
144+
}
145+
146+
el.addEventListener('transitionend', handler)
141147
setTimeout(() => {
142148
if (props.backdrop) el.classList.add('bg-slate-500/30')
143149
if (props.position === 'top') (el as HTMLElement).style.top = '0'
@@ -150,7 +156,12 @@ function onEnter(el: Element, done: () => void) {
150156
function onBeforeLeave(el: Element) {}
151157
152158
function onLeave(el: Element, done: () => void) {
153-
el.addEventListener('transitionend', done)
159+
const handler = () => {
160+
el.removeEventListener('transitionend', handler)
161+
done()
162+
}
163+
164+
el.addEventListener('transitionend', handler)
154165
setTimeout(() => {
155166
if (props.backdrop) el.classList.remove('bg-slate-500/30')
156167
if (props.position === 'top') (el as HTMLElement).style.top = `-${props.height}px`

packages/ui/src/components/select/Select.vue

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,32 @@ const selected = computed<any | any[]>({
110110
},
111111
})
112112
113+
const labelCache = computed(() => {
114+
if (!props.options) return new Map<SelectOption, string>()
115+
116+
return new Map(props.options.map((option) => [option, option.label.toLowerCase()]))
117+
})
118+
113119
const internalOptions = computed(() => {
114120
if (!props.options || props.options.length === 0) return []
115121
122+
const filterLower = filter.value.toLowerCase()
123+
const hasFilter = filter.value !== ''
124+
125+
const selectedSet = new Set(
126+
internalMultiple.value && Array.isArray(selected.value)
127+
? selected.value
128+
: [],
129+
)
130+
const singleSelectedValue = !internalMultiple.value ? selected.value : null
131+
const cache = labelCache.value
132+
116133
return props.options
117-
.filter((option) => filter.value === '' || option.label.toLowerCase().includes(filter.value.toLowerCase()))
134+
.filter((option) => !hasFilter || cache.get(option)?.includes(filterLower))
118135
.map((option) => {
119-
let isActive = false
120-
121-
if (internalMultiple.value && Array.isArray(selected.value)) {
122-
isActive = selected.value.includes(option.value)
123-
} else {
124-
isActive = option.value === selected.value
125-
}
136+
const isActive = internalMultiple.value
137+
? selectedSet.has(option.value)
138+
: option.value === singleSelectedValue
126139
127140
return {
128141
value: option.value,

packages/ui/src/components/table/Table.vue

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,28 @@ function sortHeader(header: TableHeader) {
201201
emit('update:sort', sort)
202202
}
203203
204+
const pathCache = new Map<string, string[]>()
205+
204206
function getValue(item: T, path: string | string[] | undefined): unknown {
205207
if (!path) return ''
206208
if (!item) return ''
207209
208-
const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g)
210+
let pathArray: string[] | null
211+
212+
if (Array.isArray(path)) {
213+
pathArray = path
214+
} else {
215+
// Check cache first
216+
if (pathCache.has(path)) {
217+
pathArray = pathCache.get(path)!
218+
} else {
219+
// Parse and cache the result
220+
pathArray = path.match(/([^[.\]])+/g)
221+
if (pathArray) {
222+
pathCache.set(path, pathArray)
223+
}
224+
}
225+
}
209226
210227
if (!pathArray || pathArray.length === 0) return ''
211228
@@ -225,34 +242,55 @@ const allKeys = computed<(number | string)[]>(() => {
225242
return items.value.map((item, index) => getItemKey(item, index))
226243
})
227244
245+
const selectedSet = computed(() => {
246+
if (!props.selectable || props.singleSelect) return new Set<number | string>()
247+
if (!Array.isArray(selected.value)) return new Set<number | string>()
248+
249+
return new Set(selected.value)
250+
})
251+
228252
const allRowsSelected = computed(() => {
229253
if (!props.selectable || props.singleSelect) return false
254+
if (!Array.isArray(selected.value) || selected.value.length === 0) return false
255+
256+
const keysLength = allKeys.value.length
257+
258+
if (keysLength === 0) return false
230259
231-
return Array.isArray(selected.value) && selected.value.length > 0 && allKeys.value.length > 0 && selected.value.length === allKeys.value.length
260+
return selected.value.length === keysLength
232261
})
233262
234263
const someRowsSelected = computed(() => {
235264
if (!props.selectable || props.singleSelect) return false
265+
if (!Array.isArray(selected.value) || selected.value.length === 0) return false
236266
237-
return Array.isArray(selected.value) && selected.value.length > 0 && allKeys.value.length > 0 && selected.value.length !== allKeys.value.length
267+
const keysLength = allKeys.value.length
268+
269+
if (keysLength === 0) return false
270+
271+
return selected.value.length > 0 && selected.value.length !== keysLength
238272
})
239273
240274
function isRowSelected(rowKey: number | string): boolean {
241275
if (!props.selectable) return false
276+
242277
if (props.singleSelect) {
243278
return selected.value === rowKey
244-
} else {
245-
return Array.isArray(selected.value) && selected.value.includes(rowKey)
246279
}
280+
281+
return selectedSet.value.has(rowKey)
247282
}
248283
249284
function toggleRowSelection(rowKey: number | string) {
250285
if (!props.selectable) return
286+
251287
if (props.singleSelect) {
252288
selected.value = selected.value === rowKey ? undefined : rowKey
253289
} else {
254290
if (!Array.isArray(selected.value)) selected.value = []
255-
if (selected.value.includes(rowKey)) {
291+
292+
// Use Set for O(1) lookup instead of includes O(n)
293+
if (selectedSet.value.has(rowKey)) {
256294
selected.value = selected.value.filter((k: number | string) => k !== rowKey)
257295
} else {
258296
selected.value = [...selected.value, rowKey]
@@ -308,30 +346,30 @@ const columnCount = computed(() => {
308346
})
309347
310348
watch(items, (newValue: T[]) => {
311-
// Clear expanded state for items that no longer exist
312-
if (props.expandable) {
313-
const currentKeys = new Set<number | string>()
349+
const currentKeys = new Set<number | string>()
314350
315-
newValue.forEach((item, index) => {
316-
currentKeys.add(getItemKey(item, index))
317-
})
351+
newValue.forEach((item, index) => {
352+
currentKeys.add(getItemKey(item, index))
353+
})
318354
319-
// Remove expanded state for items that no longer exist
355+
// Clear expanded state for items that no longer exist
356+
if (props.expandable) {
320357
expandedState.value.forEach((_, key) => {
321358
if (!currentKeys.has(key)) {
322359
expandedState.value.delete(key)
323360
}
324361
})
325362
}
326363
364+
// Clear selected items that no longer exist
327365
if (props.selectable && props.autoClearSelected) {
328366
if (props.singleSelect) {
329-
if (!allKeys.value.includes(selected.value as number | string)) {
367+
if (!currentKeys.has(selected.value as number | string)) {
330368
selected.value = undefined
331369
}
332370
} else {
333-
if (Array.isArray(selected.value)) {
334-
selected.value = selected.value.filter((k: number | string) => allKeys.value.includes(k))
371+
if (Array.isArray(selected.value) && selected.value.length > 0) {
372+
selected.value = selected.value.filter((k: number | string) => currentKeys.has(k))
335373
}
336374
}
337375
}

packages/ui/src/components/upload/Upload.vue

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default {
5252
</script>
5353

5454
<script setup lang="ts">
55-
import { computed, ref, watch, type ExtractPublicPropTypes, type PropType } from 'vue'
55+
import { computed, onBeforeUnmount, ref, watch, type ExtractPublicPropTypes, type PropType } from 'vue'
5656
import { useDropZone } from '@vueuse/core'
5757
import { useCommon } from '../../composables/useCommon'
5858
import { useInteractive } from '../../composables/useInteractive'
@@ -187,10 +187,20 @@ function isImage(file: File) {
187187
return file.type.startsWith('image') || imageExtensions.some((ext) => file.name.endsWith(ext))
188188
}
189189
190+
const blobUrls: string[] = []
191+
190192
function getImagePreview(file: File) {
191-
return URL.createObjectURL(file)
193+
const url = URL.createObjectURL(file)
194+
195+
blobUrls.push(url)
196+
197+
return url
192198
}
193199
200+
onBeforeUnmount(() => {
201+
blobUrls.forEach((url) => URL.revokeObjectURL(url))
202+
})
203+
194204
function calculateFileSize(size: number) {
195205
if (size < 1024) return `${size} B`
196206
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`

0 commit comments

Comments
 (0)