mertani-web-toolkit 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Icon from '../../Icon/Icon.svelte';
3
+ import { SvelteDate } from 'svelte/reactivity';
3
4
  import './DatePicker.css';
4
5
 
5
6
  interface Props {
@@ -32,8 +33,8 @@
32
33
  placement?: 'top' | 'bottom' | 'auto';
33
34
 
34
35
  // Events
35
- onSelect?: (value: Date) => void;
36
- onChange?: (value: Date) => void;
36
+ onSelect?: (value: Date | null) => void;
37
+ onChange?: (value: Date | null) => void;
37
38
 
38
39
  // Validation
39
40
  isMandatory?: boolean;
@@ -243,7 +244,7 @@
243
244
 
244
245
  const dates = $derived(() => {
245
246
  const cells: DateCell[] = [];
246
- const cur = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), 1);
247
+ const cur = new SvelteDate(calendarDate.getFullYear(), calendarDate.getMonth(), 1);
247
248
  if (cur.getDay() > 0) cur.setDate(cur.getDate() - cur.getDay());
248
249
 
249
250
  for (let i = 0; i < 42; i++) {
@@ -287,6 +288,15 @@
287
288
  error = validateInput(newDate);
288
289
  }
289
290
 
291
+ function clearDate() {
292
+ selectedDate = null;
293
+ value = null;
294
+ show = false;
295
+ error = validateInput(null);
296
+ onSelect?.(null);
297
+ onChange?.(null);
298
+ }
299
+
290
300
  function goToToday() {
291
301
  calendarDate = new Date(globalNow.getFullYear(), globalNow.getMonth(), 1);
292
302
  selectDate({
@@ -374,7 +384,34 @@
374
384
  <span class:placeholder={!displayValue}>
375
385
  {displayValue ?? placeholder}
376
386
  </span>
377
- <Icon name="bs-calendar2-date" />
387
+ <span class="date-picker-actions">
388
+ {#if displayValue && !disabled && !isLoading}
389
+ <span
390
+ class="date-picker-clear"
391
+ role="button"
392
+ tabindex="0"
393
+ aria-label="Clear date"
394
+ onmousedown={(e: MouseEvent) => {
395
+ e.preventDefault();
396
+ e.stopPropagation();
397
+ }}
398
+ onclick={(e: MouseEvent) => {
399
+ e.stopPropagation();
400
+ clearDate();
401
+ }}
402
+ onkeydown={(e: KeyboardEvent) => {
403
+ if (e.key === 'Enter' || e.key === ' ') {
404
+ e.preventDefault();
405
+ e.stopPropagation();
406
+ clearDate();
407
+ }
408
+ }}
409
+ >
410
+ <Icon name="bs-x-lg" />
411
+ </span>
412
+ {/if}
413
+ <Icon name="bs-calendar2-date" />
414
+ </span>
378
415
  </button>
379
416
 
380
417
  {#if show}
@@ -391,7 +428,7 @@
391
428
 
392
429
  <div class="calendar-body">
393
430
  <div class="date-grid">
394
- {#each DAY_LABELS[locale] as day}
431
+ {#each DAY_LABELS[locale] as day, i (i)}
395
432
  <p class="day-label">{day}</p>
396
433
  {/each}
397
434
  {#each dates() as cell (cell.year + '-' + cell.month + '-' + cell.date)}
@@ -470,4 +507,33 @@
470
507
  background: #f6f8fa;
471
508
  }
472
509
  }
510
+
511
+ .date-picker-trigger {
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: space-between;
515
+ gap: 8px;
516
+ }
517
+
518
+ .date-picker-actions {
519
+ display: inline-flex;
520
+ align-items: center;
521
+ gap: 8px;
522
+ flex: 0 0 auto;
523
+ }
524
+
525
+ .date-picker-clear {
526
+ display: inline-flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ width: 24px;
530
+ height: 24px;
531
+ border-radius: 6px;
532
+ opacity: 0.8;
533
+ }
534
+
535
+ .date-picker-clear:hover {
536
+ background: rgba(0, 0, 0, 0.06);
537
+ opacity: 1;
538
+ }
473
539
  </style>
@@ -20,8 +20,8 @@ interface Props {
20
20
  minDate?: Date | string | null;
21
21
  maxDate?: Date | string | null;
22
22
  placement?: 'top' | 'bottom' | 'auto';
23
- onSelect?: (value: Date) => void;
24
- onChange?: (value: Date) => void;
23
+ onSelect?: (value: Date | null) => void;
24
+ onChange?: (value: Date | null) => void;
25
25
  isMandatory?: boolean;
26
26
  customValidation?: (value: Date | null) => string | null;
27
27
  isLoading?: boolean;
@@ -57,6 +57,8 @@
57
57
  emptyMessage?: string;
58
58
  closeOnSelect?: boolean;
59
59
  loadingSuggestions?: boolean;
60
+ /** Jika false, suggestions tidak difilter di client (untuk data dari API yang sudah difilter) */
61
+ filterSuggestionsClientSide?: boolean;
60
62
 
61
63
  // Events
62
64
  onSelectSuggestion?: (value: string, option: SuggestionOption) => void;
@@ -126,6 +128,7 @@
126
128
  emptyMessage = 'Tidak ada hasil',
127
129
  closeOnSelect = true,
128
130
  loadingSuggestions = false,
131
+ filterSuggestionsClientSide = true,
129
132
 
130
133
  // Events
131
134
  onSelectSuggestion,
@@ -155,15 +158,15 @@
155
158
  ...props
156
159
  }: Props = $props();
157
160
 
158
- let inputValue = $state(value);
159
- let errorMessage = $state('');
160
- let isFocused = $state(false);
161
- let showList = $state(false);
162
- let activeIndex = $state(-1);
163
- let wrapperEl: HTMLDivElement | null = $state(null);
164
- let inputEl: HTMLInputElement | null = $state(null);
165
- let searchByValue = $state(searchBy);
166
- let searchByOpen = $state(false);
161
+ let inputValue = $derived(value);
162
+ let errorMessage = $derived('');
163
+ let isFocused = $derived(false);
164
+ let showList = $derived(false);
165
+ let activeIndex = $derived(-1);
166
+ let wrapperEl: HTMLDivElement | null = $derived(null);
167
+ let inputEl: HTMLInputElement | null = $derived(null);
168
+ let searchByValue = $derived(searchBy);
169
+ let searchByOpen = $derived(false);
167
170
 
168
171
  $effect(() => {
169
172
  inputValue = value;
@@ -180,6 +183,11 @@
180
183
  }
181
184
  });
182
185
 
186
+ $effect(() => {
187
+ suggestions = suggestions;
188
+ activeIndex = -1;
189
+ });
190
+
183
191
  const sizeConfig = $derived(() => {
184
192
  switch (size) {
185
193
  case 48:
@@ -309,12 +317,17 @@
309
317
  return classes.join(' ');
310
318
  });
311
319
 
312
- function normalizeSuggestion(option: SuggestionOption) {
320
+ function normalizeSuggestion(option: SuggestionOption): {
321
+ label: string;
322
+ value: string;
323
+ raw: SuggestionOption;
324
+ } {
313
325
  if (typeof option === 'string') {
314
326
  return { label: option, value: option, raw: option };
315
327
  }
316
- const val = option.value ?? option.label;
317
- return { label: option.label, value: val, raw: option };
328
+ const val = String(option.value ?? option.id ?? option.label ?? '');
329
+ const lbl = String(option.label ?? option.value ?? option.id ?? '');
330
+ return { label: lbl, value: val, raw: option };
318
331
  }
319
332
 
320
333
  const filteredSuggestions = $derived(() => {
@@ -325,9 +338,10 @@
325
338
  return [];
326
339
  }
327
340
 
328
- const result = query
329
- ? normalized.filter((item) => item.label.toLowerCase().includes(query))
330
- : normalized;
341
+ const result =
342
+ filterSuggestionsClientSide && query
343
+ ? normalized.filter((item) => item.label.toLowerCase().includes(query))
344
+ : normalized;
331
345
  return result.slice(0, maxSuggestions);
332
346
  });
333
347
 
@@ -388,12 +402,6 @@
388
402
  oninput?.(e);
389
403
  }
390
404
 
391
- function handleSearchByChange(e: Event) {
392
- const target = e.target as HTMLSelectElement;
393
- searchByValue = target.value;
394
- searchBy = target.value;
395
- }
396
-
397
405
  function selectSearchBy(option: SearchByOption) {
398
406
  searchByValue = option.value;
399
407
  searchBy = option.value;
@@ -469,12 +477,15 @@
469
477
  }
470
478
 
471
479
  $effect(() => {
472
- if (showList) {
473
- setTimeout(() => document.addEventListener('click', handleClickOutside), 10);
474
- } else {
480
+ if (!showList) {
475
481
  document.removeEventListener('click', handleClickOutside);
482
+ return;
476
483
  }
477
- return () => document.removeEventListener('click', handleClickOutside);
484
+ const tid = setTimeout(() => document.addEventListener('click', handleClickOutside), 10);
485
+ return () => {
486
+ clearTimeout(tid);
487
+ document.removeEventListener('click', handleClickOutside);
488
+ };
478
489
  });
479
490
  </script>
480
491
 
@@ -42,6 +42,8 @@ interface Props extends HTMLInputAttributes {
42
42
  emptyMessage?: string;
43
43
  closeOnSelect?: boolean;
44
44
  loadingSuggestions?: boolean;
45
+ /** Jika false, suggestions tidak difilter di client (untuk data dari API yang sudah difilter) */
46
+ filterSuggestionsClientSide?: boolean;
45
47
  onSelectSuggestion?: (value: string, option: SuggestionOption) => void;
46
48
  onclick?: (event: MouseEvent) => void;
47
49
  oninput?: (event: Event) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mertani-web-toolkit",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "homepage": "https://storybook.mertani.com/",
5
5
  "scripts": {
6
6
  "dev": "vite dev",