Caleb Porzio vor 2 Jahren
Ursprung
Commit
c5d621cb64

+ 249 - 43
index.html

@@ -1,51 +1,257 @@
 <html>
 <html>
-    <!-- <script src="./packages/intersect/dist/cdn.js" defer></script>
-    <script src="./packages/morph/dist/cdn.js" defer></script>
-    <script src="./packages/history/dist/cdn.js"></script>
-    <script src="./packages/persist/dist/cdn.js"></script>
-    <script src="./packages/focus/dist/cdn.js"></script>
-    <script src="./packages/mask/dist/cdn.js"></script>
-    <script src="./packages/ui/dist/cdn.js" defer></script> -->
-    <script src="./packages/alpinejs/dist/cdn.js" defer></script>
-    <!-- <script src="//cdn.tailwindcss.com"></script> -->
-    <script src="//cdn.tailwindcss.com"></script>
-
-    <div x-data="{ users: [{ name: 'lebowski' }] }">
-        <template x-for="(user, idx) in users">
-            <span x-text="users[idx].name" x-yo></span>
-        </template>
-
-        <button @click="users = []">Reset</button>
-    </div>
+<script src="./packages/intersect/dist/cdn.js" defer></script>
+<script src="./packages/morph/dist/cdn.js" defer></script>
+<script src="./packages/history/dist/cdn.js"></script>
+<script src="./packages/persist/dist/cdn.js"></script>
+<script src="./packages/focus/dist/cdn.js"></script>
+<script src="./packages/mask/dist/cdn.js"></script>
+<script src="./packages/ui/dist/cdn.js" defer></script>
+<script src="./packages/alpinejs/dist/cdn.js" defer></script>
+<script src="//cdn.tailwindcss.com"></script>
 
 
-    <!-- Play around here... -->
+<!-- <div
+            x-data="{
+                selected: null,
+                query: '',
+                people: [
+                    { id: 1, name: 'Wade Cooper' },
+                    { id: 2, name: 'Arlene Mccoy' },
+                    { id: 3, name: 'Devon Webb' },
+                    { id: 4, name: 'Tom Cook' },
+                    { id: 5, name: 'Tanya Fox', disabled: true },
+                    { id: 6, name: 'Hellen Schmidt' },
+                    { id: 7, name: 'Caroline Schultz' },
+                    { id: 8, name: 'Mason Heaney' },
+                    { id: 9, name: 'Claudie Smitham' },
+                    { id: 10, name: 'Emil Schaefer' },
+                ],
+                get filteredPeople() {
+                    return this.query.value === ''
+                        ? this.people
+                        : this.people.filter((person) => {
+                            return person.name.toLowerCase().includes(this.query.toLowerCase())
+                        })
+                }
+            }"
+            x-combobox
+            x-model="selected"
+        >
+            <span x-text="$combobox.active && $combobox.active.name"></span>
+            <label x-combobox:label>Assigned to</label>
 
 
-    <div x-data>
-        <div id="thing" x-yo>i do not belong here...</div>
+            <div>
+                <div>
+                </div>
+                <input x-combobox:input @change="query = $event.target.value" :display-value="() => person => person.name" type="text">
+                <button x-combobox:button x-text="selected ? selected.name : 'Select Person'">Select Person</button>
+            </div>
 
 
-        <br>
-        <br>
-        <br>
-        <br>
+            <ul x-combobox:options>
+                <template x-for="person in filteredPeople" :key="person.id">
+                    <li
+                        x-combobox:option
+                        :value="person"
+                        :disabled="person.disabled"
+                        :class="$comboboxOption.isActive && 'foo'"
+                    >
+                        <span x-show="$comboboxOption.isSelected">-</span>
+                        <span x-text="person.name"></span>
+                        <span x-show="$comboboxOption.isActive">*</span>
+                    </li>
+                </template>
+            </ul>
+        </div> -->
 
 
-        <button @click="document.getElementById('thing').remove()">remove</button>
-    </div>
+<!-- Multiple -->
+<!-- <div
+            x-data="{
+                selected: [],
+                query: '',
+                people: [
+                    { id: 1, name: 'Wade Cooper' },
+                    { id: 2, name: 'Arlene Mccoy' },
+                    { id: 3, name: 'Devon Webb' },
+                    { id: 4, name: 'Tom Cook' },
+                    { id: 5, name: 'Tanya Fox', disabled: true },
+                    { id: 6, name: 'Hellen Schmidt' },
+                    { id: 7, name: 'Caroline Schultz' },
+                    { id: 8, name: 'Mason Heaney' },
+                    { id: 9, name: 'Claudie Smitham' },
+                    { id: 10, name: 'Emil Schaefer' },
+                ],
+                get filteredPeople() {
+                    return this.query.value === ''
+                        ? this.people
+                        : this.people.filter((person) => {
+                            return person.name.toLowerCase().includes(this.query.toLowerCase())
+                        })
+                }
+            }"
+            x-combobox
+            multiple
+            by="id"
+            x-model="selected"
+        >
+            <span x-text="$combobox.active && $combobox.active.name"></span>
+            <label x-combobox:label>Assigned to</label>
+
+            <div>
+                <div x-effect="selected.forEach(i => console.log(JSON.stringify(i)))">
+                    <template x-for="selectedPerson in selected" :key="selectedPerson.id">
+                        <button x-text="selectedPerson.name" @click="selected = selected.filter(i => i.id !== selectedPerson.id)"></button>
+                    </template>
+                </div>
+                <input x-combobox:input @change="query = $event.target.value" type="text">
+                <button x-combobox:button x-text="selected.length ? selected.map(i => i.name).join(', ') : 'Selected Person'">Select Person</button>
+            </div>
+
+            <ul x-combobox:options>
+                <template x-for="person in filteredPeople" :key="person.id">
+                    <li
+                        x-combobox:option
+                        :value="person"
+                        :disabled="person.disabled"
+                        :class="$comboboxOption.isActive && 'foo'"
+                    >
+                        <span x-show="$comboboxOption.isSelected">-</span>
+                        <span x-text="person.name"></span>
+                        <span x-show="$comboboxOption.isActive">*</span>
+                    </li>
+                </template>
+            </ul>
+        </div> -->
+
+<div
+    x-data="{
+        query: '',
+        people: [
+            { id: 1, name: 'Wade Cooper' },
+            { id: 2, name: 'Arlene Mccoy' },
+            { id: 3, name: 'Devon Webb' },
+            { id: 4, name: 'Tom Cook' },
+            { id: 5, name: 'Tanya Fox' },
+            { id: 6, name: 'Hellen Schmidt' },
+            { id: 7, name: 'Caroline Schultz' },
+            { id: 8, name: 'Mason Heaney' },
+            { id: 9, name: 'Claudie Smitham' },
+            { id: 10, name: 'Emil Schaefer' },
+        ],
+        activePersons: [],
+        onSubmit(e) {
+            e.preventDefault()
+            console.log([...new FormData(e.currentTarget).entries()])
+        },
+        removePerson(person) {
+            this.activePersons = this.activePersons.filter((p) => p !== person)
+        }
+    }"
+    class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12"
+>
+    <div class="w-full max-w-4xl">
+        <div class="space-y-1">
+            <form @submit="onSubmit">
+                <div x-combobox x-model="activePersons" name="people" multiple>
+                    <label x-combobox:label class="block text-sm font-medium leading-5 text-gray-700">
+                        Assigned to
+                    </label>
 
 
-    <script>
-        document.addEventListener('alpine:init', () => {
-            Alpine.directive('yo', (el, {}, { cleanup }) => {
-                cleanup(() => {
-                    console.log('removed')
-                })
-            })
-        })
-    </script>
-
-    <div x-data="{ users: [{ name: 'lebowski' }] }">
-        <template x-for="(user, idx) in users">
-            <span x-text="users[idx].name" x-yo></span>
-        </template>
-
-        <button @click="users = []">Reset</button>
+                    <div class="relative">
+                        <span class="inline-block w-full rounded-md shadow-sm">
+                            <div class="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus-within:border-blue-700 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 sm:text-sm sm:leading-5">
+                                <span class="block flex flex-wrap gap-2">
+                                    <span x-show="activePersons.length === 0" class="p-0.5">Empty</span>
+                                    <template x-for="person in activePersons" :key="person.id">
+                                        <span class="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5">
+                                            <span x-text="person.name"></span>
+                                            <svg class="h-4 w-4 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" @click.stop.prevent="removePerson(person)">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
+                                            </svg>
+                                        </span>
+                                    </template>
+                                    <input x-combobox:input @change="query = $event.target.value" @focus="query = ''" class="border-none p-0 focus:ring-0" placeholder="Search..." />
+                                </span>
+                                <button x-combobox:button class="absolute inset-y-0 right-0 flex items-center pr-2">
+                                    <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
+                                        <path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+                                    </svg>
+                                </button>
+                            </div>
+                        </span>
+
+                        <div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
+                            <div x-combobox:options class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
+                                <template
+                                    x-for="person in people.filter((person) =>
+                                        person.name.toLowerCase().includes(query.toLowerCase())
+                                    )"
+                                    :key="person.id"
+                                >
+                                    <div x-combobox:option :value="person">
+                                        <li class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
+                                            <span x-text="person.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
+                                            </span>
+                                            <span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
+                                                <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                                                    <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
+                                                </svg>
+                                            </span>
+                                        </li>
+                                    </div>
+                                </template>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <button class="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
+                    Submit
+                </button>
+            </form>
+        </div>
     </div>
     </div>
