lupine.components 1.1.55 → 1.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.
- package/LICENSE +21 -21
- package/package.json +1 -1
- package/src/component-pool/aspect-ratio/aspect-ratio-demo.tsx +103 -103
- package/src/component-pool/avatar/avatar-demo.tsx +127 -127
- package/src/component-pool/badge/badge-demo.tsx +152 -152
- package/src/component-pool/breadcrumbs/breadcrumbs-demo.tsx +105 -105
- package/src/component-pool/card/card-demo.tsx +179 -179
- package/src/component-pool/carousel/carousel-demo.tsx +191 -191
- package/src/component-pool/cascader/cascader-demo.tsx +127 -127
- package/src/component-pool/charts/area-chart-demo.tsx +106 -106
- package/src/component-pool/charts/area-chart.tsx +254 -254
- package/src/component-pool/charts/bar-chart-demo.tsx +107 -107
- package/src/component-pool/charts/bar-chart.tsx +223 -223
- package/src/component-pool/charts/chart-utils.ts +182 -182
- package/src/component-pool/charts/column-chart-demo.tsx +111 -111
- package/src/component-pool/charts/column-chart.tsx +230 -230
- package/src/component-pool/charts/donut-chart-demo.tsx +112 -112
- package/src/component-pool/charts/donut-chart.tsx +6 -6
- package/src/component-pool/charts/gauge-chart-demo.tsx +101 -101
- package/src/component-pool/charts/gauge-chart.tsx +117 -117
- package/src/component-pool/charts/index.ts +19 -19
- package/src/component-pool/charts/line-chart-demo.tsx +109 -109
- package/src/component-pool/charts/line-chart.tsx +233 -233
- package/src/component-pool/charts/pie-chart-demo.tsx +133 -133
- package/src/component-pool/charts/pie-chart.tsx +142 -142
- package/src/component-pool/charts/radar-chart-demo.tsx +88 -88
- package/src/component-pool/charts/radar-chart.tsx +231 -231
- package/src/component-pool/charts/scatter-chart-demo.tsx +88 -88
- package/src/component-pool/charts/scatter-chart.tsx +237 -237
- package/src/component-pool/copy-button/copy-button-demo.tsx +71 -71
- package/src/component-pool/date-picker/date-picker-demo.tsx +84 -84
- package/src/component-pool/date-picker/date-picker.tsx +437 -437
- package/src/component-pool/date-picker/index.ts +2 -2
- package/src/component-pool/floating-icon-menu/floating-icon-menu-demo.tsx +94 -94
- package/src/component-pool/floating-icon-menu/floating-icon-menu.tsx +241 -241
- package/src/component-pool/floating-icon-menu/index.ts +2 -2
- package/src/component-pool/h-editor/h-editor-demo.tsx +112 -112
- package/src/component-pool/h-editor/h-editor.ts +862 -862
- package/src/component-pool/h-editor/index.ts +2 -2
- package/src/component-pool/i-editor/i-editor-demo.tsx +72 -72
- package/src/component-pool/i-editor/i-editor-frames.ts +42 -42
- package/src/component-pool/i-editor/i-editor-styles.ts +22 -22
- package/src/component-pool/i-editor/i-editor.ts +3869 -3869
- package/src/component-pool/i-editor/index.ts +2 -2
- package/src/component-pool/map-wrapper/map-wrapper-demo.tsx +55 -55
- package/src/component-pool/p-editor/index.ts +4 -4
- package/src/component-pool/p-editor/p-editor-demo.tsx +51 -51
- package/src/component-pool/p-editor/p-editor-styles.ts +44 -44
- package/src/component-pool/p-editor/p-editor-types.ts +4 -4
- package/src/component-pool/p-editor/p-editor-utils.ts +38 -38
- package/src/component-pool/p-editor/p-editor.ts +2477 -2477
- package/src/component-pool/pdf-viewer/index.ts +2 -2
- package/src/component-pool/pdf-viewer/pdf-viewer-demo.tsx +116 -116
- package/src/component-pool/pdf-viewer/pdf-viewer.tsx +56 -56
- package/src/component-pool/picker-helper.tsx +222 -222
- package/src/component-pool/pull-to-refresh/pull-to-refresh-demo.tsx +99 -99
- package/src/component-pool/qrcode/index.ts +3 -3
- package/src/component-pool/qrcode/qrcode-demo.tsx +102 -102
- package/src/component-pool/qrcode/qrcode.tsx +169 -169
- package/src/component-pool/radial-progress/index.ts +2 -2
- package/src/component-pool/radial-progress/radial-progress-demo.tsx +84 -84
- package/src/component-pool/radial-progress/radial-progress.tsx +118 -118
- package/src/component-pool/range/gauge-demo.tsx +113 -113
- package/src/component-pool/range/gauge.tsx +392 -392
- package/src/component-pool/range/range-demo.tsx +97 -97
- package/src/component-pool/search-input/index.ts +2 -2
- package/src/component-pool/search-input/search-input-demo.tsx +50 -50
- package/src/component-pool/search-input/search-input.tsx +125 -125
- package/src/component-pool/skeleton/skeleton-card.tsx +114 -114
- package/src/component-pool/skeleton/skeleton-demo.tsx +55 -55
- package/src/component-pool/svg-icons.ts +95 -95
- package/src/component-pool/svg-props.tsx +144 -144
- package/src/component-pool/tag-input/index.ts +2 -2
- package/src/component-pool/tag-input/tag-input-demo.tsx +47 -47
- package/src/component-pool/tag-input/tag-input.tsx +166 -166
- package/src/component-pool/time-picker/index.ts +2 -2
- package/src/component-pool/time-picker/time-picker-demo.tsx +80 -80
- package/src/component-pool/time-picker/time-picker.tsx +311 -311
- package/src/component-pool/timeline/timeline-demo.tsx +75 -75
- package/src/component-pool/tooltip/tooltip-demo.tsx +144 -144
- package/src/component-pool/tour/tour-demo.tsx +92 -92
- package/src/component-pool/youtube-player/index.ts +2 -2
- package/src/component-pool/youtube-player/youtube-player-demo.tsx +57 -57
- package/src/component-pool/youtube-player/youtube-player.tsx +116 -116
- package/src/components/action-sheet-color.tsx +269 -269
- package/src/components/action-sheet-date.tsx +299 -299
- package/src/components/action-sheet-demo.tsx +216 -216
- package/src/components/action-sheet-theme.tsx +150 -150
- package/src/components/action-sheet-time.tsx +157 -157
- package/src/components/input-number.tsx +206 -206
- package/src/components/input-with-title-demo.tsx +42 -42
- package/src/components/loading-spin.tsx +74 -74
- package/src/components/message-box-demo.tsx +107 -107
- package/src/components/mobile-components/mobile-side-menu-demo.tsx +47 -47
- package/src/components/modal-demo.tsx +56 -56
- package/src/components/notice-message-demo.tsx +57 -57
- package/src/components/paging-link-demo.tsx +430 -430
- package/src/components/popup-menu-demo.tsx +172 -172
- package/src/components/progress-demo.tsx +53 -53
- package/src/components/radio-label-demo.tsx +41 -41
- package/src/components/redirect-demo.tsx +36 -36
- package/src/components/resizable-splitter-demo.tsx +104 -104
- package/src/components/select-angle-demo.tsx +32 -32
- package/src/components/select-with-title-demo.tsx +53 -53
- package/src/components/slide-tab-component-demo.tsx +86 -86
- package/src/components/spinner-demo.tsx +52 -52
- package/src/components/stars-component-demo.tsx +36 -36
- package/src/components/switch-option-component.tsx +74 -74
- package/src/components/switch-option-demo.tsx +36 -36
- package/src/components/tabs-demo.tsx +42 -42
- package/src/components/text-glow-demo.tsx +46 -46
- package/src/components/text-scale-demo.tsx +41 -41
- package/src/components/text-wave-demo.tsx +48 -48
- package/src/components/toggle-button-demo.tsx +230 -230
- package/src/demo/demo-page.tsx +271 -271
- package/src/demo/demo-registry.ts +143 -143
- package/src/demo/demo-render-page.tsx +94 -94
- package/src/demo/demo-types.ts +22 -22
- package/src/demo/mock/demo-icons.ts +72 -72
- package/src/demo/mock/responsive-demo-mock-pages.tsx +197 -197
- package/src/demo/mock/side-menu-mock.tsx +113 -113
- package/src/demo/mock/user-settings-mock.tsx +310 -310
- package/src/frames/responsive-frame-demo.tsx +120 -120
- package/src/frames/slider-frame-demo.tsx +111 -111
- package/src/frames/slider-helper.tsx +194 -194
- package/src/lib/button-async-lock.tsx +43 -43
- package/src/lib/canvas/canvas-drawing.ts +503 -503
- package/src/lib/canvas/canvas-geometry.ts +255 -255
- package/src/lib/canvas/canvas-helper.tsx +176 -176
- package/src/lib/canvas/canvas-image.ts +110 -110
- package/src/lib/canvas/canvas-types.ts +93 -93
- package/src/lib/canvas/index.ts +5 -5
- package/src/lib/disable-console.ts +11 -11
- package/src/lib/disable-debug.ts +8 -8
- package/src/lib/support-safe-area.ts +25 -25
- package/tsconfig.json +113 -113
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
import { CssProps, RefProps, VNode } from 'lupine.web';
|
|
2
|
-
import { HtmlVar } from './html-var';
|
|
3
|
-
import { ActionSheet, ActionSheetCloseProps, ActionSheetCloseReasonProps } from './action-sheet';
|
|
4
|
-
|
|
5
|
-
export type ActionSheetDatePickerOrder = 'YMD' | 'DMY';
|
|
6
|
-
export type ActionSheetDatePickerFormat = 'YMD' | 'MD' | 'D';
|
|
7
|
-
|
|
8
|
-
export type ActionSheetDatePickerProps = {
|
|
9
|
-
/** Initial date value. Format depends on format prop, defaults to "YYYY-MM-DD" */
|
|
10
|
-
value?: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
/** Format of the picker. Default: 'YMD' */
|
|
13
|
-
format?: ActionSheetDatePickerFormat;
|
|
14
|
-
/** Column order. Default: 'YMD' (Year | Month | Day) */
|
|
15
|
-
order?: ActionSheetDatePickerOrder;
|
|
16
|
-
/** Month labels. Defaults to English month names. Length determines month count. */
|
|
17
|
-
months?: string[];
|
|
18
|
-
/** Years shown above/below current year. Default: 100 (range = currentYear-N to currentYear+N) */
|
|
19
|
-
yearRange?: number;
|
|
20
|
-
confirmButtonText?: string;
|
|
21
|
-
cancelButtonText?: string;
|
|
22
|
-
zIndex?: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const DEFAULT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
26
|
-
|
|
27
|
-
// Year range: ±yearRange from current year (resolved at call time)
|
|
28
|
-
const CURRENT_YEAR = new Date().getFullYear();
|
|
29
|
-
|
|
30
|
-
const ITEM_H = 44;
|
|
31
|
-
const VISIBLE = 5; // number of visible rows (must be odd)
|
|
32
|
-
const PAD = Math.floor(VISIBLE / 2); // 2 rows of padding above/below
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Mobile-friendly date picker shown via an ActionSheet slide-up panel.
|
|
36
|
-
* Shows Year / Month / Day (or Day / Month / Year) columns.
|
|
37
|
-
* Returns "YYYY-MM-DD" or undefined if cancelled.
|
|
38
|
-
*/
|
|
39
|
-
export const ActionSheetDatePicker = async ({
|
|
40
|
-
value = '',
|
|
41
|
-
title = '',
|
|
42
|
-
format = 'YMD',
|
|
43
|
-
order = 'YMD',
|
|
44
|
-
months = DEFAULT_MONTHS,
|
|
45
|
-
yearRange = 100,
|
|
46
|
-
confirmButtonText = 'OK',
|
|
47
|
-
cancelButtonText = 'Cancel',
|
|
48
|
-
zIndex,
|
|
49
|
-
}: ActionSheetDatePickerProps): Promise<string | undefined> => {
|
|
50
|
-
// Parse initial date
|
|
51
|
-
const today = new Date();
|
|
52
|
-
|
|
53
|
-
let parts: string[] = [];
|
|
54
|
-
if (format === 'MD') {
|
|
55
|
-
parts = value.match(/^(\d{2})-(\d{2})$/) ?? [];
|
|
56
|
-
parts.unshift(''); // Shift empty year so indices [1]/[2] match month/day fallback logic below
|
|
57
|
-
parts.unshift(''); // Padding to push match groups to [2][3]
|
|
58
|
-
} else if (format === 'D') {
|
|
59
|
-
parts = value.match(/^(\d{2})$/) ?? [];
|
|
60
|
-
parts.unshift('');
|
|
61
|
-
parts.unshift('');
|
|
62
|
-
parts.unshift(''); // Push match group to [3]
|
|
63
|
-
} else {
|
|
64
|
-
parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) ?? [];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const initialYear = parts[1] ? parseInt(parts[1], 10) : today.getFullYear();
|
|
68
|
-
const initialMonth = parts[2] ? parseInt(parts[2], 10) : today.getMonth() + 1;
|
|
69
|
-
const initialDay = parts[3] ? parseInt(parts[3], 10) : today.getDate();
|
|
70
|
-
|
|
71
|
-
const YEAR_COUNT = yearRange * 2 + 1;
|
|
72
|
-
const START_YEAR = CURRENT_YEAR - yearRange;
|
|
73
|
-
|
|
74
|
-
return new Promise(async (resolve) => {
|
|
75
|
-
let handleClose: ActionSheetCloseProps;
|
|
76
|
-
|
|
77
|
-
// One ref per column — values are read from scrollTop at confirm time
|
|
78
|
-
const yearRef: RefProps = {};
|
|
79
|
-
const monthRef: RefProps = {};
|
|
80
|
-
const dayRef: RefProps = {};
|
|
81
|
-
|
|
82
|
-
const getIdx = (colRef: RefProps) => {
|
|
83
|
-
if (!colRef.current) return 0;
|
|
84
|
-
return Math.round((colRef.current as HTMLElement).scrollTop / ITEM_H);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const onConfirm = () => {
|
|
88
|
-
const year = START_YEAR + getIdx(yearRef);
|
|
89
|
-
const month = 1 + getIdx(monthRef);
|
|
90
|
-
const day = 1 + getIdx(dayRef);
|
|
91
|
-
|
|
92
|
-
let result = '';
|
|
93
|
-
if (format === 'D') {
|
|
94
|
-
result = String(day).padStart(2, '0');
|
|
95
|
-
} else if (format === 'MD') {
|
|
96
|
-
result = [String(month).padStart(2, '0'), String(day).padStart(2, '0')].join('-');
|
|
97
|
-
} else {
|
|
98
|
-
result = [String(year).padStart(4, '0'), String(month).padStart(2, '0'), String(day).padStart(2, '0')].join(
|
|
99
|
-
'-'
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
resolve(result);
|
|
104
|
-
handleClose('confirm');
|
|
105
|
-
};
|
|
106
|
-
const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
|
|
107
|
-
if (reason !== 'confirm') resolve(undefined);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Build a scrollable column.
|
|
112
|
-
* Carousel pattern: scroll to item's offsetTop on load; read scrollTop on confirm.
|
|
113
|
-
*/
|
|
114
|
-
const buildColumn = (labels: string[], initialIdx: number, colRef: RefProps, onScroll?: () => void) => {
|
|
115
|
-
colRef.onLoad = async () => {
|
|
116
|
-
const el = colRef.current as HTMLElement;
|
|
117
|
-
const items = el.querySelectorAll('.dp-item');
|
|
118
|
-
if (items[initialIdx]) {
|
|
119
|
-
// offsetTop accounts for the PAD padding divs above the items
|
|
120
|
-
el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div class='col' ref={colRef} onScroll={onScroll}>
|
|
126
|
-
{Array.from({ length: PAD }, (_, i) => (
|
|
127
|
-
<div class='dp-pad' key={`t${i}`} />
|
|
128
|
-
))}
|
|
129
|
-
{labels.map((label, i) => (
|
|
130
|
-
<div class='dp-item' key={i}>
|
|
131
|
-
{label}
|
|
132
|
-
</div>
|
|
133
|
-
))}
|
|
134
|
-
{Array.from({ length: PAD }, (_, i) => (
|
|
135
|
-
<div class='dp-pad' key={`b${i}`} />
|
|
136
|
-
))}
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const buildDynamicColumn = (labelsVar: HtmlVar, initialIdx: number, colRef: RefProps) => {
|
|
142
|
-
colRef.onLoad = async () => {
|
|
143
|
-
const el = colRef.current as HTMLElement;
|
|
144
|
-
const items = el.querySelectorAll('.dp-item');
|
|
145
|
-
if (items[initialIdx]) {
|
|
146
|
-
el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<div class='col' ref={colRef}>
|
|
152
|
-
{labelsVar.node}
|
|
153
|
-
</div>
|
|
154
|
-
);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const generateDaysDOM = (maxDays: number) => {
|
|
158
|
-
const labels = Array.from({ length: maxDays }, (_, i) => String(i + 1).padStart(2, '0'));
|
|
159
|
-
return (
|
|
160
|
-
<>
|
|
161
|
-
{Array.from({ length: PAD }, (_, i) => (
|
|
162
|
-
<div class='dp-pad' key={`t${i}`} />
|
|
163
|
-
))}
|
|
164
|
-
{labels.map((label: string, i: number) => (
|
|
165
|
-
<div class='dp-item' key={i}>
|
|
166
|
-
{label}
|
|
167
|
-
</div>
|
|
168
|
-
))}
|
|
169
|
-
{Array.from({ length: PAD }, (_, i) => (
|
|
170
|
-
<div class='dp-pad' key={`b${i}`} />
|
|
171
|
-
))}
|
|
172
|
-
</>
|
|
173
|
-
);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const getMaxDays = (year: number, month: number) => {
|
|
177
|
-
if (format === 'D') return 31;
|
|
178
|
-
if (format === 'MD') return new Date(2000, month, 0).getDate(); // Leap year forced max
|
|
179
|
-
return new Date(year, month, 0).getDate();
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
let currentMaxDays = 0;
|
|
183
|
-
let scrollTimeout: any;
|
|
184
|
-
const handleDateScroll = () => {
|
|
185
|
-
clearTimeout(scrollTimeout);
|
|
186
|
-
scrollTimeout = setTimeout(() => {
|
|
187
|
-
// Read current selected year and month based on scroll heights
|
|
188
|
-
const curYear = format === 'MD' ? 2000 : START_YEAR + getIdx(yearRef);
|
|
189
|
-
const curMonth = 1 + getIdx(monthRef);
|
|
190
|
-
|
|
191
|
-
// Calculate the maximum allowed days for this selection
|
|
192
|
-
const maxDays = getMaxDays(curYear, curMonth);
|
|
193
|
-
|
|
194
|
-
// If the number of days changed, update the reactive var
|
|
195
|
-
if (currentMaxDays !== maxDays) {
|
|
196
|
-
currentMaxDays = maxDays;
|
|
197
|
-
daysVar.value = generateDaysDOM(maxDays);
|
|
198
|
-
|
|
199
|
-
// If the currently selected day is now out of bounds (e.g. Feb 30th), clamp it down.
|
|
200
|
-
const currentDayIdx = getIdx(dayRef);
|
|
201
|
-
if (currentDayIdx >= maxDays) {
|
|
202
|
-
setTimeout(() => {
|
|
203
|
-
if (dayRef.current) {
|
|
204
|
-
const el = dayRef.current as HTMLElement;
|
|
205
|
-
const items = el.querySelectorAll('.dp-item');
|
|
206
|
-
const targetItem = items[maxDays - 1] as HTMLElement;
|
|
207
|
-
if (targetItem) {
|
|
208
|
-
el.scrollTo({ top: targetItem.offsetTop - PAD * ITEM_H, behavior: 'smooth' });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}, 50);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}, 150);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const years = Array.from({ length: YEAR_COUNT }, (_, i) => String(START_YEAR + i));
|
|
218
|
-
|
|
219
|
-
const initialYearIdx = Math.max(0, Math.min(YEAR_COUNT - 1, initialYear - START_YEAR));
|
|
220
|
-
const initialMonthIdx = Math.max(0, Math.min(months.length - 1, initialMonth - 1));
|
|
221
|
-
currentMaxDays = getMaxDays(START_YEAR + initialYearIdx, initialMonthIdx + 1);
|
|
222
|
-
|
|
223
|
-
// Day array is reactive, max based on initial selection constraints
|
|
224
|
-
const daysVar = new HtmlVar(generateDaysDOM(currentMaxDays));
|
|
225
|
-
const initialDayIdx = Math.max(0, Math.min(currentMaxDays - 1, initialDay - 1));
|
|
226
|
-
|
|
227
|
-
const yearCol = buildColumn(years, initialYearIdx, yearRef, handleDateScroll);
|
|
228
|
-
const monthCol = buildColumn(months, initialMonthIdx, monthRef, handleDateScroll);
|
|
229
|
-
const dayCol = buildDynamicColumn(daysVar, initialDayIdx, dayRef);
|
|
230
|
-
|
|
231
|
-
let orderedCols: any[] = [];
|
|
232
|
-
if (format === 'D') {
|
|
233
|
-
orderedCols = [dayCol];
|
|
234
|
-
} else if (format === 'MD') {
|
|
235
|
-
orderedCols = order === 'DMY' ? [dayCol, monthCol] : [monthCol, dayCol];
|
|
236
|
-
} else {
|
|
237
|
-
orderedCols = order === 'DMY' ? [dayCol, monthCol, yearCol] : [yearCol, monthCol, dayCol];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const pickerBodyCss: CssProps = {
|
|
241
|
-
borderTop: '1px solid var(--primary-border-color)',
|
|
242
|
-
overflow: 'hidden', // prevent act-sheet-content from adding outer scrollbar
|
|
243
|
-
'.picker-body': {
|
|
244
|
-
display: 'flex',
|
|
245
|
-
height: `${ITEM_H * VISIBLE}px`,
|
|
246
|
-
position: 'relative',
|
|
247
|
-
// Center-selection highlight band
|
|
248
|
-
'&::before': {
|
|
249
|
-
content: '""',
|
|
250
|
-
position: 'absolute',
|
|
251
|
-
top: `${ITEM_H * PAD}px`,
|
|
252
|
-
left: 0,
|
|
253
|
-
right: 0,
|
|
254
|
-
height: `${ITEM_H}px`,
|
|
255
|
-
backgroundColor: 'var(--activatable-bg-color-hover, rgba(0,0,0,0.05))',
|
|
256
|
-
borderRadius: '6px',
|
|
257
|
-
pointerEvents: 'none',
|
|
258
|
-
opacity: 0.4,
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
'.col': {
|
|
262
|
-
flex: 1,
|
|
263
|
-
overflowY: 'scroll',
|
|
264
|
-
scrollSnapType: 'y mandatory',
|
|
265
|
-
scrollbarWidth: 'none',
|
|
266
|
-
'&::-webkit-scrollbar': { display: 'none' },
|
|
267
|
-
'.dp-pad': { height: `${ITEM_H}px` },
|
|
268
|
-
'.dp-item': {
|
|
269
|
-
scrollSnapAlign: 'center',
|
|
270
|
-
height: `${ITEM_H}px`,
|
|
271
|
-
lineHeight: `${ITEM_H}px`,
|
|
272
|
-
textAlign: 'center',
|
|
273
|
-
fontSize: '18px',
|
|
274
|
-
cursor: 'pointer',
|
|
275
|
-
color: 'var(--secondary-color, #aaa)',
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const pickerBody = (
|
|
281
|
-
<div css={pickerBodyCss}>
|
|
282
|
-
<div class='picker-body'>{orderedCols}</div>
|
|
283
|
-
</div>
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
handleClose = await ActionSheet.show({
|
|
287
|
-
title,
|
|
288
|
-
children: pickerBody,
|
|
289
|
-
contentMaxHeight: '33vh',
|
|
290
|
-
closeEvent,
|
|
291
|
-
closeWhenClickOutside: true,
|
|
292
|
-
zIndex,
|
|
293
|
-
confirmButtonText,
|
|
294
|
-
handleConfirmClicked: onConfirm,
|
|
295
|
-
cancelButtonText,
|
|
296
|
-
buttonsPosition: 'header',
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
};
|
|
1
|
+
import { CssProps, RefProps, VNode } from 'lupine.web';
|
|
2
|
+
import { HtmlVar } from './html-var';
|
|
3
|
+
import { ActionSheet, ActionSheetCloseProps, ActionSheetCloseReasonProps } from './action-sheet';
|
|
4
|
+
|
|
5
|
+
export type ActionSheetDatePickerOrder = 'YMD' | 'DMY';
|
|
6
|
+
export type ActionSheetDatePickerFormat = 'YMD' | 'MD' | 'D';
|
|
7
|
+
|
|
8
|
+
export type ActionSheetDatePickerProps = {
|
|
9
|
+
/** Initial date value. Format depends on format prop, defaults to "YYYY-MM-DD" */
|
|
10
|
+
value?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Format of the picker. Default: 'YMD' */
|
|
13
|
+
format?: ActionSheetDatePickerFormat;
|
|
14
|
+
/** Column order. Default: 'YMD' (Year | Month | Day) */
|
|
15
|
+
order?: ActionSheetDatePickerOrder;
|
|
16
|
+
/** Month labels. Defaults to English month names. Length determines month count. */
|
|
17
|
+
months?: string[];
|
|
18
|
+
/** Years shown above/below current year. Default: 100 (range = currentYear-N to currentYear+N) */
|
|
19
|
+
yearRange?: number;
|
|
20
|
+
confirmButtonText?: string;
|
|
21
|
+
cancelButtonText?: string;
|
|
22
|
+
zIndex?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
26
|
+
|
|
27
|
+
// Year range: ±yearRange from current year (resolved at call time)
|
|
28
|
+
const CURRENT_YEAR = new Date().getFullYear();
|
|
29
|
+
|
|
30
|
+
const ITEM_H = 44;
|
|
31
|
+
const VISIBLE = 5; // number of visible rows (must be odd)
|
|
32
|
+
const PAD = Math.floor(VISIBLE / 2); // 2 rows of padding above/below
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mobile-friendly date picker shown via an ActionSheet slide-up panel.
|
|
36
|
+
* Shows Year / Month / Day (or Day / Month / Year) columns.
|
|
37
|
+
* Returns "YYYY-MM-DD" or undefined if cancelled.
|
|
38
|
+
*/
|
|
39
|
+
export const ActionSheetDatePicker = async ({
|
|
40
|
+
value = '',
|
|
41
|
+
title = '',
|
|
42
|
+
format = 'YMD',
|
|
43
|
+
order = 'YMD',
|
|
44
|
+
months = DEFAULT_MONTHS,
|
|
45
|
+
yearRange = 100,
|
|
46
|
+
confirmButtonText = 'OK',
|
|
47
|
+
cancelButtonText = 'Cancel',
|
|
48
|
+
zIndex,
|
|
49
|
+
}: ActionSheetDatePickerProps): Promise<string | undefined> => {
|
|
50
|
+
// Parse initial date
|
|
51
|
+
const today = new Date();
|
|
52
|
+
|
|
53
|
+
let parts: string[] = [];
|
|
54
|
+
if (format === 'MD') {
|
|
55
|
+
parts = value.match(/^(\d{2})-(\d{2})$/) ?? [];
|
|
56
|
+
parts.unshift(''); // Shift empty year so indices [1]/[2] match month/day fallback logic below
|
|
57
|
+
parts.unshift(''); // Padding to push match groups to [2][3]
|
|
58
|
+
} else if (format === 'D') {
|
|
59
|
+
parts = value.match(/^(\d{2})$/) ?? [];
|
|
60
|
+
parts.unshift('');
|
|
61
|
+
parts.unshift('');
|
|
62
|
+
parts.unshift(''); // Push match group to [3]
|
|
63
|
+
} else {
|
|
64
|
+
parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) ?? [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const initialYear = parts[1] ? parseInt(parts[1], 10) : today.getFullYear();
|
|
68
|
+
const initialMonth = parts[2] ? parseInt(parts[2], 10) : today.getMonth() + 1;
|
|
69
|
+
const initialDay = parts[3] ? parseInt(parts[3], 10) : today.getDate();
|
|
70
|
+
|
|
71
|
+
const YEAR_COUNT = yearRange * 2 + 1;
|
|
72
|
+
const START_YEAR = CURRENT_YEAR - yearRange;
|
|
73
|
+
|
|
74
|
+
return new Promise(async (resolve) => {
|
|
75
|
+
let handleClose: ActionSheetCloseProps;
|
|
76
|
+
|
|
77
|
+
// One ref per column — values are read from scrollTop at confirm time
|
|
78
|
+
const yearRef: RefProps = {};
|
|
79
|
+
const monthRef: RefProps = {};
|
|
80
|
+
const dayRef: RefProps = {};
|
|
81
|
+
|
|
82
|
+
const getIdx = (colRef: RefProps) => {
|
|
83
|
+
if (!colRef.current) return 0;
|
|
84
|
+
return Math.round((colRef.current as HTMLElement).scrollTop / ITEM_H);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onConfirm = () => {
|
|
88
|
+
const year = START_YEAR + getIdx(yearRef);
|
|
89
|
+
const month = 1 + getIdx(monthRef);
|
|
90
|
+
const day = 1 + getIdx(dayRef);
|
|
91
|
+
|
|
92
|
+
let result = '';
|
|
93
|
+
if (format === 'D') {
|
|
94
|
+
result = String(day).padStart(2, '0');
|
|
95
|
+
} else if (format === 'MD') {
|
|
96
|
+
result = [String(month).padStart(2, '0'), String(day).padStart(2, '0')].join('-');
|
|
97
|
+
} else {
|
|
98
|
+
result = [String(year).padStart(4, '0'), String(month).padStart(2, '0'), String(day).padStart(2, '0')].join(
|
|
99
|
+
'-'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resolve(result);
|
|
104
|
+
handleClose('confirm');
|
|
105
|
+
};
|
|
106
|
+
const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
|
|
107
|
+
if (reason !== 'confirm') resolve(undefined);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a scrollable column.
|
|
112
|
+
* Carousel pattern: scroll to item's offsetTop on load; read scrollTop on confirm.
|
|
113
|
+
*/
|
|
114
|
+
const buildColumn = (labels: string[], initialIdx: number, colRef: RefProps, onScroll?: () => void) => {
|
|
115
|
+
colRef.onLoad = async () => {
|
|
116
|
+
const el = colRef.current as HTMLElement;
|
|
117
|
+
const items = el.querySelectorAll('.dp-item');
|
|
118
|
+
if (items[initialIdx]) {
|
|
119
|
+
// offsetTop accounts for the PAD padding divs above the items
|
|
120
|
+
el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div class='col' ref={colRef} onScroll={onScroll}>
|
|
126
|
+
{Array.from({ length: PAD }, (_, i) => (
|
|
127
|
+
<div class='dp-pad' key={`t${i}`} />
|
|
128
|
+
))}
|
|
129
|
+
{labels.map((label, i) => (
|
|
130
|
+
<div class='dp-item' key={i}>
|
|
131
|
+
{label}
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
{Array.from({ length: PAD }, (_, i) => (
|
|
135
|
+
<div class='dp-pad' key={`b${i}`} />
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const buildDynamicColumn = (labelsVar: HtmlVar, initialIdx: number, colRef: RefProps) => {
|
|
142
|
+
colRef.onLoad = async () => {
|
|
143
|
+
const el = colRef.current as HTMLElement;
|
|
144
|
+
const items = el.querySelectorAll('.dp-item');
|
|
145
|
+
if (items[initialIdx]) {
|
|
146
|
+
el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div class='col' ref={colRef}>
|
|
152
|
+
{labelsVar.node}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const generateDaysDOM = (maxDays: number) => {
|
|
158
|
+
const labels = Array.from({ length: maxDays }, (_, i) => String(i + 1).padStart(2, '0'));
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
{Array.from({ length: PAD }, (_, i) => (
|
|
162
|
+
<div class='dp-pad' key={`t${i}`} />
|
|
163
|
+
))}
|
|
164
|
+
{labels.map((label: string, i: number) => (
|
|
165
|
+
<div class='dp-item' key={i}>
|
|
166
|
+
{label}
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
{Array.from({ length: PAD }, (_, i) => (
|
|
170
|
+
<div class='dp-pad' key={`b${i}`} />
|
|
171
|
+
))}
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const getMaxDays = (year: number, month: number) => {
|
|
177
|
+
if (format === 'D') return 31;
|
|
178
|
+
if (format === 'MD') return new Date(2000, month, 0).getDate(); // Leap year forced max
|
|
179
|
+
return new Date(year, month, 0).getDate();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let currentMaxDays = 0;
|
|
183
|
+
let scrollTimeout: any;
|
|
184
|
+
const handleDateScroll = () => {
|
|
185
|
+
clearTimeout(scrollTimeout);
|
|
186
|
+
scrollTimeout = setTimeout(() => {
|
|
187
|
+
// Read current selected year and month based on scroll heights
|
|
188
|
+
const curYear = format === 'MD' ? 2000 : START_YEAR + getIdx(yearRef);
|
|
189
|
+
const curMonth = 1 + getIdx(monthRef);
|
|
190
|
+
|
|
191
|
+
// Calculate the maximum allowed days for this selection
|
|
192
|
+
const maxDays = getMaxDays(curYear, curMonth);
|
|
193
|
+
|
|
194
|
+
// If the number of days changed, update the reactive var
|
|
195
|
+
if (currentMaxDays !== maxDays) {
|
|
196
|
+
currentMaxDays = maxDays;
|
|
197
|
+
daysVar.value = generateDaysDOM(maxDays);
|
|
198
|
+
|
|
199
|
+
// If the currently selected day is now out of bounds (e.g. Feb 30th), clamp it down.
|
|
200
|
+
const currentDayIdx = getIdx(dayRef);
|
|
201
|
+
if (currentDayIdx >= maxDays) {
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
if (dayRef.current) {
|
|
204
|
+
const el = dayRef.current as HTMLElement;
|
|
205
|
+
const items = el.querySelectorAll('.dp-item');
|
|
206
|
+
const targetItem = items[maxDays - 1] as HTMLElement;
|
|
207
|
+
if (targetItem) {
|
|
208
|
+
el.scrollTo({ top: targetItem.offsetTop - PAD * ITEM_H, behavior: 'smooth' });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}, 50);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, 150);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const years = Array.from({ length: YEAR_COUNT }, (_, i) => String(START_YEAR + i));
|
|
218
|
+
|
|
219
|
+
const initialYearIdx = Math.max(0, Math.min(YEAR_COUNT - 1, initialYear - START_YEAR));
|
|
220
|
+
const initialMonthIdx = Math.max(0, Math.min(months.length - 1, initialMonth - 1));
|
|
221
|
+
currentMaxDays = getMaxDays(START_YEAR + initialYearIdx, initialMonthIdx + 1);
|
|
222
|
+
|
|
223
|
+
// Day array is reactive, max based on initial selection constraints
|
|
224
|
+
const daysVar = new HtmlVar(generateDaysDOM(currentMaxDays));
|
|
225
|
+
const initialDayIdx = Math.max(0, Math.min(currentMaxDays - 1, initialDay - 1));
|
|
226
|
+
|
|
227
|
+
const yearCol = buildColumn(years, initialYearIdx, yearRef, handleDateScroll);
|
|
228
|
+
const monthCol = buildColumn(months, initialMonthIdx, monthRef, handleDateScroll);
|
|
229
|
+
const dayCol = buildDynamicColumn(daysVar, initialDayIdx, dayRef);
|
|
230
|
+
|
|
231
|
+
let orderedCols: any[] = [];
|
|
232
|
+
if (format === 'D') {
|
|
233
|
+
orderedCols = [dayCol];
|
|
234
|
+
} else if (format === 'MD') {
|
|
235
|
+
orderedCols = order === 'DMY' ? [dayCol, monthCol] : [monthCol, dayCol];
|
|
236
|
+
} else {
|
|
237
|
+
orderedCols = order === 'DMY' ? [dayCol, monthCol, yearCol] : [yearCol, monthCol, dayCol];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const pickerBodyCss: CssProps = {
|
|
241
|
+
borderTop: '1px solid var(--primary-border-color)',
|
|
242
|
+
overflow: 'hidden', // prevent act-sheet-content from adding outer scrollbar
|
|
243
|
+
'.picker-body': {
|
|
244
|
+
display: 'flex',
|
|
245
|
+
height: `${ITEM_H * VISIBLE}px`,
|
|
246
|
+
position: 'relative',
|
|
247
|
+
// Center-selection highlight band
|
|
248
|
+
'&::before': {
|
|
249
|
+
content: '""',
|
|
250
|
+
position: 'absolute',
|
|
251
|
+
top: `${ITEM_H * PAD}px`,
|
|
252
|
+
left: 0,
|
|
253
|
+
right: 0,
|
|
254
|
+
height: `${ITEM_H}px`,
|
|
255
|
+
backgroundColor: 'var(--activatable-bg-color-hover, rgba(0,0,0,0.05))',
|
|
256
|
+
borderRadius: '6px',
|
|
257
|
+
pointerEvents: 'none',
|
|
258
|
+
opacity: 0.4,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
'.col': {
|
|
262
|
+
flex: 1,
|
|
263
|
+
overflowY: 'scroll',
|
|
264
|
+
scrollSnapType: 'y mandatory',
|
|
265
|
+
scrollbarWidth: 'none',
|
|
266
|
+
'&::-webkit-scrollbar': { display: 'none' },
|
|
267
|
+
'.dp-pad': { height: `${ITEM_H}px` },
|
|
268
|
+
'.dp-item': {
|
|
269
|
+
scrollSnapAlign: 'center',
|
|
270
|
+
height: `${ITEM_H}px`,
|
|
271
|
+
lineHeight: `${ITEM_H}px`,
|
|
272
|
+
textAlign: 'center',
|
|
273
|
+
fontSize: '18px',
|
|
274
|
+
cursor: 'pointer',
|
|
275
|
+
color: 'var(--secondary-color, #aaa)',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const pickerBody = (
|
|
281
|
+
<div css={pickerBodyCss}>
|
|
282
|
+
<div class='picker-body'>{orderedCols}</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
handleClose = await ActionSheet.show({
|
|
287
|
+
title,
|
|
288
|
+
children: pickerBody,
|
|
289
|
+
contentMaxHeight: '33vh',
|
|
290
|
+
closeEvent,
|
|
291
|
+
closeWhenClickOutside: true,
|
|
292
|
+
zIndex,
|
|
293
|
+
confirmButtonText,
|
|
294
|
+
handleConfirmClicked: onConfirm,
|
|
295
|
+
cancelButtonText,
|
|
296
|
+
buttonsPosition: 'header',
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
};
|