Add supporting themes required for Lotusdocs
This commit is contained in:
510
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/combobox.js
Normal file
510
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/combobox.js
Normal file
@@ -0,0 +1,510 @@
|
||||
import { generateContext, renderHiddenInputs } from './list-context'
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('combobox', (el, directive, { evaluate }) => {
|
||||
if (directive.value === 'input') handleInput(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'options') handleOptions(el, Alpine)
|
||||
else if (directive.value === 'option') handleOption(el, Alpine, directive, evaluate)
|
||||
else handleRoot(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('combobox', el => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return data.__value
|
||||
},
|
||||
get isOpen() {
|
||||
return data.__isOpen
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__isDisabled
|
||||
},
|
||||
get activeOption() {
|
||||
let active = data.__context?.getActiveItem()
|
||||
|
||||
return active && active.value
|
||||
},
|
||||
get activeIndex() {
|
||||
let active = data.__context?.getActiveItem()
|
||||
|
||||
if (active) {
|
||||
return Object.values(Alpine.raw(data.__context.items)).findIndex(i => Alpine.raw(active) == Alpine.raw(i))
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Alpine.magic('comboboxOption', el => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
// It's not great depending on the existance of the attribute in the DOM
|
||||
// but it's probably the fastest and most reliable at this point...
|
||||
let optionEl = Alpine.findClosest(el, i => {
|
||||
return i.hasAttribute('x-combobox:option')
|
||||
})
|
||||
|
||||
if (! optionEl) throw 'No x-combobox:option directive found...'
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return data.__context.isActiveKey(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
get isSelected() {
|
||||
return data.__isSelected(optionEl)
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__context.isDisabled(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-id'() { return ['alpine-combobox-button', 'alpine-combobox-options', 'alpine-combobox-label'] },
|
||||
'x-modelable': '__value',
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
/**
|
||||
* Combobox state...
|
||||
*/
|
||||
__ready: false,
|
||||
__value: null,
|
||||
__isOpen: false,
|
||||
__context: undefined,
|
||||
__isMultiple: undefined,
|
||||
__isStatic: false,
|
||||
__isDisabled: undefined,
|
||||
__displayValue: undefined,
|
||||
__compareBy: null,
|
||||
__inputName: null,
|
||||
__isTyping: false,
|
||||
__hold: false,
|
||||
|
||||
/**
|
||||
* Combobox initialization...
|
||||
*/
|
||||
init() {
|
||||
this.__isMultiple = Alpine.extractProp(el, 'multiple', false)
|
||||
this.__isDisabled = Alpine.extractProp(el, 'disabled', false)
|
||||
this.__inputName = Alpine.extractProp(el, 'name', null)
|
||||
this.__nullable = Alpine.extractProp(el, 'nullable', false)
|
||||
this.__compareBy = Alpine.extractProp(el, 'by')
|
||||
|
||||
this.__context = generateContext(Alpine, this.__isMultiple, 'vertical', () => this.__activateSelectedOrFirst())
|
||||
|
||||
let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
|
||||
|
||||
this.__value = defaultValue
|
||||
|
||||
// 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(() => {
|
||||
Alpine.effect(() => {
|
||||
// Everytime the value changes, we need to re-render the hidden inputs,
|
||||
// if a user passed the "name" prop...
|
||||
this.__inputName && renderHiddenInputs(Alpine, this.$el, this.__inputName, this.__value)
|
||||
})
|
||||
|
||||
// Set initial combobox values in the input and properly clear it when the value is reset programmatically...
|
||||
Alpine.effect(() => ! this.__isMultiple && this.__resetInput())
|
||||
})
|
||||
},
|
||||
__startTyping() {
|
||||
this.__isTyping = true
|
||||
},
|
||||
__stopTyping() {
|
||||
this.__isTyping = false
|
||||
},
|
||||
__resetInput() {
|
||||
let input = this.$refs.__input
|
||||
|
||||
if (! input) return
|
||||
|
||||
let value = this.__getCurrentValue()
|
||||
|
||||
input.value = value
|
||||
},
|
||||
__getCurrentValue() {
|
||||
if (! this.$refs.__input) return ''
|
||||
if (! this.__value) return ''
|
||||
if (this.__displayValue) return this.__displayValue(this.__value)
|
||||
if (typeof this.__value === 'string') return this.__value
|
||||
return ''
|
||||
},
|
||||
__open() {
|
||||
if (this.__isOpen) return
|
||||
this.__isOpen = true
|
||||
|
||||
let input = this.$refs.__input
|
||||
|
||||
// Make sure we always notify the parent component
|
||||
// that the starting value is the empty string
|
||||
// when we open the combobox (ignoring any existing value)
|
||||
// to avoid inconsistent displaying.
|
||||
// Setting the input to empty and back to the real value
|
||||
// also helps VoiceOver to annunce the content properly
|
||||
// See https://github.com/tailwindlabs/headlessui/pull/2153
|
||||
if (input) {
|
||||
let value = input.value
|
||||
let { selectionStart, selectionEnd, selectionDirection } = input
|
||||
input.value = ''
|
||||
input.dispatchEvent(new Event('change'))
|
||||
input.value = value
|
||||
if (selectionDirection !== null) {
|
||||
input.setSelectionRange(selectionStart, selectionEnd, selectionDirection)
|
||||
} else {
|
||||
input.setSelectionRange(selectionStart, selectionEnd)
|
||||
}
|
||||
}
|
||||
|
||||
// Safari needs more of a "tick" for focusing after x-show for some reason.
|
||||
// Probably because Alpine adds an extra tick when x-showing for @click.outside
|
||||
let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||
|
||||
nextTick(() => {
|
||||
this.$refs.__input.focus({ preventScroll: true })
|
||||
this.__activateSelectedOrFirst()
|
||||
})
|
||||
},
|
||||
__close() {
|
||||
this.__isOpen = false
|
||||
|
||||
this.__context.deactivate()
|
||||
},
|
||||
__activateSelectedOrFirst(activateSelected = true) {
|
||||
if (! this.__isOpen) return
|
||||
|
||||
if (this.__context.hasActive() && this.__context.wasActivatedByKeyPress()) return
|
||||
|
||||
let firstSelectedValue
|
||||
|
||||
if (this.__isMultiple) {
|
||||
let selectedItem = this.__context.getItemsByValues(this.__value)
|
||||
|
||||
firstSelectedValue = selectedItem.length ? selectedItem[0].value : null
|
||||
} else {
|
||||
firstSelectedValue = this.__value
|
||||
}
|
||||
|
||||
let firstSelected = null
|
||||
if (activateSelected && firstSelectedValue) {
|
||||
firstSelected = this.__context.getItemByValue(firstSelectedValue)
|
||||
}
|
||||
|
||||
if (firstSelected) {
|
||||
this.__context.activateAndScrollToKey(firstSelected.key)
|
||||
return
|
||||
}
|
||||
|
||||
this.__context.activateAndScrollToKey(this.__context.firstKey())
|
||||
},
|
||||
__selectActive() {
|
||||
let active = this.__context.getActiveItem()
|
||||
if (active) this.__toggleSelected(active.value)
|
||||
},
|
||||
__selectOption(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (item) this.__toggleSelected(item.value)
|
||||
},
|
||||
__isSelected(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (! item) return false
|
||||
if (item.value === null || item.value === undefined) return false
|
||||
|
||||
return this.__hasSelected(item.value)
|
||||
},
|
||||
__toggleSelected(value) {
|
||||
if (! this.__isMultiple) {
|
||||
this.__value = value
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let index = this.__value.findIndex(j => this.__compare(j, value))
|
||||
|
||||
if (index === -1) {
|
||||
this.__value.push(value)
|
||||
} else {
|
||||
this.__value.splice(index, 1)
|
||||
}
|
||||
},
|
||||
__hasSelected(value) {
|
||||
if (! this.__isMultiple) return this.__compare(this.__value, value)
|
||||
|
||||
return this.__value.some(i => this.__compare(i, value))
|
||||
},
|
||||
__compare(a, b) {
|
||||
let by = this.__compareBy
|
||||
|
||||
if (! by) by = (a, b) => Alpine.raw(a) === Alpine.raw(b)
|
||||
|
||||
if (typeof by === 'string') {
|
||||
let property = by
|
||||
by = (a, b) => {
|
||||
// Handle null values
|
||||
if ((! a || typeof a !== 'object') || (! b || typeof b !== 'object')) {
|
||||
return Alpine.raw(a) === Alpine.raw(b)
|
||||
}
|
||||
|
||||
|
||||
return a[property] === b[property];
|
||||
}
|
||||
}
|
||||
|
||||
return by(a, b)
|
||||
},
|
||||
}
|
||||
},
|
||||
// Register event listeners..
|
||||
'@mousedown.window'(e) {
|
||||
if (
|
||||
!! ! this.$refs.__input.contains(e.target)
|
||||
&& ! this.$refs.__button.contains(e.target)
|
||||
&& ! this.$refs.__options.contains(e.target)
|
||||
) {
|
||||
this.__close()
|
||||
this.__resetInput()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleInput(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__input',
|
||||
':id'() { return this.$id('alpine-combobox-input') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'combobox',
|
||||
'tabindex': '0',
|
||||
'aria-autocomplete': 'list',
|
||||
|
||||
// We need to defer this evaluation a bit because $refs that get declared later
|
||||
// in the DOM aren't available yet when x-ref is the result of an Alpine.bind object.
|
||||
async ':aria-controls'() { return await microtask(() => this.$refs.__options && this.$refs.__options.id) },
|
||||
':aria-expanded'() { return this.$data.__isDisabled ? undefined : this.$data.__isOpen },
|
||||
':aria-multiselectable'() { return this.$data.__isMultiple ? true : undefined },
|
||||
':aria-activedescendant'() {
|
||||
if (! this.$data.__context.hasActive()) return
|
||||
|
||||
let active = this.$data.__context.getActiveItem()
|
||||
|
||||
return active ? active.el.id : null
|
||||
},
|
||||
':aria-labelledby'() { return this.$refs.__label ? this.$refs.__label.id : (this.$refs.__button ? this.$refs.__button.id : null) },
|
||||
|
||||
// Initialize...
|
||||
'x-init'() {
|
||||
let displayValueFn = Alpine.extractProp(this.$el, 'display-value')
|
||||
if (displayValueFn) this.$data.__displayValue = displayValueFn
|
||||
},
|
||||
|
||||
// Register listeners...
|
||||
'@input.stop'(e) {
|
||||
if(this.$data.__isTyping) {
|
||||
this.$data.__open();
|
||||
this.$dispatch('change')
|
||||
}
|
||||
},
|
||||
'@blur'() { this.$data.__stopTyping(false) },
|
||||
'@keydown'(e) {
|
||||
queueMicrotask(() => this.$data.__context.activateByKeyEvent(e, false, () => this.$data.__isOpen, () => this.$data.__open(), (state) => this.$data.__isTyping = state))
|
||||
},
|
||||
'@keydown.enter.prevent.stop'() {
|
||||
this.$data.__selectActive()
|
||||
|
||||
this.$data.__stopTyping()
|
||||
|
||||
if (! this.$data.__isMultiple) {
|
||||
this.$data.__close()
|
||||
this.$data.__resetInput()
|
||||
}
|
||||
},
|
||||
'@keydown.escape.prevent'(e) {
|
||||
if (! this.$data.__static) e.stopPropagation()
|
||||
|
||||
this.$data.__stopTyping()
|
||||
this.$data.__close()
|
||||
this.$data.__resetInput()
|
||||
|
||||
},
|
||||
'@keydown.tab'() {
|
||||
this.$data.__stopTyping()
|
||||
if (this.$data.__isOpen) { this.$data.__close() }
|
||||
this.$data.__resetInput()
|
||||
},
|
||||
'@keydown.backspace'(e) {
|
||||
if (this.$data.__isMultiple) return
|
||||
if (! this.$data.__nullable) return
|
||||
|
||||
let input = e.target
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (input.value === '') {
|
||||
this.$data.__value = null
|
||||
|
||||
let options = this.$refs.__options
|
||||
if (options) {
|
||||
options.scrollTop = 0
|
||||
}
|
||||
|
||||
this.$data.__context.deactivate()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__button',
|
||||
':id'() { return this.$id('alpine-combobox-button') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'aria-haspopup': 'true',
|
||||
// We need to defer this evaluation a bit because $refs that get declared later
|
||||
// in the DOM aren't available yet when x-ref is the result of an Alpine.bind object.
|
||||
async ':aria-controls'() { return await microtask(() => this.$refs.__options && this.$refs.__options.id) },
|
||||
':aria-labelledby'() { return this.$refs.__label ? [this.$refs.__label.id, this.$el.id].join(' ') : null },
|
||||
':aria-expanded'() { return this.$data.__isDisabled ? null : this.$data.__isOpen },
|
||||
':disabled'() { return this.$data.__isDisabled },
|
||||
'tabindex': '-1',
|
||||
|
||||
// Initialize....
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && ! this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
|
||||
// Register listeners...
|
||||
'@click'(e) {
|
||||
if (this.$data.__isDisabled) return
|
||||
if (this.$data.__isOpen) {
|
||||
this.$data.__close()
|
||||
this.$data.__resetInput()
|
||||
} else {
|
||||
e.preventDefault()
|
||||
this.$data.__open()
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__label',
|
||||
':id'() { return this.$id('alpine-combobox-label') },
|
||||
'@click'() { this.$refs.__input.focus({ preventScroll: true }) },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOptions(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__options',
|
||||
':id'() { return this.$id('alpine-combobox-options') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'listbox',
|
||||
':aria-labelledby'() { return this.$refs.__label ? this.$refs.__label.id : (this.$refs.__button ? this.$refs.__button.id : null) },
|
||||
|
||||
// Initialize...
|
||||
'x-init'() {
|
||||
this.$data.__isStatic = Alpine.bound(this.$el, 'static', false)
|
||||
|
||||
if (Alpine.bound(this.$el, 'hold')) {
|
||||
this.$data.__hold = true;
|
||||
}
|
||||
},
|
||||
|
||||
'x-show'() { return this.$data.__isStatic ? true : this.$data.__isOpen },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOption(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-id'() { return ['alpine-combobox-option'] },
|
||||
':id'() { return this.$id('alpine-combobox-option') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'option',
|
||||
':tabindex'() { return this.$comboboxOption.isDisabled ? undefined : '-1' },
|
||||
|
||||
// Only the active element should have aria-selected="true"...
|
||||
'x-effect'() {
|
||||
this.$comboboxOption.isSelected
|
||||
? el.setAttribute('aria-selected', true)
|
||||
: el.setAttribute('aria-selected', false)
|
||||
},
|
||||
|
||||
':aria-disabled'() { return this.$comboboxOption.isDisabled },
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
'__optionKey': null,
|
||||
|
||||
init() {
|
||||
this.__optionKey = (Math.random() + 1).toString(36).substring(7)
|
||||
|
||||
let value = Alpine.extractProp(this.$el, 'value')
|
||||
let disabled = Alpine.extractProp(this.$el, 'disabled', false, false)
|
||||
|
||||
// memoize the context as it's not going to change
|
||||
// and calling this.$data on mouse action is expensive
|
||||
this.__context.registerItem(this.__optionKey, this.$el, value, disabled)
|
||||
},
|
||||
destroy() {
|
||||
this.__context.unregisterItem(this.__optionKey)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Register listeners...
|
||||
'@click'() {
|
||||
if (this.$comboboxOption.isDisabled) return;
|
||||
|
||||
this.__selectOption(this.$el)
|
||||
|
||||
if (! this.__isMultiple) {
|
||||
this.__close()
|
||||
this.__resetInput()
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
|
||||
},
|
||||
'@mouseenter'(e) {
|
||||
this.__context.activateEl(this.$el)
|
||||
},
|
||||
'@mousemove'(e) {
|
||||
if (this.__context.isActiveEl(this.$el)) return
|
||||
|
||||
this.__context.activateEl(this.$el)
|
||||
},
|
||||
'@mouseleave'(e) {
|
||||
if (this.__hold) return
|
||||
|
||||
this.__context.deactivate()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Little utility to defer a callback into the microtask queue...
|
||||
function microtask(callback) {
|
||||
return new Promise(resolve => queueMicrotask(() => resolve(callback())))
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('dialog', (el, directive) => {
|
||||
if (directive.value === 'overlay') handleOverlay(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
else if (directive.value === 'title') handleTitle(el, Alpine)
|
||||
else if (directive.value === 'description') handleDescription(el, Alpine)
|
||||
else handleRoot(el, Alpine)
|
||||
})
|
||||
|
||||
Alpine.magic('dialog', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
// Kept here for legacy. Remove after out of beta.
|
||||
get open() {
|
||||
return $data.__isOpen
|
||||
},
|
||||
get isOpen() {
|
||||
return $data.__isOpen
|
||||
},
|
||||
close() {
|
||||
$data.__close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
// If the user chose to use :open and @close instead of x-model.
|
||||
(Alpine.bound(el, 'open') !== undefined) && Alpine.effect(() => {
|
||||
this.__isOpenState = Alpine.bound(el, 'open')
|
||||
})
|
||||
|
||||
if (Alpine.bound(el, 'initial-focus') !== undefined) this.$watch('__isOpenState', () => {
|
||||
if (! this.__isOpenState) return
|
||||
|
||||
setTimeout(() => {
|
||||
Alpine.bound(el, 'initial-focus').focus()
|
||||
}, 0);
|
||||
})
|
||||
},
|
||||
__isOpenState: false,
|
||||
__close() {
|
||||
if (Alpine.bound(el, 'open')) this.$dispatch('close')
|
||||
else this.__isOpenState = false
|
||||
},
|
||||
get __isOpen() {
|
||||
return Alpine.bound(el, 'static', this.__isOpenState)
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-modelable': '__isOpenState',
|
||||
'x-id'() { return ['alpine-dialog-title', 'alpine-dialog-description'] },
|
||||
'x-show'() { return this.__isOpen },
|
||||
'x-trap.inert.noscroll'() { return this.__isOpen },
|
||||
'@keydown.escape'() { this.__close() },
|
||||
':aria-labelledby'() { return this.$id('alpine-dialog-title') },
|
||||
':aria-describedby'() { return this.$id('alpine-dialog-description') },
|
||||
'role': 'dialog',
|
||||
'aria-modal': 'true',
|
||||
})
|
||||
}
|
||||
|
||||
function handleOverlay(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { if (this.$data.__isOpen === undefined) console.warn('"x-dialog:overlay" is missing a parent element with "x-dialog".') },
|
||||
'x-show'() { return this.__isOpen },
|
||||
'@click.prevent.stop'() { this.$data.__close() },
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'@click.outside'() { this.$data.__close() },
|
||||
'x-show'() { return this.$data.__isOpen },
|
||||
})
|
||||
}
|
||||
|
||||
function handleTitle(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { if (this.$data.__isOpen === undefined) console.warn('"x-dialog:title" is missing a parent element with "x-dialog".') },
|
||||
':id'() { return this.$id('alpine-dialog-title') },
|
||||
})
|
||||
}
|
||||
|
||||
function handleDescription(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
':id'() { return this.$id('alpine-dialog-description') },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('disclosure', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('disclosure', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isOpen() {
|
||||
return $data.__isOpen
|
||||
},
|
||||
close() {
|
||||
$data.__close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__isOpen',
|
||||
'x-data'() {
|
||||
return {
|
||||
// The panel will call this...
|
||||
// We can't do this inside a microtask in x-init because, when default-open is set to "true",
|
||||
// It will cause the panel to transition in for the first time, instead of showing instantly...
|
||||
__determineDefaultOpenState() {
|
||||
let defaultIsOpen = Boolean(Alpine.bound(this.$el, 'default-open', false))
|
||||
|
||||
if (defaultIsOpen) this.__isOpen = defaultIsOpen
|
||||
},
|
||||
__isOpen: false,
|
||||
__close() {
|
||||
this.__isOpen = false
|
||||
},
|
||||
__toggle() {
|
||||
this.__isOpen = ! this.__isOpen
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-id'() { return ['alpine-disclosure-panel'] },
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() {
|
||||
if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
|
||||
},
|
||||
'@click'() {
|
||||
this.$data.__isOpen = ! this.$data.__isOpen
|
||||
},
|
||||
':aria-expanded'() {
|
||||
return this.$data.__isOpen
|
||||
},
|
||||
':aria-controls'() {
|
||||
return this.$data.$id('alpine-disclosure-panel')
|
||||
},
|
||||
'@keydown.space.prevent.stop'() { this.$data.__toggle() },
|
||||
'@keydown.enter.prevent.stop'() { this.$data.__toggle() },
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
// the Space key doesn't cancel the handleKeyUp, which in turn
|
||||
// triggers a *click*.
|
||||
'@keyup.space.prevent'() {},
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() {
|
||||
this.$data.__determineDefaultOpenState()
|
||||
},
|
||||
'x-show'() {
|
||||
return this.$data.__isOpen
|
||||
},
|
||||
':id'() {
|
||||
return this.$data.$id('alpine-disclosure-panel')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import combobox from './combobox'
|
||||
import dialog from './dialog'
|
||||
import disclosure from './disclosure'
|
||||
import listbox from './listbox'
|
||||
import popover from './popover'
|
||||
import menu from './menu'
|
||||
import notSwitch from './switch'
|
||||
import radio from './radio'
|
||||
import tabs from './tabs'
|
||||
|
||||
export default function (Alpine) {
|
||||
combobox(Alpine)
|
||||
dialog(Alpine)
|
||||
disclosure(Alpine)
|
||||
listbox(Alpine)
|
||||
menu(Alpine)
|
||||
notSwitch(Alpine)
|
||||
popover(Alpine)
|
||||
radio(Alpine)
|
||||
tabs(Alpine)
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
|
||||
export function generateContext(Alpine, multiple, orientation, activateSelectedOrFirst) {
|
||||
return {
|
||||
/**
|
||||
* Main state...
|
||||
*/
|
||||
items: [],
|
||||
activeKey: switchboard(),
|
||||
orderedKeys: [],
|
||||
activatedByKeyPress: false,
|
||||
|
||||
/**
|
||||
* Initialization...
|
||||
*/
|
||||
activateSelectedOrFirst: Alpine.debounce(function () {
|
||||
activateSelectedOrFirst(false)
|
||||
}),
|
||||
|
||||
registerItemsQueue: [],
|
||||
|
||||
registerItem(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.registerItemsQueue = []
|
||||
|
||||
this.reorderKeys()
|
||||
this.activateSelectedOrFirst()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let item = {
|
||||
key, el, value, disabled
|
||||
}
|
||||
|
||||
this.registerItemsQueue.push(item)
|
||||
},
|
||||
|
||||
unregisterKeysQueue: [],
|
||||
|
||||
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.unregisterKeysQueue.push(key)
|
||||
},
|
||||
|
||||
getItemByKey(key) {
|
||||
return this.items.find(i => i.key === key)
|
||||
},
|
||||
|
||||
getItemByValue(value) {
|
||||
return this.items.find(i => Alpine.raw(i.value) === Alpine.raw(value))
|
||||
},
|
||||
|
||||
getItemByEl(el) {
|
||||
return this.items.find(i => i.el === el)
|
||||
},
|
||||
|
||||
getItemsByValues(values) {
|
||||
let rawValues = values.map(i => Alpine.raw(i));
|
||||
let filteredValue = this.items.filter(i => rawValues.includes(Alpine.raw(i.value)))
|
||||
filteredValue = filteredValue.slice().sort((a, b) => {
|
||||
let position = a.el.compareDocumentPosition(b.el)
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
||||
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
||||
return 0
|
||||
})
|
||||
return filteredValue
|
||||
},
|
||||
|
||||
getActiveItem() {
|
||||
if (! this.hasActive()) return null
|
||||
|
||||
let item = this.items.find(i => i.key === this.activeKey.get())
|
||||
|
||||
if (! item) this.deactivateKey(this.activeKey.get())
|
||||
|
||||
return item
|
||||
},
|
||||
|
||||
activateItem(item) {
|
||||
if (! item) return
|
||||
|
||||
this.activateKey(item.key)
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle elements...
|
||||
*/
|
||||
reorderKeys: Alpine.debounce(function () {
|
||||
this.orderedKeys = this.items.map(i => i.key)
|
||||
|
||||
this.orderedKeys = this.orderedKeys.slice().sort((a, z) => {
|
||||
if (a === null || z === null) return 0
|
||||
|
||||
let aEl = this.items.find(i => i.key === a).el
|
||||
let zEl = this.items.find(i => i.key === z).el
|
||||
|
||||
let position = aEl.compareDocumentPosition(zEl)
|
||||
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
||||
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
// If there no longer is the active key in the items list, then
|
||||
// deactivate it...
|
||||
if (! this.orderedKeys.includes(this.activeKey.get())) this.deactivateKey(this.activeKey.get())
|
||||
}),
|
||||
|
||||
getActiveKey() {
|
||||
return this.activeKey.get()
|
||||
},
|
||||
|
||||
activeEl() {
|
||||
if (! this.activeKey.get()) return
|
||||
|
||||
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.is(key)
|
||||
},
|
||||
|
||||
activateEl(el) {
|
||||
let item = this.items.find(i => i.el === el)
|
||||
|
||||
this.activateKey(item.key)
|
||||
},
|
||||
|
||||
isDisabledEl(el) {
|
||||
return this.items.find(i => i.el === el).disabled
|
||||
},
|
||||
|
||||
get isScrollingTo() { return this.scrollingCount > 0 },
|
||||
|
||||
scrollingCount: 0,
|
||||
|
||||
activateAndScrollToKey(key, activatedByKeyPress) {
|
||||
if (! this.getItemByKey(key)) return
|
||||
|
||||
// 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, activatedByKeyPress)
|
||||
|
||||
let targetEl = this.items.find(i => i.key === key).el
|
||||
|
||||
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 disabled keys...
|
||||
*/
|
||||
isDisabled(key) {
|
||||
let item = this.items.find(i => i.key === key)
|
||||
|
||||
if (! item) return false
|
||||
|
||||
return item.disabled
|
||||
},
|
||||
|
||||
get nonDisabledOrderedKeys() {
|
||||
return this.orderedKeys.filter(i => ! this.isDisabled(i))
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle activated keys...
|
||||
*/
|
||||
hasActive() { return !! this.activeKey.get() },
|
||||
|
||||
/**
|
||||
* Return true if the latest active element was activated
|
||||
* by the user (i.e. using the arrow keys) and false if was
|
||||
* activated automatically by alpine (i.e. first element automatically
|
||||
* activated after filtering the list)
|
||||
*/
|
||||
wasActivatedByKeyPress() {return this.activatedByKeyPress},
|
||||
|
||||
isActiveKey(key) { return this.activeKey.is(key) },
|
||||
|
||||
activateKey(key, activatedByKeyPress = false) {
|
||||
if (this.isDisabled(key)) return
|
||||
|
||||
this.activeKey.set(key)
|
||||
this.activatedByKeyPress = activatedByKeyPress
|
||||
},
|
||||
|
||||
deactivateKey(key) {
|
||||
if (this.activeKey.get() === key) {
|
||||
this.activeKey.set(null)
|
||||
this.activatedByKeyPress = false
|
||||
}
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
if (! this.activeKey.get()) return
|
||||
if (this.isScrollingTo) return
|
||||
|
||||
this.activeKey.set(null)
|
||||
this.activatedByKeyPress = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle active key traversal...
|
||||
*/
|
||||
nextKey() {
|
||||
if (! this.activeKey.get()) return
|
||||
|
||||
let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey.get())
|
||||
|
||||
return this.nonDisabledOrderedKeys[index + 1]
|
||||
},
|
||||
|
||||
prevKey() {
|
||||
if (! this.activeKey.get()) return
|
||||
|
||||
let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey.get())
|
||||
|
||||
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.items) {
|
||||
let content = this.items[key].el.textContent.trim().toLowerCase()
|
||||
|
||||
if (content.startsWith(this.searchQuery)) {
|
||||
foundKey = this.items[key].key
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! this.nonDisabledOrderedKeys.includes(foundKey)) return
|
||||
|
||||
return foundKey
|
||||
},
|
||||
|
||||
activateByKeyEvent(e, searchable = false, isOpen = () => false, open = () => {}, setIsTyping) {
|
||||
let targetKey, hasActive
|
||||
|
||||
setIsTyping(true)
|
||||
|
||||
let activatedByKeyPress = true
|
||||
|
||||
switch (e.key) {
|
||||
// case 'Backspace':
|
||||
// case 'Delete':
|
||||
case ['ArrowDown', 'ArrowRight'][orientation === 'vertical' ? 0 : 1]:
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
|
||||
setIsTyping(false)
|
||||
|
||||
if (! isOpen()) {
|
||||
open()
|
||||
break;
|
||||
}
|
||||
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
|
||||
targetKey = hasActive ? this.nextKey() : this.firstKey()
|
||||
break;
|
||||
|
||||
case ['ArrowUp', 'ArrowLeft'][orientation === 'vertical' ? 0 : 1]:
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
|
||||
setIsTyping(false)
|
||||
|
||||
if (! isOpen()) {
|
||||
open()
|
||||
break;
|
||||
}
|
||||
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
|
||||
targetKey = hasActive ? this.prevKey() : this.lastKey()
|
||||
break;
|
||||
case 'Home':
|
||||
case 'PageUp':
|
||||
if (e.key == 'Home' && e.shiftKey) return;
|
||||
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
setIsTyping(false)
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
targetKey = this.firstKey()
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
case 'PageDown':
|
||||
if (e.key == 'End' && e.shiftKey) return;
|
||||
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
setIsTyping(false)
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
targetKey = this.lastKey()
|
||||
break;
|
||||
|
||||
default:
|
||||
activatedByKeyPress = this.activatedByKeyPress
|
||||
if (searchable && e.key.length === 1) {
|
||||
targetKey = this.searchKey(e.key)
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetKey) {
|
||||
this.activateAndScrollToKey(targetKey, activatedByKeyPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function keyByValue(object, value) {
|
||||
return Object.keys(object).find(key => object[key] === value)
|
||||
}
|
||||
|
||||
export function renderHiddenInputs(Alpine, 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
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
388
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/listbox.js
Normal file
388
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/listbox.js
Normal file
@@ -0,0 +1,388 @@
|
||||
import { generateContext, renderHiddenInputs } from './list-context'
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('listbox', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
else if (directive.value === 'options') handleOptions(el, Alpine)
|
||||
else if (directive.value === 'option') handleOption(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('listbox', (el) => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
// @deprecated:
|
||||
get selected() {
|
||||
return data.__value
|
||||
},
|
||||
// @deprecated:
|
||||
get active() {
|
||||
let active = data.__context.getActiveItem()
|
||||
|
||||
return active && active.value
|
||||
},
|
||||
get value() {
|
||||
return data.__value
|
||||
},
|
||||
get isOpen() {
|
||||
return data.__isOpen
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__isDisabled
|
||||
},
|
||||
get activeOption() {
|
||||
let active = data.__context.getActiveItem()
|
||||
|
||||
return active && active.value
|
||||
},
|
||||
get activeIndex() {
|
||||
let active = data.__context.getActiveItem()
|
||||
|
||||
return active && active.key
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Alpine.magic('listboxOption', (el) => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
// It's not great depending on the existance of the attribute in the DOM
|
||||
// but it's probably the fastest and most reliable at this point...
|
||||
let optionEl = Alpine.findClosest(el, i => {
|
||||
return i.hasAttribute('x-listbox:option')
|
||||
})
|
||||
|
||||
if (! optionEl) throw 'No x-listbox:option directive found...'
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return data.__context.isActiveKey(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
get isSelected() {
|
||||
return data.__isSelected(optionEl)
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__context.isDisabled(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-id'() { return ['alpine-listbox-button', 'alpine-listbox-options', 'alpine-listbox-label'] },
|
||||
'x-modelable': '__value',
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
/**
|
||||
* Listbox state...
|
||||
*/
|
||||
__ready: false,
|
||||
__value: null,
|
||||
__isOpen: false,
|
||||
__context: undefined,
|
||||
__isMultiple: undefined,
|
||||
__isStatic: false,
|
||||
__isDisabled: undefined,
|
||||
__compareBy: null,
|
||||
__inputName: null,
|
||||
__orientation: 'vertical',
|
||||
__hold: false,
|
||||
|
||||
/**
|
||||
* Listbox initialization...
|
||||
*/
|
||||
init() {
|
||||
this.__isMultiple = Alpine.extractProp(el, 'multiple', false)
|
||||
this.__isDisabled = Alpine.extractProp(el, 'disabled', false)
|
||||
this.__inputName = Alpine.extractProp(el, 'name', null)
|
||||
this.__compareBy = Alpine.extractProp(el, 'by')
|
||||
this.__orientation = Alpine.extractProp(el, 'horizontal', false) ? 'horizontal' : 'vertical'
|
||||
|
||||
this.__context = generateContext(Alpine, this.__isMultiple, this.__orientation, () => this.__activateSelectedOrFirst())
|
||||
|
||||
let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
|
||||
|
||||
this.__value = defaultValue
|
||||
|
||||
// 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(() => {
|
||||
Alpine.effect(() => {
|
||||
// Everytime the value changes, we need to re-render the hidden inputs,
|
||||
// if a user passed the "name" prop...
|
||||
this.__inputName && renderHiddenInputs(Alpine, this.$el, this.__inputName, this.__value)
|
||||
})
|
||||
|
||||
// Keep the currently selected value in sync with the input value...
|
||||
Alpine.effect(() => {
|
||||
this.__resetInput()
|
||||
})
|
||||
})
|
||||
},
|
||||
__resetInput() {
|
||||
let input = this.$refs.__input
|
||||
if (! input) return
|
||||
|
||||
let value = this.$data.__getCurrentValue()
|
||||
|
||||
input.value = value
|
||||
},
|
||||
__getCurrentValue() {
|
||||
if (! this.$refs.__input) return ''
|
||||
if (! this.__value) return ''
|
||||
if (this.$data.__displayValue && this.__value !== undefined) return this.$data.__displayValue(this.__value)
|
||||
if (typeof this.__value === 'string') return this.__value
|
||||
return ''
|
||||
},
|
||||
__open() {
|
||||
if (this.__isOpen) return
|
||||
this.__isOpen = true
|
||||
|
||||
this.__activateSelectedOrFirst()
|
||||
|
||||
// Safari needs more of a "tick" for focusing after x-show for some reason.
|
||||
// Probably because Alpine adds an extra tick when x-showing for @click.outside
|
||||
let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||
|
||||
nextTick(() => this.$refs.__options.focus({ preventScroll: true }))
|
||||
},
|
||||
__close() {
|
||||
this.__isOpen = false
|
||||
|
||||
this.__context.deactivate()
|
||||
|
||||
this.$nextTick(() => this.$refs.__button.focus({ preventScroll: true }))
|
||||
},
|
||||
__activateSelectedOrFirst(activateSelected = true) {
|
||||
if (! this.__isOpen) return
|
||||
|
||||
if (this.__context.getActiveKey()) {
|
||||
this.__context.activateAndScrollToKey(this.__context.getActiveKey())
|
||||
return
|
||||
}
|
||||
|
||||
let firstSelectedValue
|
||||
|
||||
if (this.__isMultiple) {
|
||||
firstSelectedValue = this.__value.find(i => {
|
||||
return !! this.__context.getItemByValue(i)
|
||||
})
|
||||
} else {
|
||||
firstSelectedValue = this.__value
|
||||
}
|
||||
|
||||
if (activateSelected && firstSelectedValue) {
|
||||
let firstSelected = this.__context.getItemByValue(firstSelectedValue)
|
||||
|
||||
firstSelected && this.__context.activateAndScrollToKey(firstSelected.key)
|
||||
} else {
|
||||
this.__context.activateAndScrollToKey(this.__context.firstKey())
|
||||
}
|
||||
},
|
||||
__selectActive() {
|
||||
let active = this.$data.__context.getActiveItem()
|
||||
if (active) this.__toggleSelected(active.value)
|
||||
},
|
||||
__selectOption(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (item) this.__toggleSelected(item.value)
|
||||
},
|
||||
__isSelected(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (! item) return false
|
||||
if (item.value === null || item.value === undefined) return false
|
||||
|
||||
return this.__hasSelected(item.value)
|
||||
},
|
||||
__toggleSelected(value) {
|
||||
if (! this.__isMultiple) {
|
||||
this.__value = value
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let index = this.__value.findIndex(j => this.__compare(j, value))
|
||||
|
||||
if (index === -1) {
|
||||
this.__value.push(value)
|
||||
} else {
|
||||
this.__value.splice(index, 1)
|
||||
}
|
||||
},
|
||||
__hasSelected(value) {
|
||||
if (! this.__isMultiple) return this.__compare(this.__value, value)
|
||||
|
||||
return this.__value.some(i => this.__compare(i, value))
|
||||
},
|
||||
__compare(a, b) {
|
||||
let by = this.__compareBy
|
||||
|
||||
if (! by) by = (a, b) => Alpine.raw(a) === Alpine.raw(b)
|
||||
|
||||
if (typeof by === 'string') {
|
||||
let property = by
|
||||
by = (a, b) => {
|
||||
// Handle null values
|
||||
if ((! a || typeof a !== 'object') || (! b || typeof b !== 'object')) {
|
||||
return Alpine.raw(a) === Alpine.raw(b)
|
||||
}
|
||||
|
||||
return a[property] === b[property];
|
||||
}
|
||||
}
|
||||
|
||||
return by(a, b)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__label',
|
||||
':id'() { return this.$id('alpine-listbox-label') },
|
||||
'@click'() { this.$refs.__button.focus({ preventScroll: true }) },
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__button',
|
||||
':id'() { return this.$id('alpine-listbox-button') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'aria-haspopup': 'true',
|
||||
':aria-labelledby'() { return this.$id('alpine-listbox-label') },
|
||||
':aria-expanded'() { return this.$data.__isOpen },
|
||||
':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-listbox-options') },
|
||||
|
||||
// Initialize....
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
|
||||
// Register listeners...
|
||||
'@click'() { this.$data.__open() },
|
||||
'@keydown'(e) {
|
||||
if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
this.$data.__open()
|
||||
}
|
||||
},
|
||||
'@keydown.space.stop.prevent'() { this.$data.__open() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__open() },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOptions(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__options',
|
||||
':id'() { return this.$id('alpine-listbox-options') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'listbox',
|
||||
tabindex: '0',
|
||||
':aria-orientation'() {
|
||||
return this.$data.__orientation
|
||||
},
|
||||
':aria-labelledby'() { return this.$id('alpine-listbox-button') },
|
||||
':aria-activedescendant'() {
|
||||
if (! this.$data.__context.hasActive()) return
|
||||
|
||||
let active = this.$data.__context.getActiveItem()
|
||||
|
||||
return active ? active.el.id : null
|
||||
},
|
||||
|
||||
// Initialize...
|
||||
'x-init'() {
|
||||
this.$data.__isStatic = Alpine.extractProp(this.$el, 'static', false)
|
||||
|
||||
if (Alpine.bound(this.$el, 'hold')) {
|
||||
this.$data.__hold = true;
|
||||
}
|
||||
},
|
||||
|
||||
'x-show'() { return this.$data.__isStatic ? true : this.$data.__isOpen },
|
||||
'x-trap'() { return this.$data.__isOpen },
|
||||
'@click.outside'() { this.$data.__close() },
|
||||
'@keydown.escape.stop.prevent'() { this.$data.__close() },
|
||||
'@focus'() { this.$data.__activateSelectedOrFirst() },
|
||||
'@keydown'(e) {
|
||||
queueMicrotask(() => this.$data.__context.activateByKeyEvent(e, true, () => this.$data.__isOpen, () => this.$data.__open(), () => {}))
|
||||
},
|
||||
'@keydown.enter.stop.prevent'() {
|
||||
this.$data.__selectActive();
|
||||
|
||||
this.$data.__isMultiple || this.$data.__close()
|
||||
},
|
||||
'@keydown.space.stop.prevent'() {
|
||||
this.$data.__selectActive();
|
||||
|
||||
this.$data.__isMultiple || this.$data.__close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOption(el, Alpine) {
|
||||
Alpine.bind(el, () => {
|
||||
return {
|
||||
'x-id'() { return ['alpine-listbox-option'] },
|
||||
':id'() { return this.$id('alpine-listbox-option') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'option',
|
||||
':tabindex'() { return this.$listboxOption.isDisabled ? false : '-1' },
|
||||
':aria-selected'() { return this.$listboxOption.isSelected },
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
'__optionKey': null,
|
||||
|
||||
init() {
|
||||
this.__optionKey = (Math.random() + 1).toString(36).substring(7)
|
||||
|
||||
let value = Alpine.extractProp(el, 'value')
|
||||
let disabled = Alpine.extractProp(el, 'disabled', false, false)
|
||||
|
||||
this.$data.__context.registerItem(this.__optionKey, el, value, disabled)
|
||||
},
|
||||
destroy() {
|
||||
this.$data.__context.unregisterItem(this.__optionKey)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// Register listeners...
|
||||
'@click'() {
|
||||
if (this.$listboxOption.isDisabled) return;
|
||||
|
||||
this.$data.__selectOption(el)
|
||||
|
||||
this.$data.__isMultiple || this.$data.__close()
|
||||
},
|
||||
'@mouseenter'() { this.$data.__context.activateEl(el) },
|
||||
'@mouseleave'() {
|
||||
this.$data.__hold || this.$data.__context.deactivate()
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Little utility to defer a callback into the microtask queue...
|
||||
function microtask(callback) {
|
||||
return new Promise(resolve => queueMicrotask(() => resolve(callback())))
|
||||
}
|
||||
240
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/menu.js
Normal file
240
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/menu.js
Normal file
@@ -0,0 +1,240 @@
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('menu', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'items') handleItems(el, Alpine)
|
||||
else if (directive.value === 'item') handleItem(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('menuItem', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return $data.__activeEl == $data.__itemEl
|
||||
},
|
||||
get isDisabled() {
|
||||
return $data.__itemEl.__isDisabled.value
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-id'() { return ['alpine-menu-button', 'alpine-menu-items'] },
|
||||
'x-modelable': '__isOpen',
|
||||
'x-data'() {
|
||||
return {
|
||||
__itemEls: [],
|
||||
__activeEl: null,
|
||||
__isOpen: false,
|
||||
__open(activationStrategy) {
|
||||
this.__isOpen = true
|
||||
|
||||
// Safari needs more of a "tick" for focusing after x-show for some reason.
|
||||
// Probably because Alpine adds an extra tick when x-showing for @click.outside
|
||||
let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||
|
||||
nextTick(() => {
|
||||
this.$refs.__items.focus({ preventScroll: true })
|
||||
|
||||
// Activate the first item every time the menu is open...
|
||||
activationStrategy && activationStrategy(Alpine, this.$refs.__items, el => el.__activate())
|
||||
})
|
||||
},
|
||||
__close(focusAfter = true) {
|
||||
this.__isOpen = false
|
||||
|
||||
focusAfter && this.$nextTick(() => this.$refs.__button.focus({ preventScroll: true }))
|
||||
},
|
||||
__contains(outer, inner) {
|
||||
return !! Alpine.findClosest(inner, el => el.isSameNode(outer))
|
||||
}
|
||||
}
|
||||
},
|
||||
'@focusin.window'() {
|
||||
if (! this.$data.__contains(this.$el, document.activeElement)) {
|
||||
this.$data.__close(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__button',
|
||||
'aria-haspopup': 'true',
|
||||
':aria-labelledby'() { return this.$id('alpine-menu-label') },
|
||||
':id'() { return this.$id('alpine-menu-button') },
|
||||
':aria-expanded'() { return this.$data.__isOpen },
|
||||
':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-menu-items') },
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && ! this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
'@click'() { this.$data.__open() },
|
||||
'@keydown.down.stop.prevent'() { this.$data.__open() },
|
||||
'@keydown.up.stop.prevent'() { this.$data.__open(dom.last) },
|
||||
'@keydown.space.stop.prevent'() { this.$data.__open() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__open() },
|
||||
})
|
||||
}
|
||||
|
||||
// When patching children:
|
||||
// The child isn't initialized until it is reached. This is normally fine
|
||||
// except when something like this happens where an "id" is added during the initializing phase
|
||||
// because the "to" element hasn't initialized yet, it doesn't have the ID, so there is a "key" mismatch
|
||||
|
||||
|
||||
function handleItems(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__items',
|
||||
'aria-orientation': 'vertical',
|
||||
'role': 'menu',
|
||||
':id'() { return this.$id('alpine-menu-items') },
|
||||
':aria-labelledby'() { return this.$id('alpine-menu-button') },
|
||||
':aria-activedescendant'() { return this.$data.__activeEl && this.$data.__activeEl.id },
|
||||
'x-show'() { return this.$data.__isOpen },
|
||||
'tabindex': '0',
|
||||
'@click.outside'() { this.$data.__close() },
|
||||
'@keydown'(e) { dom.search(Alpine, this.$refs.__items, e.key, el => el.__activate()) },
|
||||
'@keydown.down.stop.prevent'() {
|
||||
if (this.$data.__activeEl) dom.next(Alpine, this.$data.__activeEl, el => el.__activate())
|
||||
else dom.first(Alpine, this.$refs.__items, el => el.__activate())
|
||||
},
|
||||
'@keydown.up.stop.prevent'() {
|
||||
if (this.$data.__activeEl) dom.previous(Alpine, this.$data.__activeEl, el => el.__activate())
|
||||
else dom.last(Alpine, this.$refs.__items, el => el.__activate())
|
||||
},
|
||||
'@keydown.home.stop.prevent'() { dom.first(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.end.stop.prevent'() { dom.last(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.page-up.stop.prevent'() { dom.first(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.page-down.stop.prevent'() { dom.last(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.escape.stop.prevent'() { this.$data.__close() },
|
||||
'@keydown.space.stop.prevent'() { this.$data.__activeEl && this.$data.__activeEl.click() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__activeEl && this.$data.__activeEl.click() },
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
// the Space key doesn't cancel the handleKeyUp, which in turn
|
||||
// triggers a *click*.
|
||||
'@keyup.space.prevent'() { },
|
||||
})
|
||||
}
|
||||
|
||||
function handleItem(el, Alpine) {
|
||||
Alpine.bind(el, () => {
|
||||
return {
|
||||
'x-data'() {
|
||||
return {
|
||||
__itemEl: this.$el,
|
||||
init() {
|
||||
// Add current element to element list for navigating.
|
||||
let els = Alpine.raw(this.$data.__itemEls)
|
||||
let inserted = false
|
||||
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i].compareDocumentPosition(this.$el) & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
els.splice(i, 0, this.$el)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (! inserted) els.push(this.$el)
|
||||
|
||||
this.$el.__activate = () => {
|
||||
this.$data.__activeEl = this.$el
|
||||
this.$el.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
|
||||
this.$el.__deactivate = () => {
|
||||
this.$data.__activeEl = null
|
||||
}
|
||||
|
||||
|
||||
this.$el.__isDisabled = Alpine.reactive({ value: false })
|
||||
|
||||
queueMicrotask(() => {
|
||||
this.$el.__isDisabled.value = Alpine.bound(this.$el, 'disabled', false)
|
||||
})
|
||||
},
|
||||
destroy() {
|
||||
// Remove this element from the elements list.
|
||||
let els = this.$data.__itemEls
|
||||
els.splice(els.indexOf(this.$el), 1)
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-id'() { return ['alpine-menu-item'] },
|
||||
':id'() { return this.$id('alpine-menu-item') },
|
||||
':tabindex'() { return this.__itemEl.__isDisabled.value ? false : '-1' },
|
||||
'role': 'menuitem',
|
||||
'@mousemove'() { this.__itemEl.__isDisabled.value || this.$menuItem.isActive || this.__itemEl.__activate() },
|
||||
'@mouseleave'() { this.__itemEl.__isDisabled.value || ! this.$menuItem.isActive || this.__itemEl.__deactivate() },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let dom = {
|
||||
first(Alpine, parent, receive = i => i, fallback = () => { }) {
|
||||
let first = Alpine.$data(parent).__itemEls[0]
|
||||
|
||||
if (! first) return fallback()
|
||||
|
||||
if (first.tagName.toLowerCase() === 'template') {
|
||||
return this.next(Alpine, first, receive)
|
||||
}
|
||||
|
||||
if (first.__isDisabled.value) return this.next(Alpine, first, receive)
|
||||
|
||||
return receive(first)
|
||||
},
|
||||
last(Alpine, parent, receive = i => i, fallback = () => { }) {
|
||||
let last = Alpine.$data(parent).__itemEls.slice(-1)[0]
|
||||
|
||||
if (! last) return fallback()
|
||||
if (last.__isDisabled.value) return this.previous(Alpine, last, receive)
|
||||
return receive(last)
|
||||
},
|
||||
next(Alpine, el, receive = i => i, fallback = () => { }) {
|
||||
if (! el) return fallback()
|
||||
|
||||
let els = Alpine.$data(el).__itemEls
|
||||
let next = els[els.indexOf(el) + 1]
|
||||
|
||||
if (! next) return fallback()
|
||||
if (next.__isDisabled.value || next.tagName.toLowerCase() === 'template') return this.next(Alpine, next, receive, fallback)
|
||||
return receive(next)
|
||||
},
|
||||
previous(Alpine, el, receive = i => i, fallback = () => { }) {
|
||||
if (! el) return fallback()
|
||||
|
||||
let els = Alpine.$data(el).__itemEls
|
||||
let prev = els[els.indexOf(el) - 1]
|
||||
|
||||
if (! prev) return fallback()
|
||||
if (prev.__isDisabled.value || prev.tagName.toLowerCase() === 'template') return this.previous(Alpine, prev, receive, fallback)
|
||||
return receive(prev)
|
||||
},
|
||||
searchQuery: '',
|
||||
debouncedClearSearch: undefined,
|
||||
clearSearch(Alpine) {
|
||||
if (! this.debouncedClearSearch) {
|
||||
this.debouncedClearSearch = Alpine.debounce(function () { this.searchQuery = '' }, 350)
|
||||
}
|
||||
|
||||
this.debouncedClearSearch()
|
||||
},
|
||||
search(Alpine, parent, key, receiver) {
|
||||
if (key.length > 1) return
|
||||
|
||||
this.searchQuery += key
|
||||
|
||||
let els = Alpine.raw(Alpine.$data(parent).__itemEls)
|
||||
|
||||
let el = els.find(el => {
|
||||
return el.textContent.trim().toLowerCase().startsWith(this.searchQuery)
|
||||
})
|
||||
|
||||
el && ! el.__isDisabled.value && receiver(el)
|
||||
|
||||
this.clearSearch(Alpine)
|
||||
},
|
||||
}
|
||||
209
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/popover.js
Normal file
209
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/popover.js
Normal file
@@ -0,0 +1,209 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('popover', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'overlay') handleOverlay(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
else if (directive.value === 'group') handleGroup(el, Alpine)
|
||||
})
|
||||
|
||||
Alpine.magic('popover', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isOpen() {
|
||||
return $data.__isOpenState
|
||||
},
|
||||
open() {
|
||||
$data.__open()
|
||||
},
|
||||
close() {
|
||||
$data.__close()
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-id'() { return ['alpine-popover-button', 'alpine-popover-panel'] },
|
||||
'x-modelable': '__isOpenState',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
if (this.$data.__groupEl) {
|
||||
this.$data.__groupEl.addEventListener('__close-others', ({ detail }) => {
|
||||
if (detail.el.isSameNode(this.$el)) return
|
||||
|
||||
this.__close(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
__buttonEl: undefined,
|
||||
__panelEl: undefined,
|
||||
__isStatic: false,
|
||||
get __isOpen() {
|
||||
if (this.__isStatic) return true
|
||||
|
||||
return this.__isOpenState
|
||||
},
|
||||
__isOpenState: false,
|
||||
__open() {
|
||||
this.__isOpenState = true
|
||||
|
||||
this.$dispatch('__close-others', { el: this.$el })
|
||||
},
|
||||
__toggle() {
|
||||
this.__isOpenState ? this.__close() : this.__open()
|
||||
},
|
||||
__close(el) {
|
||||
if (this.__isStatic) return
|
||||
|
||||
this.__isOpenState = false
|
||||
|
||||
if (el === false) return
|
||||
|
||||
el = el || this.$data.__buttonEl
|
||||
|
||||
if (document.activeElement.isSameNode(el)) return
|
||||
|
||||
setTimeout(() => el.focus())
|
||||
},
|
||||
__contains(outer, inner) {
|
||||
return !! Alpine.findClosest(inner, el => el.isSameNode(outer))
|
||||
}
|
||||
}
|
||||
},
|
||||
'@keydown.escape.stop.prevent'() {
|
||||
this.__close()
|
||||
},
|
||||
'@focusin.window'() {
|
||||
if (this.$data.__groupEl) {
|
||||
if (! this.$data.__contains(this.$data.__groupEl, document.activeElement)) {
|
||||
this.$data.__close(false)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (! this.$data.__contains(this.$el, document.activeElement)) {
|
||||
this.$data.__close(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': 'button',
|
||||
':id'() { return this.$id('alpine-popover-button') },
|
||||
':aria-expanded'() { return this.$data.__isOpen },
|
||||
':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-popover-panel') },
|
||||
'x-init'() {
|
||||
if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
|
||||
|
||||
this.$data.__buttonEl = this.$el
|
||||
},
|
||||
'@click'() { this.$data.__toggle() },
|
||||
'@keydown.tab'(e) {
|
||||
if (! e.shiftKey && this.$data.__isOpen) {
|
||||
let firstFocusableEl = this.$focus.within(this.$data.__panelEl).getFirst()
|
||||
|
||||
if (firstFocusableEl) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.$focus.focus(firstFocusableEl)
|
||||
}
|
||||
}
|
||||
},
|
||||
'@keyup.tab'(e) {
|
||||
if (this.$data.__isOpen) {
|
||||
// Check if the last focused element was "after" this one
|
||||
let lastEl = this.$focus.previouslyFocused()
|
||||
|
||||
if (! lastEl) return
|
||||
|
||||
if (
|
||||
// Make sure the last focused wasn't part of this popover.
|
||||
(! this.$data.__buttonEl.contains(lastEl) && ! this.$data.__panelEl.contains(lastEl))
|
||||
// Also make sure it appeared "after" this button in the DOM.
|
||||
&& (lastEl && (this.$el.compareDocumentPosition(lastEl) & Node.DOCUMENT_POSITION_FOLLOWING))
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.$focus.within(this.$data.__panelEl).last()
|
||||
}
|
||||
}
|
||||
},
|
||||
'@keydown.space.stop.prevent'() { this.$data.__toggle() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__toggle() },
|
||||
// This is to stop Firefox from firing a "click".
|
||||
'@keyup.space.stop.prevent'() { },
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() {
|
||||
this.$data.__isStatic = Alpine.bound(this.$el, 'static', false)
|
||||
this.$data.__panelEl = this.$el
|
||||
},
|
||||
'x-effect'() {
|
||||
this.$data.__isOpen && Alpine.bound(el, 'focus') && this.$focus.first()
|
||||
},
|
||||
'x-ref': 'panel',
|
||||
':id'() { return this.$id('alpine-popover-panel') },
|
||||
'x-show'() { return this.$data.__isOpen },
|
||||
'@mousedown.window'($event) {
|
||||
if (! this.$data.__isOpen) return
|
||||
if (this.$data.__contains(this.$data.__buttonEl, $event.target)) return
|
||||
if (this.$data.__contains(this.$el, $event.target)) return
|
||||
|
||||
if (! this.$focus.focusable($event.target)) {
|
||||
this.$data.__close()
|
||||
}
|
||||
},
|
||||
'@keydown.tab'(e) {
|
||||
if (e.shiftKey && this.$focus.isFirst(e.target)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
Alpine.bound(el, 'focus') ? this.$data.__close() : this.$data.__buttonEl.focus()
|
||||
} else if (! e.shiftKey && this.$focus.isLast(e.target)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// Get the next panel button:
|
||||
let els = this.$focus.within(document).all()
|
||||
let buttonIdx = els.indexOf(this.$data.__buttonEl)
|
||||
|
||||
let nextEls = els
|
||||
.splice(buttonIdx + 1) // Elements after button
|
||||
.filter(el => ! this.$el.contains(el)) // Ignore items in panel
|
||||
|
||||
nextEls[0].focus()
|
||||
|
||||
Alpine.bound(el, 'focus') && this.$data.__close(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleGroup(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': 'container',
|
||||
'x-data'() {
|
||||
return {
|
||||
__groupEl: this.$el,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOverlay(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-show'() { return this.$data.__isOpen }
|
||||
})
|
||||
}
|
||||
220
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/radio.js
Normal file
220
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/radio.js
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('radio', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'option') handleOption(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'description') handleDescription(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('radioOption', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return $data.__option === $data.__active
|
||||
},
|
||||
get isChecked() {
|
||||
return $data.__option === $data.__value
|
||||
},
|
||||
get isDisabled() {
|
||||
let disabled = $data.__disabled
|
||||
|
||||
if ($data.__rootDisabled) return true
|
||||
|
||||
return disabled
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__value',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
this.__rootDisabled = Alpine.bound(el, 'disabled', false);
|
||||
this.__value = Alpine.bound(this.$el, 'default-value', false)
|
||||
this.__inputName = Alpine.bound(this.$el, 'name', false)
|
||||
this.__inputId = 'alpine-radio-'+Date.now()
|
||||
})
|
||||
|
||||
// Add `role="none"` to all non role elements.
|
||||
this.$nextTick(() => {
|
||||
let walker = document.createTreeWalker(
|
||||
this.$el,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
{
|
||||
acceptNode: node => {
|
||||
if (node.getAttribute('role') === 'radio') return NodeFilter.FILTER_REJECT
|
||||
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
|
||||
return NodeFilter.FILTER_ACCEPT
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
while (walker.nextNode()) walker.currentNode.setAttribute('role', 'none')
|
||||
})
|
||||
},
|
||||
__value: undefined,
|
||||
__active: undefined,
|
||||
__rootEl: this.$el,
|
||||
__optionValues: [],
|
||||
__disabledOptions: new Set,
|
||||
__optionElsByValue: new Map,
|
||||
__hasLabel: false,
|
||||
__hasDescription: false,
|
||||
__rootDisabled: false,
|
||||
__inputName: undefined,
|
||||
__inputId: undefined,
|
||||
__change(value) {
|
||||
if (this.__rootDisabled) return
|
||||
|
||||
this.__value = value
|
||||
},
|
||||
__addOption(option, el, disabled) {
|
||||
// Add current element to element list for navigating.
|
||||
let options = Alpine.raw(this.__optionValues)
|
||||
let els = options.map(i => this.__optionElsByValue.get(i))
|
||||
let inserted = false
|
||||
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i].compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
options.splice(i, 0, option)
|
||||
this.__optionElsByValue.set(option, el)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!inserted) {
|
||||
options.push(option)
|
||||
this.__optionElsByValue.set(option, el)
|
||||
}
|
||||
|
||||
disabled && this.__disabledOptions.add(option)
|
||||
},
|
||||
__isFirstOption(option) {
|
||||
return this.__optionValues.indexOf(option) === 0
|
||||
},
|
||||
__setActive(option) {
|
||||
this.__active = option
|
||||
},
|
||||
__focusOptionNext() {
|
||||
let option = this.__active
|
||||
let all = this.__optionValues.filter(i => !this.__disabledOptions.has(i))
|
||||
let next = all[this.__optionValues.indexOf(option) + 1]
|
||||
next = next || all[0]
|
||||
|
||||
this.__optionElsByValue.get(next).focus()
|
||||
this.__change(next)
|
||||
},
|
||||
__focusOptionPrev() {
|
||||
let option = this.__active
|
||||
let all = this.__optionValues.filter(i => !this.__disabledOptions.has(i))
|
||||
let prev = all[all.indexOf(option) - 1]
|
||||
prev = prev || all.slice(-1)[0]
|
||||
|
||||
this.__optionElsByValue.get(prev).focus()
|
||||
this.__change(prev)
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-effect'() {
|
||||
let value = this.__value
|
||||
|
||||
// Only render a hidden input if the "name" prop is passed...
|
||||
if (! this.__inputName) return
|
||||
|
||||
// First remove a previously appended hidden input (if it exists)...
|
||||
let nextEl = this.$el.nextElementSibling
|
||||
if (nextEl && String(nextEl.id) === String(this.__inputId)) {
|
||||
nextEl.remove()
|
||||
}
|
||||
|
||||
// If the value is true, create the input and append it, otherwise,
|
||||
// we already removed it in the previous step...
|
||||
if (value) {
|
||||
let input = document.createElement('input')
|
||||
|
||||
input.type = 'hidden'
|
||||
input.value = value
|
||||
input.name = this.__inputName
|
||||
input.id = this.__inputId
|
||||
|
||||
this.$el.after(input)
|
||||
}
|
||||
},
|
||||
'role': 'radiogroup',
|
||||
'x-id'() { return ['alpine-radio-label', 'alpine-radio-description'] },
|
||||
':aria-labelledby'() { return this.__hasLabel && this.$id('alpine-radio-label') },
|
||||
':aria-describedby'() { return this.__hasDescription && this.$id('alpine-radio-description') },
|
||||
'@keydown.up.prevent.stop'() { this.__focusOptionPrev() },
|
||||
'@keydown.left.prevent.stop'() { this.__focusOptionPrev() },
|
||||
'@keydown.down.prevent.stop'() { this.__focusOptionNext() },
|
||||
'@keydown.right.prevent.stop'() { this.__focusOptionNext() },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOption(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
this.__disabled = Alpine.bound(el, 'disabled', false)
|
||||
this.__option = Alpine.bound(el, 'value')
|
||||
this.$data.__addOption(this.__option, this.$el, this.__disabled)
|
||||
})
|
||||
},
|
||||
__option: undefined,
|
||||
__disabled: false,
|
||||
__hasLabel: false,
|
||||
__hasDescription: false,
|
||||
}
|
||||
},
|
||||
'x-id'() { return ['alpine-radio-label', 'alpine-radio-description'] },
|
||||
'role': 'radio',
|
||||
':aria-checked'() { return this.$radioOption.isChecked },
|
||||
':aria-disabled'() { return this.$radioOption.isDisabled },
|
||||
':aria-labelledby'() { return this.__hasLabel && this.$id('alpine-radio-label') },
|
||||
':aria-describedby'() { return this.__hasDescription && this.$id('alpine-radio-description') },
|
||||
':tabindex'() {
|
||||
if (this.$radioOption.isDisabled) return -1
|
||||
if (this.$radioOption.isChecked) return 0
|
||||
if (! this.$data.__value && this.$data.__isFirstOption(this.$data.__option)) return 0
|
||||
|
||||
return -1
|
||||
},
|
||||
'@click'() {
|
||||
if (this.$radioOption.isDisabled) return
|
||||
this.$data.__change(this.$data.__option)
|
||||
this.$el.focus()
|
||||
},
|
||||
'@focus'() {
|
||||
if (this.$radioOption.isDisabled) return
|
||||
this.$data.__setActive(this.$data.__option)
|
||||
},
|
||||
'@blur'() {
|
||||
if (this.$data.__active === this.$data.__option) this.$data.__setActive(undefined)
|
||||
},
|
||||
'@keydown.space.stop.prevent'() { this.$data.__change(this.$data.__option) },
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasLabel = true },
|
||||
':id'() { return this.$id('alpine-radio-label') },
|
||||
})
|
||||
}
|
||||
|
||||
function handleDescription(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasDescription = true },
|
||||
':id'() { return this.$id('alpine-radio-description') },
|
||||
})
|
||||
}
|
||||
116
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/switch.js
Normal file
116
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/switch.js
Normal file
@@ -0,0 +1,116 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('switch', (el, directive) => {
|
||||
if (directive.value === 'group') handleGroup(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'description') handleDescription(el, Alpine)
|
||||
else handleRoot(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('switch', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isChecked() {
|
||||
return $data.__value === true
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleGroup(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-id'() { return ['alpine-switch-label', 'alpine-switch-description'] },
|
||||
'x-data'() {
|
||||
return {
|
||||
__hasLabel: false,
|
||||
__hasDescription: false,
|
||||
__switchEl: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__value',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
this.__value = Alpine.bound(this.$el, 'default-checked', false)
|
||||
this.__inputName = Alpine.bound(this.$el, 'name', false)
|
||||
this.__inputValue = Alpine.bound(this.$el, 'value', 'on')
|
||||
this.__inputId = 'alpine-switch-'+Date.now()
|
||||
})
|
||||
},
|
||||
__value: undefined,
|
||||
__inputName: undefined,
|
||||
__inputValue: undefined,
|
||||
__inputId: undefined,
|
||||
__toggle() {
|
||||
this.__value = ! this.__value;
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-effect'() {
|
||||
let value = this.__value
|
||||
|
||||
// Only render a hidden input if the "name" prop is passed...
|
||||
if (! this.__inputName) return
|
||||
|
||||
// First remove a previously appended hidden input (if it exists)...
|
||||
let nextEl = this.$el.nextElementSibling
|
||||
if (nextEl && String(nextEl.id) === String(this.__inputId)) {
|
||||
nextEl.remove()
|
||||
}
|
||||
|
||||
// If the value is true, create the input and append it, otherwise,
|
||||
// we already removed it in the previous step...
|
||||
if (value) {
|
||||
let input = document.createElement('input')
|
||||
|
||||
input.type = 'hidden'
|
||||
input.value = this.__inputValue
|
||||
input.name = this.__inputName
|
||||
input.id = this.__inputId
|
||||
|
||||
this.$el.after(input)
|
||||
}
|
||||
},
|
||||
'x-init'() {
|
||||
if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
|
||||
this.$data.__switchEl = this.$el
|
||||
},
|
||||
'role': 'switch',
|
||||
'tabindex': "0",
|
||||
':aria-checked'() { return !!this.__value },
|
||||
':aria-labelledby'() { return this.$data.__hasLabel && this.$id('alpine-switch-label') },
|
||||
':aria-describedby'() { return this.$data.__hasDescription && this.$id('alpine-switch-description') },
|
||||
'@click.prevent'() { this.__toggle() },
|
||||
'@keyup'(e) {
|
||||
if (e.key !== 'Tab') e.preventDefault()
|
||||
if (e.key === ' ') this.__toggle()
|
||||
},
|
||||
// This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
|
||||
'@keypress.prevent'() { },
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasLabel = true },
|
||||
':id'() { return this.$id('alpine-switch-label') },
|
||||
'@click'() {
|
||||
this.$data.__switchEl.click()
|
||||
this.$data.__switchEl.focus({ preventScroll: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleDescription(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasDescription = true },
|
||||
':id'() { return this.$id('alpine-switch-description') },
|
||||
})
|
||||
}
|
||||
141
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/tabs.js
Normal file
141
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/tabs.js
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('tabs', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'list') handleList(el, Alpine)
|
||||
else if (directive.value === 'tab') handleTab(el, Alpine)
|
||||
else if (directive.value === 'panels') handlePanels(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('tab', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isSelected() {
|
||||
return $data.__selectedIndex === $data.__tabs.indexOf($data.__tabEl)
|
||||
},
|
||||
get isDisabled() {
|
||||
return $data.__isDisabled
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Alpine.magic('panel', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isSelected() {
|
||||
return $data.__selectedIndex === $data.__panels.indexOf($data.__panelEl)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__selectedIndex',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
let defaultIndex = this.__selectedIndex || Number(Alpine.bound(this.$el, 'default-index', 0))
|
||||
let tabs = this.__activeTabs()
|
||||
let clamp = (number, min, max) => Math.min(Math.max(number, min), max)
|
||||
|
||||
this.__selectedIndex = clamp(defaultIndex, 0, tabs.length -1)
|
||||
|
||||
Alpine.effect(() => {
|
||||
this.__manualActivation = Alpine.bound(this.$el, 'manual', false)
|
||||
})
|
||||
})
|
||||
},
|
||||
__tabs: [],
|
||||
__panels: [],
|
||||
__selectedIndex: null,
|
||||
__tabGroupEl: undefined,
|
||||
__manualActivation: false,
|
||||
__addTab(el) { this.__tabs.push(el) },
|
||||
__addPanel(el) { this.__panels.push(el) },
|
||||
__selectTab(el) {
|
||||
this.__selectedIndex = this.__tabs.indexOf(el)
|
||||
},
|
||||
__activeTabs() {
|
||||
return this.__tabs.filter(i => !i.__disabled)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleList(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__tabGroupEl = this.$el }
|
||||
})
|
||||
}
|
||||
|
||||
function handleTab(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
'x-data'() { return {
|
||||
init() {
|
||||
this.__tabEl = this.$el
|
||||
this.$data.__addTab(this.$el)
|
||||
this.__tabEl.__disabled = Alpine.bound(this.$el, 'disabled', false)
|
||||
this.__isDisabled = this.__tabEl.__disabled
|
||||
},
|
||||
__tabEl: undefined,
|
||||
__isDisabled: false,
|
||||
}},
|
||||
'@click'() {
|
||||
if (this.$el.__disabled) return
|
||||
|
||||
this.$data.__selectTab(this.$el)
|
||||
|
||||
this.$el.focus()
|
||||
},
|
||||
'@keydown.enter.prevent.stop'() { this.__selectTab(this.$el) },
|
||||
'@keydown.space.prevent.stop'() { this.__selectTab(this.$el) },
|
||||
'@keydown.home.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).first() },
|
||||
'@keydown.page-up.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).first() },
|
||||
'@keydown.end.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).last() },
|
||||
'@keydown.page-down.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).last() },
|
||||
'@keydown.down.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().next() },
|
||||
'@keydown.right.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().next() },
|
||||
'@keydown.up.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().prev() },
|
||||
'@keydown.left.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().prev() },
|
||||
':tabindex'() { return this.$tab.isSelected ? 0 : -1 },
|
||||
'@focus'() {
|
||||
if (this.$data.__manualActivation) {
|
||||
this.$el.focus()
|
||||
} else {
|
||||
if (this.$el.__disabled) return
|
||||
|
||||
this.$data.__selectTab(this.$el)
|
||||
|
||||
this.$el.focus()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanels(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
//
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
':tabindex'() { return this.$panel.isSelected ? 0 : -1 },
|
||||
'x-data'() { return {
|
||||
init() {
|
||||
this.__panelEl = this.$el
|
||||
this.$data.__addPanel(this.$el)
|
||||
},
|
||||
__panelEl: undefined,
|
||||
}},
|
||||
'x-show'() { return this.$panel.isSelected },
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user