+</div>
+
+<!-- <div x-data="{ active: true }">
+            <button @click="active = ! active">switch active</button>
+            <div
+                x-foo
+                :doo="active"
+                :class="$foo.isActive ? 'active' : 'not active'"
+            >
+                <div x-text="$foo.isActive ? 'active' : 'not active'"></div>
+            </div>
+        </div> -->
+
+<script>
+
+            // document.addEventListener('alpine:init', () => {
+            //     let directive = el => {
+            //         el.__isActive = Alpine.reactive({ state: false })
+
+            //         Alpine.effect(() => {
+            //             el.__isActive.state = Alpine.bound(el, 'doo');
+            //         })
+
+            //         el.__isOption.ready = true
+            //     }
+
+            //     directive.inline = el => {
+            //         el.__isOption = Alpine.reactive({ ready: false })
+            //     }
+
+            //     Alpine.directive('foo', directive)
+
+            //     Alpine.magic('foo', el => {
+            //         if (el.__isOption && ! el.__isOption.ready) {
+            //             return { isActive: true }
+            //         }
+
+            //         let optionEl = Alpine.findClosest(el, el => el.__isOption)
+
+            //         return {
+            //             isActive: optionEl.__isActive.state,
+            //         }
+            //     })
+            // })
+</script>
+
 </html>
 </html>

