|
21 | 21 | import { cubicOut } from 'svelte/easing'; |
22 | 22 | import SearchIcon from '~icons/tabler/search'; |
23 | 23 | import Delete from '~icons/tabler/trash'; |
24 | | - import { initCap, getTimeAgo } from '$lib/stores/moonbase_utilities'; |
| 24 | + import { initCap } from '$lib/stores/moonbase_utilities'; |
25 | 25 |
|
26 | 26 | import EditObject from './EditObject.svelte'; |
27 | 27 | import { modals } from 'svelte-modals'; |
28 | 28 | import Grip from '~icons/tabler/grip-vertical'; |
| 29 | + import MultiInput from './MultiInput.svelte'; |
| 30 | + import { isNumber } from 'chart.js/helpers'; |
29 | 31 |
|
30 | 32 | let { property, data = $bindable(), definition, onChange, changeOnInput } = $props(); |
31 | 33 |
|
32 | 34 | let dataEditable: any = $state({}); |
33 | 35 |
|
34 | | - let propertyEditable: string = $state(''); |
| 36 | + // let propertyEditable: string = $state(''); |
| 37 | +
|
| 38 | + let searchQuery: string = $state(''); |
| 39 | + searchQuery = "!none"; |
35 | 40 |
|
36 | 41 | //if no records added yet, add an empty array |
37 | 42 | if (data[property.name] == undefined) { |
|
67 | 72 | } |
68 | 73 |
|
69 | 74 | function addItem(propertyName: string) { |
70 | | - propertyEditable = propertyName; |
| 75 | + // propertyEditable = propertyName; |
71 | 76 | //set the default values from the definition... |
72 | 77 | dataEditable = {}; |
73 | 78 |
|
|
114 | 119 | data[propertyName] = [...data[propertyName]]; //Trigger reactivity |
115 | 120 | onChange(); |
116 | 121 | } |
| 122 | +
|
| 123 | + // Filter items based on search query - returns array of {item, originalIndex} |
| 124 | + let filteredItems = $derived.by(() => { |
| 125 | + if (!searchQuery.trim()) { |
| 126 | + return data[property.name].map((item: any, index: number) => ({ |
| 127 | + item, |
| 128 | + originalIndex: index |
| 129 | + })); |
| 130 | + } |
| 131 | +
|
| 132 | + const isNegated = searchQuery.startsWith('!'); |
| 133 | + const query = (isNegated ? searchQuery.slice(1) : searchQuery).toLowerCase().trim(); |
| 134 | +
|
| 135 | + if (!query) { |
| 136 | + return data[property.name].map((item: any, index: number) => ({ |
| 137 | + item, |
| 138 | + originalIndex: index |
| 139 | + })); |
| 140 | + } |
| 141 | +
|
| 142 | + const filtered = data[property.name] |
| 143 | + .map((item: any, index: number) => ({ item, originalIndex: index })) |
| 144 | + .filter(({ item }: { item: any }) => { |
| 145 | + // Search through the first 3 visible fields |
| 146 | + const matchFound = property.n.slice(0, 3).some((propertyN: any) => { |
| 147 | + let valueStr; |
| 148 | +
|
| 149 | + // Check dropdown - only check the SELECTED option's label, not all options |
| 150 | + if ( |
| 151 | + propertyN.values && |
| 152 | + Array.isArray(propertyN.values) && |
| 153 | + isNumber(item[propertyN.name]) |
| 154 | + ) { |
| 155 | + valueStr = propertyN.values[item[propertyN.name]]; |
| 156 | + } else { |
| 157 | + valueStr = item[propertyN.name]; |
| 158 | + } |
| 159 | +
|
| 160 | + return String(valueStr).toLowerCase().includes(query); |
| 161 | + }); |
| 162 | +
|
| 163 | + // If negated (!), return items that DON'T match |
| 164 | + return isNegated ? !matchFound : matchFound; |
| 165 | + }); |
| 166 | +
|
| 167 | + return filtered; |
| 168 | + }); |
117 | 169 | </script> |
118 | 170 |
|
119 | 171 | <div class="divider mb-2 mt-0"></div> |
|
136 | 188 | > |
137 | 189 | </div> |
138 | 190 |
|
| 191 | +<!-- Search Filter --> |
| 192 | +{#if data[property.name].length > 0} |
| 193 | + <div class="mb-3"> |
| 194 | + <div class="relative"> |
| 195 | + <SearchIcon class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-base-content/50" /> |
| 196 | + <input |
| 197 | + type="text" |
| 198 | + bind:value={searchQuery} |
| 199 | + placeholder="Search... (prefix with ! to exclude)" |
| 200 | + class="input input-bordered w-full input-sm" |
| 201 | + /> |
| 202 | + {#if searchQuery} |
| 203 | + <button |
| 204 | + class="btn btn-ghost btn-sm absolute right-1 top-1/2 -translate-y-1/2" |
| 205 | + onclick={() => (searchQuery = '')} |
| 206 | + > |
| 207 | + ✕ |
| 208 | + </button> |
| 209 | + {/if} |
| 210 | + </div> |
| 211 | + {#if searchQuery.trim()} |
| 212 | + <div class="text-sm text-base-content/60 mt-1 ml-1"> |
| 213 | + {filteredItems.length} of {data[property.name].length} items |
| 214 | + {searchQuery.startsWith('!') ? '(excluding matches)' : ''} |
| 215 | + </div> |
| 216 | + {/if} |
| 217 | + </div> |
| 218 | +{/if} |
| 219 | + |
139 | 220 | <div class="overflow-x-auto space-y-1" transition:slide|local={{ duration: 300, easing: cubicOut }}> |
140 | | - <DraggableList items={data[property.name]} onReorder={handleReorder} class="space-y-2"> |
141 | | - {#snippet children({ item: item, index }: { item: any; index: number })} |
| 221 | + <DraggableList items={filteredItems} onReorder={handleReorder} class="space-y-2"> |
| 222 | + {#snippet children({ item: itemWrapper, index }: { item: any; index: number })} |
142 | 223 | <!-- svelte-ignore a11y_click_events_have_key_events --> |
143 | 224 | <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> |
144 | 225 | <Grip class="h-6 w-6 text-base-content/30 cursor-grab flex-shrink-0" /> |
145 | 226 | <!-- Show the first 3 fields --> |
146 | 227 | {#each property.n.slice(0, 3) as propertyN} |
147 | | - {#if propertyN.type == 'time'} |
148 | | - <div> |
149 | | - <div class="font-bold"> |
150 | | - {getTimeAgo(item[propertyN.name], currentTime)} |
151 | | - </div> |
152 | | - </div> |
153 | | - {:else if propertyN.type == 'coord3D'} |
154 | | - <div> |
155 | | - <div class="font-bold"> |
156 | | - ({item[propertyN.name].x}, {item[propertyN.name].y}, {item[propertyN.name].z}) |
157 | | - </div> |
158 | | - </div> |
159 | | - {:else if propertyN.type != 'array' && propertyN.type != 'controls' && propertyN.type != 'password'} |
160 | | - <div> |
161 | | - <div class="font-bold">{item[propertyN.name]}</div> |
162 | | - </div> |
| 228 | + {#if propertyN.type != 'array' && propertyN.type != 'controls' && propertyN.type != 'password'} |
| 229 | + <MultiInput |
| 230 | + property={propertyN} |
| 231 | + bind:value={itemWrapper.item[propertyN.name]} |
| 232 | + noPrompts={true} |
| 233 | + onChange={(event) => { |
| 234 | + onChange(event); |
| 235 | + }} |
| 236 | + ></MultiInput> |
163 | 237 | {/if} |
164 | 238 | {/each} |
| 239 | + <!-- Show nr of controls --> |
165 | 240 | {#each property.n as propertyN} |
166 | 241 | {#if propertyN.type == 'array' || propertyN.type == 'controls'} |
167 | 242 | <div> |
168 | 243 | <div class="font-bold"> |
169 | | - ↓{item[propertyN.name] ? item[propertyN.name].length : ''} |
| 244 | + ↓{itemWrapper.item[propertyN.name] ? itemWrapper.item[propertyN.name].length : ''} |
170 | 245 | </div> |
171 | 246 | </div> |
172 | 247 | {/if} |
|
177 | 252 | <button |
178 | 253 | class="btn btn-ghost btn-sm" |
179 | 254 | onclick={() => { |
180 | | - handleEdit(property.name, index); |
| 255 | + handleEdit(property.name, itemWrapper.originalIndex); |
181 | 256 | }} |
182 | 257 | > |
183 | 258 | <SearchIcon class="h-6 w-6" /></button |
184 | 259 | > |
185 | 260 | <button |
186 | 261 | class="btn btn-ghost btn-sm" |
187 | 262 | onclick={() => { |
188 | | - deleteItem(property.name, index); |
| 263 | + deleteItem(property.name, itemWrapper.originalIndex); |
189 | 264 | }} |
190 | 265 | > |
191 | 266 | <Delete class="text-error h-6 w-6" /> |
|
0 commit comments