podo-ui 0.8.0 → 0.8.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/font/icon.woff +0 -0
- package/cdn/podo-datepicker.css +486 -0
- package/cdn/podo-datepicker.js +1134 -0
- package/cdn/podo-datepicker.min.css +2 -0
- package/cdn/podo-datepicker.min.js +2 -0
- package/cdn/podo-ui.css +19855 -0
- package/cdn/podo-ui.min.css +2 -0
- package/dist/react/molecule/datepicker.d.ts +20 -0
- package/dist/react/molecule/datepicker.d.ts.map +1 -1
- package/dist/react/molecule/datepicker.js +83 -8
- package/package.json +17 -3
- package/scss/icon/font/icon.woff +0 -0
- package/scss/icon/icon-name.scss +17 -0
- package/vanilla/datepicker.css +481 -0
- package/vanilla/datepicker.js +1129 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Podo UI DatePicker - Vanilla JS
|
|
3
|
+
* A pure JavaScript date picker component without dependencies
|
|
4
|
+
*
|
|
5
|
+
* @version 0.8.0
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
(function (global, factory) {
|
|
10
|
+
typeof exports === 'object' && typeof module !== 'undefined'
|
|
11
|
+
? (module.exports = factory())
|
|
12
|
+
: typeof define === 'function' && define.amd
|
|
13
|
+
? define(factory)
|
|
14
|
+
: ((global = typeof globalThis !== 'undefined' ? globalThis : global || self),
|
|
15
|
+
(global.PodoDatePicker = factory()));
|
|
16
|
+
})(this, function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// CSS class prefix
|
|
20
|
+
const PREFIX = 'podo-datepicker';
|
|
21
|
+
|
|
22
|
+
// Default texts (Korean)
|
|
23
|
+
const DEFAULT_TEXTS = {
|
|
24
|
+
weekDays: ['일', '월', '화', '수', '목', '금', '토'],
|
|
25
|
+
months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
|
|
26
|
+
yearSuffix: '년',
|
|
27
|
+
reset: '초기화',
|
|
28
|
+
apply: '적용',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ============================================
|
|
32
|
+
// Helper Functions
|
|
33
|
+
// ============================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CalendarInitial 값을 Date로 변환
|
|
37
|
+
* @param {string|Date|undefined} initial - 'now', 'prevMonth', 'nextMonth', or Date
|
|
38
|
+
* @param {Date} fallback - 기본값
|
|
39
|
+
* @returns {Date}
|
|
40
|
+
*/
|
|
41
|
+
function resolveCalendarInitial(initial, fallback) {
|
|
42
|
+
if (!initial) return fallback;
|
|
43
|
+
if (initial instanceof Date) return initial;
|
|
44
|
+
|
|
45
|
+
const now = new Date();
|
|
46
|
+
switch (initial) {
|
|
47
|
+
case 'now':
|
|
48
|
+
return new Date(now.getFullYear(), now.getMonth(), 1);
|
|
49
|
+
case 'prevMonth':
|
|
50
|
+
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
51
|
+
case 'nextMonth':
|
|
52
|
+
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
53
|
+
default:
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 커스텀 포맷으로 날짜/시간 포맷팅
|
|
60
|
+
* y: 년(4자리), m: 월(2자리), d: 일(2자리), h: 시(2자리), i: 분(2자리)
|
|
61
|
+
* @param {Date|undefined} date
|
|
62
|
+
* @param {Object|undefined} time - { hour, minute }
|
|
63
|
+
* @param {string} pattern
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function formatWithPattern(date, time, pattern) {
|
|
67
|
+
if (!date && !time) return '';
|
|
68
|
+
|
|
69
|
+
let result = pattern;
|
|
70
|
+
|
|
71
|
+
if (date) {
|
|
72
|
+
result = result.replace(/y/g, String(date.getFullYear()));
|
|
73
|
+
result = result.replace(/m/g, String(date.getMonth() + 1).padStart(2, '0'));
|
|
74
|
+
result = result.replace(/d/g, String(date.getDate()).padStart(2, '0'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (time) {
|
|
78
|
+
result = result.replace(/h/g, String(time.hour).padStart(2, '0'));
|
|
79
|
+
result = result.replace(/i/g, String(time.minute).padStart(2, '0'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 날짜만 표시하는 포맷 추출 (시간 부분 제거)
|
|
87
|
+
* @param {string|undefined} format
|
|
88
|
+
* @returns {string|undefined}
|
|
89
|
+
*/
|
|
90
|
+
function getDateOnlyFormat(format) {
|
|
91
|
+
if (!format) return undefined;
|
|
92
|
+
return format.replace(/\s*h[:\s]*i[분]?/g, '').replace(/\s*h시\s*i분/g, '').trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatDate(date) {
|
|
96
|
+
const year = date.getFullYear();
|
|
97
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
98
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
99
|
+
return `${year} - ${month} - ${day}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatTime(hour, minute) {
|
|
103
|
+
return `${String(hour).padStart(2, '0')} : ${String(minute).padStart(2, '0')}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isSameDay(date1, date2) {
|
|
107
|
+
if (!date1 || !date2) return false;
|
|
108
|
+
return (
|
|
109
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
110
|
+
date1.getMonth() === date2.getMonth() &&
|
|
111
|
+
date1.getDate() === date2.getDate()
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isInRange(date, start, end) {
|
|
116
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
117
|
+
const s = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
118
|
+
const e = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
119
|
+
return d >= s && d <= e;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isInRangeExclusive(date, start, end) {
|
|
123
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
124
|
+
const s = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
125
|
+
const e = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
126
|
+
return d > s && d < e;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getDaysInMonth(year, month) {
|
|
130
|
+
return new Date(year, month + 1, 0).getDate();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getFirstDayOfMonth(year, month) {
|
|
134
|
+
return new Date(year, month, 1).getDay();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isDateRange(condition) {
|
|
138
|
+
return typeof condition === 'object' && condition !== null && 'from' in condition && 'to' in condition;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function matchesCondition(date, condition) {
|
|
142
|
+
if (typeof condition === 'function') {
|
|
143
|
+
return condition(date);
|
|
144
|
+
}
|
|
145
|
+
if (isDateRange(condition)) {
|
|
146
|
+
return isInRange(date, condition.from, condition.to);
|
|
147
|
+
}
|
|
148
|
+
// Date type
|
|
149
|
+
return isSameDay(date, condition);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isDateDisabled(date, disable, enable) {
|
|
153
|
+
// If enable is specified: disable if not matching any condition
|
|
154
|
+
if (enable && enable.length > 0) {
|
|
155
|
+
const isEnabled = enable.some((condition) => matchesCondition(date, condition));
|
|
156
|
+
return !isEnabled;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If disable is specified: disable if matching any condition
|
|
160
|
+
if (disable && disable.length > 0) {
|
|
161
|
+
return disable.some((condition) => matchesCondition(date, condition));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isDateTimeLimit(value) {
|
|
168
|
+
return typeof value === 'object' && value !== null && 'date' in value && !(value instanceof Date);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractDateTimeLimit(limit) {
|
|
172
|
+
if (isDateTimeLimit(limit)) {
|
|
173
|
+
return { date: limit.date, time: limit.time };
|
|
174
|
+
}
|
|
175
|
+
return { date: limit };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isBeforeMinDate(date, minDate) {
|
|
179
|
+
if (!minDate) return false;
|
|
180
|
+
const { date: minDateValue } = extractDateTimeLimit(minDate);
|
|
181
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
182
|
+
const m = new Date(minDateValue.getFullYear(), minDateValue.getMonth(), minDateValue.getDate());
|
|
183
|
+
return d < m;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isAfterMaxDate(date, maxDate) {
|
|
187
|
+
if (!maxDate) return false;
|
|
188
|
+
const { date: maxDateValue } = extractDateTimeLimit(maxDate);
|
|
189
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
190
|
+
const m = new Date(maxDateValue.getFullYear(), maxDateValue.getMonth(), maxDateValue.getDate());
|
|
191
|
+
return d > m;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function createElement(tag, className, content) {
|
|
195
|
+
const el = document.createElement(tag);
|
|
196
|
+
if (className) el.className = className;
|
|
197
|
+
if (content !== undefined) {
|
|
198
|
+
if (typeof content === 'string' || typeof content === 'number') {
|
|
199
|
+
el.textContent = content;
|
|
200
|
+
} else if (content instanceof HTMLElement) {
|
|
201
|
+
el.appendChild(content);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return el;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================
|
|
208
|
+
// PodoDatePicker Class
|
|
209
|
+
// ============================================
|
|
210
|
+
|
|
211
|
+
class PodoDatePicker {
|
|
212
|
+
/**
|
|
213
|
+
* @param {HTMLElement|string} container - Container element or selector
|
|
214
|
+
* @param {Object} options - DatePicker options
|
|
215
|
+
* @param {string} [options.mode='instant'] - 'instant' or 'period'
|
|
216
|
+
* @param {string} [options.type='date'] - 'date', 'time', or 'datetime'
|
|
217
|
+
* @param {Object} [options.value] - Initial value { date, time, endDate, endTime }
|
|
218
|
+
* @param {Function} [options.onChange] - Change callback
|
|
219
|
+
* @param {string} [options.placeholder] - Placeholder text
|
|
220
|
+
* @param {boolean} [options.disabled=false] - Disabled state
|
|
221
|
+
* @param {boolean} [options.showActions] - Show action buttons
|
|
222
|
+
* @param {string} [options.align='left'] - Dropdown alignment 'left' or 'right'
|
|
223
|
+
* @param {Array} [options.disable] - Disable conditions
|
|
224
|
+
* @param {Array} [options.enable] - Enable conditions (only these dates are selectable)
|
|
225
|
+
* @param {Date|Object} [options.minDate] - Minimum selectable date
|
|
226
|
+
* @param {Date|Object} [options.maxDate] - Maximum selectable date
|
|
227
|
+
* @param {number} [options.minuteStep=1] - Minute step (1, 5, 10, 15, 20, 30)
|
|
228
|
+
* @param {Object} [options.texts] - Custom texts for localization
|
|
229
|
+
* @param {string} [options.format] - Date/time format (y: year, m: month, d: day, h: hour, i: minute)
|
|
230
|
+
* @param {Object} [options.initialCalendar] - Initial calendar display month { start, end }
|
|
231
|
+
*/
|
|
232
|
+
constructor(container, options = {}) {
|
|
233
|
+
this.container =
|
|
234
|
+
typeof container === 'string' ? document.querySelector(container) : container;
|
|
235
|
+
|
|
236
|
+
if (!this.container) {
|
|
237
|
+
throw new Error('PodoDatePicker: Container element not found');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Options
|
|
241
|
+
this.mode = options.mode || 'instant';
|
|
242
|
+
this.type = options.type || 'date';
|
|
243
|
+
this.value = options.value || {};
|
|
244
|
+
this.tempValue = { ...this.value };
|
|
245
|
+
this.onChange = options.onChange;
|
|
246
|
+
this.placeholder = options.placeholder;
|
|
247
|
+
this.disabled = options.disabled || false;
|
|
248
|
+
this.showActions = options.showActions ?? this.mode === 'period';
|
|
249
|
+
this.align = options.align || 'left';
|
|
250
|
+
this.disable = options.disable || [];
|
|
251
|
+
this.enable = options.enable || [];
|
|
252
|
+
this.minDate = options.minDate;
|
|
253
|
+
this.maxDate = options.maxDate;
|
|
254
|
+
this.minuteStep = options.minuteStep || 1;
|
|
255
|
+
this.texts = { ...DEFAULT_TEXTS, ...options.texts };
|
|
256
|
+
this.format = options.format;
|
|
257
|
+
this.initialCalendar = options.initialCalendar || {};
|
|
258
|
+
|
|
259
|
+
// State
|
|
260
|
+
this.isOpen = false;
|
|
261
|
+
this.selectingPart = null;
|
|
262
|
+
|
|
263
|
+
// 초기 달력 표시 월 계산
|
|
264
|
+
if (this.value.date) {
|
|
265
|
+
this.viewDate = new Date(this.value.date);
|
|
266
|
+
} else if (this.initialCalendar.start) {
|
|
267
|
+
this.viewDate = resolveCalendarInitial(this.initialCalendar.start, new Date());
|
|
268
|
+
} else {
|
|
269
|
+
this.viewDate = new Date();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (this.value.endDate) {
|
|
273
|
+
this.endViewDate = new Date(
|
|
274
|
+
this.value.endDate.getFullYear(),
|
|
275
|
+
this.value.endDate.getMonth() + 1,
|
|
276
|
+
1
|
|
277
|
+
);
|
|
278
|
+
} else if (this.initialCalendar.end) {
|
|
279
|
+
this.endViewDate = resolveCalendarInitial(this.initialCalendar.end, new Date());
|
|
280
|
+
} else {
|
|
281
|
+
this.endViewDate = new Date(
|
|
282
|
+
this.viewDate.getFullYear(),
|
|
283
|
+
this.viewDate.getMonth() + 1,
|
|
284
|
+
1
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Build UI
|
|
289
|
+
this.render();
|
|
290
|
+
this.bindEvents();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================
|
|
294
|
+
// Rendering
|
|
295
|
+
// ============================================
|
|
296
|
+
|
|
297
|
+
render() {
|
|
298
|
+
this.container.innerHTML = '';
|
|
299
|
+
this.container.className = PREFIX;
|
|
300
|
+
|
|
301
|
+
// Input wrapper
|
|
302
|
+
this.inputEl = createElement('div', `${PREFIX}__input`);
|
|
303
|
+
if (this.disabled) this.inputEl.classList.add(`${PREFIX}__input--disabled`);
|
|
304
|
+
|
|
305
|
+
this.inputContentEl = createElement('div', `${PREFIX}__input-content`);
|
|
306
|
+
this.renderInputContent();
|
|
307
|
+
this.inputEl.appendChild(this.inputContentEl);
|
|
308
|
+
|
|
309
|
+
// Icon
|
|
310
|
+
const iconClass = this.type === 'time' ? 'icon-time' : 'icon-calendar';
|
|
311
|
+
this.iconEl = createElement('i', `${PREFIX}__icon ${iconClass}`);
|
|
312
|
+
this.inputEl.appendChild(this.iconEl);
|
|
313
|
+
|
|
314
|
+
this.container.appendChild(this.inputEl);
|
|
315
|
+
|
|
316
|
+
// Dropdown
|
|
317
|
+
this.dropdownEl = createElement(
|
|
318
|
+
'div',
|
|
319
|
+
`${PREFIX}__dropdown ${this.align === 'right' ? `${PREFIX}__dropdown--right` : ''}`
|
|
320
|
+
);
|
|
321
|
+
this.dropdownEl.style.display = 'none';
|
|
322
|
+
this.container.appendChild(this.dropdownEl);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
renderInputContent() {
|
|
326
|
+
this.inputContentEl.innerHTML = '';
|
|
327
|
+
const displayValue = this.showActions ? this.tempValue : this.value;
|
|
328
|
+
|
|
329
|
+
if (this.type === 'date') {
|
|
330
|
+
this.renderDateInput(displayValue);
|
|
331
|
+
} else if (this.type === 'time') {
|
|
332
|
+
this.renderTimeInput(displayValue);
|
|
333
|
+
} else {
|
|
334
|
+
// datetime
|
|
335
|
+
this.renderDateTimeInput(displayValue);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
renderDateInput(displayValue) {
|
|
340
|
+
// Start date button
|
|
341
|
+
this.startDateBtn = this.createDateButton(displayValue.date, 'date');
|
|
342
|
+
this.inputContentEl.appendChild(this.startDateBtn);
|
|
343
|
+
|
|
344
|
+
if (this.mode === 'period') {
|
|
345
|
+
const sep = createElement('span', `${PREFIX}__separator`, '~');
|
|
346
|
+
this.inputContentEl.appendChild(sep);
|
|
347
|
+
|
|
348
|
+
this.endDateBtn = this.createDateButton(displayValue.endDate, 'endDate');
|
|
349
|
+
this.inputContentEl.appendChild(this.endDateBtn);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
renderTimeInput(displayValue) {
|
|
354
|
+
const startTimeSection = this.createTimeSection(displayValue.time, 'hour', 'minute');
|
|
355
|
+
this.inputContentEl.appendChild(startTimeSection);
|
|
356
|
+
|
|
357
|
+
if (this.mode === 'period') {
|
|
358
|
+
const sep = createElement('span', `${PREFIX}__separator`, '~');
|
|
359
|
+
this.inputContentEl.appendChild(sep);
|
|
360
|
+
|
|
361
|
+
const endTimeSection = this.createTimeSection(displayValue.endTime, 'endHour', 'endMinute');
|
|
362
|
+
this.inputContentEl.appendChild(endTimeSection);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
renderDateTimeInput(displayValue) {
|
|
367
|
+
// Start date
|
|
368
|
+
this.startDateBtn = this.createDateButton(displayValue.date, 'date');
|
|
369
|
+
this.inputContentEl.appendChild(this.startDateBtn);
|
|
370
|
+
|
|
371
|
+
// Start time
|
|
372
|
+
const startTimeSection = this.createTimeSection(displayValue.time, 'hour', 'minute');
|
|
373
|
+
this.inputContentEl.appendChild(startTimeSection);
|
|
374
|
+
|
|
375
|
+
if (this.mode === 'period') {
|
|
376
|
+
const sep = createElement('span', `${PREFIX}__separator`, '~');
|
|
377
|
+
this.inputContentEl.appendChild(sep);
|
|
378
|
+
|
|
379
|
+
// End date
|
|
380
|
+
this.endDateBtn = this.createDateButton(displayValue.endDate, 'endDate');
|
|
381
|
+
this.inputContentEl.appendChild(this.endDateBtn);
|
|
382
|
+
|
|
383
|
+
// End time
|
|
384
|
+
const endTimeSection = this.createTimeSection(displayValue.endTime, 'endHour', 'endMinute');
|
|
385
|
+
this.inputContentEl.appendChild(endTimeSection);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
createDateButton(date, part) {
|
|
390
|
+
const btn = createElement('button', `${PREFIX}__part`);
|
|
391
|
+
btn.type = 'button';
|
|
392
|
+
|
|
393
|
+
const dateFormat = getDateOnlyFormat(this.format);
|
|
394
|
+
|
|
395
|
+
if (!date) {
|
|
396
|
+
btn.classList.add(`${PREFIX}__part--placeholder`);
|
|
397
|
+
// format이 있으면 placeholder도 포맷에 맞게 표시
|
|
398
|
+
const placeholderText = dateFormat
|
|
399
|
+
? dateFormat.replace(/y/g, 'YYYY').replace(/m/g, 'MM').replace(/d/g, 'DD')
|
|
400
|
+
: 'YYYY - MM - DD';
|
|
401
|
+
btn.textContent = placeholderText;
|
|
402
|
+
} else {
|
|
403
|
+
// format prop이 있으면 사용 (날짜만)
|
|
404
|
+
const displayText = dateFormat
|
|
405
|
+
? formatWithPattern(date, null, dateFormat)
|
|
406
|
+
: formatDate(date);
|
|
407
|
+
btn.textContent = displayText;
|
|
408
|
+
}
|
|
409
|
+
btn.dataset.part = part;
|
|
410
|
+
return btn;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
createTimeSection(time, hourPart, minutePart) {
|
|
414
|
+
const section = createElement('div', `${PREFIX}__time-section`);
|
|
415
|
+
|
|
416
|
+
// Hour select
|
|
417
|
+
const hourSelect = this.createHourSelect(time, hourPart);
|
|
418
|
+
section.appendChild(hourSelect);
|
|
419
|
+
|
|
420
|
+
const sep = createElement('span', `${PREFIX}__time-separator`, ':');
|
|
421
|
+
section.appendChild(sep);
|
|
422
|
+
|
|
423
|
+
// Minute select
|
|
424
|
+
const minuteSelect = this.createMinuteSelect(time, minutePart);
|
|
425
|
+
section.appendChild(minuteSelect);
|
|
426
|
+
|
|
427
|
+
return section;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
createHourSelect(time, part) {
|
|
431
|
+
const select = createElement('select', `${PREFIX}__time-select`);
|
|
432
|
+
if (!time) select.classList.add(`${PREFIX}__time-select--placeholder`);
|
|
433
|
+
if (this.disabled) select.disabled = true;
|
|
434
|
+
|
|
435
|
+
const isEnd = part === 'endHour';
|
|
436
|
+
const currentDate = isEnd ? this.tempValue.endDate : this.tempValue.date;
|
|
437
|
+
|
|
438
|
+
for (let h = 0; h < 24; h++) {
|
|
439
|
+
const opt = createElement('option', null, String(h).padStart(2, '0'));
|
|
440
|
+
opt.value = h;
|
|
441
|
+
|
|
442
|
+
// Check hour disabled by minDate/maxDate
|
|
443
|
+
if (this.isHourDisabled(h, currentDate)) {
|
|
444
|
+
opt.disabled = true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
select.appendChild(opt);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
select.value = time?.hour ?? 0;
|
|
451
|
+
select.dataset.part = part;
|
|
452
|
+
return select;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
createMinuteSelect(time, part) {
|
|
456
|
+
const select = createElement('select', `${PREFIX}__time-select`);
|
|
457
|
+
if (!time) select.classList.add(`${PREFIX}__time-select--placeholder`);
|
|
458
|
+
if (this.disabled) select.disabled = true;
|
|
459
|
+
|
|
460
|
+
const isEnd = part === 'endMinute';
|
|
461
|
+
const currentDate = isEnd ? this.tempValue.endDate : this.tempValue.date;
|
|
462
|
+
const currentTime = isEnd ? this.tempValue.endTime : this.tempValue.time;
|
|
463
|
+
|
|
464
|
+
for (let m = 0; m < 60; m += this.minuteStep) {
|
|
465
|
+
const opt = createElement('option', null, String(m).padStart(2, '0'));
|
|
466
|
+
opt.value = m;
|
|
467
|
+
|
|
468
|
+
// Check minute disabled
|
|
469
|
+
if (this.isMinuteDisabled(m, currentDate, currentTime)) {
|
|
470
|
+
opt.disabled = true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
select.appendChild(opt);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let minute = time?.minute ?? 0;
|
|
477
|
+
// Adjust to valid minute step
|
|
478
|
+
if (minute % this.minuteStep !== 0) {
|
|
479
|
+
minute = Math.floor(minute / this.minuteStep) * this.minuteStep;
|
|
480
|
+
}
|
|
481
|
+
select.value = minute;
|
|
482
|
+
select.dataset.part = part;
|
|
483
|
+
return select;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
isHourDisabled(h, currentDate) {
|
|
487
|
+
if (!currentDate) return false;
|
|
488
|
+
|
|
489
|
+
const minLimit = this.minDate ? extractDateTimeLimit(this.minDate) : null;
|
|
490
|
+
const maxLimit = this.maxDate ? extractDateTimeLimit(this.maxDate) : null;
|
|
491
|
+
|
|
492
|
+
if (minLimit?.time && isSameDay(currentDate, minLimit.date)) {
|
|
493
|
+
if (h < minLimit.time.hour) return true;
|
|
494
|
+
}
|
|
495
|
+
if (maxLimit?.time && isSameDay(currentDate, maxLimit.date)) {
|
|
496
|
+
if (h > maxLimit.time.hour) return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
isMinuteDisabled(m, currentDate, currentTime) {
|
|
502
|
+
if (!currentDate || !currentTime) return false;
|
|
503
|
+
|
|
504
|
+
const minLimit = this.minDate ? extractDateTimeLimit(this.minDate) : null;
|
|
505
|
+
const maxLimit = this.maxDate ? extractDateTimeLimit(this.maxDate) : null;
|
|
506
|
+
|
|
507
|
+
if (minLimit?.time && isSameDay(currentDate, minLimit.date) && currentTime.hour === minLimit.time.hour) {
|
|
508
|
+
if (m < minLimit.time.minute) return true;
|
|
509
|
+
}
|
|
510
|
+
if (maxLimit?.time && isSameDay(currentDate, maxLimit.date) && currentTime.hour === maxLimit.time.hour) {
|
|
511
|
+
if (m > maxLimit.time.minute) return true;
|
|
512
|
+
}
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
renderDropdown() {
|
|
517
|
+
this.dropdownEl.innerHTML = '';
|
|
518
|
+
|
|
519
|
+
// Calendar(s)
|
|
520
|
+
if (this.mode === 'period') {
|
|
521
|
+
this.renderPeriodCalendars();
|
|
522
|
+
} else {
|
|
523
|
+
this.renderCalendar(this.viewDate, (date) => this.handleViewDateChange(date));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Action buttons
|
|
527
|
+
if (this.showActions) {
|
|
528
|
+
this.renderActions();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
renderCalendar(viewDate, onViewDateChange, opts = {}) {
|
|
533
|
+
const calendar = createElement('div', `${PREFIX}__calendar`);
|
|
534
|
+
|
|
535
|
+
// Navigation
|
|
536
|
+
const nav = this.renderCalendarNav(viewDate, onViewDateChange, opts);
|
|
537
|
+
calendar.appendChild(nav);
|
|
538
|
+
|
|
539
|
+
// Grid
|
|
540
|
+
const grid = this.renderCalendarGrid(viewDate);
|
|
541
|
+
calendar.appendChild(grid);
|
|
542
|
+
|
|
543
|
+
this.dropdownEl.appendChild(calendar);
|
|
544
|
+
return calendar;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
renderPeriodCalendars() {
|
|
548
|
+
const wrapper = createElement('div', `${PREFIX}__period-calendars`);
|
|
549
|
+
|
|
550
|
+
// Left calendar
|
|
551
|
+
const leftCal = createElement('div', `${PREFIX}__period-calendar-left`);
|
|
552
|
+
const leftCalendar = this.createCalendarElement(
|
|
553
|
+
this.viewDate,
|
|
554
|
+
(date) => this.handleViewDateChange(date),
|
|
555
|
+
{ maxViewDate: this.endViewDate }
|
|
556
|
+
);
|
|
557
|
+
leftCal.appendChild(leftCalendar);
|
|
558
|
+
wrapper.appendChild(leftCal);
|
|
559
|
+
|
|
560
|
+
// Right calendar
|
|
561
|
+
const rightCal = createElement('div', `${PREFIX}__period-calendar-right`);
|
|
562
|
+
const rightCalendar = this.createCalendarElement(
|
|
563
|
+
this.endViewDate,
|
|
564
|
+
(date) => this.handleEndViewDateChange(date),
|
|
565
|
+
{ minViewDate: this.viewDate }
|
|
566
|
+
);
|
|
567
|
+
rightCal.appendChild(rightCalendar);
|
|
568
|
+
wrapper.appendChild(rightCal);
|
|
569
|
+
|
|
570
|
+
this.dropdownEl.appendChild(wrapper);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
createCalendarElement(viewDate, onViewDateChange, opts = {}) {
|
|
574
|
+
const calendar = createElement('div', `${PREFIX}__calendar`);
|
|
575
|
+
|
|
576
|
+
// Navigation
|
|
577
|
+
const nav = this.renderCalendarNav(viewDate, onViewDateChange, opts);
|
|
578
|
+
calendar.appendChild(nav);
|
|
579
|
+
|
|
580
|
+
// Grid
|
|
581
|
+
const grid = this.renderCalendarGrid(viewDate);
|
|
582
|
+
calendar.appendChild(grid);
|
|
583
|
+
|
|
584
|
+
return calendar;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
renderCalendarNav(viewDate, onViewDateChange, opts = {}) {
|
|
588
|
+
const nav = createElement('div', `${PREFIX}__calendar-nav`);
|
|
589
|
+
const year = viewDate.getFullYear();
|
|
590
|
+
const month = viewDate.getMonth();
|
|
591
|
+
|
|
592
|
+
// Calculate navigation limits
|
|
593
|
+
const minViewDate = opts.minViewDate;
|
|
594
|
+
const maxViewDate = opts.maxViewDate;
|
|
595
|
+
|
|
596
|
+
const minYear = minViewDate?.getFullYear();
|
|
597
|
+
const minMonth = minViewDate?.getMonth();
|
|
598
|
+
const maxYear = maxViewDate?.getFullYear();
|
|
599
|
+
const maxMonth = maxViewDate?.getMonth();
|
|
600
|
+
|
|
601
|
+
const isPrevDisabled = minViewDate
|
|
602
|
+
? year < minYear || (year === minYear && month <= minMonth)
|
|
603
|
+
: false;
|
|
604
|
+
const isNextDisabled = maxViewDate
|
|
605
|
+
? year > maxYear || (year === maxYear && month >= maxMonth)
|
|
606
|
+
: false;
|
|
607
|
+
|
|
608
|
+
// Prev button
|
|
609
|
+
const prevBtn = createElement('button', `${PREFIX}__nav-button`);
|
|
610
|
+
prevBtn.type = 'button';
|
|
611
|
+
prevBtn.innerHTML = '<i class="icon-expand-left"></i>';
|
|
612
|
+
if (isPrevDisabled) prevBtn.disabled = true;
|
|
613
|
+
prevBtn.addEventListener('click', () => {
|
|
614
|
+
if (!isPrevDisabled) {
|
|
615
|
+
onViewDateChange(new Date(year, month - 1, 1));
|
|
616
|
+
this.renderDropdown();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
nav.appendChild(prevBtn);
|
|
620
|
+
|
|
621
|
+
// Title (Year/Month selects)
|
|
622
|
+
const title = createElement('div', `${PREFIX}__nav-title`);
|
|
623
|
+
|
|
624
|
+
// Year select
|
|
625
|
+
const yearWrapper = createElement('div', `${PREFIX}__nav-select-wrapper`);
|
|
626
|
+
const yearSelect = createElement('select', `${PREFIX}__nav-select`);
|
|
627
|
+
const currentYear = new Date().getFullYear();
|
|
628
|
+
for (let y = currentYear - 10; y <= currentYear + 10; y++) {
|
|
629
|
+
if (minYear !== undefined && y < minYear) continue;
|
|
630
|
+
if (maxYear !== undefined && y > maxYear) continue;
|
|
631
|
+
const opt = createElement('option', null, `${y}${this.texts.yearSuffix}`);
|
|
632
|
+
opt.value = y;
|
|
633
|
+
yearSelect.appendChild(opt);
|
|
634
|
+
}
|
|
635
|
+
yearSelect.value = year;
|
|
636
|
+
yearSelect.addEventListener('change', (e) => {
|
|
637
|
+
onViewDateChange(new Date(parseInt(e.target.value), month, 1));
|
|
638
|
+
this.renderDropdown();
|
|
639
|
+
});
|
|
640
|
+
yearWrapper.appendChild(yearSelect);
|
|
641
|
+
title.appendChild(yearWrapper);
|
|
642
|
+
|
|
643
|
+
// Month select
|
|
644
|
+
const monthWrapper = createElement('div', `${PREFIX}__nav-select-wrapper`);
|
|
645
|
+
const monthSelect = createElement('select', `${PREFIX}__nav-select`);
|
|
646
|
+
for (let m = 0; m < 12; m++) {
|
|
647
|
+
// Filter based on view limits
|
|
648
|
+
if (minYear !== undefined && minMonth !== undefined && year === minYear && m < minMonth) continue;
|
|
649
|
+
if (maxYear !== undefined && maxMonth !== undefined && year === maxYear && m > maxMonth) continue;
|
|
650
|
+
const opt = createElement('option', null, this.texts.months[m]);
|
|
651
|
+
opt.value = m;
|
|
652
|
+
monthSelect.appendChild(opt);
|
|
653
|
+
}
|
|
654
|
+
monthSelect.value = month;
|
|
655
|
+
monthSelect.addEventListener('change', (e) => {
|
|
656
|
+
onViewDateChange(new Date(year, parseInt(e.target.value), 1));
|
|
657
|
+
this.renderDropdown();
|
|
658
|
+
});
|
|
659
|
+
monthWrapper.appendChild(monthSelect);
|
|
660
|
+
title.appendChild(monthWrapper);
|
|
661
|
+
|
|
662
|
+
nav.appendChild(title);
|
|
663
|
+
|
|
664
|
+
// Next button
|
|
665
|
+
const nextBtn = createElement('button', `${PREFIX}__nav-button`);
|
|
666
|
+
nextBtn.type = 'button';
|
|
667
|
+
nextBtn.innerHTML = '<i class="icon-expand-right"></i>';
|
|
668
|
+
if (isNextDisabled) nextBtn.disabled = true;
|
|
669
|
+
nextBtn.addEventListener('click', () => {
|
|
670
|
+
if (!isNextDisabled) {
|
|
671
|
+
onViewDateChange(new Date(year, month + 1, 1));
|
|
672
|
+
this.renderDropdown();
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
nav.appendChild(nextBtn);
|
|
676
|
+
|
|
677
|
+
return nav;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
renderCalendarGrid(viewDate) {
|
|
681
|
+
const grid = createElement('div', `${PREFIX}__calendar-grid`);
|
|
682
|
+
const year = viewDate.getFullYear();
|
|
683
|
+
const month = viewDate.getMonth();
|
|
684
|
+
const today = new Date();
|
|
685
|
+
|
|
686
|
+
// Week days header
|
|
687
|
+
const headerRow = createElement('div', `${PREFIX}__calendar-row`);
|
|
688
|
+
this.texts.weekDays.forEach((day) => {
|
|
689
|
+
const cell = createElement('div', `${PREFIX}__calendar-cell ${PREFIX}__calendar-cell--header`, day);
|
|
690
|
+
headerRow.appendChild(cell);
|
|
691
|
+
});
|
|
692
|
+
grid.appendChild(headerRow);
|
|
693
|
+
|
|
694
|
+
// Days
|
|
695
|
+
const daysInMonth = getDaysInMonth(year, month);
|
|
696
|
+
const firstDay = getFirstDayOfMonth(year, month);
|
|
697
|
+
const prevMonthDays = getDaysInMonth(year, month - 1);
|
|
698
|
+
|
|
699
|
+
let days = [];
|
|
700
|
+
|
|
701
|
+
// Previous month days
|
|
702
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
703
|
+
const day = prevMonthDays - i;
|
|
704
|
+
const date = new Date(year, month - 1, day);
|
|
705
|
+
days.push({ day, date, isOther: true });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Current month days
|
|
709
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
710
|
+
const date = new Date(year, month, day);
|
|
711
|
+
days.push({ day, date, isOther: false });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Next month days
|
|
715
|
+
const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7;
|
|
716
|
+
const remainingDays = totalCells - (firstDay + daysInMonth);
|
|
717
|
+
for (let day = 1; day <= remainingDays; day++) {
|
|
718
|
+
const date = new Date(year, month + 1, day);
|
|
719
|
+
days.push({ day, date, isOther: true });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Render rows
|
|
723
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
724
|
+
const row = createElement('div', `${PREFIX}__calendar-row`);
|
|
725
|
+
for (let j = 0; j < 7 && i + j < days.length; j++) {
|
|
726
|
+
const { day, date, isOther } = days[i + j];
|
|
727
|
+
const cell = this.createDayCell(day, date, isOther, today);
|
|
728
|
+
row.appendChild(cell);
|
|
729
|
+
}
|
|
730
|
+
grid.appendChild(row);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return grid;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
createDayCell(day, date, isOther, today) {
|
|
737
|
+
const cell = createElement('button', `${PREFIX}__calendar-cell`);
|
|
738
|
+
cell.type = 'button';
|
|
739
|
+
cell.textContent = day;
|
|
740
|
+
|
|
741
|
+
// Check if disabled
|
|
742
|
+
const isDisabled = this.checkDateDisabled(date);
|
|
743
|
+
|
|
744
|
+
if (isOther) cell.classList.add(`${PREFIX}__calendar-cell--other`);
|
|
745
|
+
if (isDisabled) {
|
|
746
|
+
cell.classList.add(`${PREFIX}__calendar-cell--disabled`);
|
|
747
|
+
cell.disabled = true;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Check states
|
|
751
|
+
const isToday = isSameDay(date, today);
|
|
752
|
+
const isSelected = this.mode === 'instant' && isSameDay(date, this.tempValue.date);
|
|
753
|
+
const isRangeStart = this.mode === 'period' && isSameDay(date, this.tempValue.date);
|
|
754
|
+
const isRangeEnd = this.mode === 'period' && isSameDay(date, this.tempValue.endDate);
|
|
755
|
+
const isInRangeDay =
|
|
756
|
+
this.mode === 'period' &&
|
|
757
|
+
this.tempValue.date &&
|
|
758
|
+
this.tempValue.endDate &&
|
|
759
|
+
isInRangeExclusive(date, this.tempValue.date, this.tempValue.endDate);
|
|
760
|
+
|
|
761
|
+
if (isToday && !isSelected && !isRangeStart && !isRangeEnd) {
|
|
762
|
+
cell.classList.add(`${PREFIX}__calendar-cell--today`);
|
|
763
|
+
}
|
|
764
|
+
if (isSelected) cell.classList.add(`${PREFIX}__calendar-cell--selected`);
|
|
765
|
+
if (isRangeStart) cell.classList.add(`${PREFIX}__calendar-cell--range-start`);
|
|
766
|
+
if (isRangeEnd) cell.classList.add(`${PREFIX}__calendar-cell--range-end`);
|
|
767
|
+
if (isInRangeDay) cell.classList.add(`${PREFIX}__calendar-cell--in-range`);
|
|
768
|
+
|
|
769
|
+
if (!isDisabled) {
|
|
770
|
+
cell.addEventListener('click', () => this.handleDateSelect(date));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return cell;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
checkDateDisabled(date) {
|
|
777
|
+
if (isDateDisabled(date, this.disable, this.enable)) return true;
|
|
778
|
+
if (isBeforeMinDate(date, this.minDate)) return true;
|
|
779
|
+
if (isAfterMaxDate(date, this.maxDate)) return true;
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
renderActions() {
|
|
784
|
+
const actions = createElement('div', `${PREFIX}__actions`);
|
|
785
|
+
|
|
786
|
+
// Period text
|
|
787
|
+
const periodText = createElement('span', `${PREFIX}__period-text`);
|
|
788
|
+
if (this.mode === 'period' && this.tempValue.date) {
|
|
789
|
+
periodText.textContent = this.formatPeriodText();
|
|
790
|
+
}
|
|
791
|
+
actions.appendChild(periodText);
|
|
792
|
+
|
|
793
|
+
// Buttons
|
|
794
|
+
const buttons = createElement('div', `${PREFIX}__action-buttons`);
|
|
795
|
+
|
|
796
|
+
// Reset button
|
|
797
|
+
const resetBtn = createElement('button', `${PREFIX}__action-button ${PREFIX}__action-button--reset`);
|
|
798
|
+
resetBtn.type = 'button';
|
|
799
|
+
resetBtn.innerHTML = `<i class="icon-refresh"></i>${this.texts.reset}`;
|
|
800
|
+
resetBtn.addEventListener('click', () => this.handleReset());
|
|
801
|
+
buttons.appendChild(resetBtn);
|
|
802
|
+
|
|
803
|
+
// Apply button
|
|
804
|
+
const applyBtn = createElement('button', `${PREFIX}__action-button ${PREFIX}__action-button--apply`);
|
|
805
|
+
applyBtn.type = 'button';
|
|
806
|
+
applyBtn.textContent = this.texts.apply;
|
|
807
|
+
applyBtn.addEventListener('click', () => this.handleApply());
|
|
808
|
+
buttons.appendChild(applyBtn);
|
|
809
|
+
|
|
810
|
+
actions.appendChild(buttons);
|
|
811
|
+
this.dropdownEl.appendChild(actions);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
formatPeriodText() {
|
|
815
|
+
if (!this.tempValue.date) return '';
|
|
816
|
+
|
|
817
|
+
// format prop이 있으면 사용
|
|
818
|
+
if (this.format) {
|
|
819
|
+
const startText = formatWithPattern(this.tempValue.date, this.tempValue.time, this.format);
|
|
820
|
+
if (this.tempValue.endDate) {
|
|
821
|
+
const endText = formatWithPattern(this.tempValue.endDate, this.tempValue.endTime, this.format);
|
|
822
|
+
return `${startText} ~ ${endText}`;
|
|
823
|
+
}
|
|
824
|
+
return startText;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// 기본 포맷 (한국어)
|
|
828
|
+
const formatKoreanDateTime = (date, time) => {
|
|
829
|
+
const year = date.getFullYear();
|
|
830
|
+
const month = date.getMonth() + 1;
|
|
831
|
+
const day = date.getDate();
|
|
832
|
+
let dateStr = `${year}년 ${month}월 ${day}일`;
|
|
833
|
+
if (this.type === 'datetime' && time) {
|
|
834
|
+
const hours = String(time.hour).padStart(2, '0');
|
|
835
|
+
const minutes = String(time.minute).padStart(2, '0');
|
|
836
|
+
dateStr += ` ${hours}:${minutes}`;
|
|
837
|
+
}
|
|
838
|
+
return dateStr;
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const startText = formatKoreanDateTime(this.tempValue.date, this.tempValue.time);
|
|
842
|
+
if (this.tempValue.endDate) {
|
|
843
|
+
const endText = formatKoreanDateTime(this.tempValue.endDate, this.tempValue.endTime);
|
|
844
|
+
return `${startText} ~ ${endText}`;
|
|
845
|
+
}
|
|
846
|
+
return startText;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ============================================
|
|
850
|
+
// Event Handling
|
|
851
|
+
// ============================================
|
|
852
|
+
|
|
853
|
+
bindEvents() {
|
|
854
|
+
// Input click - open dropdown for date parts
|
|
855
|
+
this.inputEl.addEventListener('click', (e) => {
|
|
856
|
+
if (this.disabled) return;
|
|
857
|
+
|
|
858
|
+
const target = e.target;
|
|
859
|
+
if (target.dataset.part === 'date' || target.dataset.part === 'endDate') {
|
|
860
|
+
this.toggleDropdown(target.dataset.part);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Time select changes
|
|
865
|
+
this.inputContentEl.addEventListener('change', (e) => {
|
|
866
|
+
if (e.target.tagName !== 'SELECT') return;
|
|
867
|
+
|
|
868
|
+
const part = e.target.dataset.part;
|
|
869
|
+
const value = parseInt(e.target.value);
|
|
870
|
+
|
|
871
|
+
this.handleTimeChange(part, value);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Close on outside click
|
|
875
|
+
document.addEventListener('mousedown', (e) => {
|
|
876
|
+
if (!this.container.contains(e.target) && this.isOpen) {
|
|
877
|
+
this.close();
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
toggleDropdown(part) {
|
|
883
|
+
if (this.selectingPart === part && this.isOpen) {
|
|
884
|
+
this.close();
|
|
885
|
+
} else {
|
|
886
|
+
this.selectingPart = part;
|
|
887
|
+
this.open();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
open() {
|
|
892
|
+
this.isOpen = true;
|
|
893
|
+
this.inputEl.classList.add(`${PREFIX}__input--active`);
|
|
894
|
+
this.dropdownEl.style.display = 'flex';
|
|
895
|
+
this.renderDropdown();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
close() {
|
|
899
|
+
this.isOpen = false;
|
|
900
|
+
this.selectingPart = null;
|
|
901
|
+
this.inputEl.classList.remove(`${PREFIX}__input--active`);
|
|
902
|
+
this.dropdownEl.style.display = 'none';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
handleViewDateChange(date) {
|
|
906
|
+
this.viewDate = date;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
handleEndViewDateChange(date) {
|
|
910
|
+
this.endViewDate = date;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
handleDateSelect(date) {
|
|
914
|
+
const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
915
|
+
|
|
916
|
+
if (this.mode === 'instant') {
|
|
917
|
+
const adjustedTime = this.adjustTimeForDate(newDate, this.tempValue.time);
|
|
918
|
+
this.tempValue = { ...this.tempValue, date: newDate, time: adjustedTime };
|
|
919
|
+
|
|
920
|
+
if (!this.showActions) {
|
|
921
|
+
this.value = { ...this.tempValue };
|
|
922
|
+
this.emitChange();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
this.close();
|
|
926
|
+
this.renderInputContent();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Period mode logic
|
|
931
|
+
const existingStartDate = this.tempValue.date;
|
|
932
|
+
const existingEndDate = this.tempValue.endDate;
|
|
933
|
+
|
|
934
|
+
if (!existingStartDate) {
|
|
935
|
+
const adjustedTime = this.adjustTimeForDate(newDate, this.tempValue.time);
|
|
936
|
+
this.tempValue = { ...this.tempValue, date: newDate, time: adjustedTime };
|
|
937
|
+
} else if (!existingEndDate) {
|
|
938
|
+
const dateOnly = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
|
|
939
|
+
const startDateOnly = new Date(
|
|
940
|
+
existingStartDate.getFullYear(),
|
|
941
|
+
existingStartDate.getMonth(),
|
|
942
|
+
existingStartDate.getDate()
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (dateOnly < startDateOnly) {
|
|
946
|
+
const adjustedStartTime = this.adjustTimeForDate(newDate, this.tempValue.time);
|
|
947
|
+
const adjustedEndTime = this.adjustTimeForDate(existingStartDate, this.tempValue.time);
|
|
948
|
+
this.tempValue = {
|
|
949
|
+
date: newDate,
|
|
950
|
+
time: adjustedStartTime,
|
|
951
|
+
endDate: existingStartDate,
|
|
952
|
+
endTime: adjustedEndTime,
|
|
953
|
+
};
|
|
954
|
+
} else {
|
|
955
|
+
const adjustedEndTime = this.adjustTimeForDate(newDate, this.tempValue.endTime);
|
|
956
|
+
this.tempValue = { ...this.tempValue, endDate: newDate, endTime: adjustedEndTime };
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
const adjustedTime = this.adjustTimeForDate(newDate, this.tempValue.time);
|
|
960
|
+
this.tempValue = { date: newDate, time: adjustedTime, endDate: undefined, endTime: undefined };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
this.renderDropdown();
|
|
964
|
+
this.renderInputContent();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
adjustTimeForDate(date, time) {
|
|
968
|
+
if (!time) return time;
|
|
969
|
+
|
|
970
|
+
const minLimit = this.minDate ? extractDateTimeLimit(this.minDate) : null;
|
|
971
|
+
const maxLimit = this.maxDate ? extractDateTimeLimit(this.maxDate) : null;
|
|
972
|
+
|
|
973
|
+
let adjustedHour = time.hour;
|
|
974
|
+
let adjustedMinute = time.minute;
|
|
975
|
+
|
|
976
|
+
if (minLimit?.time && isSameDay(date, minLimit.date)) {
|
|
977
|
+
if (adjustedHour < minLimit.time.hour) {
|
|
978
|
+
adjustedHour = minLimit.time.hour;
|
|
979
|
+
adjustedMinute = Math.ceil(minLimit.time.minute / this.minuteStep) * this.minuteStep;
|
|
980
|
+
} else if (adjustedHour === minLimit.time.hour && adjustedMinute < minLimit.time.minute) {
|
|
981
|
+
adjustedMinute = Math.ceil(minLimit.time.minute / this.minuteStep) * this.minuteStep;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (maxLimit?.time && isSameDay(date, maxLimit.date)) {
|
|
986
|
+
if (adjustedHour > maxLimit.time.hour) {
|
|
987
|
+
adjustedHour = maxLimit.time.hour;
|
|
988
|
+
adjustedMinute = Math.floor(maxLimit.time.minute / this.minuteStep) * this.minuteStep;
|
|
989
|
+
} else if (adjustedHour === maxLimit.time.hour && adjustedMinute > maxLimit.time.minute) {
|
|
990
|
+
adjustedMinute = Math.floor(maxLimit.time.minute / this.minuteStep) * this.minuteStep;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (adjustedHour !== time.hour || adjustedMinute !== time.minute) {
|
|
995
|
+
return { hour: adjustedHour, minute: adjustedMinute };
|
|
996
|
+
}
|
|
997
|
+
return time;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
handleTimeChange(part, value) {
|
|
1001
|
+
const isEnd = part === 'endHour' || part === 'endMinute';
|
|
1002
|
+
const isHour = part === 'hour' || part === 'endHour';
|
|
1003
|
+
|
|
1004
|
+
const currentTime = isEnd ? this.tempValue.endTime : this.tempValue.time;
|
|
1005
|
+
let newTime = { hour: currentTime?.hour ?? 0, minute: currentTime?.minute ?? 0 };
|
|
1006
|
+
|
|
1007
|
+
if (isHour) {
|
|
1008
|
+
newTime.hour = value;
|
|
1009
|
+
// Auto-adjust minute if needed
|
|
1010
|
+
const currentDate = isEnd ? this.tempValue.endDate : this.tempValue.date;
|
|
1011
|
+
if (currentDate) {
|
|
1012
|
+
const minLimit = this.minDate ? extractDateTimeLimit(this.minDate) : null;
|
|
1013
|
+
const maxLimit = this.maxDate ? extractDateTimeLimit(this.maxDate) : null;
|
|
1014
|
+
|
|
1015
|
+
if (minLimit?.time && isSameDay(currentDate, minLimit.date) && value === minLimit.time.hour) {
|
|
1016
|
+
if (newTime.minute < minLimit.time.minute) {
|
|
1017
|
+
newTime.minute = Math.ceil(minLimit.time.minute / this.minuteStep) * this.minuteStep;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (maxLimit?.time && isSameDay(currentDate, maxLimit.date) && value === maxLimit.time.hour) {
|
|
1021
|
+
if (newTime.minute > maxLimit.time.minute) {
|
|
1022
|
+
newTime.minute = Math.floor(maxLimit.time.minute / this.minuteStep) * this.minuteStep;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
newTime.minute = value;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (isEnd) {
|
|
1031
|
+
this.tempValue = { ...this.tempValue, endTime: newTime };
|
|
1032
|
+
} else {
|
|
1033
|
+
this.tempValue = { ...this.tempValue, time: newTime };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!this.showActions) {
|
|
1037
|
+
this.value = { ...this.tempValue };
|
|
1038
|
+
this.emitChange();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
this.renderInputContent();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
handleReset() {
|
|
1045
|
+
this.tempValue = {};
|
|
1046
|
+
this.close();
|
|
1047
|
+
this.renderInputContent();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
handleApply() {
|
|
1051
|
+
this.value = { ...this.tempValue };
|
|
1052
|
+
this.emitChange();
|
|
1053
|
+
this.close();
|
|
1054
|
+
this.renderInputContent();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
emitChange() {
|
|
1058
|
+
if (this.onChange) {
|
|
1059
|
+
this.onChange(this.value);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// ============================================
|
|
1064
|
+
// Public API
|
|
1065
|
+
// ============================================
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Get current value
|
|
1069
|
+
* @returns {Object} Current value
|
|
1070
|
+
*/
|
|
1071
|
+
getValue() {
|
|
1072
|
+
return { ...this.value };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Set value programmatically
|
|
1077
|
+
* @param {Object} value - New value
|
|
1078
|
+
*/
|
|
1079
|
+
setValue(value) {
|
|
1080
|
+
this.value = value || {};
|
|
1081
|
+
this.tempValue = { ...this.value };
|
|
1082
|
+
if (value?.date) {
|
|
1083
|
+
this.viewDate = new Date(value.date);
|
|
1084
|
+
}
|
|
1085
|
+
if (value?.endDate) {
|
|
1086
|
+
this.endViewDate = new Date(value.endDate.getFullYear(), value.endDate.getMonth() + 1, 1);
|
|
1087
|
+
}
|
|
1088
|
+
this.renderInputContent();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Clear value
|
|
1093
|
+
*/
|
|
1094
|
+
clear() {
|
|
1095
|
+
this.value = {};
|
|
1096
|
+
this.tempValue = {};
|
|
1097
|
+
this.renderInputContent();
|
|
1098
|
+
this.emitChange();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Enable the datepicker
|
|
1103
|
+
*/
|
|
1104
|
+
enable() {
|
|
1105
|
+
this.disabled = false;
|
|
1106
|
+
this.inputEl.classList.remove(`${PREFIX}__input--disabled`);
|
|
1107
|
+
this.renderInputContent();
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Disable the datepicker
|
|
1112
|
+
*/
|
|
1113
|
+
disable() {
|
|
1114
|
+
this.disabled = true;
|
|
1115
|
+
this.inputEl.classList.add(`${PREFIX}__input--disabled`);
|
|
1116
|
+
this.close();
|
|
1117
|
+
this.renderInputContent();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Destroy the datepicker instance
|
|
1122
|
+
*/
|
|
1123
|
+
destroy() {
|
|
1124
|
+
this.container.innerHTML = '';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return PodoDatePicker;
|
|
1129
|
+
});
|