+ 1 - 0
packages/alpinejs/src/directives.js

@@ -217,6 +217,7 @@ let directiveOrder = [
     'listbox',
     'listbox',
     'combobox',
     'combobox',
     'bind',
     'bind',
+    'foo',
     'init',
     'init',
     'for',
     'for',
     'mask',
     'mask',

+ 5 - 0
packages/alpinejs/src/directives/x-for.js

@@ -141,6 +141,11 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
                 lookup[key]._x_effects.forEach(dequeueJob)
                 lookup[key]._x_effects.forEach(dequeueJob)
             }
             }
 
 
+            // An internal hook to unregister an item synchronously...
+            if (!! lookup[key]._x_forCleanup) {
+                lookup[key]._x_forCleanup()
+            }
+
             lookup[key].remove()
             lookup[key].remove()
 
 
             lookup[key] = null
             lookup[key] = null

+ 1 - 0
packages/alpinejs/src/evaluator.js

@@ -45,6 +45,7 @@ export function normalEvaluator(el, expression) {
 
 
     let evaluator = generateEvaluatorFromString(dataStack, expression, el)
     let evaluator = generateEvaluatorFromString(dataStack, expression, el)
 
 
+    return evaluator
     return tryCatch.bind(null, el, expression, evaluator)
     return tryCatch.bind(null, el, expression, evaluator)
 }
 }
 
 

+ 62 - 70
packages/ui/src/combobox.js

