diff --git a/packages/ui/src/list-context.js b/packages/ui/src/list-context.js index bd93a2748..481180a61 100644 --- a/packages/ui/src/list-context.js +++ b/packages/ui/src/list-context.js @@ -5,7 +5,7 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO * Main state... */ items: [], - activeKey: null, + activeKey: switchboard(), orderedKeys: [], activatedByKeyPress: false, @@ -16,26 +16,54 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO activateSelectedOrFirst(false) }), + registerItemsQueue: [], + registerItem(key, el, value, disabled) { - this.items.push({ - key, el, value, disabled - }) + // We need to queue up these additions to not slow down the + // init process for each row... + if (this.registerItemsQueue.length === 0) { + queueMicrotask(() => { + if (this.registerItemsQueue.length > 0) { + this.items = this.items.concat(this.registerItemsQueue) - this.orderedKeys.push(key) + this.registerItemsQueue = [] + + this.reorderKeys() + this.activateSelectedOrFirst() + } + }) + } + + let item = { + key, el, value, disabled + } - this.reorderKeys() - this.activateSelectedOrFirst() + this.registerItemsQueue.push(item) }, - unregisterItem(key) { - let i = this.items.findIndex((i) => i.key === key) - if (i !== -1) this.items.splice(i, 1) + unregisterKeysQueue: [], - i = this.orderedKeys.indexOf(key) - if (i !== -1) this.orderedKeys.splice(i, 1) + unregisterItem(key) { + // This gets triggered when the mutation observer picks up DOM changes. + // It will get called for every row that gets removed. If there are + // 1000x rows, we want to trigger this cleanup when the first one + // is handled, let the others add their keys to the queue, then + // handle all the cleanup in bulk at the end. Big perf gain... + if (this.unregisterKeysQueue.length === 0) { + queueMicrotask(() => { + if (this.unregisterKeysQueue.length > 0) { + this.items = this.items.filter(i => ! this.unregisterKeysQueue.includes(i.key)) + this.orderedKeys = this.orderedKeys.filter(i => ! this.unregisterKeysQueue.includes(i)) + + this.unregisterKeysQueue = [] + + this.reorderKeys() + this.activateSelectedOrFirst() + } + }) + } - this.reorderKeys() - this.activateSelectedOrFirst() + this.unregisterKeysQueue.push(key) }, getItemByKey(key) { @@ -65,9 +93,9 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO getActiveItem() { if (! this.hasActive()) return null - let item = this.items.find(i => i.key === this.activeKey) + let item = this.items.find(i => i.key === this.activeKey.get()) - if (! item) this.deactivateKey(this.activeKey) + if (! item) this.deactivateKey(this.activeKey.get()) return item }, @@ -99,19 +127,23 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO // If there no longer is the active key in the items list, then // deactivate it... - if (! this.orderedKeys.includes(this.activeKey)) this.deactivateKey(this.activeKey) + if (! this.orderedKeys.includes(this.activeKey.get())) this.deactivateKey(this.activeKey.get()) }), + getActiveKey() { + return this.activeKey.get() + }, + activeEl() { - if (! this.activeKey) return + if (! this.activeKey.get()) return - return this.items.find(i => i.key === this.activeKey).el + return this.items.find(i => i.key === this.activeKey.get()).el }, isActiveEl(el) { let key = this.items.find(i => i.el === el) - return this.activeKey === key + return this.activeKey.is(key) }, activateEl(el) { @@ -169,7 +201,7 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO /** * Handle activated keys... */ - hasActive() { return !! this.activeKey }, + hasActive() { return !! this.activeKey.get() }, /** * Return true if the latest active element was activated @@ -179,27 +211,27 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO */ wasActivatedByKeyPress() {return this.activatedByKeyPress}, - isActiveKey(key) { return this.activeKey === key }, + isActiveKey(key) { return this.activeKey.is(key) }, activateKey(key, activatedByKeyPress = false) { if (this.isDisabled(key)) return - this.activeKey = key + this.activeKey.set(key) this.activatedByKeyPress = activatedByKeyPress }, deactivateKey(key) { - if (this.activeKey === key) { - this.activeKey = null + if (this.activeKey.get() === key) { + this.activeKey.set(null) this.activatedByKeyPress = false } }, deactivate() { - if (! this.activeKey) return + if (! this.activeKey.get()) return if (this.isScrollingTo) return - this.activeKey = null + this.activeKey.set(null) this.activatedByKeyPress = false }, @@ -207,17 +239,17 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO * Handle active key traversal... */ nextKey() { - if (! this.activeKey) return + if (! this.activeKey.get()) return - let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey) + let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey.get()) return this.nonDisabledOrderedKeys[index + 1] }, prevKey() { - if (! this.activeKey) return + if (! this.activeKey.get()) return - let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey) + let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey.get()) return this.nonDisabledOrderedKeys[index - 1] }, @@ -382,3 +414,49 @@ function generateInputs(name, value, carry = []) { function isObjectOrArray(subject) { return typeof subject === 'object' && subject !== null } + +function switchboard(value) { + let lookup = {} + + let current + + let changeTracker = Alpine.reactive({ state: false }) + + let get = () => { + // Depend on the change tracker so reading "get" becomes reactive... + if (changeTracker.state) { + // + } + + return current + } + + let set = (newValue) => { + if (newValue === current) return + + if (current !== undefined) lookup[current].state = false + + current = newValue + + if (lookup[newValue] === undefined) { + lookup[newValue] = Alpine.reactive({ state: true }) + } else { + lookup[newValue].state = true + } + + changeTracker.state = ! changeTracker.state + } + + let is = (comparisonValue) => { + if (lookup[comparisonValue] === undefined) { + lookup[comparisonValue] = Alpine.reactive({ state: false }) + return lookup[comparisonValue].state + } + + return !! lookup[comparisonValue].state + } + + value === undefined || set(value) + + return { get, set, is } +} diff --git a/packages/ui/src/listbox.js b/packages/ui/src/listbox.js index beb2eaee1..08eebb972 100644 --- a/packages/ui/src/listbox.js +++ b/packages/ui/src/listbox.js @@ -159,8 +159,8 @@ function handleRoot(el, Alpine) { __activateSelectedOrFirst(activateSelected = true) { if (! this.__isOpen) return - if (this.__context.activeKey) { - this.__context.activateAndScrollToKey(this.__context.activeKey) + if (this.__context.getActiveKey()) { + this.__context.activateAndScrollToKey(this.__context.getActiveKey()) return }