podo-ui 1.1.14 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  * Podo UI DatePicker - Vanilla JS
3
3
  * A pure JavaScript date picker component without dependencies
4
4
  *
5
- * @version 0.8.0
5
+ * @version 1.2.0
6
6
  * @license MIT
7
7
  */
8
8
 
@@ -320,7 +320,10 @@
320
320
  * @param {HTMLElement|string} container - Container element or selector
321
321
  * @param {Object} options - DatePicker options
322
322
  * @param {string} [options.mode='instant'] - 'instant' or 'period'
323
- * @param {string} [options.type='date'] - 'date', 'time', or 'datetime'
323
+ * @param {string} [options.type='date'] - 'date', 'time', 'datetime', or 'hour'
324
+ * @param {string} [options.hourFormat='24'] - Hour display format ('24' | '12') when type='hour'
325
+ * @param {number[]} [options.disabledHours] - Hours (0-23) that cannot be selected when type='hour'
326
+ * @param {number} [options.hourStep=1] - Hour step interval (1,2,3,4,6,12) when type='hour'
324
327
  * @param {Object} [options.value] - Initial value { date, time, endDate, endTime }
325
328
  * @param {Function} [options.onChange] - Change callback
326
329
  * @param {string} [options.placeholder] - Placeholder text
@@ -359,7 +362,8 @@
359
362
  this.onChange = options.onChange;
360
363
  this.placeholder = options.placeholder;
361
364
  this.disabled = options.disabled || false;
362
- this.showActions = options.showActions ?? this.mode === 'period';
365
+ // hour-only는 native select 단일 입력이라 적용/초기화 액션 불필요 → 즉시 commit
366
+ this.showActions = options.showActions ?? (this.mode === 'period' && this.type !== 'hour');
363
367
  this.align = options.align || 'left';
364
368
  // Ensure disable and enable are always arrays
365
369
  this.disable = Array.isArray(options.disable) ? options.disable : [];
@@ -367,6 +371,9 @@
367
371
  this.minDate = options.minDate;
368
372
  this.maxDate = options.maxDate;
369
373
  this.minuteStep = options.minuteStep || 1;
374
+ this.hourFormat = options.hourFormat || '24';
375
+ this.disabledHours = Array.isArray(options.disabledHours) ? options.disabledHours : null;
376
+ this.hourStep = options.hourStep || 1;
370
377
  this.texts = { ...DEFAULT_TEXTS, ...options.texts };
371
378
  this.format = options.format;
372
379
  this.initialCalendar = options.initialCalendar || {};
@@ -474,8 +481,8 @@
474
481
  this.renderInputContent();
475
482
  this.inputEl.appendChild(this.inputContentEl);
476
483
 
477
- // Icon
478
- const iconClass = this.type === 'time' ? 'icon-time' : 'icon-calendar';
484
+ // Icon (time/hour 모두 icon-time)
485
+ const iconClass = this.type === 'time' || this.type === 'hour' ? 'icon-time' : 'icon-calendar';
479
486
  this.iconEl = createElement('i', `${PREFIX}__icon ${iconClass}`);
480
487
  this.inputEl.appendChild(this.iconEl);
481
488
 
@@ -528,12 +535,73 @@
528
535
  this.renderDateInput(displayValue);
529
536
  } else if (this.type === 'time') {
530
537
  this.renderTimeInput(displayValue);
538
+ } else if (this.type === 'hour') {
539
+ this.renderHourInput(displayValue);
531
540
  } else {
532
541
  // datetime
533
542
  this.renderDateTimeInput(displayValue);
534
543
  }
535
544
  }
536
545
 