@@ -1,4 +1,4 @@
-import { generateContext } from "./list-context"
+import { generateContext, renderHiddenInputs } from './list-context'
 
 
 export default function (Alpine) {
 export default function (Alpine) {
     Alpine.directive('combobox', (el, directive, { evaluate }) => {
     Alpine.directive('combobox', (el, directive, { evaluate }) => {
@@ -13,13 +13,6 @@ export default function (Alpine) {
     Alpine.magic('combobox', el => {
     Alpine.magic('combobox', el => {
         let data = Alpine.$data(el)
         let data = Alpine.$data(el)
 
 
-        if (! data.__ready) return {
-            isDisabled: false,
-            isOpen: false,
-            selected: null,
-            active: null,
-        }
-
         return {
         return {
             get isOpen() {
             get isOpen() {
                 return data.__isOpen
                 return data.__isOpen
@@ -39,17 +32,9 @@ export default function (Alpine) {
     Alpine.magic('comboboxOption', el => {
     Alpine.magic('comboboxOption', el => {
         let data = Alpine.$data(el)
         let data = Alpine.$data(el)
 
 
-        let stub = {
-            isDisabled: false,
-            isSelected: false,
-            isActive: true,
-        }
-
-        if (! data.__ready) return stub
-
         let optionEl = Alpine.findClosest(el, i => i.__optionKey)
         let optionEl = Alpine.findClosest(el, i => i.__optionKey)
 
 
-        if (! optionEl) return stub
+        if (! optionEl) throw 'No x-combobox:option directive found...'
 
 
         let context = data.__context
         let context = data.__context
 
 
@@ -97,53 +82,50 @@ function handleRoot(el, Alpine) {
 
 
                     this.__value = defaultValue
                     this.__value = defaultValue
 
 
-                    // @todo: remove me...
-                    window._reorder = () => this.__context.reorderKeys()
-
-                    // We have to wait for the rest of the HTML to initialize in Alpine before
-                    // we mark this component as "ready".
+                    // We have to wait again until after the "ready" processes are finished
+                    // to settle up currently selected Values (this prevents this next bit
+                    // of code from running multiple times on startup...)
                     queueMicrotask(() => {
                     queueMicrotask(() => {
-                        this.__ready = true
-
-                        // We have to wait again until after the "ready" processes are finished
-                        // to settle up currently selected Values (this prevents this next bit
-                        // of code from running multiple times on startup...)
-                        queueMicrotask(() => {
-                            // This "fingerprint" acts as a checksum of the last-known "value"
-                            // passed into x-model. We need to track this so that we can determine
-                            // from the reactive effect if it was the value that changed externally
-                            // or an option was selected internally...
-                            let lastValueFingerprint = false
-
-                            Alpine.effect(() => {
-                                // Accessing selected keys, so a change in it always triggers this effect...
-                                this.__context.selectedKeys
-
-                                if (lastValueFingerprint === false || lastValueFingerprint !== JSON.stringify(this.__value)) {
-                                    // Here we know that the value changed externally and we can add the selection...
-                                    this.__context.selectValue(this.__value, this.__compareBy)
-                                } else {
-                                    // Here we know that an option was selected and we can change the value...
-                                    this.__value = this.__context.selectedValueOrValues()
-                                }
-
-
-                                // Generate the "value" checksum for comparison next time...
-                                lastValueFingerprint = JSON.stringify(this.__value)
-
-                                // Everytime the value changes, we need to re-render the hidden inputs,
-                                // if a user passed the "name" prop...
-                                this.__inputName && renderHiddenInputs(this.$el, this.__inputName, this.__value)
-                            })
-
-                            Alpine.effect(() => {
-                                if (this.__value) {
-                                    this.$refs.__input.value = this.$data.__displayValue(this.__value)
-                                }
-                            })
+                        // This "fingerprint" acts as a checksum of the last-known "value"
+                        // passed into x-model. We need to track this so that we can determine
+                        // from the reactive effect if it was the value that changed externally
+                        // or an option was selected internally...
+                        let lastValueFingerprint = false
+
+                        Alpine.effect(() => {
+                            // Accessing selected keys, so a change in it always triggers this effect...
+                            this.__context.selectedKeys
+
+                            if (lastValueFingerprint === false || lastValueFingerprint !== JSON.stringify(this.__value)) {
+                                // Here we know that the value changed externally and we can add the selection...
+                                this.__context.selectValue(this.__value, this.__compareBy)
+                            } else {
+                                // Here we know that an option was selected and we can change the value...
+                                this.__value = this.__context.selectedValueOrValues()
+                            }
+
+                            // Generate the "value" checksum for comparison next time...
+                            lastValueFingerprint = JSON.stringify(this.__value)
+
+                            // Everytime the value changes, we need to re-render the hidden inputs,
+                            // if a user passed the "name" prop...
+                            this.__inputName && renderHiddenInputs(this.$el, this.__inputName, this.__value)
+                        })
+
+                        Alpine.effect(() => {
+                            if (this.__value) {
+                                let input = this.$refs.__input
+                                if (input) input.value = this.$data.__getCurrentValue()
+                            }
                         })
                         })
                     })
                     })
                 },
                 },
+                __getCurrentValue() {
+                    if (! this.$refs.__input) return ''
+                    if (this.$data.__displayValue) return this.$data.__displayValue(this.__value)
+                    if (typeof this.__value === 'string') return this.__value
+                    return ''
+                },
                 __open() {
                 __open() {
                     if (this.__isOpen) return
                     if (this.__isOpen) return
                     this.__isOpen = true
                     this.__isOpen = true
@@ -154,12 +136,13 @@ function handleRoot(el, Alpine) {
                     // Probably because Alpine adds an extra tick when x-showing for @click.outside
                     // Probably because Alpine adds an extra tick when x-showing for @click.outside
                     let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
                     let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
 
 
-                    nextTick(() => this.$refs.__options.focus({ preventScroll: true }))
+                    nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
                 },
                 },
                 __close() {
                 __close() {
                     this.__isOpen = false
                     this.__isOpen = false
 
 
-                    this.$nextTick(() => this.$refs.__button.focus({ preventScroll: true }))
+                    // I think this shouldn't be here...
+                    // this.$nextTick(() => this.$refs.__button.focus({ preventScroll: true }))
                 }
                 }
             }
             }
         },
         },
@@ -197,14 +180,17 @@ function handleInput(el, Alpine) {
         },
         },
         '@input.stop'() {
         '@input.stop'() {
             this.$data.__open(); this.$dispatch('change')
             this.$data.__open(); this.$dispatch('change')
-            setTimeout(() => this.$data.__context.reorderKeys())
+            // setTimeout(() => this.$data.__context.reorderKeys())
         },
         },
-        '@change.stop'() {
-            setTimeout(() => this.$data.__context.reorderKeys())
+        // '@change.stop'() {
+            // setTimeout(() => this.$data.__context.reorderKeys())
+        // },
+        '@keydown.enter.prevent.stop'() {
+            this.$data.__context.selectActive();
+            this.$data.__isMultiple || this.$data.__close()
         },
         },
-        '@keydown.enter.prevent.stop'() { this.$data.__context.selectActive(); this.$data.__close() },
         '@keydown'(e) {
         '@keydown'(e) {
-            this.$data.__context.activateByKeyEvent(e)
+            queueMicrotask(() => this.$data.__context.activateByKeyEvent(e))
          },
          },
         '@keydown.down'(e) { if(! this.$data.__isOpen) this.$data.__open(); },
         '@keydown.down'(e) { if(! this.$data.__isOpen) this.$data.__open(); },
         '@keydown.up'(e) { if(! this.$data.__isOpen) this.$data.__open(); },
         '@keydown.up'(e) { if(! this.$data.__isOpen) this.$data.__open(); },
@@ -311,13 +297,18 @@ function handleOption(el, Alpine) {
         'x-init'() {
         'x-init'() {
             el._x_optionReady = Alpine.reactive({ state: false })
             el._x_optionReady = Alpine.reactive({ state: false })
 
 
-            queueMicrotask(() => {
-                el._x_optionReady.state = true
+            el.__optionKey = this.$data.__context.createItem(el)
 
 
+            queueMicrotask(() => {
                 let value = Alpine.bound(el, 'value')
                 let value = Alpine.bound(el, 'value')
                 let disabled = Alpine.bound(el, 'disabled')
                 let disabled = Alpine.bound(el, 'disabled')
 
 
-                el.__optionKey = this.$data.__context.initItem(el, value, disabled)
+                this.$data.__context.updateItem(el.__optionKey, value, disabled)
+
+                // @todo: make sure this is what you want...
+                el._x_forCleanup = () => {
+                    this.$data.__context.destroyItem(el)
+                }
             })
             })
         },
         },
         ':id'() { return this.$id('alpine-combobox-option') },
         ':id'() { return this.$id('alpine-combobox-option') },
@@ -328,6 +319,7 @@ function handleOption(el, Alpine) {
             if (this.$comboboxOption.isDisabled) return;
             if (this.$comboboxOption.isDisabled) return;
             this.$data.__context.selectEl(el);
             this.$data.__context.selectEl(el);
             this.$data.__isMultiple || this.$data.__close()
             this.$data.__isMultiple || this.$data.__close()
+            this.$nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
         },
         },
         // @todo: this is a memory leak for _x_cleanups...
         // @todo: this is a memory leak for _x_cleanups...
         '@mouseenter'() { this.$data.__context.activateEl(el) },
         '@mouseenter'() { this.$data.__context.activateEl(el) },

+ 32 - 31
packages/ui/src/list-context.js

@@ -16,55 +16,49 @@ export function generateContext(multiple, orientation) {
         /**
         /**
          *  Initialization...
          *  Initialization...
          */
          */
-        initItem(el, value, disabled) {
-            // First, check if there is an existing value...
-            if (Object.values(this.values).includes(value)) {
-                let key = Object.keys(this.values).find(key => this.values[key] === value)
-
-                // Remove the old el association and replace it with this one...
-                delete this.elsByKey[key]
-                this.elsByKey[key] = el
-
-                // Refresh the searchable text...
-                this.searchableText[key] = el.textContent.trim().toLowerCase()
-
-                // Refresh disabled...
-                disabled && (! this.disabledKeys.includes(key)) && this.disabledKeys.push(key)
-
-                console.log('refreshed item')
-
-                return key
-            }
-
+        createItem(el) {
             let key = (Math.random() + 1).toString(36).substring(7)
             let key = (Math.random() + 1).toString(36).substring(7)
 
 
-            // Register value by key...
-            this.values[key] = value
-
             // Associate key with element...
             // Associate key with element...
             this.elsByKey[key] = el
             this.elsByKey[key] = el
 
 
             // Register key for ordering...
             // Register key for ordering...
             this.orderedKeys.push(key)
             this.orderedKeys.push(key)
 
 
+            return key
+        },
+
+        updateItem(key, value, disabled) {
+            // Register value by key...
+            this.values[key] = value
+
+            let el = this.elsByKey[key]
+
             // Register key for searching...
             // Register key for searching...
             this.searchableText[key] = el.textContent.trim().toLowerCase()
             this.searchableText[key] = el.textContent.trim().toLowerCase()
 
 
             // Store whether disabled or not...
             // Store whether disabled or not...
             disabled && this.disabledKeys.push(key)
             disabled && this.disabledKeys.push(key)
-
-            return key
         },
         },
 
 
         destroyItem(el) {
         destroyItem(el) {
             let key = keyByValue(this.elsByKey, el)
             let key = keyByValue(this.elsByKey, el)
 
 
-            delete this.values[key]
+            // This line makes sense to free stored values from
+            // memory, however, in a combobox, if the options change
+            // we want to preserve selected values that may not be present
+            // in the most current list. If this becomes a problem, we will
+            // need to find a way to free values from memory while preserving
+            // selected values:
+            // delete this.values[key]
+
             delete this.elsByKey[key]
             delete this.elsByKey[key]
             delete this.orderedKeys[this.orderedKeys.indexOf(key)]
             delete this.orderedKeys[this.orderedKeys.indexOf(key)]
             delete this.searchableText[key]
             delete this.searchableText[key]
             delete this.disabledKeys[key]
             delete this.disabledKeys[key]
 
 
+            this.deactivateKey(key)
+
             this.reorderKeys()
             this.reorderKeys()
         },
         },
 
 
@@ -195,7 +189,6 @@ export function generateContext(multiple, orientation) {
             }
             }
 
 
             if (multiple) {
             if (multiple) {
-                // debugger
                 let keys = []
                 let keys = []
 
 
                 value.forEach(i => {
                 value.forEach(i => {
@@ -241,6 +234,7 @@ export function generateContext(multiple, orientation) {
         },
         },
 
 
         toggleSelected(key) {
         toggleSelected(key) {
+            console.log(key)
             if (this.selectedKeys.includes(key)) {
             if (this.selectedKeys.includes(key)) {
                 this.selectedKeys.splice(this.selectedKeys.indexOf(key), 1)
                 this.selectedKeys.splice(this.selectedKeys.indexOf(key), 1)
             } else {
             } else {
@@ -266,7 +260,7 @@ export function generateContext(multiple, orientation) {
                 }
                 }
 
 
                 if (! keys.includes(this.selectedKeys[i])) {
                 if (! keys.includes(this.selectedKeys[i])) {
-                    delete this.selectedKeys[i]
+                    this.selectedKeys.splice(i, 1)
                 }
                 }
             }
             }
 
 
@@ -315,6 +309,10 @@ export function generateContext(multiple, orientation) {
             this.activeKey = key
             this.activeKey = key
         },
         },
 
 
+        deactivateKey(key) {
+            if (this.activeKey === key) this.activeKey = null
+        },
+
         deactivate() {
         deactivate() {
             if (! this.activeKey) return
             if (! this.activeKey) return
             if (this.isScrollingTo) return
             if (this.isScrollingTo) return
@@ -371,11 +369,10 @@ export function generateContext(multiple, orientation) {
         },
         },
 
 
         activateByKeyEvent(e) {
         activateByKeyEvent(e) {
-            this.reorderKeys()
+            // if (e.key === 'ArrowDown') debugger
 
 
-            let hasActive = this.hasActive()
 
 
-            let targetKey
+            let targetKey, hasActive
 
 
             switch (e.key) {
             switch (e.key) {
                 case 'Tab':
                 case 'Tab':
@@ -387,22 +384,26 @@ export function generateContext(multiple, orientation) {
                     break;
                     break;
                 case ['ArrowDown', 'ArrowRight'][orientation === 'vertical' ? 0 : 1]:
                 case ['ArrowDown', 'ArrowRight'][orientation === 'vertical' ? 0 : 1]:
                     e.preventDefault(); e.stopPropagation()
                     e.preventDefault(); e.stopPropagation()
+                    this.reorderKeys(); hasActive = this.hasActive()
                     targetKey = hasActive ? this.nextKey() : this.firstKey()
                     targetKey = hasActive ? this.nextKey() : this.firstKey()
                     break;
                     break;
 
 
                 case ['ArrowUp', 'ArrowLeft'][orientation === 'vertical' ? 0 : 1]:
                 case ['ArrowUp', 'ArrowLeft'][orientation === 'vertical' ? 0 : 1]:
                     e.preventDefault(); e.stopPropagation()
                     e.preventDefault(); e.stopPropagation()
+                    this.reorderKeys(); hasActive = this.hasActive()
                     targetKey = hasActive ? this.prevKey() : this.lastKey()
                     targetKey = hasActive ? this.prevKey() : this.lastKey()
                     break;
                     break;
                 case 'Home':
                 case 'Home':
                 case 'PageUp':
                 case 'PageUp':
                     e.preventDefault(); e.stopPropagation()
                     e.preventDefault(); e.stopPropagation()
+                    this.reorderKeys(); hasActive = this.hasActive()
                     targetKey = this.firstKey()
                     targetKey = this.firstKey()
                     break;
                     break;
 
 
                 case 'End':
                 case 'End':
                 case 'PageDown':
                 case 'PageDown':
                     e.preventDefault(); e.stopPropagation()
                     e.preventDefault(); e.stopPropagation()
+                    this.reorderKeys(); hasActive = this.hasActive()
                     targetKey = this.lastKey()
                     targetKey = this.lastKey()
                     break;
                     break;