mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-12 13:18:22 +00:00
224 lines
7.2 KiB
Plaintext
224 lines
7.2 KiB
Plaintext
{% macro select(
|
|
id=None,
|
|
name=None,
|
|
selected=None,
|
|
main_attrs=None,
|
|
trigger_attrs=None,
|
|
content_attrs=None,
|
|
listbox_attrs=None,
|
|
is_combobox=False
|
|
) %}
|
|
{% if registerComponent('select') %}
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('select', (name = null, initialValue = null) => ({
|
|
open: false,
|
|
name: null,
|
|
options: [],
|
|
disabledOptions: [],
|
|
focusedIndex: null,
|
|
selectedLabel: null,
|
|
selectedValue: null,
|
|
query: '',
|
|
|
|
init() {
|
|
this.$nextTick(() => {
|
|
if (name) this.name = name;
|
|
this.options = Array.from(this.$el.querySelectorAll('[role=option]:not([aria-disabled])'));
|
|
this.disabledOptions = Array.from(this.$el.querySelectorAll('[role=option][aria-disabled=true]'));
|
|
if (this.options.length === 0) return;
|
|
if (initialValue) {
|
|
const option = this.options.find(opt => opt.getAttribute('data-value') === initialValue);
|
|
this.selectOption(option);
|
|
this.focusedIndex = this.options.indexOf(option);
|
|
this.focusOption();
|
|
} else {
|
|
this.selectOption(this.options[0]);
|
|
}
|
|
});
|
|
},
|
|
focusOption() {
|
|
if (this.options.length === 0) return;
|
|
|
|
if (this.focusedIndex >= this.options.length) {
|
|
this.focusedIndex = this.options.length - 1;
|
|
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
|
|
this.focusedIndex = 0;
|
|
}
|
|
this.options.forEach(opt => opt.removeAttribute('data-focus'));
|
|
this.options[this.focusedIndex].setAttribute('data-focus', '');
|
|
},
|
|
focusOnSelectedOption() {
|
|
if (this.options.length === 0 || this.selectedValue === null) return;
|
|
const option = this.options.find(opt => opt.getAttribute('data-value') === this.selectedValue);
|
|
if (!option) return;
|
|
this.focusedIndex = this.options.indexOf(option);
|
|
this.focusOption();
|
|
},
|
|
moveOptionFocus(delta) {
|
|
if (this.options.length === 0) return;
|
|
|
|
if (!this.open) {
|
|
this.open = true;
|
|
this.focusOnSelectedOption();
|
|
} else {
|
|
this.focusedIndex = this.focusedIndex === null
|
|
? 0
|
|
: this.focusedIndex + delta;
|
|
}
|
|
this.focusOption();
|
|
},
|
|
handleOptionClick(event) {
|
|
const option = event.target.closest('[role=option]');
|
|
if (option && option.getAttribute('aria-disabled') !== 'true') {
|
|
this.selectOption(option);
|
|
this.open = false;
|
|
this.$nextTick(() => this.$refs.trigger.focus());
|
|
}
|
|
},
|
|
handleOptionMousemove(event) {
|
|
console.log("here");
|
|
const option = event.target.closest('[role=option]');
|
|
if (option && option.getAttribute('aria-disabled') !== 'true') {
|
|
this.focusedIndex = this.options.indexOf(option);
|
|
this.focusOption();
|
|
}
|
|
},
|
|
handleOptionEnter(event) {
|
|
this.selectOption(this.options[this.focusedIndex]);
|
|
this.open = !this.open;
|
|
},
|
|
selectOption(option) {
|
|
if (this.options.length === 0 || !option || option.disabled || !option.getAttribute('data-value')
|
|
) return;
|
|
|
|
this.options.forEach(opt => {
|
|
opt.setAttribute('aria-selected', opt === option);
|
|
});
|
|
this.selectedLabel = option.innerHTML;
|
|
this.selectedValue = option.getAttribute('data-value');
|
|
this.$dispatch('select:change', {
|
|
value: this.selectedValue,
|
|
label: this.selectedLabel
|
|
});
|
|
},
|
|
filterOptions(query) {
|
|
if (query.length > 0) {
|
|
this.disabledOptions.forEach(opt => { opt.setAttribute('aria-hidden', 'true'); });
|
|
} else {
|
|
this.disabledOptions.forEach(opt => { opt.removeAttribute('aria-hidden'); });
|
|
}
|
|
this.options.forEach(opt => {
|
|
opt.removeAttribute('aria-hidden');
|
|
if (opt.getAttribute('data-value') != null && !opt.innerHTML.toLowerCase().includes(query.toLowerCase())) {
|
|
opt.setAttribute('aria-hidden', 'true');
|
|
}
|
|
});
|
|
},
|
|
|
|
$trigger: {
|
|
'@click'() { this.open = !this.open; this.focusOnSelectedOption(); },
|
|
'@keydown.escape.prevent'() {
|
|
this.open = false;
|
|
this.$refs.trigger.focus();
|
|
},
|
|
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
|
|
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
|
|
'@keydown.home.prevent'() { this.focusOption(0) },
|
|
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
|
|
'@keydown.enter.prevent'() { this.handleOptionEnter() },
|
|
':aria-expanded'() { return this.open },
|
|
'x-ref': 'trigger'
|
|
},
|
|
|
|
$content: {
|
|
'@click'(e) { this.handleOptionClick(e) },
|
|
'@mousemove'(e) { this.handleOptionMousemove(e) },
|
|
':aria-hidden'() { return !this.open },
|
|
'x-cloak': ''
|
|
},
|
|
|
|
$filter: {
|
|
'@input'(e) { this.filterOptions(e.target.value) },
|
|
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
|
|
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
|
|
'@keydown.home.prevent'() { this.focusOption(0) },
|
|
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
|
|
'@keydown.enter.prevent'() { this.handleOptionEnter() },
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
|
|
<div
|
|
class="popover {{ main_attrs.class }}"
|
|
x-data="select('{{ name }}', '{{ selected }}')"
|
|
@click.away="open = false"
|
|
{% if id %}id="{{ id }}"{% endif %}
|
|
{% for key, value in main_attrs %}
|
|
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
{% endfor %}
|
|
>
|
|
<button
|
|
type="button"
|
|
aria-haspopup="listbox"
|
|
aria-expanded="false"
|
|
x-bind="$trigger"
|
|
{% if id %}
|
|
id="{{ id }}-trigger"
|
|
aria-controls="{{ id }}-content"
|
|
{% endif %}
|
|
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
|
|
{% for key, value in trigger_attrs %}
|
|
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
{% endfor %}
|
|
>
|
|
<div x-html="selectedLabel" class="flex items-center gap-x-2"
|
|
></div>
|
|
{% lucide "chevron-down", class="text-muted-foreground opacity-50 shrink-0" %}
|
|
</button>
|
|
<div
|
|
data-popover
|
|
aria-hidden="true"
|
|
x-bind="$content"
|
|
{% if id %}id="{{ id }}-content"{% endif %}
|
|
{% for key, value in content_attrs %}
|
|
{{ key }}="{{ value }}"
|
|
{% endfor %}
|
|
>
|
|
{% if is_combobox %}
|
|
<header>
|
|
{% lucide "search" %}
|
|
<input
|
|
type="text"
|
|
value=""
|
|
placeholder="Search framework..."
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
spellcheck="false"
|
|
aria-autocomplete="list"
|
|
role="combobox"
|
|
aria-expanded="true"
|
|
aria-controls="{{ id }}-content"
|
|
aria-labelledby="{{ id }}-trigger"
|
|
x-model="query"
|
|
x-bind="$filter"
|
|
>
|
|
</header>
|
|
{% endif %}
|
|
<div
|
|
role="listbox"
|
|
aria-orientation="vertical"
|
|
{% for key, value in listbox_attrs %}
|
|
{{ key }}="{{ value }}"
|
|
{% endfor %}
|
|
>
|
|
{% if caller %}{{ caller() }}{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if name %}
|
|
<input type="hidden" name="{{ name }}" x-model="selectedValue">
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %} |