546
+ // hour-only: 분 컬럼/구분자 없이 시 select만 렌더
547
+ renderHourInput(displayValue) {
548
+ const startSection = this.createHourSection(displayValue.time, 'hour');
549
+ this.inputContentEl.appendChild(startSection);
550
+
551
+ if (this.mode === 'period') {
552
+ const sep = createElement('span', `${PREFIX}__separator`, '~');
553
+ this.inputContentEl.appendChild(sep);
554
+
555
+ const endSection = this.createHourSection(displayValue.endTime, 'endHour');
556
+ this.inputContentEl.appendChild(endSection);
557
+ }
558
+ }
559
+
560
+ createHourSection(time, part) {
561
+ const section = createElement('div', `${PREFIX}__time-section ${PREFIX}__hour-section`);
562
+ const select = this.createHourOnlySelect(time, part);
563
+ section.appendChild(select);
564
+ return section;
565
+ }
566
+
567
+ // hour-only 라벨 포맷
568
+ formatHourLabel(h) {
569
+ if (this.hourFormat === '12') {
570
+ const period = h < 12 ? '오전' : '오후';
571
+ const h12 = h % 12 === 0 ? 12 : h % 12;
572
+ return `${period} ${h12}시`;
573
+ }
574
+ return `${h}시`;
575
+ }
576
+
577
+ createHourOnlySelect(time, part) {
578
+ const select = createElement('select', `${PREFIX}__time-select ${PREFIX}__hour-select`);
579
+ if (!time) select.classList.add(`${PREFIX}__time-select--placeholder`);
580
+ if (this.disabled) select.disabled = true;
581
+ select.setAttribute('aria-label', part === 'endHour' ? '종료 시간 선택' : '시간 선택');
582
+
583
+ const isEnd = part === 'endHour';
584
+ const currentDate = isEnd ? this.tempValue.endDate : this.tempValue.date;
585
+
586
+ for (let h = 0; h < 24; h += this.hourStep) {
587
+ const opt = createElement('option', null, this.formatHourLabel(h));
588
+ opt.value = h;
589
+
590
+ // disabledHours 우선
591
+ if (this.disabledHours && this.disabledHours.includes(h)) {
592
+ opt.disabled = true;
593
+ } else if (this.isHourDisabled(h, currentDate)) {
594
+ opt.disabled = true;
595
+ }
596
+
597
+ select.appendChild(opt);
598
+ }
599
+
600
+ select.value = time?.hour ?? 0;
601
+ select.dataset.part = part;
602
+ return select;
603
+ }
604
+
537
605
  renderDateInput(displayValue) {
538
606
  // Start date button
539
607
  this.startDateBtn = this.createDateButton(displayValue.date, 'date');
@@ -1390,8 +1458,12 @@
1390
1458
 
1391
1459
  if (isHour) {
1392
1460
  newTime.hour = value;
1461
+ // hour-only: 분을 0으로 정규화하고 min/max time 보정 건너뜀
1462
+ if (this.type === 'hour') {
1463
+ newTime.minute = 0;
1464
+ }
1393
1465
  // Auto-adjust minute if needed
1394
- const currentDate = isEnd ? this.tempValue.endDate : this.tempValue.date;
1466
+ const currentDate = this.type === 'hour' ? null : (isEnd ? this.tempValue.endDate : this.tempValue.date);
1395
1467
  if (currentDate) {
1396
1468
  const minLimit = this.minDate ? extractDateTimeLimit(this.minDate) : null;
1397
1469
  const maxLimit = this.maxDate ? extractDateTimeLimit(this.maxDate) : null;
@@ -1412,15 +1484,14 @@
1412
1484
  }
1413
1485
 
1414
1486
  if (isEnd) {
1415
- // Auto-set end date to today if not selected when changing end time
1416
- if (!this.tempValue.endDate) {
1487
+ // hour-only는 date 자동 채움을 건너뜀 (value는 time만 가져야 함)
1488
+ if (this.type !== 'hour' && !this.tempValue.endDate) {
1417
1489
  this.tempValue = { ...this.tempValue, endDate: new Date(), endTime: newTime };
1418
1490
  } else {
1419
1491
  this.tempValue = { ...this.tempValue, endTime: newTime };
1420
1492
  }
1421
1493
  } else {
1422
- // Auto-set start date to today if not selected when changing start time
1423
- if (!this.tempValue.date) {
1494
+ if (this.type !== 'hour' && !this.tempValue.date) {
1424
1495
  this.tempValue = { ...this.tempValue, date: new Date(), time: newTime };
1425
1496
  } else {
1426
1497
  this.tempValue = { ...this.tempValue, time: newTime };
@@ -1538,8 +1609,16 @@
1538
1609
  }
1539
1610
 
1540
1611
  handleReset() {
1612
+ // 선택값만 비우고 팝오버는 열린 상태 유지
1613
+ // close()를 호출하면 사용자가 "적용"을 누를 기회 없이 닫혀버리는 문제 수정
1541
1614
  this.tempValue = {};
1542
- this.close();
1615
+ this._activePresetKey = null;
1616
+ this.navigationStep = null;
1617
+ this._navOffset = 0;
1618
+ // 캘린더 그리드/액션 영역을 다시 그려서 비워진 선택 상태를 반영
1619
+ if (this.isOpen) {
1620
+ this.renderDropdown();
1621
+ }
1543
1622
  this.renderInputContent();
1544
1623
  if (typeof this.options.onReset === 'function') {
1545
1624
  this.options.onReset();