mithril-materialized 3.6.0 → 3.8.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.
package/dist/index.umd.js CHANGED
@@ -1610,7 +1610,7 @@
1610
1610
  };
1611
1611
  };
1612
1612
 
1613
- const defaultI18n$3 = {
1613
+ const defaultI18n$4 = {
1614
1614
  cancel: 'Cancel',
1615
1615
  clear: 'Clear',
1616
1616
  done: 'Ok',
@@ -1718,9 +1718,9 @@
1718
1718
  else if (attrs.displayFormat) {
1719
1719
  finalFormat = attrs.displayFormat;
1720
1720
  }
1721
- const merged = Object.assign({ autoClose: false, format: finalFormat, parse: null, defaultDate: null, setDefaultDate: false, disableWeekends: false, disableDayFn: null, firstDay: 0, minDate: null, maxDate: null, yearRange, showClearBtn: false, showWeekNumbers: false, weekNumbering: 'iso', i18n: defaultI18n$3, onSelect: null, onOpen: null, onClose: null }, attrs);
1721
+ const merged = Object.assign({ autoClose: false, format: finalFormat, parse: null, defaultDate: null, setDefaultDate: false, disableWeekends: false, disableDayFn: null, firstDay: 0, minDate: null, maxDate: null, yearRange, showClearBtn: false, showWeekNumbers: false, weekNumbering: 'iso', i18n: defaultI18n$4, onSelect: null, onOpen: null, onClose: null }, attrs);
1722
1722
  // Merge i18n properly
1723
- merged.i18n = Object.assign(Object.assign({}, defaultI18n$3), attrs.i18n);
1723
+ merged.i18n = Object.assign(Object.assign({}, defaultI18n$4), attrs.i18n);
1724
1724
  return merged;
1725
1725
  };
1726
1726
  const toString = (date, format) => {
@@ -4521,7 +4521,7 @@
4521
4521
  opacity: 1,
4522
4522
  };
4523
4523
  };
4524
- const updatePortalDropdown = (items, selectedLabel, onSelectItem) => {
4524
+ const updatePortalDropdown = (items, selectedLabel, onSelectItem, maxHeight) => {
4525
4525
  if (!state.isInsideModal)
4526
4526
  return;
4527
4527
  // Clean up existing portal
@@ -4565,7 +4565,7 @@
4565
4565
  // Create dropdown with proper positioning
4566
4566
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4567
4567
  tabindex: 0,
4568
- style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4568
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
4569
4569
  oncreate: ({ dom }) => {
4570
4570
  state.dropdownRef = dom;
4571
4571
  },
@@ -4629,7 +4629,7 @@
4629
4629
  state.focusedIndex = -1;
4630
4630
  handleSelection(item.id);
4631
4631
  }
4632
- });
4632
+ }, attrs.maxHeight);
4633
4633
  }
4634
4634
  return m('.dropdown-wrapper.input-field', { className, key, style }, [
4635
4635
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
@@ -5298,45 +5298,417 @@
5298
5298
  };
5299
5299
  };
5300
5300
 
