sv5ui-plus 5.0.2 → 5.0.3
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.
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
</script>
|
|
3
3
|
|
|
4
4
|
<script lang="ts">import { Select } from "sv5ui";
|
|
5
|
-
|
|
5
|
+
// default เป็น string ว่าง — อย่าใช้ $bindable() เปล่า (UNINITIALIZED ตอน async)
|
|
6
|
+
let { name, value = $bindable(""), disabled = false, ...selectProps } = $props();
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<Select {...selectProps} bind:value {disabled} />
|
|
@@ -1,48 +1,91 @@
|
|
|
1
1
|
<script lang="ts">import { twMerge } from "tailwind-merge";
|
|
2
|
-
import { onMount } from "svelte";
|
|
2
|
+
import { onMount, untrack } from "svelte";
|
|
3
3
|
import { numberTickerVariants } from "./number-ticker.variants.js";
|
|
4
4
|
let { value, duration = 2e3, decimals = 0, delay = 0, class: className, ...restProps } = $props();
|
|
5
5
|
let styles = $derived(numberTickerVariants());
|
|
6
6
|
let displayValue = $state(0);
|
|
7
7
|
let isVisible = $state(false);
|
|
8
8
|
let nodeRef = $state(null);
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* อ่าน value อย่างปลอดภัย — parent expression (เช่น overview.lowStock.length)
|
|
11
|
+
* อาจ throw ตอน nav/refresh ถ้า overview เป็น undefined
|
|
12
|
+
*/
|
|
13
|
+
const readSafeValue = () => {
|
|
14
|
+
try {
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
return Number.isFinite(n) ? n : 0;
|
|
17
|
+
} catch {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
10
21
|
const easeOutExpo = (x) => {
|
|
11
22
|
return x === 1 ? 1 : 1 - 2 ** (-10 * x);
|
|
12
23
|
};
|
|
24
|
+
let animationFrame = 0;
|
|
25
|
+
let delayTimeout;
|
|
26
|
+
/** target ล่าสุดที่เริ่ม animate แล้ว — ใช้กัน re-start ซ้ำ */
|
|
27
|
+
let lastAnimatedTarget;
|
|
28
|
+
const cancelRunning = () => {
|
|
29
|
+
if (animationFrame) cancelAnimationFrame(animationFrame);
|
|
30
|
+
animationFrame = 0;
|
|
31
|
+
if (delayTimeout !== undefined) {
|
|
32
|
+
clearTimeout(delayTimeout);
|
|
33
|
+
delayTimeout = undefined;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* ใช้ targetSnapshot ใน rAF เท่านั้น — ห้ามอ่าน value prop ระหว่าง frame
|
|
38
|
+
* (การอ่าน value จะ re-evaluate expression ของ parent และอาจ throw)
|
|
39
|
+
*/
|
|
40
|
+
const startAnimation = (from, targetSnapshot, withDelay) => {
|
|
41
|
+
cancelRunning();
|
|
42
|
+
lastAnimatedTarget = targetSnapshot;
|
|
43
|
+
const run = () => {
|
|
44
|
+
let startTime = null;
|
|
45
|
+
const animate = (timestamp) => {
|
|
46
|
+
if (!startTime) startTime = timestamp;
|
|
47
|
+
const progress = timestamp - startTime;
|
|
48
|
+
if (progress < duration) {
|
|
49
|
+
const percentage = progress / duration;
|
|
50
|
+
const easedProgress = easeOutExpo(percentage);
|
|
51
|
+
displayValue = from + (targetSnapshot - from) * easedProgress;
|
|
52
|
+
animationFrame = requestAnimationFrame(animate);
|
|
53
|
+
} else {
|
|
54
|
+
displayValue = targetSnapshot;
|
|
55
|
+
animationFrame = 0;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
animationFrame = requestAnimationFrame(animate);
|
|
59
|
+
};
|
|
60
|
+
if (withDelay && delay > 0) {
|
|
61
|
+
delayTimeout = setTimeout(run, delay);
|
|
62
|
+
} else {
|
|
63
|
+
run();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
13
66
|
onMount(() => {
|
|
14
67
|
if (!nodeRef) return;
|
|
15
|
-
let startTime = null;
|
|
16
|
-
let animationFrame;
|
|
17
|
-
const animate = (timestamp) => {
|
|
18
|
-
if (!startTime) startTime = timestamp;
|
|
19
|
-
const progress = timestamp - startTime;
|
|
20
|
-
if (progress < duration) {
|
|
21
|
-
const percentage = progress / duration;
|
|
22
|
-
const easedProgress = easeOutExpo(percentage);
|
|
23
|
-
displayValue = value * easedProgress;
|
|
24
|
-
animationFrame = requestAnimationFrame(animate);
|
|
25
|
-
} else {
|
|
26
|
-
displayValue = value;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
68
|
const observer = new IntersectionObserver((entries) => {
|
|
30
69
|
if (entries[0].isIntersecting && !isVisible) {
|
|
31
70
|
isVisible = true;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}, delay);
|
|
71
|
+
// snapshot ตอนเริ่ม intersection เท่านั้น
|
|
72
|
+
startAnimation(0, readSafeValue(), true);
|
|
35
73
|
}
|
|
36
74
|
}, { threshold: .1 });
|
|
37
75
|
observer.observe(nodeRef);
|
|
38
76
|
return () => {
|
|
39
77
|
observer.disconnect();
|
|
40
|
-
|
|
78
|
+
cancelRunning();
|
|
41
79
|
};
|
|
42
80
|
});
|
|
43
|
-
//
|
|
81
|
+
// value เปลี่ยนหลัง visible → re-animate จาก display ปัจจุบันไป target ใหม่
|
|
44
82
|
$effect(() => {
|
|
45
|
-
|
|
83
|
+
const nextTarget = readSafeValue();
|
|
84
|
+
if (!isVisible) return;
|
|
85
|
+
if (lastAnimatedTarget === nextTarget) return;
|
|
86
|
+
// untrack เพื่อไม่ให้ effect ไล่ตามทุก frame ของ displayValue
|
|
87
|
+
const from = untrack(() => displayValue);
|
|
88
|
+
startAnimation(from, nextTarget, false);
|
|
46
89
|
});
|
|
47
90
|
let formattedValue = $derived(new Intl.NumberFormat("en-US", {
|
|
48
91
|
minimumFractionDigits: decimals,
|
|
@@ -50,10 +93,10 @@ let formattedValue = $derived(new Intl.NumberFormat("en-US", {
|
|
|
50
93
|
}).format(displayValue));
|
|
51
94
|
</script>
|
|
52
95
|
|
|
53
|
-
<span
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
<span
|
|
97
|
+
bind:this={nodeRef}
|
|
98
|
+
class={twMerge(styles.base() as string, className)}
|
|
99
|
+
{...restProps}
|
|
57
100
|
>
|
|
58
|
-
|
|
101
|
+
{formattedValue}
|
|
59
102
|
</span>
|
|
@@ -16,9 +16,12 @@ let localColumnSizing = $state({});
|
|
|
16
16
|
let localGlobalFilter = $state("");
|
|
17
17
|
let localPage = $state(0);
|
|
18
18
|
let localRef = $state(null);
|
|
19
|
-
let localColumnVisibility = $state(
|
|
20
|
-
let localColumnPinning = $state(
|
|
19
|
+
let localColumnVisibility = $state({});
|
|
20
|
+
let localColumnPinning = $state({});
|
|
21
21
|
let { ref = $bindable(localRef), sorting = $bindable(localSorting), selectedRows = $bindable(localSelectedRows), pinnedRows = $bindable(localPinnedRows), expandedRows = $bindable(localExpandedRows), columnFilters = $bindable(localColumnFilters), columnSizing = $bindable(localColumnSizing), globalFilter = $bindable(localGlobalFilter), page = $bindable(localPage), columnVisibility = $bindable(localColumnVisibility), columnPinning = $bindable(localColumnPinning), ...rest } = $props();
|
|
22
|
+
function isPlainObject(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
22
25
|
</script>
|
|
23
26
|
|
|
24
27
|
<Table
|
|
@@ -52,34 +55,48 @@ let { ref = $bindable(localRef), sorting = $bindable(localSorting), selectedRows
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
bind:columnFilters={
|
|
55
|
-
() =>
|
|
56
|
-
columnFilters && typeof columnFilters === 'object' && !Array.isArray(columnFilters)
|
|
57
|
-
? columnFilters
|
|
58
|
-
: localColumnFilters,
|
|
58
|
+
() => (isPlainObject(columnFilters) ? columnFilters : localColumnFilters),
|
|
59
59
|
(value) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: ({} as Record<string, string>)
|
|
64
|
-
columnFilters = next
|
|
60
|
+
columnFilters = isPlainObject(value)
|
|
61
|
+
? (value as Record<string, string>)
|
|
62
|
+
: ({} as Record<string, string>)
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
bind:columnSizing={
|
|
68
|
-
() =>
|
|
69
|
-
columnSizing && typeof columnSizing === 'object' && !Array.isArray(columnSizing)
|
|
70
|
-
? columnSizing
|
|
71
|
-
: localColumnSizing,
|
|
66
|
+
() => (isPlainObject(columnSizing) ? columnSizing : localColumnSizing),
|
|
72
67
|
(value) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
columnSizing = isPlainObject(value)
|
|
69
|
+
? (value as Record<string, number>)
|
|
70
|
+
: ({} as Record<string, number>)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
bind:globalFilter={
|
|
74
|
+
() => (typeof globalFilter === 'string' ? globalFilter : localGlobalFilter),
|
|
75
|
+
(value) => {
|
|
76
|
+
globalFilter = typeof value === 'string' ? value : ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
bind:page={
|
|
80
|
+
() => (typeof page === 'number' && Number.isFinite(page) ? page : localPage),
|
|
81
|
+
(value) => {
|
|
82
|
+
page = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
bind:columnVisibility={
|
|
86
|
+
() => (isPlainObject(columnVisibility) ? columnVisibility : localColumnVisibility),
|
|
87
|
+
(value) => {
|
|
88
|
+
columnVisibility = isPlainObject(value)
|
|
89
|
+
? (value as Record<string, boolean>)
|
|
90
|
+
: ({} as Record<string, boolean>)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
bind:columnPinning={
|
|
94
|
+
() => (isPlainObject(columnPinning) ? columnPinning : localColumnPinning),
|
|
95
|
+
(value) => {
|
|
96
|
+
columnPinning = isPlainObject(value)
|
|
97
|
+
? (value as { left?: string[]; right?: string[] })
|
|
98
|
+
: ({} as { left?: string[]; right?: string[] })
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
|
-
bind:globalFilter
|
|
81
|
-
bind:page
|
|
82
|
-
bind:columnVisibility
|
|
83
|
-
bind:columnPinning
|
|
84
101
|
{...rest}
|
|
85
102
|
/>
|