mirror of
https://github.com/filamentphp/filament.git
synced 2026-10-24 12:04:06 +08:00
336 lines
14 KiB
PHP
336 lines
14 KiB
PHP
@pushonce('filament-scripts:select-component')
|
|
<script>
|
|
function select(config) {
|
|
return {
|
|
autofocus: config.autofocus,
|
|
|
|
displayValue: config.initialDisplayValue,
|
|
|
|
emptyOptionsMessage: config.emptyOptionsMessage,
|
|
|
|
focusedOptionIndex: null,
|
|
|
|
loading: false,
|
|
|
|
name: config.name,
|
|
|
|
noSearchResultsMessage: config.noSearchResultsMessage,
|
|
|
|
open: false,
|
|
|
|
options: config.initialOptions,
|
|
|
|
required: config.required,
|
|
|
|
search: '',
|
|
|
|
value: config.value,
|
|
|
|
clearValue: function () {
|
|
this.value = null
|
|
this.displayValue = null
|
|
|
|
this.closeListbox()
|
|
},
|
|
|
|
closeListbox: function () {
|
|
this.open = false
|
|
|
|
this.focusedOptionIndex = null
|
|
|
|
this.search = ''
|
|
},
|
|
|
|
evaluatePosition: function () {
|
|
let availableHeight = window.innerHeight - this.$refs.button.offsetHeight
|
|
|
|
let element = this.$refs.button
|
|
|
|
while (element) {
|
|
availableHeight -= element.offsetTop
|
|
|
|
element = element.offsetParent
|
|
}
|
|
|
|
if (this.$refs.listbox.offsetHeight <= availableHeight) {
|
|
this.$refs.listbox.style.bottom = 'auto'
|
|
|
|
return
|
|
}
|
|
|
|
this.$refs.listbox.style.bottom = `${this.$refs.button.offsetHeight}px`
|
|
},
|
|
|
|
focusNextOption: function () {
|
|
if (this.focusedOptionIndex === null) {
|
|
this.focusedOptionIndex = Object.keys(this.options).length - 1
|
|
|
|
return
|
|
}
|
|
|
|
if (this.focusedOptionIndex + 1 >= Object.keys(this.options).length) return
|
|
|
|
this.focusedOptionIndex++
|
|
|
|
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
|
|
block: 'center',
|
|
})
|
|
},
|
|
|
|
focusPreviousOption: function () {
|
|
if (this.focusedOptionIndex === null) {
|
|
this.focusedOptionIndex = 0
|
|
|
|
return
|
|
}
|
|
|
|
if (this.focusedOptionIndex <= 0) return
|
|
|
|
this.focusedOptionIndex--
|
|
|
|
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
|
|
block: 'center',
|
|
})
|
|
},
|
|
|
|
init: function () {
|
|
if (this.autofocus) this.openListbox()
|
|
|
|
this.$watch('search', () => {
|
|
if (! this.open || this.search === '' || this.search === null) {
|
|
this.options = config.initialOptions
|
|
this.focusedOptionIndex = 0
|
|
|
|
return
|
|
}
|
|
|
|
if (Object.keys(config.initialOptions).length) {
|
|
this.options = {}
|
|
|
|
let search = this.search.trim().toLowerCase()
|
|
|
|
for (let key in config.initialOptions) {
|
|
if (config.initialOptions[key].trim().toLowerCase().includes(search)) {
|
|
this.options[key] = config.initialOptions[key]
|
|
}
|
|
}
|
|
|
|
this.focusedOptionIndex = 0
|
|
} else {
|
|
this.loading = true
|
|
|
|
this.$wire.getSelectFieldOptionSearchResults(this.name, this.search).then((options) => {
|
|
this.options = options
|
|
this.focusedOptionIndex = 0
|
|
this.loading = false
|
|
})
|
|
}
|
|
})
|
|
|
|
this.$watch('value', () => {
|
|
if (this.value in this.options) {
|
|
this.displayValue = this.options[this.value]
|
|
}
|
|
})
|
|
},
|
|
|
|
openListbox: function () {
|
|
this.focusedOptionIndex = Object.keys(this.options).indexOf(this.value)
|
|
|
|
if (this.focusedOptionIndex < 0) this.focusedOptionIndex = 0
|
|
|
|
this.open = true
|
|
|
|
this.$nextTick(() => {
|
|
this.$refs.search.focus()
|
|
|
|
this.evaluatePosition()
|
|
|
|
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
|
|
block: 'center'
|
|
})
|
|
})
|
|
},
|
|
|
|
selectOption: function (index = null) {
|
|
if (!this.open) {
|
|
this.closeListbox()
|
|
|
|
return
|
|
}
|
|
|
|
this.value = Object.keys(this.options)[index ?? this.focusedOptionIndex]
|
|
this.displayValue = this.options[this.value]
|
|
|
|
this.closeListbox()
|
|
},
|
|
|
|
toggleListboxVisibility: function () {
|
|
if (this.open) {
|
|
this.closeListbox()
|
|
|
|
return
|
|
}
|
|
|
|
this.openListbox()
|
|
},
|
|
}
|
|
}
|
|
</script>
|
|
@endpushonce
|
|
|
|
<x-forms::field-group
|
|
:column-span="$formComponent->getColumnSpan()"
|
|
:error-key="$formComponent->getName()"
|
|
:for="$formComponent->getId()"
|
|
:help-message="$formComponent->getHelpMessage()"
|
|
:hint="$formComponent->getHint()"
|
|
:label="$formComponent->getLabel()"
|
|
:required="$formComponent->isRequired()"
|
|
>
|
|
<div
|
|
x-data="select({
|
|
autofocus: {{ $formComponent->isAutofocused() ? 'true' : 'false' }},
|
|
emptyOptionsMessage: '{{ __($formComponent->getEmptyOptionsMessage()) }}',
|
|
initialDisplayValue: {{ data_get($this, $formComponent->getName()) !== null ? '\'' . addslashes($formComponent->getDisplayValue(data_get($this, $formComponent->getName()))) . '\'' : 'null' }},
|
|
initialOptions: {{ json_encode($formComponent->getOptions()) }},
|
|
name: '{{ $formComponent->getName() }}',
|
|
noSearchResultsMessage: '{{ __($formComponent->getNoSearchResultsMessage()) }}',
|
|
required: {{ $formComponent->isRequired() ? 'true' : 'false' }},
|
|
@if (Str::of($formComponent->getBindingAttribute())->startsWith('wire:model'))
|
|
value: @entangle($formComponent->getName()){{ Str::of($formComponent->getBindingAttribute())->after('wire:model') }},
|
|
@endif
|
|
})"
|
|
x-init="init()"
|
|
x-on:click.away="closeListbox()"
|
|
x-on:keydown.escape.stop="closeListbox()"
|
|
{!! $formComponent->getId() ? "id=\"{$formComponent->getId()}\"" : null !!}
|
|
class="relative"
|
|
{!! Filament\format_attributes($formComponent->getExtraAttributes()) !!}
|
|
>
|
|
@unless (Str::of($formComponent->getBindingAttribute())->startsWith(['wire:model', 'x-model']))
|
|
<input
|
|
x-model="value"
|
|
{!! $formComponent->getName() ? "{$formComponent->getBindingAttribute()}=\"{$formComponent->getName()}\"" : null !!}
|
|
type="hidden"
|
|
/>
|
|
@endif
|
|
|
|
<div
|
|
@unless($formComponent->isDisabled())
|
|
x-ref="button"
|
|
x-on:click="toggleListboxVisibility()"
|
|
x-on:keydown.enter.stop.prevent="open ? selectOption() : openListbox()"
|
|
x-on:keydown.space="if (! open) openListbox()"
|
|
x-on:keydown.backspace="if (! search) clearValue()"
|
|
x-on:keydown.clear="if (! search) clearValue()"
|
|
x-on:keydown.delete="if (! search) clearValue()"
|
|
x-bind:aria-expanded="open"
|
|
aria-haspopup="listbox"
|
|
tabindex="1"
|
|
@endunless
|
|
class="bg-white relative w-full border rounded shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50 {{ $formComponent->isDisabled() ? 'text-gray-500' : '' }} {{ $errors->has($formComponent->getName()) ? 'border-danger-600 motion-safe:animate-shake' : 'border-gray-300' }}"
|
|
>
|
|
<span
|
|
x-show="! open"
|
|
x-text="displayValue ?? '{{ __($formComponent->getPlaceholder()) }}'"
|
|
x-bind:class="{
|
|
'text-gray-400': displayValue === null,
|
|
}"
|
|
class="block truncate"
|
|
></span>
|
|
|
|
@unless ($formComponent->isDisabled())
|
|
<input
|
|
x-ref="search"
|
|
x-show="open"
|
|
x-model.debounce.500="search"
|
|
x-on:keydown.enter.stop.prevent="selectOption()"
|
|
x-on:keydown.arrow-up.stop.prevent="focusPreviousOption()"
|
|
x-on:keydown.arrow-down.stop.prevent="focusNextOption()"
|
|
type="search"
|
|
class="w-full h-full p-0 border-0 focus:ring-0 focus:outline-none"
|
|
/>
|
|
|
|
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
<x-heroicon-s-selector x-show="! loading" x-cloak class="w-5 h-5 text-gray-400" />
|
|
|
|
<svg x-show="loading" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="currentColor"
|
|
class="w-5 h-5 text-gray-400">
|
|
<path
|
|
d="M6.306 28.014c1.72 10.174 11.362 17.027 21.536 15.307C38.016 41.6 44.87 31.958 43.15 21.784l-4.011.678c1.345 7.958-4.015 15.502-11.974 16.847-7.959 1.346-15.501-4.014-16.847-11.973l-4.011.678z">
|
|
<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 25 25"
|
|
to="360 25 25" dur=".7s" repeatCount="indefinite" /></path>
|
|
</svg>
|
|
</span>
|
|
@endunless
|
|
</div>
|
|
|
|
@unless($formComponent->isDisabled())
|
|
<div
|
|
x-ref="listbox"
|
|
x-show="open"
|
|
x-transition:leave="transition ease-in duration-100"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
role="listbox"
|
|
x-bind:aria-activedescendant="focusedOptionIndex ? '{{ $formComponent->getName() }}' + 'Option' + focusedOptionIndex : null"
|
|
tabindex="-1"
|
|
x-cloak
|
|
class="absolute z-50 w-full my-1 bg-white border border-gray-300 rounded shadow-sm"
|
|
>
|
|
<ul
|
|
x-ref="listboxOptionsList"
|
|
class="py-1 overflow-auto text-base leading-6 rounded shadow-sm max-h-60 focus:outline-none"
|
|
>
|
|
<template x-for="(key, index) in Object.keys(options)" :key="index">
|
|
<li
|
|
x-bind:id="'{{ $formComponent->getName() }}' + 'Option' + focusedOptionIndex"
|
|
x-on:click="selectOption(index)"
|
|
x-on:mouseenter="focusedOptionIndex = index"
|
|
x-on:mouseleave="focusedOptionIndex = null"
|
|
role="option"
|
|
x-bind:aria-selected="focusedOptionIndex === index"
|
|
x-bind:class="{
|
|
'text-white bg-blue-600': index === focusedOptionIndex,
|
|
'text-gray-900': index !== focusedOptionIndex,
|
|
}"
|
|
class="relative py-2 pl-3 text-gray-900 cursor-default select-none pr-9"
|
|
>
|
|
<span
|
|
x-text="Object.values(options)[index]"
|
|
x-bind:class="{
|
|
'font-medium': index === focusedOptionIndex,
|
|
'font-normal': index !== focusedOptionIndex,
|
|
}"
|
|
class="block font-normal truncate"
|
|
></span>
|
|
|
|
<span
|
|
x-show="key === value"
|
|
x-bind:class="{
|
|
'text-white': index === focusedOptionIndex,
|
|
'text-blue-600': index !== focusedOptionIndex,
|
|
}"
|
|
class="absolute inset-y-0 right-0 flex items-center pr-4 text-blue-600"
|
|
>
|
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd"
|
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
clip-rule="evenodd" />
|
|
</svg>
|
|
</span>
|
|
</li>
|
|
</template>
|
|
|
|
<div
|
|
x-show="! Object.keys(options).length"
|
|
x-text="! search || loading ? emptyOptionsMessage : noSearchResultsMessage"
|
|
class="px-3 py-2 text-sm text-gray-900 cursor-default select-none"
|
|
></div>
|
|
</ul>
|
|
</div>
|
|
@endunless
|
|
</div>
|
|
</x-forms::field-group>
|