5301
- const defaultI18n$2 = {
5302
- cancel: 'Cancel',
5303
- clear: 'Clear',
5304
- done: 'Ok',
5301
+ /**
5302
+ * Shared utility functions for TimePicker and TimeRangePicker components
5303
+ */
5304
+ const addLeadingZero = (num) => {
5305
+ return (num < 10 ? '0' : '') + num;
5305
5306
  };
5306
- const defaultOptions = {
5307
- dialRadius: 135,
5308
- outerRadius: 105,
5309
- innerRadius: 70,
5310
- tickRadius: 20,
5311
- duration: 350,
5312
- container: null,
5313
- defaultTime: 'now',
5314
- fromNow: 0,
5315
- showClearBtn: false,
5316
- i18n: defaultI18n$2,
5317
- autoClose: false,
5318
- twelveHour: true,
5319
- vibrate: true,
5320
- roundBy5: false,
5321
- onOpen: () => { },
5322
- onOpenStart: () => { },
5323
- onOpenEnd: () => { },
5324
- onCloseStart: () => { },
5325
- onCloseEnd: () => { },
5326
- onSelect: () => { },
5307
+ const parseTime = (timeStr, twelveHour) => {
5308
+ if (!timeStr) {
5309
+ return { hours: twelveHour ? 12 : 0, minutes: 0, amOrPm: 'AM' };
5310
+ }
5311
+ const parts = timeStr.trim().split(/[:\s]+/);
5312
+ if (parts.length < 2) {
5313
+ return { hours: twelveHour ? 12 : 0, minutes: 0, amOrPm: 'AM' };
5314
+ }
5315
+ let hours = parseInt(parts[0]) || 0;
5316
+ const minutes = parseInt(parts[1]) || 0;
5317
+ let amOrPm = 'AM';
5318
+ const upperStr = timeStr.toUpperCase();
5319
+ if (upperStr.includes('PM')) {
5320
+ amOrPm = 'PM';
5321
+ }
5322
+ else if (upperStr.includes('AM')) {
5323
+ amOrPm = 'AM';
5324
+ }
5325
+ if (twelveHour) {
5326
+ if (hours >= 12) {
5327
+ amOrPm = 'PM';
5328
+ if (hours > 12)
5329
+ hours -= 12;
5330
+ }
5331
+ else if (hours === 0) {
5332
+ hours = 12;
5333
+ }
5334
+ }
5335
+ return { hours, minutes, amOrPm };
5336
+ };
5337
+ const formatTime = (time, twelveHour) => {
5338
+ if (!time)
5339
+ return '';
5340
+ const { hours, minutes, amOrPm } = time;
5341
+ const hoursStr = addLeadingZero(hours);
5342
+ const minutesStr = addLeadingZero(minutes);
5343
+ if (twelveHour) {
5344
+ return `${hoursStr}:${minutesStr} ${amOrPm}`;
5345
+ }
5346
+ let hours24 = hours;
5347
+ if (amOrPm === 'PM' && hours !== 12) {
5348
+ hours24 = hours + 12;
5349
+ }
5350
+ else if (amOrPm === 'AM' && hours === 12) {
5351
+ hours24 = 0;
5352
+ }
5353
+ return `${addLeadingZero(hours24)}:${minutesStr}`;
5354
+ };
5355
+ const timeToMinutes = (time, twelveHour) => {
5356
+ let h = time.hours;
5357
+ if (twelveHour) {
5358
+ if (time.amOrPm === 'PM' && h !== 12)
5359
+ h += 12;
5360
+ if (time.amOrPm === 'AM' && h === 12)
5361
+ h = 0;
5362
+ }
5363
+ return h * 60 + time.minutes;
5364
+ };
5365
+ const generateHourOptions = (twelveHour, hourStep) => {
5366
+ const start = twelveHour ? 1 : 0;
5367
+ const end = twelveHour ? 12 : 23;
5368
+ const hours = [];
5369
+ for (let i = start; i <= end; i += hourStep) {
5370
+ hours.push(i);
5371
+ }
5372
+ // Special case for 12-hour: include 12 first
5373
+ if (twelveHour && hourStep === 1) {
5374
+ return [12, ...hours.filter((h) => h !== 12)];
5375
+ }
5376
+ return hours;
5377
+ };
5378
+ const generateMinuteOptions = (minuteStep) => {
5379
+ const minutes = [];
5380
+ for (let i = 0; i < 60; i += minuteStep) {
5381
+ minutes.push(i);
5382
+ }
5383
+ return minutes;
5384
+ };
5385
+ const isTimeDisabled = (hours, minutes, amOrPm, minTime, maxTime, twelveHour) => {
5386
+ if (!minTime && !maxTime)
5387
+ return false;
5388
+ const currentMins = timeToMinutes({ hours, minutes, amOrPm }, twelveHour || false);
5389
+ if (minTime) {
5390
+ const minParsed = parseTime(minTime, twelveHour || false);
5391
+ const minMins = timeToMinutes(minParsed, twelveHour || false);
5392
+ if (currentMins < minMins)
5393
+ return true;
5394
+ }
5395
+ if (maxTime) {
5396
+ const maxParsed = parseTime(maxTime, twelveHour || false);
5397
+ const maxMins = timeToMinutes(maxParsed, twelveHour || false);
5398
+ if (currentMins > maxMins)
5399
+ return true;
5400
+ }
5401
+ return false;
5402
+ };
5403
+ const scrollToValue = (container, index, itemHeight, animated = true) => {
5404
+ const scrollTop = index * itemHeight - (container.clientHeight / 2 - itemHeight / 2);
5405
+ if (animated) {
5406
+ container.scrollTo({ top: scrollTop, behavior: 'smooth' });
5407
+ }
5408
+ else {
5409
+ container.scrollTop = scrollTop;
5410
+ }
5411
+ };
5412
+ const snapToNearestItem = (container, itemHeight, onSnap) => {
5413
+ const scrollTop = container.scrollTop;
5414
+ const centerOffset = container.clientHeight / 2;
5415
+ const nearestIndex = Math.round((scrollTop + centerOffset - itemHeight / 2) / itemHeight);
5416
+ scrollToValue(container, nearestIndex, itemHeight, true);
5417
+ onSnap(nearestIndex);
5327
5418
  };
5419
+
5328
5420
  /**
5329
- * TimePicker component based on original Materialize CSS timepicker
5421
+ * DigitalClock component - A scrollable digital time picker
5422
+ *
5423
+ * @example
5424
+ * ```typescript
5425
+ * m(DigitalClock, {
5426
+ * hours: 10,
5427
+ * minutes: 30,
5428
+ * amOrPm: 'AM',
5429
+ * twelveHour: true,
5430
+ * minuteStep: 5,
5431
+ * onTimeChange: (hours, minutes, amOrPm) => {
5432
+ * console.log(`Time changed to ${hours}:${minutes} ${amOrPm}`);
5433
+ * }
5434
+ * })
5435
+ * ```
5330
5436
  */
5331
- const TimePicker = () => {
5332
- let state;
5333
- let options;
5334
- const addLeadingZero = (num) => {
5335
- return (num < 10 ? '0' : '') + num;
5437
+ const DigitalClock = () => {
5438
+ const ITEM_HEIGHT = 48;
5439
+ const state = {};
5440
+ return {
5441
+ view: ({ attrs }) => {
5442
+ const { hours, minutes, amOrPm, twelveHour, minuteStep = 5, hourStep = 1, minTime, maxTime, onTimeChange, spanHours, spanMinutes, spanAmPm, } = attrs;
5443
+ const hourOptions = generateHourOptions(twelveHour, hourStep);
5444
+ const minuteOptions = generateMinuteOptions(minuteStep);
5445
+ return m('.timepicker-digital-clock', [
5446
+ // Hours column
5447
+ m('.digital-clock-column', {
5448
+ oncreate: (vnode) => {
5449
+ state.hourScrollContainer = vnode.dom;
5450
+ const currentIndex = hourOptions.indexOf(hours);
5451
+ if (currentIndex >= 0) {
5452
+ scrollToValue(state.hourScrollContainer, currentIndex + 2, ITEM_HEIGHT, false);
5453
+ }
5454
+ },
5455
+ onwheel: (e) => {
5456
+ e.preventDefault();
5457
+ if (!state.hourScrollContainer)
5458
+ return;
5459
+ const delta = Math.sign(e.deltaY);
5460
+ const currentIndex = hourOptions.indexOf(hours);
5461
+ const newIndex = Math.max(0, Math.min(hourOptions.length - 1, currentIndex + delta));
5462
+ const newHour = hourOptions[newIndex];
5463
+ if (!isTimeDisabled(newHour, minutes, amOrPm, minTime, maxTime, twelveHour)) {
5464
+ onTimeChange(newHour, minutes, amOrPm);
5465
+ if (spanHours) {
5466
+ spanHours.innerHTML = addLeadingZero(newHour);
5467
+ }
5468
+ scrollToValue(state.hourScrollContainer, newIndex + 2, ITEM_HEIGHT, true);
5469
+ m.redraw();
5470
+ }
5471
+ },
5472
+ onscroll: () => {
5473
+ if (state.hourScrollTimeout) {
5474
+ clearTimeout(state.hourScrollTimeout);
5475
+ }
5476
+ state.hourScrollTimeout = window.setTimeout(() => {
5477
+ if (!state.hourScrollContainer)
5478
+ return;
5479
+ snapToNearestItem(state.hourScrollContainer, ITEM_HEIGHT, (index) => {
5480
+ const actualIndex = index - 2; // Account for padding
5481
+ if (actualIndex >= 0 && actualIndex < hourOptions.length) {
5482
+ const newHour = hourOptions[actualIndex];
5483
+ if (!isTimeDisabled(newHour, minutes, amOrPm, minTime, maxTime, twelveHour)) {
5484
+ onTimeChange(newHour, minutes, amOrPm);
5485
+ if (spanHours) {
5486
+ spanHours.innerHTML = addLeadingZero(newHour);
5487
+ }
5488
+ m.redraw();
5489
+ }
5490
+ }
5491
+ });
5492
+ }, 150);
5493
+ },
5494
+ }, [
5495
+ // Padding items for centering
5496
+ m('.digital-clock-item.padding'),
5497
+ m('.digital-clock-item.padding'),
5498
+ // Hour items
5499
+ ...hourOptions.map((hour) => {
5500
+ const disabled = isTimeDisabled(hour, minutes, amOrPm, minTime, maxTime, twelveHour);
5501
+ return m('.digital-clock-item', {
5502
+ class: `${hour === hours ? 'selected' : ''} ${disabled ? 'disabled' : ''}`,
5503
+ onclick: () => {
5504
+ if (disabled)
5505
+ return;
5506
+ onTimeChange(hour, minutes, amOrPm);
5507
+ if (spanHours) {
5508
+ spanHours.innerHTML = addLeadingZero(hour);
5509
+ }
5510
+ if (state.hourScrollContainer) {
5511
+ const index = hourOptions.indexOf(hour);
5512
+ scrollToValue(state.hourScrollContainer, index + 2, ITEM_HEIGHT, true);
5513
+ }
5514
+ m.redraw();
5515
+ },
5516
+ }, addLeadingZero(hour));
5517
+ }),
5518
+ // Padding items for centering
5519
+ m('.digital-clock-item.padding'),
5520
+ m('.digital-clock-item.padding'),
5521
+ ]),
5522
+ // Separator
5523
+ m('.digital-clock-separator', ':'),
5524
+ // Minutes column
5525
+ m('.digital-clock-column', {
5526
+ oncreate: (vnode) => {
5527
+ state.minuteScrollContainer = vnode.dom;
5528
+ const currentIndex = minuteOptions.indexOf(minutes);
5529
+ if (currentIndex >= 0) {
5530
+ scrollToValue(state.minuteScrollContainer, currentIndex + 2, ITEM_HEIGHT, false);
5531
+ }
5532
+ },
5533
+ onwheel: (e) => {
5534
+ e.preventDefault();
5535
+ if (!state.minuteScrollContainer)
5536
+ return;
5537
+ const delta = Math.sign(e.deltaY);
5538
+ const currentIndex = minuteOptions.indexOf(minutes);
5539
+ const newIndex = Math.max(0, Math.min(minuteOptions.length - 1, currentIndex + delta));
5540
+ const newMinute = minuteOptions[newIndex];
5541
+ if (!isTimeDisabled(hours, newMinute, amOrPm, minTime, maxTime, twelveHour)) {
5542
+ onTimeChange(hours, newMinute, amOrPm);
5543
+ if (spanMinutes) {
5544
+ spanMinutes.innerHTML = addLeadingZero(newMinute);
5545
+ }
5546
+ scrollToValue(state.minuteScrollContainer, newIndex + 2, ITEM_HEIGHT, true);
5547
+ m.redraw();
5548
+ }
5549
+ },
5550
+ onscroll: () => {
5551
+ if (state.minuteScrollTimeout) {
5552
+ clearTimeout(state.minuteScrollTimeout);
5553
+ }
5554
+ state.minuteScrollTimeout = window.setTimeout(() => {
5555
+ if (!state.minuteScrollContainer)
5556
+ return;
5557
+ snapToNearestItem(state.minuteScrollContainer, ITEM_HEIGHT, (index) => {
5558
+ const actualIndex = index - 2; // Account for padding
5559
+ if (actualIndex >= 0 && actualIndex < minuteOptions.length) {
5560
+ const newMinute = minuteOptions[actualIndex];
5561
+ if (!isTimeDisabled(hours, newMinute, amOrPm, minTime, maxTime, twelveHour)) {
5562
+ onTimeChange(hours, newMinute, amOrPm);
5563
+ if (spanMinutes) {
5564
+ spanMinutes.innerHTML = addLeadingZero(newMinute);
5565
+ }
5566
+ m.redraw();
5567
+ }
5568
+ }
5569
+ });
5570
+ }, 150);
5571
+ },
5572
+ }, [
5573
+ // Padding items for centering
5574
+ m('.digital-clock-item.padding'),
5575
+ m('.digital-clock-item.padding'),
5576
+ // Minute items
5577
+ ...minuteOptions.map((minute) => {
5578
+ const disabled = isTimeDisabled(hours, minute, amOrPm, minTime, maxTime, twelveHour);
5579
+ return m('.digital-clock-item', {
5580
+ class: `${minute === minutes ? 'selected' : ''} ${disabled ? 'disabled' : ''}`,
5581
+ onclick: () => {
5582
+ if (disabled)
5583
+ return;
5584
+ onTimeChange(hours, minute, amOrPm);
5585
+ if (spanMinutes) {
5586
+ spanMinutes.innerHTML = addLeadingZero(minute);
5587
+ }
5588
+ if (state.minuteScrollContainer) {
5589
+ const index = minuteOptions.indexOf(minute);
5590
+ scrollToValue(state.minuteScrollContainer, index + 2, ITEM_HEIGHT, true);
5591
+ }
5592
+ m.redraw();
5593
+ },
5594
+ }, addLeadingZero(minute));
5595
+ }),
5596
+ // Padding items for centering
5597
+ m('.digital-clock-item.padding'),
5598
+ m('.digital-clock-item.padding'),
5599
+ ]),
5600
+ // AM/PM column (if twelveHour)
5601
+ twelveHour &&
5602
+ m('.digital-clock-column.ampm-column', {
5603
+ oncreate: (vnode) => {
5604
+ state.amPmScrollContainer = vnode.dom;
5605
+ const amPmOptions = ['AM', 'PM'];
5606
+ const currentIndex = amPmOptions.indexOf(amOrPm);
5607
+ if (currentIndex >= 0) {
5608
+ scrollToValue(state.amPmScrollContainer, currentIndex + 2, ITEM_HEIGHT, false);
5609
+ }
5610
+ },
5611
+ onwheel: (e) => {
5612
+ e.preventDefault();
5613
+ const delta = Math.sign(e.deltaY);
5614
+ const newAmPm = delta > 0 ? 'PM' : 'AM';
5615
+ if (newAmPm !== amOrPm && !isTimeDisabled(hours, minutes, newAmPm, minTime, maxTime, twelveHour)) {
5616
+ onTimeChange(hours, minutes, newAmPm);
5617
+ if (spanAmPm) {
5618
+ spanAmPm.innerHTML = newAmPm;
5619
+ }
5620
+ const amPmOptions = ['AM', 'PM'];
5621
+ const newIndex = amPmOptions.indexOf(newAmPm);
5622
+ if (state.amPmScrollContainer) {
5623
+ scrollToValue(state.amPmScrollContainer, newIndex + 2, ITEM_HEIGHT, true);
5624
+ }
5625
+ m.redraw();
5626
+ }
5627
+ },
5628
+ onscroll: () => {
5629
+ if (state.amPmScrollTimeout) {
5630
+ clearTimeout(state.amPmScrollTimeout);
5631
+ }
5632
+ state.amPmScrollTimeout = window.setTimeout(() => {
5633
+ if (!state.amPmScrollContainer)
5634
+ return;
5635
+ snapToNearestItem(state.amPmScrollContainer, ITEM_HEIGHT, (index) => {
5636
+ const actualIndex = index - 2;
5637
+ const amPmOptions = ['AM', 'PM'];
5638
+ if (actualIndex >= 0 && actualIndex < amPmOptions.length) {
5639
+ const newAmPm = amPmOptions[actualIndex];
5640
+ if (!isTimeDisabled(hours, minutes, newAmPm, minTime, maxTime, twelveHour)) {
5641
+ onTimeChange(hours, minutes, newAmPm);
5642
+ if (spanAmPm) {
5643
+ spanAmPm.innerHTML = newAmPm;
5644
+ }
5645
+ m.redraw();
5646
+ }
5647
+ }
5648
+ });
5649
+ }, 150);
5650
+ },
5651
+ }, [
5652
+ // Padding items
5653
+ m('.digital-clock-item.padding'),
5654
+ m('.digital-clock-item.padding'),
5655
+ // AM/PM items
5656
+ ['AM', 'PM'].map((ampm) => {
5657
+ const disabled = isTimeDisabled(hours, minutes, ampm, minTime, maxTime, twelveHour);
5658
+ return m('.digital-clock-item', {
5659
+ class: `${ampm === amOrPm ? 'selected' : ''} ${disabled ? 'disabled' : ''}`,
5660
+ onclick: () => {
5661
+ if (disabled)
5662
+ return;
5663
+ onTimeChange(hours, minutes, ampm);
5664
+ if (spanAmPm) {
5665
+ spanAmPm.innerHTML = ampm;
5666
+ }
5667
+ if (state.amPmScrollContainer) {
5668
+ const amPmOptions = ['AM', 'PM'];
5669
+ const index = amPmOptions.indexOf(ampm);
5670
+ scrollToValue(state.amPmScrollContainer, index + 2, ITEM_HEIGHT, true);
5671
+ }
5672
+ m.redraw();
5673
+ },
5674
+ }, ampm);
5675
+ }),
5676
+ // Padding items
5677
+ m('.digital-clock-item.padding'),
5678
+ m('.digital-clock-item.padding'),
5679
+ ]),
5680
+ ]);
5681
+ },
5336
5682
  };
5337
- const createSVGEl = (name) => {
5338
- const svgNS = 'http://www.w3.org/2000/svg';
5339
- return document.createElementNS(svgNS, name);
5683
+ };
5684
+
5685
+ /**
5686
+ * AnalogClock component - A draggable analog clock face for time selection
5687
+ *
5688
+ * @example
5689
+ * ```typescript
5690
+ * m(AnalogClock, {
5691
+ * hours: 10,
5692
+ * minutes: 30,
5693
+ * amOrPm: 'AM',
5694
+ * currentView: 'hours',
5695
+ * twelveHour: true,
5696
+ * onTimeChange: (hours, minutes) => {
5697
+ * console.log(`Time changed to ${hours}:${minutes}`);
5698
+ * },
5699
+ * onViewChange: (view) => {
5700
+ * console.log(`View changed to ${view}`);
5701
+ * }
5702
+ * })
5703
+ * ```
5704
+ */
5705
+ const AnalogClock = () => {
5706
+ const state = {
5707
+ moved: false,
5708
+ x0: 0,
5709
+ y0: 0,
5710
+ dx: 0,
5711
+ dy: 0,
5340
5712
  };
5341
5713
  const getPos = (e) => {
5342
5714
  const touchEvent = e;
@@ -5346,202 +5718,36 @@
5346
5718
  }
5347
5719
  return { x: mouseEvent.clientX, y: mouseEvent.clientY };
5348
5720
  };
5349
- const vibrate = () => {
5721
+ const vibrate = (attrs) => {
5350
5722
  if (state.vibrateTimer) {
5351
5723
  clearTimeout(state.vibrateTimer);
5352
5724
  }
5353
- if (options.vibrate && navigator.vibrate) {
5725
+ if (attrs.vibrate && navigator.vibrate) {
5354
5726
  navigator.vibrate(10);
5355
5727
  state.vibrateTimer = window.setTimeout(() => {
5356
5728
  state.vibrateTimer = undefined;
5357
5729
  }, 100);
5358
5730
  }
5359
5731
  };
5360
- const handleClockClickStart = (e) => {
5361
- e.preventDefault();
5362
- if (!state.plate)
5363
- return;
5364
- const clockPlateBR = state.plate.getBoundingClientRect();
5365
- const offset = { x: clockPlateBR.left, y: clockPlateBR.top };
5366
- state.x0 = offset.x + options.dialRadius;
5367
- state.y0 = offset.y + options.dialRadius;
5368
- state.moved = false;
5369
- const clickPos = getPos(e);
5370
- state.dx = clickPos.x - state.x0;
5371
- state.dy = clickPos.y - state.y0;
5372
- setHand(state.dx, state.dy, options.roundBy5);
5373
- document.addEventListener('mousemove', handleDocumentClickMove);
5374
- document.addEventListener('touchmove', handleDocumentClickMove);
5375
- document.addEventListener('mouseup', handleDocumentClickEnd);
5376
- document.addEventListener('touchend', handleDocumentClickEnd);
5377
- };
5378
- const handleDocumentClickMove = (e) => {
5379
- e.preventDefault();
5380
- const clickPos = getPos(e);
5381
- const x = clickPos.x - state.x0;
5382
- const y = clickPos.y - state.y0;
5383
- state.moved = true;
5384
- setHand(x, y, options.roundBy5);
5385
- m.redraw();
5386
- };
5387
- const handleDocumentClickEnd = (e) => {
5388
- e.preventDefault();
5389
- document.removeEventListener('mouseup', handleDocumentClickEnd);
5390
- document.removeEventListener('touchend', handleDocumentClickEnd);
5391
- document.removeEventListener('mousemove', handleDocumentClickMove);
5392
- document.removeEventListener('touchmove', handleDocumentClickMove);
5393
- const clickPos = getPos(e);
5394
- const x = clickPos.x - state.x0;
5395
- const y = clickPos.y - state.y0;
5396
- if (state.moved && x === state.dx && y === state.dy) {
5397
- setHand(x, y);
5398
- }
5399
- if (state.currentView === 'hours') {
5400
- showView('minutes', options.duration / 2);
5401
- }
5402
- else if (options.autoClose) {
5403
- if (state.minutesView) {
5404
- state.minutesView.classList.add('timepicker-dial-out');
5405
- }
5406
- setTimeout(() => {
5407
- done();
5408
- }, options.duration / 2);
5409
- }
5410
- if (options.onSelect) {
5411
- options.onSelect(state.hours, state.minutes);
5412
- }
5413
- m.redraw();
5414
- };
5415
- const updateTimeFromInput = (inputValue) => {
5416
- let value = ((inputValue || options.defaultTime || '') + '').split(':');
5417
- let amPmWasProvided = false;
5418
- if (options.twelveHour && value.length > 1) {
5419
- if (value[1].toUpperCase().indexOf('AM') > -1) {
5420
- state.amOrPm = 'AM';
5421
- amPmWasProvided = true;
5422
- }
5423
- else if (value[1].toUpperCase().indexOf('PM') > -1) {
5424
- state.amOrPm = 'PM';
5425
- amPmWasProvided = true;
5426
- }
5427
- value[1] = value[1].replace('AM', '').replace('PM', '').trim();
5428
- }
5429
- if (value[0] === 'now') {
5430
- const now = new Date(+new Date() + options.fromNow);
5431
- value = [now.getHours().toString(), now.getMinutes().toString()];
5432
- if (options.twelveHour) {
5433
- state.amOrPm = parseInt(value[0]) >= 12 ? 'PM' : 'AM';
5434
- amPmWasProvided = false; // For 'now', we need to do conversion
5435
- }
5436
- }
5437
- let hours = +value[0] || 0;
5438
- let minutes = +value[1] || 0;
5439
- if (options.twelveHour) {
5440
- if (!amPmWasProvided) {
5441
- // No AM/PM was provided, assume this is 24-hour format input - convert it
5442
- if (hours >= 12) {
5443
- state.amOrPm = 'PM';
5444
- if (hours > 12) {
5445
- hours = hours - 12;
5446
- }
5447
- }
5448
- else {
5449
- state.amOrPm = 'AM';
5450
- if (hours === 0) {
5451
- hours = 12;
5452
- }
5453
- }
5454
- }
5455
- else {
5456
- // AM/PM was provided, hours are already in 12-hour format
5457
- // Just handle midnight/noon edge cases
5458
- if (hours === 0 && state.amOrPm === 'AM') {
5459
- hours = 12;
5460
- }
5461
- }
5462
- }
5463
- state.hours = hours;
5464
- state.minutes = minutes;
5465
- if (state.spanHours) {
5466
- state.spanHours.innerHTML = addLeadingZero(state.hours);
5467
- }
5468
- if (state.spanMinutes) {
5469
- state.spanMinutes.innerHTML = addLeadingZero(state.minutes);
5470
- }
5471
- updateAmPmView();
5472
- };
5473
- const updateAmPmView = () => {
5474
- if (options.twelveHour && state.amBtn && state.pmBtn) {
5475
- state.amBtn.classList.toggle('text-primary', state.amOrPm === 'AM');
5476
- state.pmBtn.classList.toggle('text-primary', state.amOrPm === 'PM');
5477
- }
5478
- };
5479
- const showView = (view, delay) => {
5480
- const isHours = view === 'hours';
5481
- const nextView = isHours ? state.hoursView : state.minutesView;
5482
- const hideView = isHours ? state.minutesView : state.hoursView;
5483
- state.currentView = view;
5484
- if (state.spanHours) {
5485
- state.spanHours.classList.toggle('text-primary', isHours);
5486
- }
5487
- if (state.spanMinutes) {
5488
- state.spanMinutes.classList.toggle('text-primary', !isHours);
5489
- }
5490
- if (hideView) {
5491
- hideView.classList.add('timepicker-dial-out');
5492
- }
5493
- if (nextView) {
5494
- nextView.style.visibility = 'visible';
5495
- nextView.classList.remove('timepicker-dial-out');
5496
- }
5497
- resetClock(delay);
5498
- if (state.toggleViewTimer) {
5499
- clearTimeout(state.toggleViewTimer);
5500
- }
5501
- state.toggleViewTimer = window.setTimeout(() => {
5502
- if (hideView) {
5503
- hideView.style.visibility = 'hidden';
5504
- }
5505
- }, options.duration);
5506
- };
5507
- const resetClock = (delay) => {
5508
- const view = state.currentView;
5509
- const value = state[view];
5510
- const isHours = view === 'hours';
5511
- const unit = Math.PI / (isHours ? 6 : 30);
5512
- const radian = value * unit;
5513
- const radius = isHours && value > 0 && value < 13 ? options.innerRadius : options.outerRadius;
5514
- const x = Math.sin(radian) * radius;
5515
- const y = -Math.cos(radian) * radius;
5516
- if (delay && state.canvas) {
5517
- state.canvas.classList.add('timepicker-canvas-out');
5518
- setTimeout(() => {
5519
- if (state.canvas) {
5520
- state.canvas.classList.remove('timepicker-canvas-out');
5521
- }
5522
- setHand(x, y);
5523
- }, delay);
5524
- }
5525
- else {
5526
- setHand(x, y);
5527
- }
5528
- };
5529
- const setHand = (x, y, roundBy5, _dragging) => {
5732
+ const setHand = (x, y, attrs, roundBy5, _dragging) => {
5733
+ const outerRadius = attrs.outerRadius || 105;
5734
+ const innerRadius = attrs.innerRadius || 70;
5735
+ const tickRadius = attrs.tickRadius || 20;
5530
5736
  let radian = Math.atan2(x, -y);
5531
- const isHours = state.currentView === 'hours';
5737
+ const isHours = attrs.currentView === 'hours';
5532
5738
  const unit = Math.PI / (isHours || roundBy5 ? 6 : 30);
5533
5739
  const z = Math.sqrt(x * x + y * y);
5534
- const inner = isHours && z < (options.outerRadius + options.innerRadius) / 2;
5535
- let radius = inner ? options.innerRadius : options.outerRadius;
5536
- if (options.twelveHour) {
5537
- radius = options.outerRadius;
5740
+ const inner = isHours && z < (outerRadius + innerRadius) / 2;
5741
+ let radius = inner ? innerRadius : outerRadius;
5742
+ if (attrs.twelveHour) {
5743
+ radius = outerRadius;
5538
5744
  }
5539
5745
  if (radian < 0) {
5540
5746
  radian = Math.PI * 2 + radian;
5541
5747
  }
5542
5748
  let value = Math.round(radian / unit);
5543
5749
  radian = value * unit;
5544
- if (options.twelveHour) {
5750
+ if (attrs.twelveHour) {
5545
5751
  if (isHours) {
5546
5752
  if (value === 0)
5547
5753
  value = 12;
@@ -5566,20 +5772,27 @@
5566
5772
  value = 0;
5567
5773
  }
5568
5774
  }
5569
- if (state[state.currentView] !== value) {
5570
- vibrate();
5775
+ const currentValue = isHours ? attrs.hours : attrs.minutes;
5776
+ if (currentValue !== value) {
5777
+ vibrate(attrs);
5571
5778
  }
5572
- state[state.currentView] = value;
5573
- if (isHours && state.spanHours) {
5574
- state.spanHours.innerHTML = addLeadingZero(value);
5575
- }
5576
- else if (!isHours && state.spanMinutes) {
5577
- state.spanMinutes.innerHTML = addLeadingZero(value);
5779
+ // Update the value
5780
+ if (isHours) {
5781
+ attrs.onTimeChange(value, attrs.minutes);
5782
+ if (attrs.spanHours) {
5783
+ attrs.spanHours.innerHTML = addLeadingZero(value);
5784
+ }
5785
+ }
5786
+ else {
5787
+ attrs.onTimeChange(attrs.hours, value);
5788
+ if (attrs.spanMinutes) {
5789
+ attrs.spanMinutes.innerHTML = addLeadingZero(value);
5790
+ }
5578
5791
  }
5579
5792
  // Set clock hand position
5580
5793
  if (state.hand && state.bg) {
5581
- const cx1 = Math.sin(radian) * (radius - options.tickRadius);
5582
- const cy1 = -Math.cos(radian) * (radius - options.tickRadius);
5794
+ const cx1 = Math.sin(radian) * (radius - tickRadius);
5795
+ const cy1 = -Math.cos(radian) * (radius - tickRadius);
5583
5796
  const cx2 = Math.sin(radian) * radius;
5584
5797
  const cy2 = -Math.cos(radian) * radius;
5585
5798
  state.hand.setAttribute('x2', cx1.toString());
@@ -5588,79 +5801,339 @@
5588
5801
  state.bg.setAttribute('cy', cy2.toString());
5589
5802
  }
5590
5803
  };
5591
- const buildSVGClock = () => {
5592
- if (!state.canvas)
5593
- return;
5594
- const dialRadius = options.dialRadius;
5595
- const tickRadius = options.tickRadius;
5596
- const diameter = dialRadius * 2;
5597
- const svg = createSVGEl('svg');
5598
- svg.setAttribute('class', 'timepicker-svg');
5599
- svg.setAttribute('width', diameter.toString());
5600
- svg.setAttribute('height', diameter.toString());
5601
- const g = createSVGEl('g');
5602
- g.setAttribute('transform', `translate(${dialRadius},${dialRadius})`);
5603
- const bearing = createSVGEl('circle');
5604
- bearing.setAttribute('class', 'timepicker-canvas-bearing');
5605
- bearing.setAttribute('cx', '0');
5606
- bearing.setAttribute('cy', '0');
5607
- bearing.setAttribute('r', '4');
5608
- const hand = createSVGEl('line');
5609
- hand.setAttribute('x1', '0');
5610
- hand.setAttribute('y1', '0');
5611
- const bg = createSVGEl('circle');
5612
- bg.setAttribute('class', 'timepicker-canvas-bg');
5613
- bg.setAttribute('r', tickRadius.toString());
5614
- g.appendChild(hand);
5615
- g.appendChild(bg);
5616
- g.appendChild(bearing);
5617
- svg.appendChild(g);
5618
- state.canvas.appendChild(svg);
5619
- state.hand = hand;
5620
- state.bg = bg;
5621
- state.bearing = bearing;
5622
- state.g = g;
5623
- };
5624
- const buildHoursView = () => {
5625
- if (!state.hoursView)
5804
+ const resetClock = (attrs) => {
5805
+ const view = attrs.currentView;
5806
+ const value = view === 'hours' ? attrs.hours : attrs.minutes;
5807
+ const isHours = view === 'hours';
5808
+ const unit = Math.PI / (isHours ? 6 : 30);
5809
+ const radian = value * unit;
5810
+ const outerRadius = attrs.outerRadius || 105;
5811
+ const innerRadius = attrs.innerRadius || 70;
5812
+ // In 12-hour mode, always use outer radius
5813
+ // In 24-hour mode, use inner radius for hours 1-12, outer for 0 and 13-23
5814
+ let radius = outerRadius;
5815
+ if (!attrs.twelveHour && isHours && value > 0 && value < 13) {
5816
+ radius = innerRadius;
5817
+ }
5818
+ const x = Math.sin(radian) * radius;
5819
+ const y = -Math.cos(radian) * radius;
5820
+ setHand(x, y, attrs);
5821
+ };
5822
+ const handleClockClickStart = (e, attrs) => {
5823
+ e.preventDefault();
5824
+ if (!state.plate)
5626
5825
  return;
5627
- if (options.twelveHour) {
5628
- for (let i = 1; i < 13; i++) {
5629
- const tick = document.createElement('div');
5630
- tick.className = 'timepicker-tick';
5631
- const radian = (i / 6) * Math.PI;
5632
- const radius = options.outerRadius;
5633
- tick.style.left = options.dialRadius + Math.sin(radian) * radius - options.tickRadius + 'px';
5634
- tick.style.top = options.dialRadius - Math.cos(radian) * radius - options.tickRadius + 'px';
5635
- tick.innerHTML = i === 0 ? '00' : i.toString();
5636
- state.hoursView.appendChild(tick);
5826
+ const _dialRadius = attrs.dialRadius || 135;
5827
+ const clockPlateBR = state.plate.getBoundingClientRect();
5828
+ const offset = { x: clockPlateBR.left, y: clockPlateBR.top };
5829
+ state.x0 = offset.x + _dialRadius;
5830
+ state.y0 = offset.y + _dialRadius;
5831
+ state.moved = false;
5832
+ const clickPos = getPos(e);
5833
+ state.dx = clickPos.x - state.x0;
5834
+ state.dy = clickPos.y - state.y0;
5835
+ const startX = clickPos.x;
5836
+ const startY = clickPos.y;
5837
+ setHand(state.dx, state.dy, attrs, attrs.roundBy5);
5838
+ m.redraw();
5839
+ // Add document-level listeners to track dragging
5840
+ const moveHandler = (e) => {
5841
+ e.preventDefault();
5842
+ const clickPos = getPos(e);
5843
+ const x = clickPos.x - state.x0;
5844
+ const y = clickPos.y - state.y0;
5845
+ // Only consider it "moved" if dragged more than 5 pixels
5846
+ const distance = Math.sqrt(Math.pow(clickPos.x - startX, 2) + Math.pow(clickPos.y - startY, 2));
5847
+ if (distance > 5) {
5848
+ state.moved = true;
5849
+ }
5850
+ setHand(x, y, attrs, attrs.roundBy5);
5851
+ };
5852
+ const endHandler = () => {
5853
+ document.removeEventListener('mousemove', moveHandler);
5854
+ document.removeEventListener('touchmove', moveHandler);
5855
+ // After setting hour (either by click or drag), switch to minutes view
5856
+ if (attrs.currentView === 'hours' && attrs.onViewChange) {
5857
+ attrs.onViewChange('minutes');
5858
+ }
5859
+ state.moved = false;
5860
+ m.redraw();
5861
+ };
5862
+ document.addEventListener('mousemove', moveHandler);
5863
+ document.addEventListener('touchmove', moveHandler);
5864
+ document.addEventListener('mouseup', endHandler, { once: true });
5865
+ document.addEventListener('touchend', endHandler, { once: true });
5866
+ };
5867
+ const HourTicks = () => {
5868
+ return {
5869
+ view: ({ attrs }) => {
5870
+ const dialRadius = attrs.dialRadius || 135;
5871
+ const outerRadius = attrs.outerRadius || 105;
5872
+ const innerRadius = attrs.innerRadius || 70;
5873
+ const tickRadius = attrs.tickRadius || 20;
5874
+ const ticks = [];
5875
+ if (attrs.twelveHour) {
5876
+ for (let i = 1; i < 13; i++) {
5877
+ const radian = (i / 6) * Math.PI;
5878
+ const radius = outerRadius;
5879
+ const left = dialRadius + Math.sin(radian) * radius - tickRadius;
5880
+ const top = dialRadius - Math.cos(radian) * radius - tickRadius;
5881
+ ticks.push(m('.timepicker-tick', {
5882
+ style: {
5883
+ left: `${left}px`,
5884
+ top: `${top}px`,
5885
+ },
5886
+ }, i === 0 ? '00' : i.toString()));
5887
+ }
5888
+ }
5889
+ else {
5890
+ for (let i = 0; i < 24; i++) {
5891
+ const radian = (i / 6) * Math.PI;
5892
+ const inner = i > 0 && i < 13;
5893
+ const radius = inner ? innerRadius : outerRadius;
5894
+ const left = dialRadius + Math.sin(radian) * radius - tickRadius;
5895
+ const top = dialRadius - Math.cos(radian) * radius - tickRadius;
5896
+ ticks.push(m('.timepicker-tick', {
5897
+ style: {
5898
+ left: `${left}px`,
5899
+ top: `${top}px`,
5900
+ },
5901
+ }, i === 0 ? '00' : i.toString()));
5902
+ }
5903
+ }
5904
+ return ticks;
5905
+ },
5906
+ };
5907
+ };
5908
+ const MinuteTicks = () => {
5909
+ return {
5910
+ view: ({ attrs }) => {
5911
+ const dialRadius = attrs.dialRadius || 135;
5912
+ const outerRadius = attrs.outerRadius || 105;
5913
+ const tickRadius = attrs.tickRadius || 20;
5914
+ const ticks = [];
5915
+ for (let i = 0; i < 60; i += 5) {
5916
+ const radian = (i / 30) * Math.PI;
5917
+ const left = dialRadius + Math.sin(radian) * outerRadius - tickRadius;
5918
+ const top = dialRadius - Math.cos(radian) * outerRadius - tickRadius;
5919
+ ticks.push(m('.timepicker-tick', {
5920
+ style: {
5921
+ left: `${left}px`,
5922
+ top: `${top}px`,
5923
+ },
5924
+ }, addLeadingZero(i)));
5925
+ }
5926
+ return ticks;
5927
+ },
5928
+ };
5929
+ };
5930
+ return {
5931
+ oncreate: ({ attrs }) => {
5932
+ resetClock(attrs);
5933
+ },
5934
+ view: ({ attrs }) => {
5935
+ // Handle view transitions
5936
+ const isHours = attrs.currentView === 'hours';
5937
+ const dialRadius = attrs.dialRadius || 135;
5938
+ const tickRadius = attrs.tickRadius || 20;
5939
+ const diameter = dialRadius * 2;
5940
+ // Calculate hand and background positions
5941
+ const view = attrs.currentView;
5942
+ const value = view === 'hours' ? attrs.hours : attrs.minutes;
5943
+ const unit = Math.PI / (view === 'hours' ? 6 : 30);
5944
+ const radian = value * unit;
5945
+ const outerRadius = attrs.outerRadius || 105;
5946
+ const innerRadius = attrs.innerRadius || 70;
5947
+ // In 12-hour mode, always use outer radius
5948
+ // In 24-hour mode, use inner radius for hours 1-12, outer for 0 and 13-23
5949
+ let radius = outerRadius;
5950
+ if (!attrs.twelveHour && view === 'hours' && value > 0 && value < 13) {
5951
+ radius = innerRadius;
5952
+ }
5953
+ const cx1 = Math.sin(radian) * (radius - tickRadius);
5954
+ const cy1 = -Math.cos(radian) * (radius - tickRadius);
5955
+ const cx2 = Math.sin(radian) * radius;
5956
+ const cy2 = -Math.cos(radian) * radius;
5957
+ return [
5958
+ m('.timepicker-canvas', {
5959
+ oncreate: (vnode) => {
5960
+ state.canvas = vnode.dom;
5961
+ state.plate = vnode.dom.parentElement;
5962
+ },
5963
+ onmousedown: (e) => handleClockClickStart(e, attrs),
5964
+ ontouchstart: (e) => handleClockClickStart(e, attrs),
5965
+ }, [
5966
+ m('svg.timepicker-svg', {
5967
+ width: diameter,
5968
+ height: diameter,
5969
+ xmlns: 'http://www.w3.org/2000/svg',
5970
+ }, [
5971
+ m('g', {
5972
+ transform: `translate(${dialRadius},${dialRadius})`,
5973
+ oncreate: (vnode) => {
5974
+ state.g = vnode.dom;
5975
+ },
5976
+ }, [
5977
+ m('line', {
5978
+ x1: '0',
5979
+ y1: '0',
5980
+ x2: cx1,
5981
+ y2: cy1,
5982
+ oncreate: (vnode) => {
5983
+ state.hand = vnode.dom;
5984
+ },
5985
+ }),
5986
+ m('circle.timepicker-canvas-bg', {
5987
+ cx: cx2,
5988
+ cy: cy2,
5989
+ r: tickRadius,
5990
+ oncreate: (vnode) => {
5991
+ state.bg = vnode.dom;
5992
+ },
5993
+ }),
5994
+ m('circle.timepicker-canvas-bearing', {
5995
+ cx: '0',
5996
+ cy: '0',
5997
+ r: '4',
5998
+ oncreate: (vnode) => {
5999
+ state.bearing = vnode.dom;
6000
+ },
6001
+ }),
6002
+ ]),
6003
+ ]),
6004
+ ]),
6005
+ m(`.timepicker-dial.timepicker-hours${isHours ? '' : '.timepicker-dial-out'}`, {
6006
+ oncreate: (vnode) => {
6007
+ state.hoursView = vnode.dom;
6008
+ },
6009
+ style: {
6010
+ visibility: isHours ? 'visible' : 'hidden',
6011
+ },
6012
+ }, m(HourTicks, attrs)),
6013
+ m(`.timepicker-dial.timepicker-minutes${!isHours ? '' : '.timepicker-dial-out'}`, {
6014
+ oncreate: (vnode) => {
6015
+ state.minutesView = vnode.dom;
6016
+ },
6017
+ style: {
6018
+ visibility: !isHours ? 'visible' : 'hidden',
6019
+ },
6020
+ }, m(MinuteTicks, attrs)),
6021
+ ];
6022
+ },
6023
+ onupdate: ({ attrs }) => {
6024
+ // Update clock hand when time or view changes
6025
+ resetClock(attrs);
6026
+ },
6027
+ };
6028
+ };
6029
+
6030
+ const defaultI18n$3 = {
6031
+ cancel: 'Cancel',
6032
+ clear: 'Clear',
6033
+ done: 'Ok',
6034
+ next: 'Next',
6035
+ };
6036
+ const defaultOptions = {
6037
+ dialRadius: 135,
6038
+ outerRadius: 105,
6039
+ innerRadius: 70,
6040
+ tickRadius: 20,
6041
+ duration: 350,
6042
+ container: null,
6043
+ defaultTime: 'now',
6044
+ fromNow: 0,
6045
+ showClearBtn: false,
6046
+ i18n: defaultI18n$3,
6047
+ autoClose: false,
6048
+ twelveHour: true,
6049
+ vibrate: true,
6050
+ roundBy5: false,
6051
+ displayMode: 'analog',
6052
+ minuteStep: 5,
6053
+ hourStep: 1,
6054
+ minTime: undefined,
6055
+ maxTime: undefined,
6056
+ onOpen: () => { },
6057
+ onOpenStart: () => { },
6058
+ onOpenEnd: () => { },
6059
+ onCloseStart: () => { },
6060
+ onCloseEnd: () => { },
6061
+ onSelect: () => { },
6062
+ };
6063
+ /**
6064
+ * TimePicker component based on original Materialize CSS timepicker
6065
+ */
6066
+ const TimePicker = () => {
6067
+ let state;
6068
+ let options;
6069
+ // Use shared utilities from time-utils
6070
+ // const addLeadingZero = sharedAddLeadingZero;
6071
+ // const generateHourOptions = sharedGenerateHourOptions;
6072
+ // const generateMinuteOptions = sharedGenerateMinuteOptions;
6073
+ // const scrollToValue = sharedScrollToValue;
6074
+ // const snapToNearestItem = sharedSnapToNearestItem;
6075
+ const updateTimeFromInput = (inputValue) => {
6076
+ let value = ((inputValue || options.defaultTime || '') + '').split(':');
6077
+ let amPmWasProvided = false;
6078
+ if (options.twelveHour && value.length > 1) {
6079
+ if (value[1].toUpperCase().indexOf('AM') > -1) {
6080
+ state.amOrPm = 'AM';
6081
+ amPmWasProvided = true;
5637
6082
  }
6083
+ else if (value[1].toUpperCase().indexOf('PM') > -1) {
6084
+ state.amOrPm = 'PM';
6085
+ amPmWasProvided = true;
6086
+ }
6087
+ value[1] = value[1].replace('AM', '').replace('PM', '').trim();
5638
6088
  }
5639
- else {
5640
- for (let i = 0; i < 24; i++) {
5641
- const tick = document.createElement('div');
5642
- tick.className = 'timepicker-tick';
5643
- const radian = (i / 6) * Math.PI;
5644
- const inner = i > 0 && i < 13;
5645
- const radius = inner ? options.innerRadius : options.outerRadius;
5646
- tick.style.left = options.dialRadius + Math.sin(radian) * radius - options.tickRadius + 'px';
5647
- tick.style.top = options.dialRadius - Math.cos(radian) * radius - options.tickRadius + 'px';
5648
- tick.innerHTML = i === 0 ? '00' : i.toString();
5649
- state.hoursView.appendChild(tick);
6089
+ if (value[0] === 'now') {
6090
+ const now = new Date(+new Date() + options.fromNow);
6091
+ value = [now.getHours().toString(), now.getMinutes().toString()];
6092
+ if (options.twelveHour) {
6093
+ state.amOrPm = parseInt(value[0]) >= 12 ? 'PM' : 'AM';
6094
+ amPmWasProvided = false; // For 'now', we need to do conversion
6095
+ }
6096
+ }
6097
+ let hours = +value[0] || 0;
6098
+ let minutes = +value[1] || 0;
6099
+ if (options.twelveHour) {
6100
+ if (!amPmWasProvided) {
6101
+ // No AM/PM was provided, assume this is 24-hour format input - convert it
6102
+ if (hours >= 12) {
6103
+ state.amOrPm = 'PM';
6104
+ if (hours > 12) {
6105
+ hours = hours - 12;
6106
+ }
6107
+ }
6108
+ else {
6109
+ state.amOrPm = 'AM';
6110
+ if (hours === 0) {
6111
+ hours = 12;
6112
+ }
6113
+ }
5650
6114
  }
6115
+ else {
6116
+ // AM/PM was provided, hours are already in 12-hour format
6117
+ // Just handle midnight/noon edge cases
6118
+ if (hours === 0 && state.amOrPm === 'AM') {
6119
+ hours = 12;
6120
+ }
6121
+ }
6122
+ }
6123
+ state.hours = hours;
6124
+ state.minutes = minutes;
6125
+ if (state.spanHours) {
6126
+ state.spanHours.innerHTML = addLeadingZero(state.hours);
6127
+ }
6128
+ if (state.spanMinutes) {
6129
+ state.spanMinutes.innerHTML = addLeadingZero(state.minutes);
5651
6130
  }
6131
+ updateAmPmView();
5652
6132
  };
5653
- const buildMinutesView = () => {
5654
- if (!state.minutesView)
5655
- return;
5656
- for (let i = 0; i < 60; i += 5) {
5657
- const tick = document.createElement('div');
5658
- tick.className = 'timepicker-tick';
5659
- const radian = (i / 30) * Math.PI;
5660
- tick.style.left = options.dialRadius + Math.sin(radian) * options.outerRadius - options.tickRadius + 'px';
5661
- tick.style.top = options.dialRadius - Math.cos(radian) * options.outerRadius - options.tickRadius + 'px';
5662
- tick.innerHTML = addLeadingZero(i);
5663
- state.minutesView.appendChild(tick);
6133
+ const updateAmPmView = () => {
6134
+ if (options.twelveHour && state.amBtn && state.pmBtn) {
6135
+ state.amBtn.classList.toggle('text-primary', state.amOrPm === 'AM');
6136
+ state.pmBtn.classList.toggle('text-primary', state.amOrPm === 'PM');
5664
6137
  }
5665
6138
  };
5666
6139
  const handleAmPmClick = (ampm) => {
@@ -5672,7 +6145,7 @@
5672
6145
  return;
5673
6146
  state.isOpen = true;
5674
6147
  updateTimeFromInput(inputValue);
5675
- showView('hours');
6148
+ state.currentView = 'hours';
5676
6149
  if (options.onOpen)
5677
6150
  options.onOpen();
5678
6151
  if (options.onOpenStart)
@@ -5706,6 +6179,7 @@
5706
6179
  return {
5707
6180
  view: ({ attrs }) => {
5708
6181
  const { i18n, showClearBtn } = attrs;
6182
+ const isDigitalMode = options.displayMode === 'digital';
5709
6183
  return [
5710
6184
  m('.modal-content.timepicker-container', [
5711
6185
  m('.timepicker-digital-display', [
@@ -5713,7 +6187,12 @@
5713
6187
  m('.timepicker-display-column', [
5714
6188
  m('span.timepicker-span-hours', {
5715
6189
  class: state.currentView === 'hours' ? 'text-primary' : '',
5716
- onclick: () => showView('hours'),
6190
+ onclick: () => {
6191
+ if (!isDigitalMode) {
6192
+ state.currentView = 'hours';
6193
+ m.redraw();
6194
+ }
6195
+ },
5717
6196
  oncreate: (vnode) => {
5718
6197
  state.spanHours = vnode.dom;
5719
6198
  },
@@ -5721,7 +6200,12 @@
5721
6200
  ':',
5722
6201
  m('span.timepicker-span-minutes', {
5723
6202
  class: state.currentView === 'minutes' ? 'text-primary' : '',
5724
- onclick: () => showView('minutes'),
6203
+ onclick: () => {
6204
+ if (!isDigitalMode) {
6205
+ state.currentView = 'minutes';
6206
+ m.redraw();
6207
+ }
6208
+ },
5725
6209
  oncreate: (vnode) => {
5726
6210
  state.spanMinutes = vnode.dom;
5727
6211
  },
@@ -5752,66 +6236,108 @@
5752
6236
  ]),
5753
6237
  ]),
5754
6238
  ]),
5755
- m('.timepicker-analog-display', [
5756
- m('.timepicker-plate', {
5757
- oncreate: (vnode) => {
5758
- state.plate = vnode.dom;
5759
- state.plate.addEventListener('mousedown', handleClockClickStart);
5760
- state.plate.addEventListener('touchstart', handleClockClickStart);
5761
- },
5762
- onremove: () => {
5763
- if (state.plate) {
5764
- state.plate.removeEventListener('mousedown', handleClockClickStart);
5765
- state.plate.removeEventListener('touchstart', handleClockClickStart);
5766
- }
5767
- },
5768
- }, [
5769
- m('.timepicker-canvas', {
5770
- oncreate: (vnode) => {
5771
- state.canvas = vnode.dom;
5772
- buildSVGClock();
5773
- // Position the hand after SVG is built
5774
- setTimeout(() => resetClock(), 10);
5775
- },
5776
- }),
5777
- m('.timepicker-dial.timepicker-hours', {
5778
- oncreate: (vnode) => {
5779
- state.hoursView = vnode.dom;
5780
- buildHoursView();
6239
+ // Conditional rendering: digital or analog mode
6240
+ isDigitalMode
6241
+ ? m('.timepicker-digital-mode', [
6242
+ m(DigitalClock, {
6243
+ hours: state.hours,
6244
+ minutes: state.minutes,
6245
+ amOrPm: state.amOrPm,
6246
+ twelveHour: options.twelveHour,
6247
+ minuteStep: options.minuteStep,
6248
+ hourStep: options.hourStep,
6249
+ minTime: options.minTime,
6250
+ maxTime: options.maxTime,
6251
+ onTimeChange: (hours, minutes, amOrPm) => {
6252
+ state.hours = hours;
6253
+ state.minutes = minutes;
6254
+ state.amOrPm = amOrPm;
6255
+ updateAmPmView();
6256
+ if (options.onSelect)
6257
+ options.onSelect(hours, minutes);
5781
6258
  },
6259
+ spanHours: state.spanHours,
6260
+ spanMinutes: state.spanMinutes,
6261
+ spanAmPm: state.spanAmPm,
5782
6262
  }),
5783
- m('.timepicker-dial.timepicker-minutes.timepicker-dial-out', {
6263
+ m('.timepicker-footer', {
5784
6264
  oncreate: (vnode) => {
5785
- state.minutesView = vnode.dom;
5786
- buildMinutesView();
6265
+ state.footer = vnode.dom;
5787
6266
  },
5788
- }),
5789
- ]),
5790
- m('.timepicker-footer', {
5791
- oncreate: (vnode) => {
5792
- state.footer = vnode.dom;
5793
- },
5794
- }, [
5795
- m('button.btn-flat.timepicker-clear.waves-effect', {
5796
- type: 'button',
5797
- tabindex: options.twelveHour ? '3' : '1',
5798
- style: showClearBtn ? '' : 'visibility: hidden;',
5799
- onclick: () => clear(),
5800
- }, i18n.clear),
5801
- m('.confirmation-btns', [
5802
- m('button.btn-flat.timepicker-close.waves-effect', {
6267
+ }, [
6268
+ m('button.btn-flat.timepicker-clear.waves-effect', {
5803
6269
  type: 'button',
5804
6270
  tabindex: options.twelveHour ? '3' : '1',
5805
- onclick: () => close(),
5806
- }, i18n.cancel),
5807
- m('button.btn-flat.timepicker-close.waves-effect', {
6271
+ style: showClearBtn ? '' : 'visibility: hidden;',
6272
+ onclick: () => clear(),
6273
+ }, i18n.clear),
6274
+ m('.confirmation-btns', [
6275
+ m('button.btn-flat.timepicker-close.waves-effect', {
6276
+ type: 'button',
6277
+ tabindex: options.twelveHour ? '3' : '1',
6278
+ onclick: () => close(),
6279
+ }, i18n.cancel),
6280
+ m('button.btn-flat.timepicker-close.waves-effect', {
6281
+ type: 'button',
6282
+ tabindex: options.twelveHour ? '3' : '1',
6283
+ onclick: () => done(),
6284
+ }, i18n.done),
6285
+ ]),
6286
+ ]),
6287
+ ])
6288
+ : m('.timepicker-analog-display', [
6289
+ m('.timepicker-plate', m(AnalogClock, {
6290
+ hours: state.hours,
6291
+ minutes: state.minutes,
6292
+ amOrPm: state.amOrPm,
6293
+ currentView: state.currentView,
6294
+ twelveHour: options.twelveHour,
6295
+ dialRadius: options.dialRadius,
6296
+ outerRadius: options.outerRadius,
6297
+ innerRadius: options.innerRadius,
6298
+ tickRadius: options.tickRadius,
6299
+ roundBy5: options.roundBy5,
6300
+ vibrate: options.vibrate,
6301
+ onTimeChange: (hours, minutes) => {
6302
+ state.hours = hours;
6303
+ state.minutes = minutes;
6304
+ if (options.onSelect)
6305
+ options.onSelect(hours, minutes);
6306
+ },
6307
+ onViewChange: (view) => {
6308
+ state.currentView = view;
6309
+ if (view === 'minutes' && options.autoClose) {
6310
+ setTimeout(() => done(), options.duration / 2);
6311
+ }
6312
+ },
6313
+ spanHours: state.spanHours,
6314
+ spanMinutes: state.spanMinutes,
6315
+ })),
6316
+ m('.timepicker-footer', {
6317
+ oncreate: (vnode) => {
6318
+ state.footer = vnode.dom;
6319
+ },
6320
+ }, [
6321
+ m('button.btn-flat.timepicker-clear.waves-effect', {
5808
6322
  type: 'button',
5809
6323
  tabindex: options.twelveHour ? '3' : '1',
5810
- onclick: () => done(),
5811
- }, i18n.done),
6324
+ style: showClearBtn ? '' : 'visibility: hidden;',
6325
+ onclick: () => clear(),
6326
+ }, i18n.clear),
6327
+ m('.confirmation-btns', [
6328
+ m('button.btn-flat.timepicker-close.waves-effect', {
6329
+ type: 'button',
6330
+ tabindex: options.twelveHour ? '3' : '1',
6331
+ onclick: () => close(),
6332
+ }, i18n.cancel),
6333
+ m('button.btn-flat.timepicker-close.waves-effect', {
6334
+ type: 'button',
6335
+ tabindex: options.twelveHour ? '3' : '1',
6336
+ onclick: () => done(),
6337
+ }, i18n.done),
6338
+ ]),
5812
6339
  ]),
5813
6340
  ]),
5814
- ]),
5815
6341
  ]),
5816
6342
  ];
5817
6343
  },
@@ -5824,7 +6350,7 @@
5824
6350
  m.redraw();
5825
6351
  }
5826
6352
  };
5827
- const renderPickerToPortal = (attrs) => {
6353
+ const renderPickerToPortal = () => {
5828
6354
  const pickerModal = m('.timepicker-modal-wrapper', {
5829
6355
  style: {
5830
6356
  position: 'fixed',
@@ -5885,11 +6411,6 @@
5885
6411
  minutes: 0,
5886
6412
  amOrPm: 'AM',
5887
6413
  currentView: 'hours',
5888
- moved: false,
5889
- x0: 0,
5890
- y0: 0,
5891
- dx: 0,
5892
- dy: 0,
5893
6414
  portalContainerId: `timepicker-portal-${uniqueId()}`,
5894
6415
  };
5895
6416
  // Handle value after options are set
@@ -5901,27 +6422,17 @@
5901
6422
  },
5902
6423
  onremove: () => {
5903
6424
  // Cleanup
5904
- if (state.toggleViewTimer) {
5905
- clearTimeout(state.toggleViewTimer);
5906
- }
5907
- if (state.vibrateTimer) {
5908
- clearTimeout(state.vibrateTimer);
5909
- }
5910
- document.removeEventListener('mousemove', handleDocumentClickMove);
5911
- document.removeEventListener('touchmove', handleDocumentClickMove);
5912
- document.removeEventListener('mouseup', handleDocumentClickEnd);
5913
- document.removeEventListener('touchend', handleDocumentClickEnd);
5914
6425
  document.removeEventListener('keydown', handleKeyDown);
5915
6426
  // Clean up portal if picker was open
5916
6427
  if (state.isOpen) {
5917
6428
  clearPortal(state.portalContainerId);
5918
6429
  }
5919
6430
  },
5920
- onupdate: (vnode) => {
5921
- const { useModal = true } = vnode.attrs;
6431
+ onupdate: ({ attrs }) => {
6432
+ const { useModal = true } = attrs;
5922
6433
  // Only render to portal when using modal mode
5923
6434
  if (useModal && state.isOpen) {
5924
- renderPickerToPortal(vnode.attrs);
6435
+ renderPickerToPortal();
5925
6436
  }
5926
6437
  else {
5927
6438
  clearPortal(state.portalContainerId);
@@ -6802,46 +7313,50 @@
6802
7313
  };
6803
7314
 
6804
7315
  // Proper components to avoid anonymous closures
6805
- const SelectedChip = {
6806
- view: ({ attrs: { option, onRemove } }) => m('.chip', [
6807
- option.label || option.id.toString(),
6808
- m(MaterialIcon, {
6809
- name: 'close',
6810
- className: 'close',
6811
- onclick: (e) => {
6812
- e.stopPropagation();
6813
- onRemove(option.id);
6814
- },
6815
- }),
6816
- ]),
6817
- };
6818
- const DropdownOption = {
6819
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
6820
- const checkboxId = `search-select-option-${option.id}`;
6821
- const optionLabel = option.label || option.id.toString();
6822
- return m('li', {
6823
- key: option.id,
6824
- onclick: (e) => {
6825
- e.preventDefault();
6826
- e.stopPropagation();
6827
- onToggle(option);
6828
- },
6829
- class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
6830
- onmouseover: () => {
6831
- if (!option.disabled) {
6832
- onMouseOver(index);
6833
- }
6834
- },
6835
- }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6836
- showCheckbox &&
6837
- m('input', {
6838
- type: 'checkbox',
6839
- id: checkboxId,
6840
- checked: selectedIds.includes(option.id),
6841
- }),
6842
- m('span', optionLabel),
6843
- ]));
6844
- },
7316
+ const SelectedChip = () => {
7317
+ return {
7318
+ view: ({ attrs: { option, onRemove } }) => m('.chip', [
7319
+ option.label || option.id.toString(),
7320
+ m(MaterialIcon, {
7321
+ name: 'close',
7322
+ className: 'close',
7323
+ onclick: (e) => {
7324
+ e.stopPropagation();
7325
+ onRemove(option.id);
7326
+ },
7327
+ }),
7328
+ ]),
7329
+ };
7330
+ };
7331
+ const DropdownOption = () => {
7332
+ return {
7333
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
7334
+ const checkboxId = `search-select-option-${option.id}`;
7335
+ const optionLabel = option.label || option.id.toString();
7336
+ return m('li', {
7337
+ key: option.id,
7338
+ onclick: (e) => {
7339
+ e.preventDefault();
7340
+ e.stopPropagation();
7341
+ onToggle(option);
7342
+ },
7343
+ class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
7344
+ onmouseover: () => {
7345
+ if (!option.disabled) {
7346
+ onMouseOver(index);
7347
+ }
7348
+ },
7349
+ }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
7350
+ showCheckbox &&
7351
+ m('input', {
7352
+ type: 'checkbox',
7353
+ id: checkboxId,
7354
+ checked: selectedIds.includes(option.id),
7355
+ }),
7356
+ m('span', optionLabel),
7357
+ ]));
7358
+ },
7359
+ };
6845
7360
  };
6846
7361
  /**
6847
7362
  * Mithril Factory Component for Multi-Select Dropdown with search
@@ -7080,8 +7595,7 @@
7080
7595
  style: { position: 'absolute', left: '-9999px', opacity: 0 },
7081
7596
  }),
7082
7597
  // Selected Options (chips)
7083
- ...selectedOptions.map((option) => m(SelectedChip, {
7084
- // key: option.id,
7598
+ ...selectedOptions.map((option) => m(SelectedChip(), {
7085
7599
  option,
7086
7600
  onRemove: (id) => removeOption(id, attrs),
7087
7601
  })),
@@ -7197,8 +7711,7 @@
7197
7711
  ]
7198
7712
  : []),
7199
7713
  // List of filtered options
7200
- ...displayedOptions.map((option, index) => m(DropdownOption, {
7201
- // key: option.id,
7714
+ ...displayedOptions.map((option, index) => m(DropdownOption(), {
7202
7715
  option,
7203
7716
  index,
7204
7717
  selectedIds,
@@ -7215,6 +7728,411 @@
7215
7728
  };
7216
7729
  };
7217
7730
 
7731
+ const defaultI18n$2 = {
7732
+ cancel: 'Cancel',
7733
+ clear: 'Clear',
7734
+ done: 'Ok',
7735
+ next: 'Next',
7736
+ };
7737
+ /**
7738
+ * TimeRangePicker component for selecting time ranges
7739
+ * Custom implementation with embedded digital clock picker
7740
+ */
7741
+ const TimeRangePicker = () => {
7742
+ let state;
7743
+ const calculateMinTime = (startTime, twelveHour) => {
7744
+ if (!startTime)
7745
+ return undefined;
7746
+ let hours = startTime.hours;
7747
+ let minutes = startTime.minutes + 1;
7748
+ let amOrPm = startTime.amOrPm;
7749
+ if (minutes >= 60) {
7750
+ minutes = 0;
7751
+ hours++;
7752
+ if (twelveHour) {
7753
+ if (hours > 12) {
7754
+ hours = 1;
7755
+ amOrPm = amOrPm === 'AM' ? 'PM' : 'AM';
7756
+ }
7757
+ else if (hours === 12) {
7758
+ amOrPm = amOrPm === 'AM' ? 'PM' : 'AM';
7759
+ }
7760
+ }
7761
+ else {
7762
+ if (hours >= 24)
7763
+ hours = 0;
7764
+ }
7765
+ }
7766
+ return formatTime({ hours, minutes, amOrPm }, twelveHour);
7767
+ };
7768
+ const handleNextOrDone = (validateRange, twelveHour, onchange) => {
7769
+ if (state.currentSelection === 'start') {
7770
+ // Move to end time selection
7771
+ state.currentSelection = 'end';
7772
+ state.currentView = 'hours'; // Reset to hours view for end time
7773
+ // If validation is enabled and end time is before or equal to start time, reset end time
7774
+ if (validateRange) {
7775
+ const startMins = timeToMinutes(state.tempStartTime, twelveHour);
7776
+ const endMins = timeToMinutes(state.tempEndTime, twelveHour);
7777
+ if (endMins <= startMins) {
7778
+ // Reset end time to start time
7779
+ state.tempEndTime = Object.assign({}, state.tempStartTime);
7780
+ }
7781
+ }
7782
+ }
7783
+ else {
7784
+ // Finalize selection
7785
+ state.startTime = Object.assign({}, state.tempStartTime);
7786
+ state.endTime = Object.assign({}, state.tempEndTime);
7787
+ state.isPickerOpen = false;
7788
+ state.currentSelection = 'start';
7789
+ // Call onchange callback
7790
+ if (onchange && state.startTime && state.endTime) {
7791
+ const startTimeStr = formatTime(state.startTime, twelveHour);
7792
+ const endTimeStr = formatTime(state.endTime, twelveHour);
7793
+ onchange(startTimeStr, endTimeStr);
7794
+ }
7795
+ }
7796
+ };
7797
+ const TimeRangePickerModal = () => {
7798
+ return {
7799
+ view: ({ attrs }) => {
7800
+ const { i18n, showClearBtn, twelveHour, minuteStep, hourStep, minTime, maxTime, validateRange, displayMode = 'digital', dialRadius = 135, outerRadius = 105, innerRadius = 70, tickRadius = 20, roundBy5 = false, vibrate = true, } = attrs;
7801
+ const isAnalogMode = displayMode === 'analog';
7802
+ // Calculate effective minTime for end time selection
7803
+ const effectiveMinTime = state.currentSelection === 'end' && validateRange
7804
+ ? calculateMinTime(state.tempStartTime, twelveHour)
7805
+ : minTime;
7806
+ return m('.modal-content.timepicker-container', [
7807
+ // Vertical time range display on the left
7808
+ m('.timerange-display-vertical', [
7809
+ m('.timerange-time-section', { class: state.currentSelection === 'start' ? 'active' : '' }, [
7810
+ m('.timerange-label', 'Start'),
7811
+ m('.timerange-time', [
7812
+ m('span.timerange-hours', {
7813
+ oncreate: (vnode) => {
7814
+ state.spanStartHours = vnode.dom;
7815
+ state.spanStartHours.innerHTML = addLeadingZero(state.tempStartTime.hours);
7816
+ },
7817
+ }, addLeadingZero(state.tempStartTime.hours)),
7818
+ ':',
7819
+ m('span.timerange-minutes', {
7820
+ oncreate: (vnode) => {
7821
+ state.spanStartMinutes = vnode.dom;
7822
+ state.spanStartMinutes.innerHTML = addLeadingZero(state.tempStartTime.minutes);
7823
+ },
7824
+ }, addLeadingZero(state.tempStartTime.minutes)),
7825
+ twelveHour &&
7826
+ m('span.timerange-ampm', {
7827
+ oncreate: (vnode) => {
7828
+ state.spanStartAmPm = vnode.dom;
7829
+ state.spanStartAmPm.innerHTML = ` ${state.tempStartTime.amOrPm}`;
7830
+ },
7831
+ }, ` ${state.tempStartTime.amOrPm}`),
7832
+ ]),
7833
+ ]),
7834
+ m('.timerange-time-section', { class: state.currentSelection === 'end' ? 'active' : '' }, [
7835
+ m('.timerange-label', 'End'),
7836
+ m('.timerange-time', [
7837
+ m('span.timerange-hours', {
7838
+ oncreate: (vnode) => {
7839
+ state.spanEndHours = vnode.dom;
7840
+ state.spanEndHours.innerHTML = addLeadingZero(state.tempEndTime.hours);
7841
+ },
7842
+ }, addLeadingZero(state.tempEndTime.hours)),
7843
+ ':',
7844
+ m('span.timerange-minutes', {
7845
+ oncreate: (vnode) => {
7846
+ state.spanEndMinutes = vnode.dom;
7847
+ state.spanEndMinutes.innerHTML = addLeadingZero(state.tempEndTime.minutes);
7848
+ },
7849
+ }, addLeadingZero(state.tempEndTime.minutes)),
7850
+ twelveHour &&
7851
+ m('span.timerange-ampm', {
7852
+ oncreate: (vnode) => {
7853
+ state.spanEndAmPm = vnode.dom;
7854
+ state.spanEndAmPm.innerHTML = ` ${state.tempEndTime.amOrPm}`;
7855
+ },
7856
+ }, ` ${state.tempEndTime.amOrPm}`),
7857
+ ]),
7858
+ ]),
7859
+ ]),
7860
+ // Clock picker (analog or digital mode)
7861
+ isAnalogMode
7862
+ ? m('.timepicker-analog-display', [
7863
+ m('.timepicker-plate', m(AnalogClock, {
7864
+ key: state.currentSelection, // Force component recreation when switching between start/end
7865
+ hours: state.currentSelection === 'start' ? state.tempStartTime.hours : state.tempEndTime.hours,
7866
+ minutes: state.currentSelection === 'start' ? state.tempStartTime.minutes : state.tempEndTime.minutes,
7867
+ amOrPm: state.currentSelection === 'start' ? state.tempStartTime.amOrPm : state.tempEndTime.amOrPm,
7868
+ currentView: state.currentView,
7869
+ twelveHour,
7870
+ dialRadius,
7871
+ outerRadius,
7872
+ innerRadius,
7873
+ tickRadius,
7874
+ roundBy5,
7875
+ vibrate,
7876
+ onTimeChange: (hours, minutes) => {
7877
+ if (state.currentSelection === 'start') {
7878
+ state.tempStartTime = Object.assign(Object.assign({}, state.tempStartTime), { hours, minutes });
7879
+ if (state.spanStartHours)
7880
+ state.spanStartHours.innerHTML = addLeadingZero(hours);
7881
+ if (state.spanStartMinutes)
7882
+ state.spanStartMinutes.innerHTML = addLeadingZero(minutes);
7883
+ }
7884
+ else {
7885
+ state.tempEndTime = Object.assign(Object.assign({}, state.tempEndTime), { hours, minutes });
7886
+ if (state.spanEndHours)
7887
+ state.spanEndHours.innerHTML = addLeadingZero(hours);
7888
+ if (state.spanEndMinutes)
7889
+ state.spanEndMinutes.innerHTML = addLeadingZero(minutes);
7890
+ }
7891
+ },
7892
+ onViewChange: (view) => {
7893
+ state.currentView = view;
7894
+ },
7895
+ spanHours: state.currentSelection === 'start' ? state.spanStartHours : state.spanEndHours,
7896
+ spanMinutes: state.currentSelection === 'start' ? state.spanStartMinutes : state.spanEndMinutes,
7897
+ })),
7898
+ // Footer (inside analog display)
7899
+ m('.timepicker-footer', [
7900
+ m('button.btn-flat.timepicker-clear.waves-effect', {
7901
+ type: 'button',
7902
+ style: showClearBtn ? '' : 'visibility: hidden;',
7903
+ onclick: () => {
7904
+ state.isPickerOpen = false;
7905
+ },
7906
+ }, i18n.clear),
7907
+ m('.confirmation-btns', [
7908
+ m('button.btn-flat.timepicker-close.waves-effect', {
7909
+ type: 'button',
7910
+ onclick: () => {
7911
+ state.isPickerOpen = false;
7912
+ state.currentSelection = 'start';
7913
+ state.currentView = 'hours'; // Reset to hours view
7914
+ },
7915
+ }, i18n.cancel),
7916
+ m('button.btn-flat.timepicker-close.waves-effect', {
7917
+ type: 'button',
7918
+ onclick: () => {
7919
+ handleNextOrDone(validateRange, twelveHour, attrs.onchange);
7920
+ },
7921
+ }, state.currentSelection === 'start' ? i18n.next : i18n.done),
7922
+ ]),
7923
+ ]),
7924
+ ])
7925
+ : m('.timepicker-digital-mode', [
7926
+ m(DigitalClock, {
7927
+ key: state.currentSelection, // Force component recreation when switching between start/end
7928
+ hours: state.currentSelection === 'start' ? state.tempStartTime.hours : state.tempEndTime.hours,
7929
+ minutes: state.currentSelection === 'start' ? state.tempStartTime.minutes : state.tempEndTime.minutes,
7930
+ amOrPm: state.currentSelection === 'start' ? state.tempStartTime.amOrPm : state.tempEndTime.amOrPm,
7931
+ twelveHour,
7932
+ minuteStep,
7933
+ hourStep,
7934
+ minTime: effectiveMinTime,
7935
+ maxTime,
7936
+ onTimeChange: (hours, minutes, amOrPm) => {
7937
+ if (state.currentSelection === 'start') {
7938
+ state.tempStartTime = { hours, minutes, amOrPm };
7939
+ if (state.spanStartHours)
7940
+ state.spanStartHours.innerHTML = addLeadingZero(hours);
7941
+ if (state.spanStartMinutes)
7942
+ state.spanStartMinutes.innerHTML = addLeadingZero(minutes);
7943
+ if (state.spanStartAmPm)
7944
+ state.spanStartAmPm.innerHTML = ` ${amOrPm}`;
7945
+ }
7946
+ else {
7947
+ state.tempEndTime = { hours, minutes, amOrPm };
7948
+ if (state.spanEndHours)
7949
+ state.spanEndHours.innerHTML = addLeadingZero(hours);
7950
+ if (state.spanEndMinutes)
7951
+ state.spanEndMinutes.innerHTML = addLeadingZero(minutes);
7952
+ if (state.spanEndAmPm)
7953
+ state.spanEndAmPm.innerHTML = ` ${amOrPm}`;
7954
+ }
7955
+ },
7956
+ spanHours: state.currentSelection === 'start' ? state.spanStartHours : state.spanEndHours,
7957
+ spanMinutes: state.currentSelection === 'start' ? state.spanStartMinutes : state.spanEndMinutes,
7958
+ spanAmPm: state.currentSelection === 'start' ? state.spanStartAmPm : state.spanEndAmPm,
7959
+ }),
7960
+ // Footer (inside digital mode)
7961
+ m('.timepicker-footer', { key: 'timepicker-footer' }, [
7962
+ m('button.btn-flat.timepicker-clear.waves-effect', {
7963
+ type: 'button',
7964
+ style: showClearBtn ? '' : 'visibility: hidden;',
7965
+ onclick: () => {
7966
+ state.isPickerOpen = false;
7967
+ },
7968
+ }, i18n.clear),
7969
+ m('.confirmation-btns', [
7970
+ m('button.btn-flat.timepicker-close.waves-effect', {
7971
+ type: 'button',
7972
+ onclick: () => {
7973
+ state.isPickerOpen = false;
7974
+ state.currentSelection = 'start';
7975
+ state.currentView = 'hours'; // Reset to hours view
7976
+ },
7977
+ }, i18n.cancel),
7978
+ m('button.btn-flat.timepicker-close.waves-effect', {
7979
+ type: 'button',
7980
+ onclick: () => {
7981
+ handleNextOrDone(validateRange, twelveHour, attrs.onchange);
7982
+ },
7983
+ }, state.currentSelection === 'start' ? i18n.next : i18n.done),
7984
+ ]),
7985
+ ]),
7986
+ ]),
7987
+ ]);
7988
+ },
7989
+ };
7990
+ };
7991
+ const handleKeyDown = (e) => {
7992
+ if (e.key === 'Escape' && state.isPickerOpen) {
7993
+ state.isPickerOpen = false;
7994
+ clearPortal(state.portalContainerId);
7995
+ }
7996
+ };
7997
+ const renderPickerToPortal = (attrs) => {
7998
+ const mergedI18n = Object.assign(Object.assign({}, defaultI18n$2), attrs.i18n);
7999
+ const pickerModal = m('.timepicker-modal-wrapper', {
8000
+ style: {
8001
+ position: 'fixed',
8002
+ top: '0',
8003
+ left: '0',
8004
+ width: '100%',
8005
+ height: '100%',
8006
+ pointerEvents: 'auto',
8007
+ display: 'flex',
8008
+ alignItems: 'center',
8009
+ justifyContent: 'center',
8010
+ },
8011
+ }, [
8012
+ // Modal overlay
8013
+ m('.modal-overlay', {
8014
+ style: {
8015
+ position: 'absolute',
8016
+ top: '0',
8017
+ left: '0',
8018
+ width: '100%',
8019
+ height: '100%',
8020
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
8021
+ zIndex: '1002',
8022
+ },
8023
+ onclick: () => {
8024
+ state.isPickerOpen = false;
8025
+ state.currentSelection = 'start';
8026
+ state.currentView = 'hours'; // Reset to hours view
8027
+ },
8028
+ }),
8029
+ // Modal content
8030
+ m('.modal.timepicker-modal.open.timerange-modal', {
8031
+ style: {
8032
+ position: 'relative',
8033
+ zIndex: '1003',
8034
+ display: 'block',
8035
+ opacity: 1,
8036
+ top: 'auto',
8037
+ transform: 'scaleX(1) scaleY(1)',
8038
+ margin: '0 auto',
8039
+ },
8040
+ }, [
8041
+ m(TimeRangePickerModal, {
8042
+ i18n: mergedI18n,
8043
+ showClearBtn: attrs.showClearBtn || false,
8044
+ twelveHour: attrs.twelveHour !== undefined ? attrs.twelveHour : true,
8045
+ minuteStep: attrs.minuteStep || 5,
8046
+ hourStep: attrs.hourStep || 1,
8047
+ minTime: attrs.minTime,
8048
+ maxTime: attrs.maxTime,
8049
+ validateRange: attrs.validateRange || false,
8050
+ onchange: attrs.onchange,
8051
+ displayMode: attrs.displayMode,
8052
+ dialRadius: attrs.dialRadius,
8053
+ outerRadius: attrs.outerRadius,
8054
+ innerRadius: attrs.innerRadius,
8055
+ tickRadius: attrs.tickRadius,
8056
+ roundBy5: attrs.roundBy5,
8057
+ vibrate: attrs.vibrate,
8058
+ }),
8059
+ ]),
8060
+ ]);
8061
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
8062
+ };
8063
+ return {
8064
+ oninit: (vnode) => {
8065
+ const attrs = vnode.attrs;
8066
+ const twelveHour = attrs.twelveHour !== undefined ? attrs.twelveHour : true;
8067
+ const startTime = parseTime(attrs.startValue || '', twelveHour);
8068
+ const endTime = parseTime(attrs.endValue || '', twelveHour);
8069
+ state = {
8070
+ id: uniqueId(),
8071
+ currentSelection: 'start',
8072
+ startTime,
8073
+ endTime,
8074
+ tempStartTime: Object.assign({}, startTime),
8075
+ tempEndTime: Object.assign({}, endTime),
8076
+ isPickerOpen: false,
8077
+ portalContainerId: `timerange-portal-${uniqueId()}`,
8078
+ currentView: 'hours',
8079
+ };
8080
+ document.addEventListener('keydown', handleKeyDown);
8081
+ },
8082
+ onremove: () => {
8083
+ document.removeEventListener('keydown', handleKeyDown);
8084
+ if (state.isPickerOpen) {
8085
+ clearPortal(state.portalContainerId);
8086
+ }
8087
+ },
8088
+ onupdate: ({ attrs }) => {
8089
+ if (state.isPickerOpen) {
8090
+ renderPickerToPortal(attrs);
8091
+ }
8092
+ else {
8093
+ clearPortal(state.portalContainerId);
8094
+ }
8095
+ },
8096
+ view: ({ attrs }) => {
8097
+ const { id = state.id, label, placeholder = 'Select time range', disabled, readonly, required, iconName, helperText, className: cn1, class: cn2, twelveHour = true, } = attrs;
8098
+ const className = cn1 || cn2 || 'col s12';
8099
+ const displayValue = state.startTime && state.endTime
8100
+ ? `${formatTime(state.startTime, twelveHour)} - ${formatTime(state.endTime, twelveHour)}`
8101
+ : state.startTime
8102
+ ? `${formatTime(state.startTime, twelveHour)} - ...`
8103
+ : '';
8104
+ return m('.input-field', { className }, [
8105
+ iconName && m('i.material-icons.prefix', iconName),
8106
+ // Display input
8107
+ m('input.timerangepicker', {
8108
+ id,
8109
+ type: 'text',
8110
+ value: displayValue,
8111
+ placeholder,
8112
+ readonly: true,
8113
+ disabled,
8114
+ required,
8115
+ onclick: () => {
8116
+ if (!disabled && !readonly) {
8117
+ state.isPickerOpen = true;
8118
+ state.currentSelection = 'start';
8119
+ state.currentView = 'hours'; // Reset to hours view when opening
8120
+ state.tempStartTime = Object.assign({}, state.startTime);
8121
+ state.tempEndTime = Object.assign({}, state.endTime);
8122
+ }
8123
+ },
8124
+ }),
8125
+ label &&
8126
+ m('label', {
8127
+ for: id,
8128
+ class: displayValue || placeholder ? 'active' : '',
8129
+ }, label),
8130
+ helperText && m('span.helper-text', helperText),
8131
+ ]);
8132
+ },
8133
+ };
8134
+ };
8135
+
7218
8136
  class Toast {
7219
8137
  constructor(options = {}) {
7220
8138
  this.options = Object.assign(Object.assign({}, Toast.defaults), options);
@@ -9797,6 +10715,126 @@
9797
10715
  };
9798
10716
  };
9799
10717
 
10718
+ /**
10719
+ * ToggleButton component.
10720
+ *
10721
+ * A button that can be toggled on/off. Typically used within a ToggleGroup
10722
+ * to create grouped button controls where one or more buttons can be selected.
10723
+ *
10724
+ * @example
10725
+ * ```typescript
10726
+ * m(ToggleButton, {
10727
+ * value: 'bold',
10728
+ * iconName: 'format_bold',
10729
+ * checked: true,
10730
+ * tooltip: 'Bold',
10731
+ * onchange: () => console.log('Toggled')
10732
+ * })
10733
+ * ```
10734
+ */
10735
+ const ToggleButton = () => {
10736
+ return {
10737
+ view: ({ attrs }) => {
10738
+ const { checked, iconName, icon, label, onchange, className, tooltip } = attrs, rest = __rest(attrs, ["checked", "iconName", "icon", "label", "onchange", "className", "tooltip"]);
10739
+ const classes = [className, checked ? 'checked' : ''].filter(Boolean).join(' ');
10740
+ return m('button.btn-flat.waves-effect.toggle-button', Object.assign(Object.assign({}, rest), { className: classes, title: tooltip, onclick: () => {
10741
+ if (onchange) {
10742
+ onchange();
10743
+ }
10744
+ }, onmousedown: WavesEffect.onMouseDown, onmouseup: WavesEffect.onMouseUp, onmouseleave: WavesEffect.onMouseLeave, ontouchstart: WavesEffect.onTouchStart, ontouchend: WavesEffect.onTouchEnd }), [icon, iconName && m(Icon, { iconName, className: attrs.iconClass }), label]);
10745
+ },
10746
+ };
10747
+ };
10748
+
10749
+ /**
10750
+ * ToggleGroup component.
10751
+ *
10752
+ * A group of toggle buttons that can operate in single-select or multi-select mode.
10753
+ * The component supports both controlled and uncontrolled modes.
10754
+ *
10755
+ * **Controlled mode**: Manage the state externally using the `value` prop.
10756
+ * **Uncontrolled mode**: Let the component manage its own state using `defaultValue`.
10757
+ *
10758
+ * @example
10759
+ * ```typescript
10760
+ * // Single-select, controlled mode
10761
+ * let selected = 'left';
10762
+ * m(ToggleGroup, {
10763
+ * value: selected,
10764
+ * onchange: (v) => selected = v,
10765
+ * items: [
10766
+ * { value: 'left', iconName: 'format_align_left', tooltip: 'Align Left' },
10767
+ * { value: 'center', iconName: 'format_align_center', tooltip: 'Align Center' },
10768
+ * { value: 'right', iconName: 'format_align_right', tooltip: 'Align Right' }
10769
+ * ]
10770
+ * });
10771
+ *
10772
+ * // Multi-select, controlled mode
10773
+ * let selected = ['bold', 'italic'];
10774
+ * m(ToggleGroup, {
10775
+ * multiple: true,
10776
+ * value: selected,
10777
+ * onchange: (v) => selected = v,
10778
+ * items: [
10779
+ * { value: 'bold', iconName: 'format_bold', tooltip: 'Bold' },
10780
+ * { value: 'italic', iconName: 'format_italic', tooltip: 'Italic' },
10781
+ * { value: 'underline', iconName: 'format_underlined', tooltip: 'Underline' }
10782
+ * ]
10783
+ * });
10784
+ *
10785
+ * // Uncontrolled mode
10786
+ * m(ToggleGroup, {
10787
+ * defaultValue: 'left',
10788
+ * onchange: (v) => console.log('Changed to:', v),
10789
+ * items: [...]
10790
+ * });
10791
+ * ```
10792
+ */
10793
+ const ToggleGroup = () => {
10794
+ let internalValue;
10795
+ const handleSelect = (attrs, item, currentValue) => {
10796
+ if (attrs.disabled || item.disabled) {
10797
+ return;
10798
+ }
10799
+ const { value, multiple, onchange } = attrs;
10800
+ const isControlled = value !== undefined;
10801
+ if (multiple) {
10802
+ const currentValues = (Array.isArray(currentValue) ? currentValue : currentValue !== undefined ? [currentValue] : []);
10803
+ const newValues = currentValues.includes(item.value)
10804
+ ? currentValues.filter((v) => v !== item.value)
10805
+ : [...currentValues, item.value];
10806
+ if (!isControlled) {
10807
+ internalValue = newValues;
10808
+ }
10809
+ if (onchange) {
10810
+ onchange(newValues);
10811
+ }
10812
+ }
10813
+ else {
10814
+ const newValue = item.value;
10815
+ if (!isControlled) {
10816
+ internalValue = newValue;
10817
+ }
10818
+ if (onchange) {
10819
+ onchange(newValue);
10820
+ }
10821
+ }
10822
+ };
10823
+ return {
10824
+ oninit: ({ attrs }) => {
10825
+ internalValue = attrs.defaultValue;
10826
+ },
10827
+ view: ({ attrs }) => {
10828
+ const { value, items, multiple } = attrs;
10829
+ const isControlled = value !== undefined;
10830
+ const currentValue = isControlled ? value : internalValue;
10831
+ return m('.toggle-group', items.map((item) => m(ToggleButton, Object.assign(Object.assign({}, item), { checked: multiple && Array.isArray(currentValue)
10832
+ ? currentValue.includes(item.value)
10833
+ : currentValue === item.value, onchange: () => handleSelect(attrs, item, currentValue) }))));
10834
+ },
10835
+ };
10836
+ };
10837
+
9800
10838
  /**
9801
10839
  * @fileoverview Core TypeScript utility types for mithril-materialized library
9802
10840
  * These types improve type safety and developer experience across all components
@@ -9818,6 +10856,7 @@
9818
10856
  // ============================================================================
9819
10857
  // All types are already exported via individual export declarations above
9820
10858
 
10859
+ exports.AnalogClock = AnalogClock;
9821
10860
  exports.AnchorItem = AnchorItem;
9822
10861
  exports.Autocomplete = Autocomplete;
9823
10862
  exports.Breadcrumb = Breadcrumb;
@@ -9834,6 +10873,7 @@
9834
10873
  exports.ColorInput = ColorInput;
9835
10874
  exports.DataTable = DataTable;
9836
10875
  exports.DatePicker = DatePicker;
10876
+ exports.DigitalClock = DigitalClock;
9837
10877
  exports.DoubleRangeSlider = DoubleRangeSlider;
9838
10878
  exports.Dropdown = Dropdown;
9839
10879
  exports.EmailInput = EmailInput;
@@ -9886,28 +10926,39 @@
9886
10926
  exports.ThemeSwitcher = ThemeSwitcher;
9887
10927
  exports.ThemeToggle = ThemeToggle;
9888
10928
  exports.TimePicker = TimePicker;
10929
+ exports.TimeRangePicker = TimeRangePicker;
9889
10930
  exports.Timeline = Timeline;
9890
10931
  exports.Toast = Toast;
9891
10932
  exports.ToastComponent = ToastComponent;
10933
+ exports.ToggleGroup = ToggleGroup;
9892
10934
  exports.Tooltip = Tooltip;
9893
10935
  exports.TooltipComponent = TooltipComponent;
9894
10936
  exports.TreeView = TreeView;
9895
10937
  exports.UrlInput = UrlInput;
9896
10938
  exports.Wizard = Wizard;
10939
+ exports.addLeadingZero = addLeadingZero;
9897
10940
  exports.clearPortal = clearPortal;
9898
10941
  exports.createBreadcrumb = createBreadcrumb;
10942
+ exports.formatTime = formatTime;
10943
+ exports.generateHourOptions = generateHourOptions;
10944
+ exports.generateMinuteOptions = generateMinuteOptions;
9899
10945
  exports.getDropdownStyles = getDropdownStyles;
9900
10946
  exports.getPortalContainer = getPortalContainer;
9901
10947
  exports.initPushpins = initPushpins;
9902
10948
  exports.initTooltips = initTooltips;
9903
10949
  exports.isNumeric = isNumeric;
10950
+ exports.isTimeDisabled = isTimeDisabled;
9904
10951
  exports.isValidationError = isValidationError;
9905
10952
  exports.isValidationSuccess = isValidationSuccess;
9906
10953
  exports.padLeft = padLeft;
10954
+ exports.parseTime = parseTime;
9907
10955
  exports.range = range;
9908
10956
  exports.releasePortalContainer = releasePortalContainer;
9909
10957
  exports.renderToPortal = renderToPortal;
10958
+ exports.scrollToValue = scrollToValue;
10959
+ exports.snapToNearestItem = snapToNearestItem;
9910
10960
  exports.sortOptions = sortOptions;
10961
+ exports.timeToMinutes = timeToMinutes;
9911
10962
  exports.toast = toast;
9912
10963
  exports.uniqueId = uniqueId;
9913
10964
  exports.uuid4 = uuid4;