podo-ui 0.9.7 → 1.0.2
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/cdn/podo-datepicker.css +1 -1
- package/cdn/podo-datepicker.js +1 -1
- package/cdn/podo-datepicker.min.css +1 -1
- package/cdn/podo-datepicker.min.js +1 -1
- package/cdn/podo-ui.css +4 -1
- package/cdn/podo-ui.min.css +1 -1
- package/dist/react/atom/editor.d.ts.map +1 -1
- package/dist/react/atom/editor.js +94 -2
- package/dist/svelte/actions/portal.d.ts +18 -0
- package/dist/svelte/actions/portal.js +42 -0
- package/dist/svelte/atom/Avatar.svelte +97 -0
- package/dist/svelte/atom/Avatar.svelte.d.ts +31 -0
- package/dist/svelte/atom/Button.svelte +86 -0
- package/dist/svelte/atom/Button.svelte.d.ts +26 -0
- package/dist/svelte/atom/Checkbox.svelte +56 -0
- package/dist/svelte/atom/Checkbox.svelte.d.ts +16 -0
- package/dist/svelte/atom/Chip.svelte +60 -0
- package/dist/svelte/atom/Chip.svelte.d.ts +25 -0
- package/dist/svelte/atom/Editor.svelte +1314 -0
- package/dist/svelte/atom/Editor.svelte.d.ts +30 -0
- package/dist/svelte/atom/EditorView.svelte +16 -0
- package/dist/svelte/atom/EditorView.svelte.d.ts +9 -0
- package/dist/svelte/atom/File.svelte +33 -0
- package/dist/svelte/atom/File.svelte.d.ts +14 -0
- package/dist/svelte/atom/Input.svelte +80 -0
- package/dist/svelte/atom/Input.svelte.d.ts +19 -0
- package/dist/svelte/atom/Label.svelte +43 -0
- package/dist/svelte/atom/Label.svelte.d.ts +19 -0
- package/dist/svelte/atom/Radio.svelte +69 -0
- package/dist/svelte/atom/Radio.svelte.d.ts +26 -0
- package/dist/svelte/atom/RadioGroup.svelte +46 -0
- package/dist/svelte/atom/RadioGroup.svelte.d.ts +16 -0
- package/dist/svelte/atom/Select.svelte +65 -0
- package/dist/svelte/atom/Select.svelte.d.ts +26 -0
- package/dist/svelte/atom/Textarea.svelte +53 -0
- package/dist/svelte/atom/Textarea.svelte.d.ts +13 -0
- package/dist/svelte/atom/Toggle.svelte +48 -0
- package/dist/svelte/atom/Toggle.svelte.d.ts +14 -0
- package/dist/svelte/atom/Tooltip.svelte +78 -0
- package/dist/svelte/atom/Tooltip.svelte.d.ts +23 -0
- package/dist/svelte/atom/avatar.module.scss +82 -0
- package/dist/svelte/atom/editor-view.module.scss +251 -0
- package/dist/svelte/atom/input.module.scss +98 -0
- package/dist/svelte/atom/textarea.module.scss +17 -0
- package/dist/svelte/atom/tooltip.module.scss +227 -0
- package/dist/svelte/index.d.ts +26 -0
- package/dist/svelte/index.js +30 -0
- package/dist/svelte/molecule/DatePicker.svelte +986 -0
- package/dist/svelte/molecule/DatePicker.svelte.d.ts +71 -0
- package/dist/svelte/molecule/Field.svelte +81 -0
- package/dist/svelte/molecule/Field.svelte.d.ts +26 -0
- package/dist/svelte/molecule/Pagination.svelte +95 -0
- package/dist/svelte/molecule/Pagination.svelte.d.ts +14 -0
- package/dist/svelte/molecule/Tab.svelte +69 -0
- package/dist/svelte/molecule/Tab.svelte.d.ts +26 -0
- package/dist/svelte/molecule/TabPanel.svelte +24 -0
- package/dist/svelte/molecule/TabPanel.svelte.d.ts +14 -0
- package/dist/svelte/molecule/Table.svelte +109 -0
- package/dist/svelte/molecule/Table.svelte.d.ts +54 -0
- package/dist/svelte/molecule/Toast.svelte +111 -0
- package/dist/svelte/molecule/Toast.svelte.d.ts +25 -0
- package/dist/svelte/molecule/ToastProvider.svelte +74 -0
- package/dist/svelte/molecule/ToastProvider.svelte.d.ts +8 -0
- package/dist/svelte/molecule/field.module.scss +22 -0
- package/dist/svelte/molecule/pagination.module.scss +61 -0
- package/dist/svelte/molecule/toast-container.module.scss +70 -0
- package/dist/svelte/molecule/toast.module.scss +12 -0
- package/dist/svelte/stores/toast.d.ts +45 -0
- package/dist/svelte/stores/toast.js +55 -0
- package/dist/svelte/stores/validation.d.ts +15 -0
- package/dist/svelte/stores/validation.js +38 -0
- package/global.scss +1 -0
- package/package.json +32 -5
- package/vite-fonts.scss +1 -1
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import styles from '../../react/molecule/datepicker.module.scss';
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export type DatePickerMode = 'instant' | 'period';
|
|
7
|
+
export type DatePickerType = 'date' | 'time' | 'datetime';
|
|
8
|
+
|
|
9
|
+
export interface TimeValue {
|
|
10
|
+
hour: number;
|
|
11
|
+
minute: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DatePickerValue {
|
|
15
|
+
date?: Date;
|
|
16
|
+
time?: TimeValue;
|
|
17
|
+
endDate?: Date;
|
|
18
|
+
endTime?: TimeValue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DateRange {
|
|
22
|
+
from: Date;
|
|
23
|
+
to: Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type DateCondition = Date | DateRange | ((date: Date) => boolean);
|
|
27
|
+
|
|
28
|
+
export interface DateTimeLimit {
|
|
29
|
+
date: Date;
|
|
30
|
+
time?: TimeValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type MinuteStep = 1 | 5 | 10 | 15 | 20 | 30;
|
|
34
|
+
|
|
35
|
+
export interface YearRange {
|
|
36
|
+
min?: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CalendarInitial = 'now' | 'prevMonth' | 'nextMonth' | Date;
|
|
41
|
+
|
|
42
|
+
export interface InitialCalendar {
|
|
43
|
+
start?: CalendarInitial;
|
|
44
|
+
end?: CalendarInitial;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Props {
|
|
48
|
+
/** Selection mode: instant | period */
|
|
49
|
+
mode?: DatePickerMode;
|
|
50
|
+
/** Value type: date | time | datetime */
|
|
51
|
+
type?: DatePickerType;
|
|
52
|
+
/** Selected value */
|
|
53
|
+
value?: DatePickerValue;
|
|
54
|
+
/** Value change handler */
|
|
55
|
+
onchange?: (value: DatePickerValue) => void;
|
|
56
|
+
/** Placeholder */
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
/** Disabled state */
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
/** Show action buttons (default true for period mode) */
|
|
61
|
+
showActions?: boolean;
|
|
62
|
+
/** Dropdown alignment */
|
|
63
|
+
align?: 'left' | 'right';
|
|
64
|
+
/** Additional class name */
|
|
65
|
+
class?: string;
|
|
66
|
+
/** Disabled date conditions */
|
|
67
|
+
disable?: DateCondition[];
|
|
68
|
+
/** Enabled date conditions (only these dates are selectable) */
|
|
69
|
+
enable?: DateCondition[];
|
|
70
|
+
/** Minimum selectable date */
|
|
71
|
+
minDate?: Date | DateTimeLimit;
|
|
72
|
+
/** Maximum selectable date */
|
|
73
|
+
maxDate?: Date | DateTimeLimit;
|
|
74
|
+
/** Minute selection step */
|
|
75
|
+
minuteStep?: MinuteStep;
|
|
76
|
+
/** Date/time format pattern */
|
|
77
|
+
format?: string;
|
|
78
|
+
/** Initial calendar display month for period mode */
|
|
79
|
+
initialCalendar?: InitialCalendar;
|
|
80
|
+
/** Year selection range */
|
|
81
|
+
yearRange?: YearRange;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let {
|
|
85
|
+
mode = 'instant',
|
|
86
|
+
type = 'date',
|
|
87
|
+
value = $bindable<DatePickerValue>({}),
|
|
88
|
+
onchange,
|
|
89
|
+
placeholder,
|
|
90
|
+
disabled = false,
|
|
91
|
+
showActions,
|
|
92
|
+
align = 'left',
|
|
93
|
+
class: className = '',
|
|
94
|
+
disable,
|
|
95
|
+
enable,
|
|
96
|
+
minDate,
|
|
97
|
+
maxDate,
|
|
98
|
+
minuteStep = 1,
|
|
99
|
+
format,
|
|
100
|
+
initialCalendar,
|
|
101
|
+
yearRange,
|
|
102
|
+
...rest
|
|
103
|
+
}: Props & Record<string, unknown> = $props();
|
|
104
|
+
|
|
105
|
+
// State
|
|
106
|
+
type SelectingPart = 'date' | 'hour' | 'minute' | 'endDate' | 'endHour' | 'endMinute' | null;
|
|
107
|
+
|
|
108
|
+
let selectingPart = $state<SelectingPart>(null);
|
|
109
|
+
let tempValue = $state<DatePickerValue>(value || {});
|
|
110
|
+
let viewDate = $state(new Date());
|
|
111
|
+
let endViewDate = $state(new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1));
|
|
112
|
+
let containerRef = $state<HTMLDivElement | null>(null);
|
|
113
|
+
|
|
114
|
+
const today = new Date();
|
|
115
|
+
|
|
116
|
+
// Computed
|
|
117
|
+
const shouldShowActions = $derived(showActions ?? mode === 'period');
|
|
118
|
+
const isOpen = $derived(selectingPart === 'date' || selectingPart === 'endDate');
|
|
119
|
+
const displayValue = $derived(shouldShowActions ? tempValue : value);
|
|
120
|
+
const hasStartValue = $derived(!!displayValue?.date);
|
|
121
|
+
const hasEndValue = $derived(!!displayValue?.endDate);
|
|
122
|
+
const inputIcon = $derived(type === 'time' ? 'icon-time' : 'icon-calendar');
|
|
123
|
+
|
|
124
|
+
// Helper functions
|
|
125
|
+
const resolveCalendarInitial = (initial: CalendarInitial | undefined, fallback: Date): Date => {
|
|
126
|
+
if (!initial) return fallback;
|
|
127
|
+
if (initial instanceof Date) return initial;
|
|
128
|
+
|
|
129
|
+
const now = new Date();
|
|
130
|
+
switch (initial) {
|
|
131
|
+
case 'now':
|
|
132
|
+
return new Date(now.getFullYear(), now.getMonth(), 1);
|
|
133
|
+
case 'prevMonth':
|
|
134
|
+
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
135
|
+
case 'nextMonth':
|
|
136
|
+
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
137
|
+
default:
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const formatWithPattern = (date: Date | undefined, time: TimeValue | undefined, pattern: string): string => {
|
|
143
|
+
if (!date && !time) return '';
|
|
144
|
+
|
|
145
|
+
let result = pattern;
|
|
146
|
+
|
|
147
|
+
if (date) {
|
|
148
|
+
result = result.replace(/y/g, String(date.getFullYear()));
|
|
149
|
+
result = result.replace(/m/g, String(date.getMonth() + 1).padStart(2, '0'));
|
|
150
|
+
result = result.replace(/d/g, String(date.getDate()).padStart(2, '0'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (time) {
|
|
154
|
+
result = result.replace(/h/g, String(time.hour).padStart(2, '0'));
|
|
155
|
+
result = result.replace(/i/g, String(time.minute).padStart(2, '0'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const isSameDay = (date1: Date, date2: Date): boolean => {
|
|
162
|
+
return (
|
|
163
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
164
|
+
date1.getMonth() === date2.getMonth() &&
|
|
165
|
+
date1.getDate() === date2.getDate()
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const isInRange = (date: Date, start: Date, end: Date): boolean => {
|
|
170
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
171
|
+
const s = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
172
|
+
const e = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
173
|
+
return d >= s && d <= e;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const isInRangeExclusive = (date: Date, start: Date, end: Date): boolean => {
|
|
177
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
178
|
+
const s = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
179
|
+
const e = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
180
|
+
return d > s && d < e;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getDaysInMonth = (year: number, month: number): number => {
|
|
184
|
+
return new Date(year, month + 1, 0).getDate();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getFirstDayOfMonth = (year: number, month: number): number => {
|
|
188
|
+
return new Date(year, month, 1).getDay();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const isDateRange = (condition: DateCondition): condition is DateRange => {
|
|
192
|
+
return typeof condition === 'object' && 'from' in condition && 'to' in condition;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const matchesCondition = (date: Date, condition: DateCondition): boolean => {
|
|
196
|
+
if (typeof condition === 'function') {
|
|
197
|
+
return condition(date);
|
|
198
|
+
}
|
|
199
|
+
if (isDateRange(condition)) {
|
|
200
|
+
return isInRange(date, condition.from, condition.to);
|
|
201
|
+
}
|
|
202
|
+
return isSameDay(date, condition);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const isDateDisabled = (date: Date): boolean => {
|
|
206
|
+
if (enable && enable.length > 0) {
|
|
207
|
+
const isEnabled = enable.some((condition) => matchesCondition(date, condition));
|
|
208
|
+
return !isEnabled;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (disable && disable.length > 0) {
|
|
212
|
+
return disable.some((condition) => matchesCondition(date, condition));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const isDateTimeLimit = (val: Date | DateTimeLimit): val is DateTimeLimit => {
|
|
219
|
+
return typeof val === 'object' && 'date' in val && !(val instanceof Date);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const extractDateTimeLimit = (limit: Date | DateTimeLimit): { date: Date; time?: TimeValue } => {
|
|
223
|
+
if (isDateTimeLimit(limit)) {
|
|
224
|
+
return { date: limit.date, time: limit.time };
|
|
225
|
+
}
|
|
226
|
+
return { date: limit };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const isBeforeMinDate = (date: Date): boolean => {
|
|
230
|
+
if (!minDate) return false;
|
|
231
|
+
const { date: minDateValue } = extractDateTimeLimit(minDate);
|
|
232
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
233
|
+
const m = new Date(minDateValue.getFullYear(), minDateValue.getMonth(), minDateValue.getDate());
|
|
234
|
+
return d < m;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const isAfterMaxDate = (date: Date): boolean => {
|
|
238
|
+
if (!maxDate) return false;
|
|
239
|
+
const { date: maxDateValue } = extractDateTimeLimit(maxDate);
|
|
240
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
241
|
+
const m = new Date(maxDateValue.getFullYear(), maxDateValue.getMonth(), maxDateValue.getDate());
|
|
242
|
+
return d > m;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const checkDateDisabled = (date: Date): boolean => {
|
|
246
|
+
if (isDateDisabled(date)) return true;
|
|
247
|
+
if (isBeforeMinDate(date)) return true;
|
|
248
|
+
if (isAfterMaxDate(date)) return true;
|
|
249
|
+
return false;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Calendar generation
|
|
253
|
+
const weekDays = ['일', '월', '화', '수', '목', '금', '토'];
|
|
254
|
+
|
|
255
|
+
const calculateYearBounds = () => {
|
|
256
|
+
const currentYear = today.getFullYear();
|
|
257
|
+
let minYearBound = currentYear - 100;
|
|
258
|
+
let maxYearBound = currentYear + 100;
|
|
259
|
+
|
|
260
|
+
if (minDate) {
|
|
261
|
+
const { date } = extractDateTimeLimit(minDate);
|
|
262
|
+
minYearBound = Math.max(minYearBound, date.getFullYear());
|
|
263
|
+
}
|
|
264
|
+
if (maxDate) {
|
|
265
|
+
const { date } = extractDateTimeLimit(maxDate);
|
|
266
|
+
maxYearBound = Math.min(maxYearBound, date.getFullYear());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (yearRange?.min !== undefined) minYearBound = yearRange.min;
|
|
270
|
+
if (yearRange?.max !== undefined) maxYearBound = yearRange.max;
|
|
271
|
+
|
|
272
|
+
return { minYearBound, maxYearBound };
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const { minYearBound, maxYearBound } = calculateYearBounds();
|
|
276
|
+
const yearOptions = Array.from(
|
|
277
|
+
{ length: Math.max(0, maxYearBound - minYearBound + 1) },
|
|
278
|
+
(_, i) => minYearBound + i
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const monthOptions = Array.from({ length: 12 }, (_, i) => i);
|
|
282
|
+
|
|
283
|
+
const generateCalendarDays = (vDate: Date) => {
|
|
284
|
+
const year = vDate.getFullYear();
|
|
285
|
+
const month = vDate.getMonth();
|
|
286
|
+
const daysInMonth = getDaysInMonth(year, month);
|
|
287
|
+
const firstDay = getFirstDayOfMonth(year, month);
|
|
288
|
+
const prevMonthDays = getDaysInMonth(year, month - 1);
|
|
289
|
+
|
|
290
|
+
const days: Array<{ day: number; date: Date; isOther: boolean; isDisabled: boolean }> = [];
|
|
291
|
+
|
|
292
|
+
// Previous month days
|
|
293
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
294
|
+
const day = prevMonthDays - i;
|
|
295
|
+
const date = new Date(year, month - 1, day);
|
|
296
|
+
days.push({ day, date, isOther: true, isDisabled: checkDateDisabled(date) });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Current month days
|
|
300
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
301
|
+
const date = new Date(year, month, day);
|
|
302
|
+
days.push({ day, date, isOther: false, isDisabled: checkDateDisabled(date) });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Next month days
|
|
306
|
+
const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7;
|
|
307
|
+
const remainingDays = totalCells - (firstDay + daysInMonth);
|
|
308
|
+
for (let day = 1; day <= remainingDays; day++) {
|
|
309
|
+
const date = new Date(year, month + 1, day);
|
|
310
|
+
days.push({ day, date, isOther: true, isDisabled: checkDateDisabled(date) });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Group into weeks
|
|
314
|
+
const weeks: typeof days[] = [];
|
|
315
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
316
|
+
weeks.push(days.slice(i, i + 7));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return weeks;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Format display
|
|
323
|
+
const formatPeriodText = () => {
|
|
324
|
+
if (!tempValue.date) return '';
|
|
325
|
+
|
|
326
|
+
if (format) {
|
|
327
|
+
const startText = formatWithPattern(tempValue.date, tempValue.time, format);
|
|
328
|
+
if (tempValue.endDate) {
|
|
329
|
+
const endText = formatWithPattern(tempValue.endDate, tempValue.endTime, format);
|
|
330
|
+
return `${startText} ~ ${endText}`;
|
|
331
|
+
}
|
|
332
|
+
return startText;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const formatKoreanDateTime = (date: Date, time?: TimeValue) => {
|
|
336
|
+
const year = date.getFullYear();
|
|
337
|
+
const month = date.getMonth() + 1;
|
|
338
|
+
const day = date.getDate();
|
|
339
|
+
const dateStr = `${year}년 ${month}월 ${day}일`;
|
|
340
|
+
if (type === 'datetime' && time) {
|
|
341
|
+
const hours = String(time.hour).padStart(2, '0');
|
|
342
|
+
const minutes = String(time.minute).padStart(2, '0');
|
|
343
|
+
return `${dateStr} ${hours}:${minutes}`;
|
|
344
|
+
}
|
|
345
|
+
return dateStr;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const startText = formatKoreanDateTime(tempValue.date, tempValue.time);
|
|
349
|
+
if (tempValue.endDate) {
|
|
350
|
+
const endText = formatKoreanDateTime(tempValue.endDate, tempValue.endTime);
|
|
351
|
+
return `${startText} ~ ${endText}`;
|
|
352
|
+
}
|
|
353
|
+
return startText;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const getDateOnlyFormat = (): string | undefined => {
|
|
357
|
+
if (!format) return undefined;
|
|
358
|
+
return format.replace(/\s*h[:\s]*i[분]?/g, '').replace(/\s*h시\s*i분/g, '').trim();
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const formatDateDisplay = (date: Date | undefined): string => {
|
|
362
|
+
if (!date) {
|
|
363
|
+
const dateFormat = getDateOnlyFormat();
|
|
364
|
+
return dateFormat
|
|
365
|
+
? dateFormat.replace(/y/g, 'YYYY').replace(/m/g, 'MM').replace(/d/g, 'DD')
|
|
366
|
+
: 'YYYY - MM - DD';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const dateFormat = getDateOnlyFormat();
|
|
370
|
+
return dateFormat
|
|
371
|
+
? formatWithPattern(date, undefined, dateFormat)
|
|
372
|
+
: `${date.getFullYear()} - ${String(date.getMonth() + 1).padStart(2, '0')} - ${String(date.getDate()).padStart(2, '0')}`;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Event handlers
|
|
376
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
377
|
+
if (containerRef && !containerRef.contains(event.target as Node)) {
|
|
378
|
+
selectingPart = null;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const handlePartClick = (part: SelectingPart) => {
|
|
383
|
+
if (disabled) return;
|
|
384
|
+
selectingPart = selectingPart === part ? null : part;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const adjustTimeForDate = (date: Date, time: TimeValue | undefined): TimeValue | undefined => {
|
|
388
|
+
if (!time) return time;
|
|
389
|
+
|
|
390
|
+
const minLimit = minDate ? extractDateTimeLimit(minDate) : null;
|
|
391
|
+
const maxLimit = maxDate ? extractDateTimeLimit(maxDate) : null;
|
|
392
|
+
|
|
393
|
+
let adjustedHour = time.hour;
|
|
394
|
+
let adjustedMinute = time.minute;
|
|
395
|
+
|
|
396
|
+
if (minLimit?.time && isSameDay(date, minLimit.date)) {
|
|
397
|
+
if (adjustedHour < minLimit.time.hour) {
|
|
398
|
+
adjustedHour = minLimit.time.hour;
|
|
399
|
+
adjustedMinute = Math.ceil(minLimit.time.minute / minuteStep) * minuteStep;
|
|
400
|
+
} else if (adjustedHour === minLimit.time.hour && adjustedMinute < minLimit.time.minute) {
|
|
401
|
+
adjustedMinute = Math.ceil(minLimit.time.minute / minuteStep) * minuteStep;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (maxLimit?.time && isSameDay(date, maxLimit.date)) {
|
|
406
|
+
if (adjustedHour > maxLimit.time.hour) {
|
|
407
|
+
adjustedHour = maxLimit.time.hour;
|
|
408
|
+
adjustedMinute = Math.floor(maxLimit.time.minute / minuteStep) * minuteStep;
|
|
409
|
+
} else if (adjustedHour === maxLimit.time.hour && adjustedMinute > maxLimit.time.minute) {
|
|
410
|
+
adjustedMinute = Math.floor(maxLimit.time.minute / minuteStep) * minuteStep;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (adjustedHour !== time.hour || adjustedMinute !== time.minute) {
|
|
415
|
+
return { hour: adjustedHour, minute: adjustedMinute };
|
|
416
|
+
}
|
|
417
|
+
return time;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const handleDateSelect = (date: Date) => {
|
|
421
|
+
const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
422
|
+
|
|
423
|
+
if (mode === 'instant') {
|
|
424
|
+
const adjustedTime = adjustTimeForDate(newDate, tempValue.time);
|
|
425
|
+
const newValue = { ...tempValue, date: newDate, time: adjustedTime };
|
|
426
|
+
tempValue = newValue;
|
|
427
|
+
if (!shouldShowActions) {
|
|
428
|
+
value = newValue;
|
|
429
|
+
onchange?.(newValue);
|
|
430
|
+
}
|
|
431
|
+
selectingPart = null;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Period mode
|
|
436
|
+
const existingStartDate = tempValue.date;
|
|
437
|
+
const existingEndDate = tempValue.endDate;
|
|
438
|
+
|
|
439
|
+
if (!existingStartDate) {
|
|
440
|
+
const adjustedTime = adjustTimeForDate(newDate, tempValue.time);
|
|
441
|
+
tempValue = { ...tempValue, date: newDate, time: adjustedTime };
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!existingEndDate) {
|
|
446
|
+
const dateOnly = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
|
|
447
|
+
const startDateOnly = new Date(existingStartDate.getFullYear(), existingStartDate.getMonth(), existingStartDate.getDate());
|
|
448
|
+
|
|
449
|
+
if (dateOnly < startDateOnly) {
|
|
450
|
+
const adjustedStartTime = adjustTimeForDate(newDate, tempValue.time);
|
|
451
|
+
const adjustedEndTime = adjustTimeForDate(existingStartDate, tempValue.time);
|
|
452
|
+
tempValue = {
|
|
453
|
+
date: newDate,
|
|
454
|
+
time: adjustedStartTime,
|
|
455
|
+
endDate: existingStartDate,
|
|
456
|
+
endTime: adjustedEndTime,
|
|
457
|
+
};
|
|
458
|
+
} else {
|
|
459
|
+
const adjustedEndTime = adjustTimeForDate(newDate, tempValue.endTime);
|
|
460
|
+
tempValue = { ...tempValue, endDate: newDate, endTime: adjustedEndTime };
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Reset when both are selected
|
|
466
|
+
const adjustedTime = adjustTimeForDate(newDate, tempValue.time);
|
|
467
|
+
tempValue = { date: newDate, time: adjustedTime, endDate: undefined, endTime: undefined };
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const handleReset = () => {
|
|
471
|
+
tempValue = {};
|
|
472
|
+
selectingPart = null;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const handleApply = () => {
|
|
476
|
+
value = tempValue;
|
|
477
|
+
onchange?.(tempValue);
|
|
478
|
+
selectingPart = null;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const handleYearChange = (e: Event, isEnd = false) => {
|
|
482
|
+
const target = e.target as HTMLSelectElement;
|
|
483
|
+
const newYear = parseInt(target.value);
|
|
484
|
+
if (isEnd) {
|
|
485
|
+
endViewDate = new Date(newYear, endViewDate.getMonth(), 1);
|
|
486
|
+
} else {
|
|
487
|
+
viewDate = new Date(newYear, viewDate.getMonth(), 1);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const handleMonthChange = (e: Event, isEnd = false) => {
|
|
492
|
+
const target = e.target as HTMLSelectElement;
|
|
493
|
+
const newMonth = parseInt(target.value);
|
|
494
|
+
if (isEnd) {
|
|
495
|
+
endViewDate = new Date(endViewDate.getFullYear(), newMonth, 1);
|
|
496
|
+
} else {
|
|
497
|
+
viewDate = new Date(viewDate.getFullYear(), newMonth, 1);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const handlePrevMonth = (isEnd = false) => {
|
|
502
|
+
if (isEnd) {
|
|
503
|
+
endViewDate = new Date(endViewDate.getFullYear(), endViewDate.getMonth() - 1, 1);
|
|
504
|
+
} else {
|
|
505
|
+
viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const handleNextMonth = (isEnd = false) => {
|
|
510
|
+
if (isEnd) {
|
|
511
|
+
endViewDate = new Date(endViewDate.getFullYear(), endViewDate.getMonth() + 1, 1);
|
|
512
|
+
} else {
|
|
513
|
+
viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const handleHourChange = (e: Event, isEnd = false) => {
|
|
518
|
+
const target = e.target as HTMLSelectElement;
|
|
519
|
+
const hour = parseInt(target.value);
|
|
520
|
+
const currentTime = isEnd ? tempValue.endTime : tempValue.time;
|
|
521
|
+
const newTime: TimeValue = { hour, minute: currentTime?.minute ?? 0 };
|
|
522
|
+
|
|
523
|
+
const newValue = isEnd
|
|
524
|
+
? { ...tempValue, endTime: newTime }
|
|
525
|
+
: { ...tempValue, time: newTime };
|
|
526
|
+
tempValue = newValue;
|
|
527
|
+
if (!shouldShowActions) {
|
|
528
|
+
value = newValue;
|
|
529
|
+
onchange?.(newValue);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const handleMinuteChange = (e: Event, isEnd = false) => {
|
|
534
|
+
const target = e.target as HTMLSelectElement;
|
|
535
|
+
const minute = parseInt(target.value);
|
|
536
|
+
const currentTime = isEnd ? tempValue.endTime : tempValue.time;
|
|
537
|
+
const newTime: TimeValue = { hour: currentTime?.hour ?? 0, minute };
|
|
538
|
+
|
|
539
|
+
const newValue = isEnd
|
|
540
|
+
? { ...tempValue, endTime: newTime }
|
|
541
|
+
: { ...tempValue, time: newTime };
|
|
542
|
+
tempValue = newValue;
|
|
543
|
+
if (!shouldShowActions) {
|
|
544
|
+
value = newValue;
|
|
545
|
+
onchange?.(newValue);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Time select options
|
|
550
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
551
|
+
const minutes = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep);
|
|
552
|
+
|
|
553
|
+
// Calendar cell class computation
|
|
554
|
+
const getCellClass = (dayInfo: { day: number; date: Date; isOther: boolean; isDisabled: boolean }) => {
|
|
555
|
+
const { date, isOther, isDisabled } = dayInfo;
|
|
556
|
+
const classes = [styles.calendarCell];
|
|
557
|
+
|
|
558
|
+
if (isDisabled) classes.push(styles.disabled);
|
|
559
|
+
if (isOther) classes.push(styles.other);
|
|
560
|
+
|
|
561
|
+
const isToday = isSameDay(date, today);
|
|
562
|
+
const isSelected = tempValue.date && isSameDay(date, tempValue.date);
|
|
563
|
+
const isRangeStart = mode === 'period' && tempValue.date && isSameDay(date, tempValue.date);
|
|
564
|
+
const isRangeEnd = mode === 'period' && tempValue.endDate && isSameDay(date, tempValue.endDate);
|
|
565
|
+
const isInRangeDay = mode === 'period' && tempValue.date && tempValue.endDate &&
|
|
566
|
+
isInRangeExclusive(date, tempValue.date, tempValue.endDate);
|
|
567
|
+
|
|
568
|
+
if (isToday && !isSelected && !isRangeStart && !isRangeEnd) classes.push(styles.today);
|
|
569
|
+
if (mode === 'instant' && isSelected) classes.push(styles.selected);
|
|
570
|
+
if (isRangeStart) classes.push(styles.rangeStart);
|
|
571
|
+
if (isRangeEnd) classes.push(styles.rangeEnd);
|
|
572
|
+
if (isInRangeDay) classes.push(styles.inRange);
|
|
573
|
+
|
|
574
|
+
return classes.join(' ');
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Initialize
|
|
578
|
+
$effect(() => {
|
|
579
|
+
tempValue = value || {};
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
$effect(() => {
|
|
583
|
+
if (value?.date) {
|
|
584
|
+
viewDate = value.date;
|
|
585
|
+
} else if (initialCalendar?.start) {
|
|
586
|
+
viewDate = resolveCalendarInitial(initialCalendar.start, new Date());
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (value?.endDate) {
|
|
590
|
+
endViewDate = new Date(value.endDate.getFullYear(), value.endDate.getMonth() + 1, 1);
|
|
591
|
+
} else if (initialCalendar?.end) {
|
|
592
|
+
endViewDate = resolveCalendarInitial(initialCalendar.end, new Date());
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
onMount(() => {
|
|
597
|
+
if (typeof document !== 'undefined') {
|
|
598
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
onDestroy(() => {
|
|
603
|
+
if (typeof document !== 'undefined') {
|
|
604
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
</script>
|
|
608
|
+
|
|
609
|
+
<div bind:this={containerRef} class="{styles.datepicker} {className}" {...rest}>
|
|
610
|
+
<div class="{styles.input} {isOpen ? styles.active : ''} {disabled ? styles.disabled : ''}">
|
|
611
|
+
<div class={styles.inputContent}>
|
|
612
|
+
{#if type === 'date'}
|
|
613
|
+
{#if mode === 'period'}
|
|
614
|
+
<button
|
|
615
|
+
type="button"
|
|
616
|
+
class="{styles.inputPart} {selectingPart === 'date' ? styles.active : ''} {!hasStartValue ? styles.placeholder : ''}"
|
|
617
|
+
onclick={() => handlePartClick('date')}
|
|
618
|
+
>
|
|
619
|
+
{formatDateDisplay(displayValue?.date)}
|
|
620
|
+
</button>
|
|
621
|
+
<span class={styles.separator}>~</span>
|
|
622
|
+
<button
|
|
623
|
+
type="button"
|
|
624
|
+
class="{styles.inputPart} {selectingPart === 'endDate' ? styles.active : ''} {!hasEndValue ? styles.placeholder : ''}"
|
|
625
|
+
onclick={() => handlePartClick('endDate')}
|
|
626
|
+
>
|
|
627
|
+
{formatDateDisplay(displayValue?.endDate)}
|
|
628
|
+
</button>
|
|
629
|
+
{:else}
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
class="{styles.inputPart} {selectingPart === 'date' ? styles.active : ''} {!hasStartValue ? styles.placeholder : ''}"
|
|
633
|
+
onclick={() => handlePartClick('date')}
|
|
634
|
+
>
|
|
635
|
+
{formatDateDisplay(displayValue?.date)}
|
|
636
|
+
</button>
|
|
637
|
+
{/if}
|
|
638
|
+
{:else if type === 'time'}
|
|
639
|
+
{#if mode === 'period'}
|
|
640
|
+
<div class={styles.timeSection}>
|
|
641
|
+
<select
|
|
642
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
643
|
+
value={displayValue?.time?.hour ?? 0}
|
|
644
|
+
onchange={(e) => handleHourChange(e, false)}
|
|
645
|
+
{disabled}
|
|
646
|
+
>
|
|
647
|
+
{#each hours as h}
|
|
648
|
+
<option value={h}>{String(h).padStart(2, '0')}</option>
|
|
649
|
+
{/each}
|
|
650
|
+
</select>
|
|
651
|
+
<span class={styles.timeSeparator}>:</span>
|
|
652
|
+
<select
|
|
653
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
654
|
+
value={displayValue?.time?.minute ?? 0}
|
|
655
|
+
onchange={(e) => handleMinuteChange(e, false)}
|
|
656
|
+
{disabled}
|
|
657
|
+
>
|
|
658
|
+
{#each minutes as m}
|
|
659
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
660
|
+
{/each}
|
|
661
|
+
</select>
|
|
662
|
+
</div>
|
|
663
|
+
<span class={styles.separator}>~</span>
|
|
664
|
+
<div class={styles.timeSection}>
|
|
665
|
+
<select
|
|
666
|
+
class="{styles.timeSelect} {!displayValue?.endTime ? styles.placeholder : ''}"
|
|
667
|
+
value={displayValue?.endTime?.hour ?? 0}
|
|
668
|
+
onchange={(e) => handleHourChange(e, true)}
|
|
669
|
+
{disabled}
|
|
670
|
+
>
|
|
671
|
+
{#each hours as h}
|
|
672
|
+
<option value={h}>{String(h).padStart(2, '0')}</option>
|
|
673
|
+
{/each}
|
|
674
|
+
</select>
|
|
675
|
+
<span class={styles.timeSeparator}>:</span>
|
|
676
|
+
<select
|
|
677
|
+
class="{styles.timeSelect} {!displayValue?.endTime ? styles.placeholder : ''}"
|
|
678
|
+
value={displayValue?.endTime?.minute ?? 0}
|
|
679
|
+
onchange={(e) => handleMinuteChange(e, true)}
|
|
680
|
+
{disabled}
|
|
681
|
+
>
|
|
682
|
+
{#each minutes as m}
|
|
683
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
684
|
+
{/each}
|
|
685
|
+
</select>
|
|
686
|
+
</div>
|
|
687
|
+
{:else}
|
|
688
|
+
<div class={styles.timeSection}>
|
|
689
|
+
<select
|
|
690
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
691
|
+
value={displayValue?.time?.hour ?? 0}
|
|
692
|
+
onchange={(e) => handleHourChange(e, false)}
|
|
693
|
+
{disabled}
|
|
694
|
+
>
|
|
695
|
+
{#each hours as h}
|
|
696
|
+
<option value={h}>{String(h).padStart(2, '0')}</option>
|
|
697
|
+
{/each}
|
|
698
|
+
</select>
|
|
699
|
+
<span class={styles.timeSeparator}>:</span>
|
|
700
|
+
<select
|
|
701
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
702
|
+
value={displayValue?.time?.minute ?? 0}
|
|
703
|
+
onchange={(e) => handleMinuteChange(e, false)}
|
|
704
|
+
{disabled}
|
|
705
|
+
>
|
|
706
|
+
{#each minutes as m}
|
|
707
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
708
|
+
{/each}
|
|
709
|
+
</select>
|
|
710
|
+
</div>
|
|
711
|
+
{/if}
|
|
712
|
+
{:else}
|
|
713
|
+
<!-- datetime -->
|
|
714
|
+
{#if mode === 'period'}
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
class="{styles.inputPart} {selectingPart === 'date' ? styles.active : ''} {!hasStartValue ? styles.placeholder : ''}"
|
|
718
|
+
onclick={() => handlePartClick('date')}
|
|
719
|
+
>
|
|
720
|
+
{formatDateDisplay(displayValue?.date)}
|
|
721
|
+
</button>
|
|
722
|
+
<div class={styles.timeSection}>
|
|
723
|
+
<select
|
|
724
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
725
|
+
value={displayValue?.time?.hour ?? 0}
|
|
726
|
+
onchange={(e) => handleHourChange(e, false)}
|
|
727
|
+
{disabled}
|
|
728
|
+
>
|
|
729
|
+
{#each hours as h}
|
|
730
|
+
<option value={h}>{String(h).padStart(2, '0')}</option>
|
|
731
|
+
{/each}
|
|
732
|
+
</select>
|
|
733
|
+
<span class={styles.timeSeparator}>:</span>
|
|
734
|
+
<select
|
|
735
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
736
|
+
value={displayValue?.time?.minute ?? 0}
|
|
737
|
+
onchange={(e) => handleMinuteChange(e, false)}
|
|
738
|
+
{disabled}
|
|
739
|
+
>
|
|
740
|
+
{#each minutes as m}
|
|
741
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
742
|
+
{/each}
|
|
743
|
+
</select>
|
|
744
|
+
</div>
|
|
745
|
+
<span class={styles.separator}>~</span>
|
|
746
|
+
<button
|
|
747
|
+
type="button"
|
|
748
|
+
class="{styles.inputPart} {selectingPart === 'endDate' ? styles.active : ''} {!hasEndValue ? styles.placeholder : ''}"
|
|
749
|
+
onclick={() => handlePartClick('endDate')}
|
|
750
|
+
>
|
|
751
|
+
{formatDateDisplay(displayValue?.endDate)}
|
|
752
|
+
</button>
|
|
753
|
+
<div class={styles.timeSection}>
|
|
754
|
+
<select
|
|
755
|
+
class="{styles.timeSelect} {!displayValue?.endTime ? styles.placeholder : ''}"
|
|
756
|
+
value={displayValue?.endTime?.hour ?? 0}
|
|
757
|
+
onchange={(e) => handleHourChange(e, true)}
|
|
758
|
+
{disabled}
|
|
759
|
+
>
|
|
760
|
+
{#each hours as h}
|
|
761
|
+
<option value={h}>{String(h).padStart(2, '0')}</option>
|
|
762
|
+
{/each}
|
|
763
|
+
</select>
|
|
764
|
+
<span class={styles.timeSeparator}>:</span>
|
|
765
|
+
<select
|
|
766
|
+
class="{styles.timeSelect} {!displayValue?.endTime ? styles.placeholder : ''}"
|
|
767
|
+
value={displayValue?.endTime?.minute ?? 0}
|
|
768
|
+
onchange={(e) => handleMinuteChange(e, true)}
|
|
769
|
+
{disabled}
|
|
770
|
+
>
|
|
771
|
+
{#each minutes as m}
|
|
772
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
773
|
+
{/each}
|
|
774
|
+
</select>
|
|
775
|
+
</div>
|
|
776
|
+
{:else}
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
class="{styles.inputPart} {selectingPart === 'date' ? styles.active : ''} {!hasStartValue ? styles.placeholder : ''}"
|
|
780
|
+
onclick={() => handlePartClick('date')}
|
|
781
|
+
>
|
|
782
|
+
{formatDateDisplay(displayValue?.date)}
|
|
783
|
+
</button>
|
|
784
|
+
<div class={styles.timeSection}>
|
|
785
|
+
<select
|
|
786
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
787
|
+
value={displayValue?.time?.hour ?? 0}
|
|
788
|
+
onchange={(e) => handleHourChange(e, false)}
|
|
789
|
+
{disabled}
|
|
790
|
+
>
|
|
791
|
+
{#each hours as h}
|
|
792
|
+
<option value={h}>{String(h).padStart(2, '0')}</option>
|
|
793
|
+
{/each}
|
|
794
|
+
</select>
|
|
795
|
+
<span class={styles.timeSeparator}>:</span>
|
|
796
|
+
<select
|
|
797
|
+
class="{styles.timeSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
798
|
+
value={displayValue?.time?.minute ?? 0}
|
|
799
|
+
onchange={(e) => handleMinuteChange(e, false)}
|
|
800
|
+
{disabled}
|
|
801
|
+
>
|
|
802
|
+
{#each minutes as m}
|
|
803
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
804
|
+
{/each}
|
|
805
|
+
</select>
|
|
806
|
+
</div>
|
|
807
|
+
{/if}
|
|
808
|
+
{/if}
|
|
809
|
+
</div>
|
|
810
|
+
<i class="{styles.inputIcon} {inputIcon}"></i>
|
|
811
|
+
</div>
|
|
812
|
+
|
|
813
|
+
{#if isOpen}
|
|
814
|
+
<div class="{styles.dropdown} {align === 'right' ? styles.right : ''}">
|
|
815
|
+
{#if mode === 'period'}
|
|
816
|
+
<!-- Period mode: two calendars -->
|
|
817
|
+
<div class={styles.periodCalendars}>
|
|
818
|
+
<div class={styles.periodCalendarLeft}>
|
|
819
|
+
<div class={styles.calendar}>
|
|
820
|
+
<div class={styles.calendarNav}>
|
|
821
|
+
<button type="button" class={styles.navButton} onclick={() => handlePrevMonth(false)}>
|
|
822
|
+
<i class="icon-expand-left"></i>
|
|
823
|
+
</button>
|
|
824
|
+
<div class={styles.navTitle}>
|
|
825
|
+
<div class={styles.navSelectWrapper}>
|
|
826
|
+
<select class={styles.navSelect} value={viewDate.getFullYear()} onchange={(e) => handleYearChange(e, false)}>
|
|
827
|
+
{#each yearOptions as y}
|
|
828
|
+
<option value={y}>{y}년</option>
|
|
829
|
+
{/each}
|
|
830
|
+
</select>
|
|
831
|
+
</div>
|
|
832
|
+
<div class={styles.navSelectWrapper}>
|
|
833
|
+
<select class={styles.navSelect} value={viewDate.getMonth()} onchange={(e) => handleMonthChange(e, false)}>
|
|
834
|
+
{#each monthOptions as m}
|
|
835
|
+
<option value={m}>{m + 1}월</option>
|
|
836
|
+
{/each}
|
|
837
|
+
</select>
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
<button type="button" class={styles.navButton} onclick={() => handleNextMonth(false)}>
|
|
841
|
+
<i class="icon-expand-right"></i>
|
|
842
|
+
</button>
|
|
843
|
+
</div>
|
|
844
|
+
<div class={styles.calendarGrid}>
|
|
845
|
+
<div class={styles.calendarRow}>
|
|
846
|
+
{#each weekDays as day}
|
|
847
|
+
<div class="{styles.calendarCell} {styles.header}">{day}</div>
|
|
848
|
+
{/each}
|
|
849
|
+
</div>
|
|
850
|
+
{#each generateCalendarDays(viewDate) as week}
|
|
851
|
+
<div class={styles.calendarRow}>
|
|
852
|
+
{#each week as dayInfo}
|
|
853
|
+
<button
|
|
854
|
+
type="button"
|
|
855
|
+
class={getCellClass(dayInfo)}
|
|
856
|
+
onclick={() => !dayInfo.isDisabled && handleDateSelect(dayInfo.date)}
|
|
857
|
+
disabled={dayInfo.isDisabled}
|
|
858
|
+
>
|
|
859
|
+
{dayInfo.day}
|
|
860
|
+
</button>
|
|
861
|
+
{/each}
|
|
862
|
+
</div>
|
|
863
|
+
{/each}
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
<div class={styles.periodCalendarRight}>
|
|
868
|
+
<div class={styles.calendar}>
|
|
869
|
+
<div class={styles.calendarNav}>
|
|
870
|
+
<button type="button" class={styles.navButton} onclick={() => handlePrevMonth(true)}>
|
|
871
|
+
<i class="icon-expand-left"></i>
|
|
872
|
+
</button>
|
|
873
|
+
<div class={styles.navTitle}>
|
|
874
|
+
<div class={styles.navSelectWrapper}>
|
|
875
|
+
<select class={styles.navSelect} value={endViewDate.getFullYear()} onchange={(e) => handleYearChange(e, true)}>
|
|
876
|
+
{#each yearOptions as y}
|
|
877
|
+
<option value={y}>{y}년</option>
|
|
878
|
+
{/each}
|
|
879
|
+
</select>
|
|
880
|
+
</div>
|
|
881
|
+
<div class={styles.navSelectWrapper}>
|
|
882
|
+
<select class={styles.navSelect} value={endViewDate.getMonth()} onchange={(e) => handleMonthChange(e, true)}>
|
|
883
|
+
{#each monthOptions as m}
|
|
884
|
+
<option value={m}>{m + 1}월</option>
|
|
885
|
+
{/each}
|
|
886
|
+
</select>
|
|
887
|
+
</div>
|
|
888
|
+
</div>
|
|
889
|
+
<button type="button" class={styles.navButton} onclick={() => handleNextMonth(true)}>
|
|
890
|
+
<i class="icon-expand-right"></i>
|
|
891
|
+
</button>
|
|
892
|
+
</div>
|
|
893
|
+
<div class={styles.calendarGrid}>
|
|
894
|
+
<div class={styles.calendarRow}>
|
|
895
|
+
{#each weekDays as day}
|
|
896
|
+
<div class="{styles.calendarCell} {styles.header}">{day}</div>
|
|
897
|
+
{/each}
|
|
898
|
+
</div>
|
|
899
|
+
{#each generateCalendarDays(endViewDate) as week}
|
|
900
|
+
<div class={styles.calendarRow}>
|
|
901
|
+
{#each week as dayInfo}
|
|
902
|
+
<button
|
|
903
|
+
type="button"
|
|
904
|
+
class={getCellClass(dayInfo)}
|
|
905
|
+
onclick={() => !dayInfo.isDisabled && handleDateSelect(dayInfo.date)}
|
|
906
|
+
disabled={dayInfo.isDisabled}
|
|
907
|
+
>
|
|
908
|
+
{dayInfo.day}
|
|
909
|
+
</button>
|
|
910
|
+
{/each}
|
|
911
|
+
</div>
|
|
912
|
+
{/each}
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
{:else}
|
|
918
|
+
<!-- Instant mode: single calendar -->
|
|
919
|
+
<div class={styles.calendar}>
|
|
920
|
+
<div class={styles.calendarNav}>
|
|
921
|
+
<button type="button" class={styles.navButton} onclick={() => handlePrevMonth(false)}>
|
|
922
|
+
<i class="icon-expand-left"></i>
|
|
923
|
+
</button>
|
|
924
|
+
<div class={styles.navTitle}>
|
|
925
|
+
<div class={styles.navSelectWrapper}>
|
|
926
|
+
<select class={styles.navSelect} value={viewDate.getFullYear()} onchange={(e) => handleYearChange(e, false)}>
|
|
927
|
+
{#each yearOptions as y}
|
|
928
|
+
<option value={y}>{y}년</option>
|
|
929
|
+
{/each}
|
|
930
|
+
</select>
|
|
931
|
+
</div>
|
|
932
|
+
<div class={styles.navSelectWrapper}>
|
|
933
|
+
<select class={styles.navSelect} value={viewDate.getMonth()} onchange={(e) => handleMonthChange(e, false)}>
|
|
934
|
+
{#each monthOptions as m}
|
|
935
|
+
<option value={m}>{m + 1}월</option>
|
|
936
|
+
{/each}
|
|
937
|
+
</select>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
<button type="button" class={styles.navButton} onclick={() => handleNextMonth(false)}>
|
|
941
|
+
<i class="icon-expand-right"></i>
|
|
942
|
+
</button>
|
|
943
|
+
</div>
|
|
944
|
+
<div class={styles.calendarGrid}>
|
|
945
|
+
<div class={styles.calendarRow}>
|
|
946
|
+
{#each weekDays as day}
|
|
947
|
+
<div class="{styles.calendarCell} {styles.header}">{day}</div>
|
|
948
|
+
{/each}
|
|
949
|
+
</div>
|
|
950
|
+
{#each generateCalendarDays(viewDate) as week}
|
|
951
|
+
<div class={styles.calendarRow}>
|
|
952
|
+
{#each week as dayInfo}
|
|
953
|
+
<button
|
|
954
|
+
type="button"
|
|
955
|
+
class={getCellClass(dayInfo)}
|
|
956
|
+
onclick={() => !dayInfo.isDisabled && handleDateSelect(dayInfo.date)}
|
|
957
|
+
disabled={dayInfo.isDisabled}
|
|
958
|
+
>
|
|
959
|
+
{dayInfo.day}
|
|
960
|
+
</button>
|
|
961
|
+
{/each}
|
|
962
|
+
</div>
|
|
963
|
+
{/each}
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
{/if}
|
|
967
|
+
|
|
968
|
+
{#if shouldShowActions}
|
|
969
|
+
<div class={styles.bottomActions}>
|
|
970
|
+
<span class={styles.periodText}>
|
|
971
|
+
{mode === 'period' && tempValue.date ? formatPeriodText() : ''}
|
|
972
|
+
</span>
|
|
973
|
+
<div class={styles.actionButtons}>
|
|
974
|
+
<button type="button" class="{styles.actionButton} {styles.reset}" onclick={handleReset}>
|
|
975
|
+
<i class="icon-refresh"></i>
|
|
976
|
+
초기화
|
|
977
|
+
</button>
|
|
978
|
+
<button type="button" class="{styles.actionButton} {styles.apply}" onclick={handleApply}>
|
|
979
|
+
적용
|
|
980
|
+
</button>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
{/if}
|
|
984
|
+
</div>
|
|
985
|
+
{/if}
|
|
986
|
+
</div>
|