9
9
10
10
class Choices extends Component
11
11
{
12
+ public string $ uuid ;
13
+
12
14
public function __construct (
13
15
public ?string $ label = null ,
14
16
public ?string $ icon = null ,
15
17
public ?string $ hint = null ,
16
- public ?bool $ inline = false ,
17
18
public ?bool $ searchable = false ,
18
19
public ?bool $ single = false ,
20
+ public ?string $ searchFunction = 'search ' ,
19
21
public ?string $ optionValue = 'id ' ,
20
22
public ?string $ optionLabel = 'name ' ,
21
23
public ?string $ optionSubLabel = 'description ' ,
@@ -26,6 +28,7 @@ public function __construct(
26
28
// slots
27
29
public mixed $ item = null
28
30
) {
31
+ $ this ->uuid = md5 (serialize ($ this ));
29
32
}
30
33
31
34
public function modelName ()
@@ -36,68 +39,125 @@ public function modelName()
36
39
public function render (): View |Closure |string
37
40
{
38
41
return <<<'HTML'
39
- <div>
42
+ <div x-data="{
43
+ open: false,
44
+ focused: false,
45
+ items: [],
46
+ selection: @entangle($attributes->wire('model')),
47
+ get selectedItems() {
48
+ @if ($single)
49
+ return this.items.filter(i => i.{{ $optionValue }} == this.selection);
50
+ @else
51
+ return this.items.filter(i => this.selection.includes(i.{{ $optionValue }}));
52
+ @endif
53
+ }
54
+ }"
55
+ >
56
+
40
57
<!-- STANDARD LABEL -->
41
- @if($label && !$inline )
58
+ @if($label)
42
59
<label class="pt-0 label label-text font-semibold">{{ $label }}</label>
43
- @endif
44
-
45
- <div x-data="{ open: false, display: ''}" @click.outside="open = false" class="relative">
46
- <div class="relative">
47
-
48
- <!-- SELECTION DISPLAY -->
49
- <span
50
- class="bg-base-100 top-2 pt-1.5 h-8 @if($icon) left-8 @else left-3 @endif px-2 rounded-md font-semibold text-sm underline decoration-dashed hover:bg-base-300 cursor-pointer absolute"
51
- x-show="!open && display"
52
- x-text="display"
53
- @click="open = true; @if($searchable) $refs.searchInput.focus(); @endif">
54
- </span>
55
-
56
- <!-- SEARCH INPUT -->
57
- <input
58
- x-ref="searchInput"
59
- @focus="open = true;"
60
- :value="display"
61
- placeholder="{{ $attributes->whereStartsWith('placeholder')->first() }} "
62
-
63
- @if($searchable)
64
- wire:keyup.debounce="search($el.value)"
60
+ @endif
61
+
62
+ <div
63
+ x-data="
64
+ {
65
+ options: @js($options),
66
+ toggle(option) {
67
+ value = option.{{ $optionValue }};
68
+
69
+ @if($single)
70
+ this.items = [];
71
+ this.items.push(option);
72
+ this.selection = value;
65
73
@else
66
- readonly
67
- @endif
74
+ if(this.selection.includes(value)){
75
+ this.items = this.items.filter(i => i.{{ $optionValue }} !== value);
76
+ this.selection = this.selection.filter(i => i !== value);
77
+ }else{
78
+ this.items.push(option);
79
+ this.selection.push(value);
80
+ }
81
+ @endif
82
+ }
83
+ }"
84
+
85
+ @click.outside="open = false"
86
+ class="relative"
87
+ >
88
+
89
+ <div @click="$refs.inputSearch.focus(); open = true">
90
+
91
+ <!-- DISPLAY SELECTION + INPUT -->
92
+ <div class="peer absolute top-3 left-4" :class="{'focused-input': focused}">
68
93
69
- {{
70
- $attributes
71
- ->except('wire:model')
72
- ->class([
73
- 'select select-primary w-full',
74
- 'pl-10' => ($icon),
75
- 'h-14' => ($inline),
76
- 'pt-3' => ($inline && $label),
77
- 'select-error' => $errors->has($modelName())
78
- ])
79
- }}
80
- />
81
-
82
- <!-- ICON -->
83
- @if($icon)
84
- <x-icon :name="$icon" class="absolute top-1/2 -translate-y-1/2 left-3 text-gray-400 " />
85
- @endif
94
+ <!-- DISPLAY SELECTION -->
95
+ <span @if($single && $searchable) x-show="!focused" @endif x-transition>
96
+ <template x-for="item in selectedItems" :key="item.{{ $optionValue }}">
97
+ <span
98
+ x-text="item.{{ $optionLabel}}"
99
+ class="bg-base-200 hover:bg-base-300 rounded px-2 py-1 mr-2 font-semibold text-sm cursor-pointer"
100
+
101
+ @if(!$single)
102
+ @click.stop="toggle(item)"
103
+ @endif
104
+ >
105
+ </span>
106
+ </template>
107
+ </span>
108
+
109
+ <!-- INPUT -->
110
+ <input
111
+ x-transition
112
+ x-ref="inputSearch"
113
+ @focus="focused = true"
114
+ @blur="focused = false; $el.value = ''"
115
+ class="outline-none bg-transparent"
116
+
117
+ @if(!$searchable)
118
+ readonly
119
+ @else
120
+ wire:keydown.debounce="{{ $searchFunction }}($el.value);"
121
+ @endif
122
+ />
123
+ </div>
124
+
125
+ <!-- FAKE INPUT CONTAINER -->
126
+ <div class="select select-primary w-full peer-[.focused-input]:border-2"></div>
86
127
</div>
87
-
88
- <!-- OPTIONS CONTAINER -->
89
- <div x-show="open" @click="open = false" class="relative">
90
128
129
+
130
+ <!-- OPTIONS CONTAINER -->
131
+ <div x-show="open" class="relative" wire:key="options-container">
132
+
91
133
<!-- PROGRESS -->
92
134
<progress wire:loading.delay wire:target="search" class="progress absolute progress-primary h-0.5"></progress>
93
135
94
136
<!-- OPTIONS -->
95
137
@if($options->count() || $noResultText)
96
- <div class="absolute w-full bg-base-100 z-10 top-2 border border-base-300 shadow-xl cursor-pointer rounded-lg">
97
- @forelse($options as $option)
98
- <div
99
- @click="$wire.{{ $modelName() }} = {{ $option->{$optionValue} }}; display = '{{ $option->{$optionLabel} }}'"
100
- :class="$wire.{{ $modelName() }} == {{ $option->{$optionValue} }} && 'bg-base-200'"
138
+ <div class="absolute w-full bg-base-100 z-10 top-2 pb-0.5 border border-base-300 shadow-xl cursor-pointer rounded-lg">
139
+ @foreach($options as $option)
140
+ <div
141
+ wire:key="option-{{ $option->{$optionValue} }}"
142
+ @click="
143
+ $refs.inputSearch.value = '';
144
+ toggle({{ $option }});
145
+
146
+ @if($single)
147
+ open = false;
148
+ @endif
149
+
150
+ @if($searchable && !$single)
151
+ $refs.inputSearch.focus();
152
+ @endif
153
+ "
154
+ :class="
155
+ @if($single)
156
+ selection == {{ $option->{$optionValue} }} && 'bg-primary/5'
157
+ @else
158
+ selection.includes({{ $option->{$optionValue} }}) && 'bg-primary/5'
159
+ @endif
160
+ "
101
161
>
102
162
<!-- ITEM SLOT -->
103
163
@if($item)
@@ -106,15 +166,14 @@ class="bg-base-100 top-2 pt-1.5 h-8 @if($icon) left-8 @else left-3 @endif px-2 r
106
166
<x-list-item :item="$option" :value="$optionLabel" :sub-value="$optionSubLabel" :avatar="$optionAvatar" />
107
167
@endif
108
168
</div>
109
- @empty
110
- <div class="p-3">{{ $noResultText }}</div>
111
- @endforelse
169
+ @endforeach
170
+ @if($options->count() == 0 && $noResultText)
171
+ <div class="p-3">{{ $noResultText }}</div>
172
+ @endif
112
173
</div>
113
174
@endif
114
175
</div>
115
176
116
- <!-- HIDDEN SELECTED VALUE -->
117
- <input type="hidden" {{ $attributes->only('wire:model') }} />
118
177
119
178
<!-- ERROR -->
120
179
@error($modelName())
@@ -124,8 +183,9 @@ class="bg-base-100 top-2 pt-1.5 h-8 @if($icon) left-8 @else left-3 @endif px-2 r
124
183
<!-- HINT -->
125
184
@if($hint)
126
185
<div class="label-text-alt text-gray-400 p-1 pb-0">{{ $hint }}</div>
127
- @endif
186
+ @endif
128
187
</div>
188
+
129
189
</div>
130
190
HTML;
131
191
}
0 commit comments