@poirazis/supercomponents-shared 1.0.26 → 1.0.28

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.
@@ -0,0 +1,597 @@
1
+ <script>
2
+ import { createEventDispatcher, getContext, onMount } from "svelte";
3
+ import SuperPopover from "../SuperPopover/SuperPopover.svelte";
4
+ import { DatePicker } from "date-picker-svelte";
5
+ const dispatch = new createEventDispatcher();
6
+ const { processStringSync } = getContext("sdk");
7
+ import fsm from "svelte-fsm";
8
+
9
+ export let value;
10
+ export let formattedValue;
11
+ export let cellOptions;
12
+ export let autofocus;
13
+
14
+ let originalValue;
15
+
16
+ export let cellState = fsm("View", {
17
+ "*": {
18
+ goTo(state) {
19
+ return state;
20
+ },
21
+ },
22
+ View: {
23
+ focus() {
24
+ if (!cellOptions.readonly) return "Editing";
25
+ },
26
+ },
27
+ Hovered: {
28
+ cancel: () => {
29
+ return "View";
30
+ },
31
+ },
32
+ Focused: {
33
+ unfocus() {
34
+ return "View";
35
+ },
36
+ },
37
+ Error: { check: "View" },
38
+ Editing: {
39
+ _enter() {
40
+ originalValue = value ? { ...value } : null;
41
+ open = true;
42
+ dispatch("enteredit");
43
+ },
44
+ _exit() {
45
+ dispatch("exitedit");
46
+ },
47
+ handleKeyboard(e) {
48
+ if (e.keyCode == 32) {
49
+ e.stopPropagation();
50
+ e.preventDefault();
51
+ open = !open;
52
+ }
53
+ },
54
+ focusout(e) {
55
+ const isInPicker = picker?.contains(e.relatedTarget);
56
+ const isInFromTime = fromTimePicker?.contains
57
+ ? fromTimePicker?.contains(e.relatedTarget)
58
+ : false;
59
+ const isInToTime = toTimePicker?.contains
60
+ ? toTimePicker?.contains(e.relatedTarget)
61
+ : false;
62
+
63
+ if (!isInPicker && !isInFromTime && !isInToTime) {
64
+ open = false;
65
+ if (JSON.stringify(value) != JSON.stringify(originalValue)) {
66
+ dispatch("change", value);
67
+ }
68
+ return "View";
69
+ }
70
+ },
71
+ cancel() {
72
+ value = originalValue ? { ...originalValue } : null;
73
+ return "View";
74
+ },
75
+ },
76
+ });
77
+
78
+ let anchor;
79
+ let picker;
80
+ let popup;
81
+ let open = false;
82
+ let fromPicker;
83
+ let toPicker;
84
+ let fromTimePicker;
85
+ let toTimePicker;
86
+
87
+ // Current date format and showTime - extracted to ensure reactivity
88
+ $: currentDateFormat = cellOptions?.dateFormat;
89
+ $: currentShowTime = cellOptions?.showTime;
90
+
91
+ // Time values (format: HH:mm)
92
+ $: fromTime = currentShowTime && value?.fromTime ? value.fromTime : "00:00";
93
+ $: toTime = currentShowTime && value?.toTime ? value.toTime : "00:00";
94
+
95
+ $: fromDate = value?.from ? new Date(value.from) : new Date();
96
+ $: toDate = value?.to ? new Date(value.to) : new Date();
97
+
98
+ // Combine date and time for validation
99
+ $: fromDateTime =
100
+ currentShowTime && value?.from
101
+ ? new Date(`${fromDate.toISOString().split("T")[0]}T${fromTime}`)
102
+ : fromDate;
103
+ $: toDateTime =
104
+ currentShowTime && value?.to
105
+ ? new Date(`${toDate.toISOString().split("T")[0]}T${toTime}`)
106
+ : toDate;
107
+
108
+ // DateTime formatting helper function
109
+ function formatDateRangeWithTime(
110
+ fromDate,
111
+ toDate,
112
+ dateFormat,
113
+ fromTime,
114
+ toTime,
115
+ showTime
116
+ ) {
117
+ if (!showTime) {
118
+ return {
119
+ from: formatDateRange(fromDate, fromDate, dateFormat).from,
120
+ to: formatDateRange(toDate, toDate, dateFormat).to,
121
+ };
122
+ }
123
+
124
+ const fromFormatted =
125
+ formatDateRange(fromDate, fromDate, dateFormat).from + " " + fromTime;
126
+ const toFormatted =
127
+ formatDateRange(toDate, toDate, dateFormat).to + " " + toTime;
128
+
129
+ return { from: fromFormatted, to: toFormatted };
130
+ }
131
+
132
+ // Date formatting helper function
133
+ function formatDateRange(fromDate, toDate, dateFormat) {
134
+ console.log("🔍 formatDateRange called with:", {
135
+ fromDate,
136
+ toDate,
137
+ dateFormat,
138
+ });
139
+
140
+ if (!dateFormat || dateFormat === "default") {
141
+ console.log("➡️ Using default format");
142
+ return {
143
+ from: fromDate.toDateString(),
144
+ to: toDate.toDateString(),
145
+ };
146
+ }
147
+
148
+ // Manual formatting for specific formats to ensure exact order
149
+ if (dateFormat === "MM/DD/YYYY") {
150
+ const fromFormatted = `${(fromDate.getMonth() + 1).toString().padStart(2, "0")}/${fromDate.getDate().toString().padStart(2, "0")}/${fromDate.getFullYear()}`;
151
+ const toFormatted = `${(toDate.getMonth() + 1).toString().padStart(2, "0")}/${toDate.getDate().toString().padStart(2, "0")}/${toDate.getFullYear()}`;
152
+ console.log("✅ MM/DD/YYYY formatted:", {
153
+ from: fromFormatted,
154
+ to: toFormatted,
155
+ });
156
+ return { from: fromFormatted, to: toFormatted };
157
+ }
158
+
159
+ if (dateFormat === "DD/MM/YYYY") {
160
+ const fromFormatted = `${fromDate.getDate().toString().padStart(2, "0")}/${(fromDate.getMonth() + 1).toString().padStart(2, "0")}/${fromDate.getFullYear()}`;
161
+ const toFormatted = `${toDate.getDate().toString().padStart(2, "0")}/${(toDate.getMonth() + 1).toString().padStart(2, "0")}/${toDate.getFullYear()}`;
162
+ console.log("✅ DD/MM/YYYY formatted:", {
163
+ from: fromFormatted,
164
+ to: toFormatted,
165
+ });
166
+ return { from: fromFormatted, to: toFormatted };
167
+ }
168
+
169
+ if (dateFormat === "YYYY-MM-DD") {
170
+ const fromFormatted = `${fromDate.getFullYear()}-${(fromDate.getMonth() + 1).toString().padStart(2, "0")}-${fromDate.getDate().toString().padStart(2, "0")}`;
171
+ const toFormatted = `${toDate.getFullYear()}-${(toDate.getMonth() + 1).toString().padStart(2, "0")}-${toDate.getDate().toString().padStart(2, "0")}`;
172
+ console.log("✅ YYYY-MM-DD formatted:", {
173
+ from: fromFormatted,
174
+ to: toFormatted,
175
+ });
176
+ return { from: fromFormatted, to: toFormatted };
177
+ }
178
+
179
+ // For shorter formats, still use locale-specific formatting
180
+ const options = {
181
+ "MMM DD, YYYY": {
182
+ month: "short",
183
+ day: "numeric",
184
+ year: "numeric",
185
+ timeZone: "UTC",
186
+ },
187
+ "DD MMM YYYY": {
188
+ day: "numeric",
189
+ month: "short",
190
+ year: "numeric",
191
+ timeZone: "UTC",
192
+ },
193
+ };
194
+
195
+ const format = options[dateFormat];
196
+ console.log("📅 Format object:", format);
197
+
198
+ if (format) {
199
+ const fromFormatted = fromDate.toLocaleDateString("en-US", format);
200
+ const toFormatted = toDate.toLocaleDateString("en-US", format);
201
+ console.log("✅ Formatted result:", {
202
+ from: fromFormatted,
203
+ to: toFormatted,
204
+ });
205
+ return {
206
+ from: fromFormatted,
207
+ to: toFormatted,
208
+ };
209
+ }
210
+
211
+ console.log("⚠️ Format not found, using fallback");
212
+ // Fallback to default
213
+ return {
214
+ from: fromDate.toDateString(),
215
+ to: toDate.toDateString(),
216
+ };
217
+ }
218
+
219
+ // Ensure range is valid (from date shouldn't be after to date)
220
+ $: isValidRange = (() => {
221
+ if (!value?.from || !value?.to) return true;
222
+ if (currentShowTime) {
223
+ return fromDateTime <= toDateTime;
224
+ }
225
+ return fromDate <= toDate;
226
+ })();
227
+
228
+ $: formattedValue =
229
+ cellOptions.template && value
230
+ ? processStringSync(cellOptions.template, { value })
231
+ : undefined;
232
+
233
+ $: rangeDisplay = (() => {
234
+ if (formattedValue) return formattedValue;
235
+ if (!value?.from && !value?.to) return placeholder;
236
+
237
+ const dateFormat = currentDateFormat; // Use the extracted reactive format
238
+ let fromFormatted = placeholder;
239
+ let toFormatted = placeholder;
240
+
241
+ if (value?.from) {
242
+ if (dateFormat === "default" || !dateFormat) {
243
+ fromFormatted = currentShowTime
244
+ ? fromDate.toDateString() + " " + fromTime
245
+ : fromDate.toDateString();
246
+ } else {
247
+ if (currentShowTime) {
248
+ const formatted = formatDateRangeWithTime(
249
+ fromDate,
250
+ fromDate,
251
+ dateFormat,
252
+ fromTime,
253
+ fromTime,
254
+ true
255
+ );
256
+ fromFormatted = formatted.from;
257
+ } else {
258
+ const formatted = formatDateRange(fromDate, fromDate, dateFormat);
259
+ fromFormatted = formatted.from;
260
+ }
261
+ }
262
+ }
263
+
264
+ if (value?.to) {
265
+ if (dateFormat === "default" || !dateFormat) {
266
+ toFormatted = currentShowTime
267
+ ? toDate.toDateString() + " " + toTime
268
+ : toDate.toDateString();
269
+ } else {
270
+ if (currentShowTime) {
271
+ const formatted = formatDateRangeWithTime(
272
+ toDate,
273
+ toDate,
274
+ dateFormat,
275
+ toTime,
276
+ toTime,
277
+ true
278
+ );
279
+ toFormatted = formatted.to;
280
+ } else {
281
+ const formatted = formatDateRange(toDate, toDate, dateFormat);
282
+ toFormatted = formatted.to;
283
+ }
284
+ }
285
+ }
286
+
287
+ if (value?.from && value?.to) {
288
+ return `${fromFormatted} - ${toFormatted}`;
289
+ } else if (value?.from) {
290
+ return `${fromFormatted} - [Select end date]`;
291
+ } else if (value?.to) {
292
+ return `[Select start date] - ${toFormatted}`;
293
+ }
294
+
295
+ return placeholder;
296
+ })();
297
+
298
+ $: placeholder =
299
+ cellOptions.readonly || cellOptions.disabled
300
+ ? ""
301
+ : cellOptions.placeholder || "Select date range";
302
+
303
+ $: inEdit = $cellState == "Editing";
304
+ $: inline = cellOptions.role == "inlineInput";
305
+ $: isDirty = inEdit && JSON.stringify(value) != JSON.stringify(originalValue);
306
+
307
+ onMount(() => {
308
+ if (autofocus)
309
+ setTimeout(() => {
310
+ cellState.focus();
311
+ }, 30);
312
+ });
313
+
314
+ const handleFromDateChange = (e) => {
315
+ const newFromDate = e.detail;
316
+ value = {
317
+ ...value,
318
+ from: newFromDate,
319
+ };
320
+
321
+ // If to date exists and is before from date, clear it
322
+ if (value.to && new Date(value.to) < newFromDate) {
323
+ value = {
324
+ ...value,
325
+ to: null,
326
+ };
327
+ }
328
+
329
+ dispatch("change", value);
330
+ };
331
+
332
+ const handleToDateChange = (e) => {
333
+ const newToDate = e.detail;
334
+ value = {
335
+ ...value,
336
+ to: newToDate,
337
+ };
338
+
339
+ dispatch("change", value);
340
+ };
341
+
342
+ const handleFromTimeChange = (e) => {
343
+ const newFromTime = e.target.value;
344
+ value = {
345
+ ...value,
346
+ fromTime: newFromTime,
347
+ };
348
+
349
+ dispatch("change", value);
350
+ };
351
+
352
+ const handleToTimeChange = (e) => {
353
+ const newToTime = e.target.value;
354
+ value = {
355
+ ...value,
356
+ toTime: newToTime,
357
+ };
358
+
359
+ dispatch("change", value);
360
+ };
361
+
362
+ const clearRange = () => {
363
+ value = null;
364
+ dispatch("change", null);
365
+ };
366
+
367
+ $: console.log(cellOptions);
368
+ </script>
369
+
370
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
371
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
372
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
373
+ <div
374
+ bind:this={anchor}
375
+ class="superCell"
376
+ tabindex={cellOptions.readonly || cellOptions.disabled ? "-1" : "0"}
377
+ class:inEdit
378
+ class:isDirty={isDirty && cellOptions.showDirty}
379
+ class:inline
380
+ class:tableCell={cellOptions.role == "tableCell"}
381
+ class:formInput={cellOptions.role == "formInput"}
382
+ class:disabled={cellOptions.disabled}
383
+ class:readonly={cellOptions.readonly}
384
+ class:error={cellOptions.error}
385
+ style:color={cellOptions.color}
386
+ style:background={cellOptions.background}
387
+ style:font-weight={cellOptions.fontWeight}
388
+ on:focus={cellState.focus}
389
+ on:keypress={cellState.handleKeyboard}
390
+ on:focusout={cellState.focusout}
391
+ >
392
+ {#if cellOptions.icon}
393
+ <i class={cellOptions.icon + " icon"}></i>
394
+ {/if}
395
+
396
+ {#if inEdit}
397
+ <div
398
+ class="editor"
399
+ class:with-icon={cellOptions.icon}
400
+ class:placeholder={!value}
401
+ on:click={() => (open = !open)}
402
+ >
403
+ <span>{rangeDisplay}</span>
404
+ <i
405
+ class="ri-calendar-line"
406
+ style="font-size: 16px; justify-self: flex-end"
407
+ ></i>
408
+
409
+ {#if value && cellOptions.showDirty != false}
410
+ <button
411
+ class="clear-button ri-close-line"
412
+ style="font-size: 14px; color: var(--spectrum-global-color-gray-500);"
413
+ on:click|stopPropagation={clearRange}
414
+ aria-label="Clear date range"
415
+ ></button>
416
+ {/if}
417
+ </div>
418
+ {:else}
419
+ <div
420
+ class="value"
421
+ class:with-icon={cellOptions.icon}
422
+ class:placeholder={!value}
423
+ style:justify-content={cellOptions.align}
424
+ >
425
+ <span>
426
+ {#if rangeDisplay}
427
+ {rangeDisplay}
428
+ {:else}
429
+ {placeholder}
430
+ {/if}
431
+ </span>
432
+ </div>
433
+ {/if}
434
+ </div>
435
+
436
+ {#if inEdit}
437
+ <SuperPopover {anchor} dismissible={false} {open} align="right">
438
+ <div
439
+ bind:this={picker}
440
+ class="range-picker-container"
441
+ style:--date-picker-background="var(--spectrum-global-color-gray-75)"
442
+ style:--date-picker-foreground="var(--spectrum-global-color-gray-800)"
443
+ style:--date-picker-selected-background="var(--accent-color)"
444
+ >
445
+ <div class="datepickers-container">
446
+ <div class="range-section">
447
+ <!-- svelte-ignore a11y-label-has-associated-control -->
448
+ <label class="range-label">From:</label>
449
+ <div bind:this={fromPicker}>
450
+ <DatePicker value={fromDate} on:select={handleFromDateChange} />
451
+ </div>
452
+
453
+ {#if currentShowTime}
454
+ <div class="time-section">
455
+ <!-- svelte-ignore a11y-label-has-associated-control -->
456
+ <input
457
+ bind:this={fromTimePicker}
458
+ type="time"
459
+ bind:value={fromTime}
460
+ on:change={handleFromTimeChange}
461
+ class="time-input"
462
+ step="900"
463
+ />
464
+ </div>
465
+ {/if}
466
+ </div>
467
+
468
+ <div class="range-section">
469
+ <!-- svelte-ignore a11y-label-has-associated-control -->
470
+ <label class="range-label">To:</label>
471
+ <div bind:this={toPicker}>
472
+ <DatePicker
473
+ value={toDate}
474
+ on:select={handleToDateChange}
475
+ min={fromDate}
476
+ />
477
+ </div>
478
+
479
+ {#if currentShowTime}
480
+ <div class="time-section">
481
+ <!-- svelte-ignore a11y-label-has-associated-control -->
482
+ <input
483
+ bind:this={toTimePicker}
484
+ type="time"
485
+ bind:value={toTime}
486
+ on:change={handleToTimeChange}
487
+ class="time-input"
488
+ step="900"
489
+ />
490
+ </div>
491
+ {/if}
492
+ </div>
493
+ </div>
494
+
495
+ {#if !isValidRange}
496
+ <div class="range-error">
497
+ <i
498
+ class="ri-error-warning-line"
499
+ style="color: var(--spectrum-global-color-red-500);"
500
+ ></i>
501
+ <span
502
+ style="color: var(--spectrum-global-color-red-500); font-size: 12px;"
503
+ >
504
+ End date cannot be before start date
505
+ </span>
506
+ </div>
507
+ {/if}
508
+ </div>
509
+ </SuperPopover>
510
+ {/if}
511
+
512
+ <style>
513
+ .range-picker-container {
514
+ display: flex;
515
+ flex-direction: column;
516
+ gap: 1rem;
517
+ padding: 0.5rem;
518
+ min-width: 300px;
519
+ }
520
+
521
+ .datepickers-container {
522
+ display: flex;
523
+ flex-direction: row;
524
+ gap: 1rem;
525
+ }
526
+
527
+ .range-section {
528
+ display: flex;
529
+ flex-direction: column;
530
+ gap: 8px;
531
+ }
532
+
533
+ .range-label {
534
+ font-size: 14px;
535
+ font-weight: 500;
536
+ color: var(--spectrum-global-color-gray-800);
537
+ }
538
+
539
+ .time-section {
540
+ display: flex;
541
+ flex-direction: column;
542
+ gap: 4px;
543
+ }
544
+
545
+ .time-label {
546
+ font-size: 12px;
547
+ font-weight: 500;
548
+ color: var(--spectrum-global-color-gray-700);
549
+ }
550
+
551
+ .time-input {
552
+ padding: 6px 8px;
553
+ border: 1px solid var(--spectrum-global-color-gray-300);
554
+ border-radius: 4px;
555
+ font-size: 14px;
556
+ min-width: 120px;
557
+ background: var(--spectrum-global-color-gray-50);
558
+ color: var(--spectrum-global-color-gray-800);
559
+ }
560
+
561
+ .time-input:focus {
562
+ outline: none;
563
+ border-color: var(--spectrum-global-color-blue-500);
564
+ box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.2);
565
+ }
566
+
567
+ .range-error {
568
+ display: flex;
569
+ align-items: center;
570
+ gap: 6px;
571
+ padding: 8px;
572
+ background: var(--spectrum-global-color-red-200);
573
+ border-radius: 4px;
574
+ }
575
+
576
+ .clear-button {
577
+ background: none;
578
+ border: none;
579
+ cursor: pointer;
580
+ padding: 4px;
581
+ border-radius: 2px;
582
+ transition: all 0.2s ease;
583
+ margin-left: 8px;
584
+ }
585
+
586
+ .clear-button:hover {
587
+ background-color: var(--spectrum-global-color-gray-200);
588
+ }
589
+
590
+ .editor {
591
+ display: flex;
592
+ align-items: center;
593
+ justify-content: space-between;
594
+ cursor: pointer;
595
+ padding: 8px 4px;
596
+ }
597
+ </style>