123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- import Alpine from "../../alpinejs/src/alpine"
- export function generateContext(multiple, orientation) {
- return {
- /**
- * Main state...
- */
- searchableText: {},
- disabledKeys: [],
- activeKey: null,
- selectedKeys: [],
- orderedKeys: [],
- elsByKey: {},
- values: {},
- /**
- * Initialization...
- */
- createItem(el) {
- let key = (Math.random() + 1).toString(36).substring(7)
- // Associate key with element...
- this.elsByKey[key] = el
- // Register key for ordering...
- 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...
- this.searchableText[key] = el.textContent.trim().toLowerCase()
- // Store whether disabled or not...
- disabled && this.disabledKeys.push(key)
- },
- destroyItem(el) {
- let key = keyByValue(this.elsByKey, el)
- // 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.orderedKeys[this.orderedKeys.indexOf(key)]
- delete this.searchableText[key]
- delete this.disabledKeys[key]
- this.deactivateKey(key)
- this.reorderKeys()
- },
- /**
- * Handle elements...
- */
- reorderKeys() {
- // Filter out elements removed from the dom...
- this.orderedKeys.forEach((key) => {
- let el = this.elsByKey[key]
- if (el.isConnected) return
- this.destroyItem(el)
- })
- this.orderedKeys = this.orderedKeys.slice().sort((a, z) => {
- if (a === null || z === null) return 0
- let aEl = this.elsByKey[a]
- let zEl = this.elsByKey[z]
- let position = aEl.compareDocumentPosition(zEl)
- if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
- if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
- return 0
- })
- },
- activeEl() {
- if (! this.activeKey) return
- return this.elsByKey[this.activeKey]
- },
- isActiveEl(el) {
- let key = keyByValue(this.elsByKey, el)
- if (! key) return
- return this.activeKey === key
- },
- activateEl(el) {
- let key = keyByValue(this.elsByKey, el)
- if (! key) return
- this.activateKey(key)
- },
- selectEl(el) {
- let key = keyByValue(this.elsByKey, el)
- if (! key) return
- this.selectKey(key)
- },
- isSelectedEl(el) {
- let key = keyByValue(this.elsByKey, el)
- if (! key) return
- return this.isSelected(key)
- },
- isDisabledEl(el) {
- let key = keyByValue(this.elsByKey, el)
- if (! key) return
- return this.isDisabled(key)
- },
- get isScrollingTo() { return this.scrollingCount > 0 },
- scrollingCount: 0,
- activateAndScrollToKey(key) {
- // This addresses the following problem:
- // If deactivate is hooked up to mouseleave,
- // scrolling to an element will trigger deactivation.
- // This "isScrollingTo" is exposed to prevent that.
- this.scrollingCount++
- this.activateKey(key)
- let targetEl = this.elsByKey[key]
- targetEl.scrollIntoView({ block: 'nearest' })
- setTimeout(() => {
- this.scrollingCount--
- // Unfortunately, browser experimentation has shown me
- // that 25ms is the sweet spot when holding down an
- // arrow key to scroll the list of items...
- }, 25)
- },
- /**
- * Handle values...
- */
- selectedValueOrValues() {
- if (multiple) {
- return this.selectedValues()
- } else {
- return this.selectedValue()
- }
- },
- selectedValues() {
- return this.selectedKeys.map(i => this.values[i])
- },
- selectedValue() {
- return this.selectedKeys[0] ? this.values[this.selectedKeys[0]] : null
- },
- selectValue(value, by) {
- if (!value) value = (multiple ? [] : null)
- if (! by) by = (a, b) => a === b
- if (typeof by === 'string') {
- let property = by
- by = (a, b) => a[property] === b[property]
- }
- if (multiple) {
- let keys = []
- value.forEach(i => {
- for (let key in this.values) {
- if (by(this.values[key], i)) {
- if (! keys.includes(key)) {
- keys.push(key)
- }
- }
- }
- })
- this.selectExclusive(keys)
- } else {
- for (let key in this.values) {
- if (value && by(this.values[key], value)) {
- this.selectKey(key)
- }
- }
- }
- },
- /**
- * Handle disabled keys...
- */
- isDisabled(key) { return this.disabledKeys.includes(key) },
- get nonDisabledOrderedKeys() {
- return this.orderedKeys.filter(i => ! this.isDisabled(i))
- },
- /**
- * Handle selected keys...
- */
- selectKey(key) {
- if (this.isDisabled(key)) return
- if (multiple) {
- this.toggleSelected(key)
- } else {
- this.selectOnly(key)
- }
- },
- toggleSelected(key) {
- console.log(key)
- if (this.selectedKeys.includes(key)) {
- this.selectedKeys.splice(this.selectedKeys.indexOf(key), 1)
- } else {
- this.selectedKeys.push(key)
- }
- },
- selectOnly(key) {
- this.selectedKeys = []
- this.selectedKeys.push(key)
- },
- selectExclusive(keys) {
- // We can't just do this.selectedKeys = keys,
- // because we need to preserve reactivity...
- let toAdd = [...keys]
- for (let i = 0; i < this.selectedKeys.length; i++) {
- if (keys.includes(this.selectedKeys[i])) {
- delete toAdd[toAdd.indexOf(this.selectedKeys[i])]
- continue;
- }
- if (! keys.includes(this.selectedKeys[i])) {
- this.selectedKeys.splice(i, 1)
- }
- }
- toAdd.forEach(i => {
- this.selectedKeys.push(i)
- })
- },
- selectActive(key) {
- if (! this.activeKey) return
- this.selectKey(this.activeKey)
- },
- isSelected(key) { return this.selectedKeys.includes(key) },
- firstSelectedKey() { return this.selectedKeys[0] },
- /**
- * Handle activated keys...
- */
- hasActive() { return !! this.activeKey },
- isActiveKey(key) { return this.activeKey === key },
- get active() { return this.hasActive() && this.values[this.activeKey] },
- activateSelectedOrFirst() {
- let firstSelected = this.firstSelectedKey()
- if (firstSelected) {
- return this.activateKey(firstSelected)
- }
- let firstKey = this.firstKey()
- if (firstKey) {
- this.activateKey(firstKey)
- }
- },
- activateKey(key) {
- if (this.isDisabled(key)) return
- this.activeKey = key
- },
- deactivateKey(key) {
- if (this.activeKey === key) this.activeKey = null
- },
- deactivate() {
- if (! this.activeKey) return
- if (this.isScrollingTo) return
- this.activeKey = null
- },
- /**
- * Handle active key traveral...
- */
- nextKey() {
- if (! this.activeKey) return
- let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey)
- return this.nonDisabledOrderedKeys[index + 1]
- },
- prevKey() {
- if (! this.activeKey) return
- let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey)
- return this.nonDisabledOrderedKeys[index - 1]
- },
- firstKey() { return this.nonDisabledOrderedKeys[0] },
- lastKey() { return this.nonDisabledOrderedKeys[this.nonDisabledOrderedKeys.length - 1] },
- searchQuery: '',
- clearSearch: Alpine.debounce(function () { this.searchQuery = '' }, 350),
- searchKey(query) {
- this.clearSearch()
- this.searchQuery += query
- let foundKey
- for (let key in this.searchableText) {
- let content = this.searchableText[key]
- if (content.startsWith(this.searchQuery)) {
- foundKey = key
- break;
- }
- }
- if (! this.nonDisabledOrderedKeys.includes(foundKey)) return
- return foundKey
- },
- activateByKeyEvent(e) {
- // if (e.key === 'ArrowDown') debugger
- let targetKey, hasActive
- switch (e.key) {
- case 'Tab':
- case 'Backspace':
- case 'Delete':
- case 'Meta':
- break;
- break;
- case ['ArrowDown', 'ArrowRight'][orientation === 'vertical' ? 0 : 1]:
- e.preventDefault(); e.stopPropagation()
- this.reorderKeys(); hasActive = this.hasActive()
- targetKey = hasActive ? this.nextKey() : this.firstKey()
- break;
- case ['ArrowUp', 'ArrowLeft'][orientation === 'vertical' ? 0 : 1]:
- e.preventDefault(); e.stopPropagation()
- this.reorderKeys(); hasActive = this.hasActive()
- targetKey = hasActive ? this.prevKey() : this.lastKey()
- break;
- case 'Home':
- case 'PageUp':
- e.preventDefault(); e.stopPropagation()
- this.reorderKeys(); hasActive = this.hasActive()
- targetKey = this.firstKey()
- break;
- case 'End':
- case 'PageDown':
- e.preventDefault(); e.stopPropagation()
- this.reorderKeys(); hasActive = this.hasActive()
- targetKey = this.lastKey()
- break;
- default:
- if (e.key.length === 1) {
- targetKey = this.searchKey(e.key)
- }
- break;
- }
- if (targetKey) {
- this.activateAndScrollToKey(targetKey)
- }
- }
- }
- }
- function keyByValue(object, value) {
- return Object.keys(object).find(key => object[key] === value)
- }
- export function renderHiddenInputs(el, name, value) {
- // Create input elements...
- let newInputs = generateInputs(name, value)
- // Mark them for later tracking...
- newInputs.forEach(i => i._x_hiddenInput = true)
- // Mark them for Alpine ignoring...
- newInputs.forEach(i => i._x_ignore = true)
- // Gather old elements for removal...
- let children = el.children
- let oldInputs = []
- for (let i = 0; i < children.length; i++) {
- let child = children[i];
- if (child._x_hiddenInput) oldInputs.push(child)
- else break
- }
- // Remove old, and insert new ones into the DOM...
- Alpine.mutateDom(() => {
- oldInputs.forEach(i => i.remove())
- newInputs.reverse().forEach(i => el.prepend(i))
- })
- }
- function generateInputs(name, value, carry = []) {
- if (isObjectOrArray(value)) {
- for (let key in value) {
- carry = carry.concat(
- generateInputs(`${name}[${key}]`, value[key])
- )
- }
- } else {
- let el = document.createElement('input')
- el.setAttribute('type', 'hidden')
- el.setAttribute('name', name)
- el.setAttribute('value', '' + value)
- return [el]
- }
- return carry
- }
- function isObjectOrArray(subject) {
- return typeof subject === 'object' && subject !== null
- }
|