11<script setup>
22import axios from ' axios' ;
3+ import fuzzysort from ' fuzzysort' ;
34import Fieldtype from ' @/components/fieldtypes/fieldtype.js' ;
45import { computed , ref , watch } from ' vue' ;
5- import { Combobox , Icon } from ' @/components/ui' ;
6+ import { Button , Combobox , Icon , Input , Modal } from ' @/components/ui' ;
67
78const emit = defineEmits (Fieldtype .emits );
89const props = defineProps (Fieldtype .props );
@@ -13,11 +14,14 @@ const loading = ref(true);
1314const loaders = ref ({});
1415const iconsCache = ref ({});
1516const cacheKey = computed (() => props .config .set ?? ' __default__' );
17+ const isCompact = computed (() => props .config .mode === ' compact' );
18+ const isOpen = ref (false );
19+ const search = ref (' ' );
1620
1721const options = computed (() => {
1822 let options = [];
1923
20- for (let [name, html] of Object .entries (icons .value )) {
24+ for (let [name, html] of Object .entries (icons .value || {} )) {
2125 options .push ({
2226 value: name,
2327 label: name,
@@ -28,6 +32,23 @@ const options = computed(() => {
2832 return options;
2933});
3034
35+ const selectedOption = computed (() => {
36+ return options .value .find ((option ) => option .value === props .value ) ?? null ;
37+ });
38+
39+ const noneOption = { value: null , label: ' No Icon' , none: true };
40+
41+ const filteredOptions = computed (() => {
42+ const matches = fuzzysort
43+ .go (search .value , options .value , {
44+ all: true ,
45+ key: ' label' ,
46+ })
47+ .map ((result ) => result .obj );
48+
49+ return search .value ? matches : [noneOption, ... matches];
50+ });
51+
3152function request () {
3253 if (loaders .value [cacheKey .value ]) return ;
3354
@@ -50,6 +71,20 @@ function comboboxUpdated(value) {
5071 update (value || null );
5172}
5273
74+ function openPicker () {
75+ if (isReadOnly .value || props .config .disabled ) return ;
76+
77+ isOpen .value = true ;
78+ }
79+
80+ function selectIcon (name ) {
81+ if (isReadOnly .value || props .config .disabled ) return ;
82+
83+ update (name);
84+ isOpen .value = false ;
85+ search .value = ' ' ;
86+ }
87+
5388watch (
5489 () => loaders .value [cacheKey .value ],
5590 (loadingState ) => {
@@ -58,12 +93,16 @@ watch(
5893 }
5994);
6095
96+ watch (isOpen, (open ) => {
97+ if (! open) search .value = ' ' ;
98+ });
99+
61100request ();
62101< / script>
63102
64103< template>
65104 < Combobox
66- v- if = " !loading"
105+ v- if = " !loading && !isCompact "
67106 clearable
68107 : disabled= " config.disabled"
69108 : model- value= " value"
@@ -95,4 +134,65 @@ request();
95134 < / div>
96135 < / template>
97136 < / Combobox>
137+
138+ < div v- else - if = " !loading" class = " flex items-center" >
139+ < Button
140+ icon- only
141+ size= " base"
142+ : aria- label= " value || __('Select Icon')"
143+ : disabled= " config.disabled"
144+ : read- only= " isReadOnly"
145+ : title= " value || undefined"
146+ @click= " openPicker"
147+ >
148+ < Icon v- if = " selectedOption && !selectedOption.html" : name= " selectedOption.label" / >
149+ < div
150+ v- else - if = " selectedOption?.html"
151+ v- html= " selectedOption.html"
152+ class = " flex items-center justify-center [&>svg]:size-4.5"
153+ / >
154+ < Icon v- else name= " plus" class = " opacity-40" / >
155+ < / Button>
156+
157+ < Modal
158+ v- model: open= " isOpen"
159+ : blur= " false"
160+ : title= " __('Select Icon')"
161+ class = " xl:max-w-3xl 2xl:max-w-page"
162+ >
163+ < Input
164+ v- model= " search"
165+ : placeholder= " __('Search...')"
166+ icon- prepend= " magnifying-glass"
167+ size= " sm"
168+ type= " text"
169+ / >
170+
171+ < div class = " st-custom-scrollbar max-h-[40vh] overflow-auto" >
172+ < div class = " grid grid-cols-6 gap-1 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12" >
173+ < button
174+ v- for = " option in filteredOptions"
175+ : key= " option.none ? '__none__' : option.value"
176+ type= " button"
177+ class = " flex size-9 items-center justify-center rounded-lg"
178+ : aria- label= " __(option.label)"
179+ : aria- pressed= " option.value === value"
180+ : class = " {
181+ 'bg-gray-100 dark:bg-gray-900': option.value === value,
182+ 'hover:bg-gray-100 dark:hover:bg-gray-900': option.value !== value,
183+ }"
184+ : title= " __(option.label)"
185+ @click= " selectIcon(option.value)"
186+ >
187+ < Icon v- if = " option.none" name= " x" class = " size-3.5 opacity-40" / >
188+ < Icon v- else - if = " !option.html" : name= " option.label" class = " size-5" / >
189+ < div v- else v- html= " option.html" class = " [&>svg]:size-5" / >
190+ < / button>
191+ < / div>
192+ < div v- if = " filteredOptions.length === 0" class = " p-3 text-center text-xs text-gray-600" >
193+ {{ search ? __ (' No results' ) : __ (' No icons available' ) }}
194+ < / div>
195+ < / div>
196+ < / Modal>
197+ < / div>
98198< / template>
0 commit comments