mithril-materialized 3.5.10 → 3.7.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
@@ -66,6 +66,28 @@
66
66
  };
67
67
  /** Check if a string or number is numeric. @see https://stackoverflow.com/a/9716488/319711 */
68
68
  const isNumeric = (n) => !isNaN(parseFloat(n)) && isFinite(n);
69
+ /**
70
+ * Sort options array based on sorting configuration
71
+ * @param options - Array of options to sort
72
+ * @param sortConfig - Sort configuration: 'asc', 'desc', 'none', or custom comparator function
73
+ * @returns Sorted array (or original if 'none' or undefined)
74
+ */
75
+ const sortOptions = (options, sortConfig) => {
76
+ if (!sortConfig || sortConfig === 'none') {
77
+ return options;
78
+ }
79
+ const sorted = [...options]; // Create a copy to avoid mutating original
80
+ if (typeof sortConfig === 'function') {
81
+ return sorted.sort(sortConfig);
82
+ }
83
+ // Sort by label, fallback to id if no label
84
+ return sorted.sort((a, b) => {
85
+ const aLabel = (a.label || a.id.toString()).toLowerCase();
86
+ const bLabel = (b.label || b.id.toString()).toLowerCase();
87
+ const comparison = aLabel.localeCompare(bLabel);
88
+ return sortConfig === 'asc' ? comparison : -comparison;
89
+ });
90
+ };
69
91
  /**
70
92
  * Pad left, default width 2 with a '0'
71
93
  *
@@ -1588,7 +1610,7 @@
1588
1610
  };
1589
1611
  };
1590
1612
 
1591
- const defaultI18n$3 = {
1613
+ const defaultI18n$4 = {
1592
1614
  cancel: 'Cancel',
1593
1615
  clear: 'Clear',
1594
1616
  done: 'Ok',
@@ -1696,9 +1718,9 @@
1696
1718
  else if (attrs.displayFormat) {
1697
1719
  finalFormat = attrs.displayFormat;
1698
1720
  }
1699
- 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);
1700
1722
  // Merge i18n properly
1701
- merged.i18n = Object.assign(Object.assign({}, defaultI18n$3), attrs.i18n);
1723
+ merged.i18n = Object.assign(Object.assign({}, defaultI18n$4), attrs.i18n);
1702
1724
  return merged;
1703
1725
  };
1704
1726
  const toString = (date, format) => {
@@ -4499,7 +4521,7 @@
4499
4521
  opacity: 1,
4500
4522
  };
4501
4523
  };
4502
- const updatePortalDropdown = (items, selectedLabel, onSelectItem) => {
4524
+ const updatePortalDropdown = (items, selectedLabel, onSelectItem, maxHeight) => {
4503
4525
  if (!state.isInsideModal)
4504
4526
  return;
4505
4527
  // Clean up existing portal
@@ -4543,7 +4565,7 @@
4543
4565
  // Create dropdown with proper positioning
4544
4566
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4545
4567
  tabindex: 0,
4546
- style: getPortalStyles(state.inputRef),
4568
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
4547
4569
  oncreate: ({ dom }) => {
4548
4570
  state.dropdownRef = dom;
4549
4571
  },
@@ -4607,7 +4629,7 @@
4607
4629
  state.focusedIndex = -1;
4608
4630
  handleSelection(item.id);
4609
4631
  }
4610
- });
4632
+ }, attrs.maxHeight);
4611
4633
  }
4612
4634
  return m('.dropdown-wrapper.input-field', { className, key, style }, [
4613
4635
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
@@ -4655,7 +4677,7 @@
4655
4677
  onremove: () => {
4656
4678
  state.dropdownRef = null;
4657
4679
  },
4658
- style: getDropdownStyles(state.inputRef, true, items, true),
4680
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, items, true)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4659
4681
  }, items.map((item) => {
4660
4682
  if (item.divider) {
4661
4683
  return m('li.divider');
@@ -5276,45 +5298,417 @@
5276
5298
  };
5277
5299
  };
5278
5300
 
5279
- const defaultI18n$2 = {
5280
- cancel: 'Cancel',
5281
- clear: 'Clear',
5282
- done: 'Ok',
5301
+ /**
5302
+ * Shared utility functions for TimePicker and TimeRangePicker components
5303
+ */
5304
+ const addLeadingZero = (num) => {
5305
+ return (num < 10 ? '0' : '') + num;
5283
5306
  };
5284
- const defaultOptions = {
5285
- dialRadius: 135,
5286
- outerRadius: 105,
5287
- innerRadius: 70,
5288
- tickRadius: 20,
5289
- duration: 350,
5290
- container: null,
5291
- defaultTime: 'now',
5292
- fromNow: 0,
5293
- showClearBtn: false,
5294
- i18n: defaultI18n$2,
5295
- autoClose: false,
5296
- twelveHour: true,
5297
- vibrate: true,
5298
- roundBy5: false,
5299
- onOpen: () => { },
5300
- onOpenStart: () => { },
5301
- onOpenEnd: () => { },
5302
- onCloseStart: () => { },
5303
- onCloseEnd: () => { },
5304
- 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
+ }
5305
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);
5418
+ };
5419
+
5306
5420
  /**
5307
- * 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
+ * ```
5308
5436
  */
5309
- const TimePicker = () => {
5310
- let state;
5311
- let options;
5312
- const addLeadingZero = (num) => {
5313
- 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
+ },
5314
5682
  };
5315
- const createSVGEl = (name) => {
5316
- const svgNS = 'http://www.w3.org/2000/svg';
5317
- 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,
5318
5712
  };
5319
5713
  const getPos = (e) => {
5320
5714
  const touchEvent = e;
@@ -5324,321 +5718,422 @@
5324
5718
  }
5325
5719
  return { x: mouseEvent.clientX, y: mouseEvent.clientY };
5326
5720
  };
5327
- const vibrate = () => {
5721
+ const vibrate = (attrs) => {
5328
5722
  if (state.vibrateTimer) {
5329
5723
  clearTimeout(state.vibrateTimer);
5330
5724
  }
5331
- if (options.vibrate && navigator.vibrate) {
5725
+ if (attrs.vibrate && navigator.vibrate) {
5332
5726
  navigator.vibrate(10);
5333
5727
  state.vibrateTimer = window.setTimeout(() => {
5334
5728
  state.vibrateTimer = undefined;
5335
5729
  }, 100);
5336
5730
  }
5337
5731
  };
5338
- const handleClockClickStart = (e) => {
5339
- e.preventDefault();
5340
- if (!state.plate)
5341
- return;
5342
- const clockPlateBR = state.plate.getBoundingClientRect();
5343
- const offset = { x: clockPlateBR.left, y: clockPlateBR.top };
5344
- state.x0 = offset.x + options.dialRadius;
5345
- state.y0 = offset.y + options.dialRadius;
5346
- state.moved = false;
5347
- const clickPos = getPos(e);
5348
- state.dx = clickPos.x - state.x0;
5349
- state.dy = clickPos.y - state.y0;
5350
- setHand(state.dx, state.dy, options.roundBy5);
5351
- document.addEventListener('mousemove', handleDocumentClickMove);
5352
- document.addEventListener('touchmove', handleDocumentClickMove);
5353
- document.addEventListener('mouseup', handleDocumentClickEnd);
5354
- document.addEventListener('touchend', handleDocumentClickEnd);
5355
- };
5356
- const handleDocumentClickMove = (e) => {
5357
- e.preventDefault();
5358
- const clickPos = getPos(e);
5359
- const x = clickPos.x - state.x0;
5360
- const y = clickPos.y - state.y0;
5361
- state.moved = true;
5362
- setHand(x, y, options.roundBy5);
5363
- m.redraw();
5364
- };
5365
- const handleDocumentClickEnd = (e) => {
5366
- e.preventDefault();
5367
- document.removeEventListener('mouseup', handleDocumentClickEnd);
5368
- document.removeEventListener('touchend', handleDocumentClickEnd);
5369
- document.removeEventListener('mousemove', handleDocumentClickMove);
5370
- document.removeEventListener('touchmove', handleDocumentClickMove);
5371
- const clickPos = getPos(e);
5372
- const x = clickPos.x - state.x0;
5373
- const y = clickPos.y - state.y0;
5374
- if (state.moved && x === state.dx && y === state.dy) {
5375
- setHand(x, y);
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;
5736
+ let radian = Math.atan2(x, -y);
5737
+ const isHours = attrs.currentView === 'hours';
5738
+ const unit = Math.PI / (isHours || roundBy5 ? 6 : 30);
5739
+ const z = Math.sqrt(x * x + y * y);
5740
+ const inner = isHours && z < (outerRadius + innerRadius) / 2;
5741
+ let radius = inner ? innerRadius : outerRadius;
5742
+ if (attrs.twelveHour) {
5743
+ radius = outerRadius;
5376
5744
  }
5377
- if (state.currentView === 'hours') {
5378
- showView('minutes', options.duration / 2);
5745
+ if (radian < 0) {
5746
+ radian = Math.PI * 2 + radian;
5379
5747
  }
5380
- else if (options.autoClose) {
5381
- if (state.minutesView) {
5382
- state.minutesView.classList.add('timepicker-dial-out');
5748
+ let value = Math.round(radian / unit);
5749
+ radian = value * unit;
5750
+ if (attrs.twelveHour) {
5751
+ if (isHours) {
5752
+ if (value === 0)
5753
+ value = 12;
5754
+ }
5755
+ else {
5756
+ if (roundBy5)
5757
+ value *= 5;
5758
+ if (value === 60)
5759
+ value = 0;
5383
5760
  }
5384
- setTimeout(() => {
5385
- done();
5386
- }, options.duration / 2);
5387
- }
5388
- if (options.onSelect) {
5389
- options.onSelect(state.hours, state.minutes);
5390
5761
  }
5391
- m.redraw();
5392
- };
5393
- const updateTimeFromInput = (inputValue) => {
5394
- let value = ((inputValue || options.defaultTime || '') + '').split(':');
5395
- let amPmWasProvided = false;
5396
- if (options.twelveHour && value.length > 1) {
5397
- if (value[1].toUpperCase().indexOf('AM') > -1) {
5398
- state.amOrPm = 'AM';
5399
- amPmWasProvided = true;
5762
+ else {
5763
+ if (isHours) {
5764
+ if (value === 12)
5765
+ value = 0;
5766
+ value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12;
5400
5767
  }
5401
- else if (value[1].toUpperCase().indexOf('PM') > -1) {
5402
- state.amOrPm = 'PM';
5403
- amPmWasProvided = true;
5768
+ else {
5769
+ if (roundBy5)
5770
+ value *= 5;
5771
+ if (value === 60)
5772
+ value = 0;
5404
5773
  }
5405
- value[1] = value[1].replace('AM', '').replace('PM', '').trim();
5406
5774
  }
5407
- if (value[0] === 'now') {
5408
- const now = new Date(+new Date() + options.fromNow);
5409
- value = [now.getHours().toString(), now.getMinutes().toString()];
5410
- if (options.twelveHour) {
5411
- state.amOrPm = parseInt(value[0]) >= 12 ? 'PM' : 'AM';
5412
- amPmWasProvided = false; // For 'now', we need to do conversion
5413
- }
5775
+ const currentValue = isHours ? attrs.hours : attrs.minutes;
5776
+ if (currentValue !== value) {
5777
+ vibrate(attrs);
5414
5778
  }
5415
- let hours = +value[0] || 0;
5416
- let minutes = +value[1] || 0;
5417
- if (options.twelveHour) {
5418
- if (!amPmWasProvided) {
5419
- // No AM/PM was provided, assume this is 24-hour format input - convert it
5420
- if (hours >= 12) {
5421
- state.amOrPm = 'PM';
5422
- if (hours > 12) {
5423
- hours = hours - 12;
5424
- }
5425
- }
5426
- else {
5427
- state.amOrPm = 'AM';
5428
- if (hours === 0) {
5429
- hours = 12;
5430
- }
5431
- }
5432
- }
5433
- else {
5434
- // AM/PM was provided, hours are already in 12-hour format
5435
- // Just handle midnight/noon edge cases
5436
- if (hours === 0 && state.amOrPm === 'AM') {
5437
- hours = 12;
5438
- }
5779
+ // Update the value
5780
+ if (isHours) {
5781
+ attrs.onTimeChange(value, attrs.minutes);
5782
+ if (attrs.spanHours) {
5783
+ attrs.spanHours.innerHTML = addLeadingZero(value);
5439
5784
  }
5440
5785
  }
5441
- state.hours = hours;
5442
- state.minutes = minutes;
5443
- if (state.spanHours) {
5444
- state.spanHours.innerHTML = addLeadingZero(state.hours);
5786
+ else {
5787
+ attrs.onTimeChange(attrs.hours, value);
5788
+ if (attrs.spanMinutes) {
5789
+ attrs.spanMinutes.innerHTML = addLeadingZero(value);
5790
+ }
5445
5791
  }
5446
- if (state.spanMinutes) {
5447
- state.spanMinutes.innerHTML = addLeadingZero(state.minutes);
5792
+ // Set clock hand position
5793
+ if (state.hand && state.bg) {
5794
+ const cx1 = Math.sin(radian) * (radius - tickRadius);
5795
+ const cy1 = -Math.cos(radian) * (radius - tickRadius);
5796
+ const cx2 = Math.sin(radian) * radius;
5797
+ const cy2 = -Math.cos(radian) * radius;
5798
+ state.hand.setAttribute('x2', cx1.toString());
5799
+ state.hand.setAttribute('y2', cy1.toString());
5800
+ state.bg.setAttribute('cx', cx2.toString());
5801
+ state.bg.setAttribute('cy', cy2.toString());
5448
5802
  }
5449
- updateAmPmView();
5450
5803
  };
5451
- const updateAmPmView = () => {
5452
- if (options.twelveHour && state.amBtn && state.pmBtn) {
5453
- state.amBtn.classList.toggle('text-primary', state.amOrPm === 'AM');
5454
- state.pmBtn.classList.toggle('text-primary', state.amOrPm === 'PM');
5455
- }
5456
- };
5457
- const showView = (view, delay) => {
5458
- const isHours = view === 'hours';
5459
- const nextView = isHours ? state.hoursView : state.minutesView;
5460
- const hideView = isHours ? state.minutesView : state.hoursView;
5461
- state.currentView = view;
5462
- if (state.spanHours) {
5463
- state.spanHours.classList.toggle('text-primary', isHours);
5464
- }
5465
- if (state.spanMinutes) {
5466
- state.spanMinutes.classList.toggle('text-primary', !isHours);
5467
- }
5468
- if (hideView) {
5469
- hideView.classList.add('timepicker-dial-out');
5470
- }
5471
- if (nextView) {
5472
- nextView.style.visibility = 'visible';
5473
- nextView.classList.remove('timepicker-dial-out');
5474
- }
5475
- resetClock(delay);
5476
- if (state.toggleViewTimer) {
5477
- clearTimeout(state.toggleViewTimer);
5478
- }
5479
- state.toggleViewTimer = window.setTimeout(() => {
5480
- if (hideView) {
5481
- hideView.style.visibility = 'hidden';
5482
- }
5483
- }, options.duration);
5484
- };
5485
- const resetClock = (delay) => {
5486
- const view = state.currentView;
5487
- const value = state[view];
5804
+ const resetClock = (attrs) => {
5805
+ const view = attrs.currentView;
5806
+ const value = view === 'hours' ? attrs.hours : attrs.minutes;
5488
5807
  const isHours = view === 'hours';
5489
5808
  const unit = Math.PI / (isHours ? 6 : 30);
5490
5809
  const radian = value * unit;
5491
- const radius = isHours && value > 0 && value < 13 ? options.innerRadius : options.outerRadius;
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
+ }
5492
5818
  const x = Math.sin(radian) * radius;
5493
5819
  const y = -Math.cos(radian) * radius;
5494
- if (delay && state.canvas) {
5495
- state.canvas.classList.add('timepicker-canvas-out');
5496
- setTimeout(() => {
5497
- if (state.canvas) {
5498
- state.canvas.classList.remove('timepicker-canvas-out');
5820
+ setHand(x, y, attrs);
5821
+ };
5822
+ const handleClockClickStart = (e, attrs) => {
5823
+ e.preventDefault();
5824
+ if (!state.plate)
5825
+ return;
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
+ }
5499
5888
  }
5500
- setHand(x, y);
5501
- }, delay);
5502
- }
5503
- else {
5504
- setHand(x, y);
5505
- }
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
+ };
5506
5907
  };
5507
- const setHand = (x, y, roundBy5, _dragging) => {
5508
- let radian = Math.atan2(x, -y);
5509
- const isHours = state.currentView === 'hours';
5510
- const unit = Math.PI / (isHours || roundBy5 ? 6 : 30);
5511
- const z = Math.sqrt(x * x + y * y);
5512
- const inner = isHours && z < (options.outerRadius + options.innerRadius) / 2;
5513
- let radius = inner ? options.innerRadius : options.outerRadius;
5514
- if (options.twelveHour) {
5515
- radius = options.outerRadius;
5516
- }
5517
- if (radian < 0) {
5518
- radian = Math.PI * 2 + radian;
5519
- }
5520
- let value = Math.round(radian / unit);
5521
- radian = value * unit;
5522
- if (options.twelveHour) {
5523
- if (isHours) {
5524
- if (value === 0)
5525
- value = 12;
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;
5526
6082
  }
5527
- else {
5528
- if (roundBy5)
5529
- value *= 5;
5530
- if (value === 60)
5531
- value = 0;
6083
+ else if (value[1].toUpperCase().indexOf('PM') > -1) {
6084
+ state.amOrPm = 'PM';
6085
+ amPmWasProvided = true;
5532
6086
  }
6087
+ value[1] = value[1].replace('AM', '').replace('PM', '').trim();
5533
6088
  }
5534
- else {
5535
- if (isHours) {
5536
- if (value === 12)
5537
- value = 0;
5538
- value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12;
5539
- }
5540
- else {
5541
- if (roundBy5)
5542
- value *= 5;
5543
- if (value === 60)
5544
- value = 0;
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
5545
6095
  }
5546
6096
  }
5547
- if (state[state.currentView] !== value) {
5548
- vibrate();
5549
- }
5550
- state[state.currentView] = value;
5551
- if (isHours && state.spanHours) {
5552
- state.spanHours.innerHTML = addLeadingZero(value);
5553
- }
5554
- else if (!isHours && state.spanMinutes) {
5555
- state.spanMinutes.innerHTML = addLeadingZero(value);
5556
- }
5557
- // Set clock hand position
5558
- if (state.hand && state.bg) {
5559
- const cx1 = Math.sin(radian) * (radius - options.tickRadius);
5560
- const cy1 = -Math.cos(radian) * (radius - options.tickRadius);
5561
- const cx2 = Math.sin(radian) * radius;
5562
- const cy2 = -Math.cos(radian) * radius;
5563
- state.hand.setAttribute('x2', cx1.toString());
5564
- state.hand.setAttribute('y2', cy1.toString());
5565
- state.bg.setAttribute('cx', cx2.toString());
5566
- state.bg.setAttribute('cy', cy2.toString());
5567
- }
5568
- };
5569
- const buildSVGClock = () => {
5570
- if (!state.canvas)
5571
- return;
5572
- const dialRadius = options.dialRadius;
5573
- const tickRadius = options.tickRadius;
5574
- const diameter = dialRadius * 2;
5575
- const svg = createSVGEl('svg');
5576
- svg.setAttribute('class', 'timepicker-svg');
5577
- svg.setAttribute('width', diameter.toString());
5578
- svg.setAttribute('height', diameter.toString());
5579
- const g = createSVGEl('g');
5580
- g.setAttribute('transform', `translate(${dialRadius},${dialRadius})`);
5581
- const bearing = createSVGEl('circle');
5582
- bearing.setAttribute('class', 'timepicker-canvas-bearing');
5583
- bearing.setAttribute('cx', '0');
5584
- bearing.setAttribute('cy', '0');
5585
- bearing.setAttribute('r', '4');
5586
- const hand = createSVGEl('line');
5587
- hand.setAttribute('x1', '0');
5588
- hand.setAttribute('y1', '0');
5589
- const bg = createSVGEl('circle');
5590
- bg.setAttribute('class', 'timepicker-canvas-bg');
5591
- bg.setAttribute('r', tickRadius.toString());
5592
- g.appendChild(hand);
5593
- g.appendChild(bg);
5594
- g.appendChild(bearing);
5595
- svg.appendChild(g);
5596
- state.canvas.appendChild(svg);
5597
- state.hand = hand;
5598
- state.bg = bg;
5599
- state.bearing = bearing;
5600
- state.g = g;
5601
- };
5602
- const buildHoursView = () => {
5603
- if (!state.hoursView)
5604
- return;
6097
+ let hours = +value[0] || 0;
6098
+ let minutes = +value[1] || 0;
5605
6099
  if (options.twelveHour) {
5606
- for (let i = 1; i < 13; i++) {
5607
- const tick = document.createElement('div');
5608
- tick.className = 'timepicker-tick';
5609
- const radian = (i / 6) * Math.PI;
5610
- const radius = options.outerRadius;
5611
- tick.style.left = options.dialRadius + Math.sin(radian) * radius - options.tickRadius + 'px';
5612
- tick.style.top = options.dialRadius - Math.cos(radian) * radius - options.tickRadius + 'px';
5613
- tick.innerHTML = i === 0 ? '00' : i.toString();
5614
- state.hoursView.appendChild(tick);
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
+ }
5615
6114
  }
5616
- }
5617
- else {
5618
- for (let i = 0; i < 24; i++) {
5619
- const tick = document.createElement('div');
5620
- tick.className = 'timepicker-tick';
5621
- const radian = (i / 6) * Math.PI;
5622
- const inner = i > 0 && i < 13;
5623
- const radius = inner ? options.innerRadius : options.outerRadius;
5624
- tick.style.left = options.dialRadius + Math.sin(radian) * radius - options.tickRadius + 'px';
5625
- tick.style.top = options.dialRadius - Math.cos(radian) * radius - options.tickRadius + 'px';
5626
- tick.innerHTML = i === 0 ? '00' : i.toString();
5627
- state.hoursView.appendChild(tick);
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
+ }
5628
6121
  }
5629
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);
6130
+ }
6131
+ updateAmPmView();
5630
6132
  };
5631
- const buildMinutesView = () => {
5632
- if (!state.minutesView)
5633
- return;
5634
- for (let i = 0; i < 60; i += 5) {
5635
- const tick = document.createElement('div');
5636
- tick.className = 'timepicker-tick';
5637
- const radian = (i / 30) * Math.PI;
5638
- tick.style.left = options.dialRadius + Math.sin(radian) * options.outerRadius - options.tickRadius + 'px';
5639
- tick.style.top = options.dialRadius - Math.cos(radian) * options.outerRadius - options.tickRadius + 'px';
5640
- tick.innerHTML = addLeadingZero(i);
5641
- 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');
5642
6137
  }
5643
6138
  };
5644
6139
  const handleAmPmClick = (ampm) => {
@@ -5650,7 +6145,7 @@
5650
6145
  return;
5651
6146
  state.isOpen = true;
5652
6147
  updateTimeFromInput(inputValue);
5653
- showView('hours');
6148
+ state.currentView = 'hours';
5654
6149
  if (options.onOpen)
5655
6150
  options.onOpen();
5656
6151
  if (options.onOpenStart)
@@ -5684,6 +6179,7 @@
5684
6179
  return {
5685
6180
  view: ({ attrs }) => {
5686
6181
  const { i18n, showClearBtn } = attrs;
6182
+ const isDigitalMode = options.displayMode === 'digital';
5687
6183
  return [
5688
6184
  m('.modal-content.timepicker-container', [
5689
6185
  m('.timepicker-digital-display', [
@@ -5691,7 +6187,12 @@
5691
6187
  m('.timepicker-display-column', [
5692
6188
  m('span.timepicker-span-hours', {
5693
6189
  class: state.currentView === 'hours' ? 'text-primary' : '',
5694
- onclick: () => showView('hours'),
6190
+ onclick: () => {
6191
+ if (!isDigitalMode) {
6192
+ state.currentView = 'hours';
6193
+ m.redraw();
6194
+ }
6195
+ },
5695
6196
  oncreate: (vnode) => {
5696
6197
  state.spanHours = vnode.dom;
5697
6198
  },
@@ -5699,7 +6200,12 @@
5699
6200
  ':',
5700
6201
  m('span.timepicker-span-minutes', {
5701
6202
  class: state.currentView === 'minutes' ? 'text-primary' : '',
5702
- onclick: () => showView('minutes'),
6203
+ onclick: () => {
6204
+ if (!isDigitalMode) {
6205
+ state.currentView = 'minutes';
6206
+ m.redraw();
6207
+ }
6208
+ },
5703
6209
  oncreate: (vnode) => {
5704
6210
  state.spanMinutes = vnode.dom;
5705
6211
  },
@@ -5730,66 +6236,108 @@
5730
6236
  ]),
5731
6237
  ]),
5732
6238
  ]),
5733
- m('.timepicker-analog-display', [
5734
- m('.timepicker-plate', {
5735
- oncreate: (vnode) => {
5736
- state.plate = vnode.dom;
5737
- state.plate.addEventListener('mousedown', handleClockClickStart);
5738
- state.plate.addEventListener('touchstart', handleClockClickStart);
5739
- },
5740
- onremove: () => {
5741
- if (state.plate) {
5742
- state.plate.removeEventListener('mousedown', handleClockClickStart);
5743
- state.plate.removeEventListener('touchstart', handleClockClickStart);
5744
- }
5745
- },
5746
- }, [
5747
- m('.timepicker-canvas', {
5748
- oncreate: (vnode) => {
5749
- state.canvas = vnode.dom;
5750
- buildSVGClock();
5751
- // Position the hand after SVG is built
5752
- setTimeout(() => resetClock(), 10);
5753
- },
5754
- }),
5755
- m('.timepicker-dial.timepicker-hours', {
5756
- oncreate: (vnode) => {
5757
- state.hoursView = vnode.dom;
5758
- 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);
5759
6258
  },
6259
+ spanHours: state.spanHours,
6260
+ spanMinutes: state.spanMinutes,
6261
+ spanAmPm: state.spanAmPm,
5760
6262
  }),
5761
- m('.timepicker-dial.timepicker-minutes.timepicker-dial-out', {
6263
+ m('.timepicker-footer', {
5762
6264
  oncreate: (vnode) => {
5763
- state.minutesView = vnode.dom;
5764
- buildMinutesView();
6265
+ state.footer = vnode.dom;
5765
6266
  },
5766
- }),
5767
- ]),
5768
- m('.timepicker-footer', {
5769
- oncreate: (vnode) => {
5770
- state.footer = vnode.dom;
5771
- },
5772
- }, [
5773
- m('button.btn-flat.timepicker-clear.waves-effect', {
5774
- type: 'button',
5775
- tabindex: options.twelveHour ? '3' : '1',
5776
- style: showClearBtn ? '' : 'visibility: hidden;',
5777
- onclick: () => clear(),
5778
- }, i18n.clear),
5779
- m('.confirmation-btns', [
5780
- m('button.btn-flat.timepicker-close.waves-effect', {
6267
+ }, [
6268
+ m('button.btn-flat.timepicker-clear.waves-effect', {
5781
6269
  type: 'button',
5782
6270
  tabindex: options.twelveHour ? '3' : '1',
5783
- onclick: () => close(),
5784
- }, i18n.cancel),
5785
- 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', {
5786
6322
  type: 'button',
5787
6323
  tabindex: options.twelveHour ? '3' : '1',
5788
- onclick: () => done(),
5789
- }, 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
+ ]),
5790
6339
  ]),
5791
6340
  ]),
5792
- ]),
5793
6341
  ]),
5794
6342
  ];
5795
6343
  },
@@ -5802,7 +6350,7 @@
5802
6350
  m.redraw();
5803
6351
  }
5804
6352
  };
5805
- const renderPickerToPortal = (attrs) => {
6353
+ const renderPickerToPortal = () => {
5806
6354
  const pickerModal = m('.timepicker-modal-wrapper', {
5807
6355
  style: {
5808
6356
  position: 'fixed',
@@ -5863,11 +6411,6 @@
5863
6411
  minutes: 0,
5864
6412
  amOrPm: 'AM',
5865
6413
  currentView: 'hours',
5866
- moved: false,
5867
- x0: 0,
5868
- y0: 0,
5869
- dx: 0,
5870
- dy: 0,
5871
6414
  portalContainerId: `timepicker-portal-${uniqueId()}`,
5872
6415
  };
5873
6416
  // Handle value after options are set
@@ -5879,27 +6422,17 @@
5879
6422
  },
5880
6423
  onremove: () => {
5881
6424
  // Cleanup
5882
- if (state.toggleViewTimer) {
5883
- clearTimeout(state.toggleViewTimer);
5884
- }
5885
- if (state.vibrateTimer) {
5886
- clearTimeout(state.vibrateTimer);
5887
- }
5888
- document.removeEventListener('mousemove', handleDocumentClickMove);
5889
- document.removeEventListener('touchmove', handleDocumentClickMove);
5890
- document.removeEventListener('mouseup', handleDocumentClickEnd);
5891
- document.removeEventListener('touchend', handleDocumentClickEnd);
5892
6425
  document.removeEventListener('keydown', handleKeyDown);
5893
6426
  // Clean up portal if picker was open
5894
6427
  if (state.isOpen) {
5895
6428
  clearPortal(state.portalContainerId);
5896
6429
  }
5897
6430
  },
5898
- onupdate: (vnode) => {
5899
- const { useModal = true } = vnode.attrs;
6431
+ onupdate: ({ attrs }) => {
6432
+ const { useModal = true } = attrs;
5900
6433
  // Only render to portal when using modal mode
5901
6434
  if (useModal && state.isOpen) {
5902
- renderPickerToPortal(vnode.attrs);
6435
+ renderPickerToPortal();
5903
6436
  }
5904
6437
  else {
5905
6438
  clearPortal(state.portalContainerId);
@@ -6429,7 +6962,7 @@
6429
6962
  // Create dropdown with proper positioning
6430
6963
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6431
6964
  tabindex: 0,
6432
- style: getPortalStyles(state.inputRef),
6965
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6433
6966
  oncreate: ({ dom }) => {
6434
6967
  state.dropdownRef = dom;
6435
6968
  },
@@ -6498,7 +7031,8 @@
6498
7031
  selectedIds = state.internalSelectedIds;
6499
7032
  }
6500
7033
  const finalClassName = newRow ? `${className} clear` : className;
6501
- const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
7034
+ const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
7035
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6502
7036
  // Update portal dropdown when inside modal
6503
7037
  if (state.isInsideModal) {
6504
7038
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -6543,7 +7077,7 @@
6543
7077
  onremove: () => {
6544
7078
  state.dropdownRef = null;
6545
7079
  },
6546
- style: getDropdownStyles(state.inputRef, true, options),
7080
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, options)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6547
7081
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder)),
6548
7082
  m(MaterialIcon, {
6549
7083
  name: 'caret',
@@ -6779,45 +7313,50 @@
6779
7313
  };
6780
7314
 
6781
7315
  // Proper components to avoid anonymous closures
6782
- const SelectedChip = {
6783
- view: ({ attrs: { option, onRemove } }) => m('.chip', [
6784
- option.label || option.id.toString(),
6785
- m(MaterialIcon, {
6786
- name: 'close',
6787
- className: 'close',
6788
- onclick: (e) => {
6789
- e.stopPropagation();
6790
- onRemove(option.id);
6791
- },
6792
- }),
6793
- ]),
6794
- };
6795
- const DropdownOption = {
6796
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6797
- const checkboxId = `search-select-option-${option.id}`;
6798
- const optionLabel = option.label || option.id.toString();
6799
- return m('li', {
6800
- key: option.id,
6801
- onclick: (e) => {
6802
- e.preventDefault();
6803
- e.stopPropagation();
6804
- onToggle(option);
6805
- },
6806
- class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
6807
- onmouseover: () => {
6808
- if (!option.disabled) {
6809
- onMouseOver(index);
6810
- }
6811
- },
6812
- }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6813
- m('input', {
6814
- type: 'checkbox',
6815
- id: checkboxId,
6816
- checked: selectedIds.includes(option.id),
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
+ },
6817
7327
  }),
6818
- m('span', optionLabel),
6819
- ]));
6820
- },
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
+ };
6821
7360
  };
6822
7361
  /**
6823
7362
  * Mithril Factory Component for Multi-Select Dropdown with search
@@ -6832,6 +7371,7 @@
6832
7371
  dropdownRef: null,
6833
7372
  focusedIndex: -1,
6834
7373
  internalSelectedIds: [],
7374
+ createdOptions: [],
6835
7375
  };
6836
7376
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
6837
7377
  const componentId = uniqueId();
@@ -6874,7 +7414,10 @@
6874
7414
  // Handle add new option
6875
7415
  return 'addNew';
6876
7416
  }
6877
- else if (state.focusedIndex < filteredOptions.length) ;
7417
+ else if (state.focusedIndex < filteredOptions.length) {
7418
+ // This will be handled in the view method where attrs are available
7419
+ return 'selectOption';
7420
+ }
6878
7421
  }
6879
7422
  break;
6880
7423
  case 'Escape':
@@ -6885,11 +7428,22 @@
6885
7428
  }
6886
7429
  return null;
6887
7430
  };
7431
+ // Create new option and add to state
7432
+ const createAndSelectOption = async (attrs) => {
7433
+ if (!attrs.oncreateNewOption || !state.searchTerm)
7434
+ return;
7435
+ const newOption = await attrs.oncreateNewOption(state.searchTerm);
7436
+ // Store the created option internally
7437
+ state.createdOptions.push(newOption);
7438
+ // Select the new option
7439
+ toggleOption(newOption, attrs);
7440
+ };
6888
7441
  // Toggle option selection
6889
7442
  const toggleOption = (option, attrs) => {
6890
7443
  if (option.disabled)
6891
7444
  return;
6892
7445
  const controlled = isControlled(attrs);
7446
+ const { maxSelectedOptions } = attrs;
6893
7447
  // Get current selected IDs from props or internal state
6894
7448
  const currentSelectedIds = controlled
6895
7449
  ? attrs.checkedId !== undefined
@@ -6898,9 +7452,29 @@
6898
7452
  : [attrs.checkedId]
6899
7453
  : []
6900
7454
  : state.internalSelectedIds;
6901
- const newIds = currentSelectedIds.includes(option.id)
6902
- ? currentSelectedIds.filter((id) => id !== option.id)
6903
- : [...currentSelectedIds, option.id];
7455
+ const isSelected = currentSelectedIds.includes(option.id);
7456
+ let newIds;
7457
+ if (isSelected) {
7458
+ // Remove if already selected
7459
+ newIds = currentSelectedIds.filter((id) => id !== option.id);
7460
+ }
7461
+ else {
7462
+ // Check if we've reached the max selection limit
7463
+ if (maxSelectedOptions && currentSelectedIds.length >= maxSelectedOptions) {
7464
+ // If max=1, replace the selection
7465
+ if (maxSelectedOptions === 1) {
7466
+ newIds = [option.id];
7467
+ }
7468
+ else {
7469
+ // Otherwise, don't add more
7470
+ return;
7471
+ }
7472
+ }
7473
+ else {
7474
+ // Add to selection
7475
+ newIds = [...currentSelectedIds, option.id];
7476
+ }
7477
+ }
6904
7478
  // Update internal state for uncontrolled mode
6905
7479
  if (!controlled) {
6906
7480
  state.internalSelectedIds = newIds;
@@ -6962,21 +7536,32 @@
6962
7536
  : [attrs.checkedId]
6963
7537
  : []
6964
7538
  : state.internalSelectedIds;
6965
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
7539
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
6966
7540
  // Use i18n values if provided, otherwise use defaults
6967
7541
  const texts = {
6968
7542
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6969
7543
  addNewPrefix: i18n.addNewPrefix || '+',
7544
+ showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7545
+ maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
6970
7546
  };
7547
+ // Check if max selections is reached
7548
+ const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7549
+ // Merge provided options with internally created options
7550
+ const allOptions = [...options, ...state.createdOptions];
6971
7551
  // Get selected options for display
6972
- const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
7552
+ const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7553
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6973
7554
  // Safely filter options
6974
- const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
7555
+ const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6975
7556
  !selectedIds.includes(option.id));
7557
+ // Apply display limit if configured
7558
+ const totalFilteredCount = filteredOptions.length;
7559
+ const displayedOptions = maxDisplayedOptions ? filteredOptions.slice(0, maxDisplayedOptions) : filteredOptions;
7560
+ const isTruncated = maxDisplayedOptions && totalFilteredCount > maxDisplayedOptions;
6976
7561
  // Check if we should show the "add new option" element
6977
7562
  const showAddNew = oncreateNewOption &&
6978
7563
  state.searchTerm &&
6979
- !filteredOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
7564
+ !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
6980
7565
  // Render the dropdown
6981
7566
  return m('.input-field.multi-select-dropdown', { className }, [
6982
7567
  m('.chips.chips-initial.chips-container', {
@@ -7010,8 +7595,7 @@
7010
7595
  style: { position: 'absolute', left: '-9999px', opacity: 0 },
7011
7596
  }),
7012
7597
  // Selected Options (chips)
7013
- ...selectedOptions.map((option) => m(SelectedChip, {
7014
- // key: option.id,
7598
+ ...selectedOptions.map((option) => m(SelectedChip(), {
7015
7599
  option,
7016
7600
  onRemove: (id) => removeOption(id, attrs),
7017
7601
  })),
@@ -7049,7 +7633,7 @@
7049
7633
  onremove: () => {
7050
7634
  state.dropdownRef = null;
7051
7635
  },
7052
- style: getDropdownStyles(state.inputRef),
7636
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
7053
7637
  }, [
7054
7638
  m('li', // Search Input
7055
7639
  {
@@ -7069,42 +7653,65 @@
7069
7653
  state.focusedIndex = -1; // Reset focus when typing
7070
7654
  },
7071
7655
  onkeydown: async (e) => {
7072
- const result = handleKeyDown(e, filteredOptions, !!showAddNew);
7656
+ const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7073
7657
  if (result === 'addNew' && oncreateNewOption) {
7074
- const option = await oncreateNewOption(state.searchTerm);
7075
- toggleOption(option, attrs);
7658
+ await createAndSelectOption(attrs);
7076
7659
  }
7077
- else if (e.key === 'Enter' &&
7078
- state.focusedIndex >= 0 &&
7079
- state.focusedIndex < filteredOptions.length) {
7080
- toggleOption(filteredOptions[state.focusedIndex], attrs);
7660
+ else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
7661
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
7081
7662
  }
7082
7663
  },
7083
7664
  class: 'search-select-input',
7084
7665
  }),
7085
7666
  ]),
7086
7667
  // No options found message or list of options
7087
- ...(filteredOptions.length === 0 && !showAddNew
7668
+ ...(displayedOptions.length === 0 && !showAddNew
7088
7669
  ? [m('li.search-select-no-options', texts.noOptionsFound)]
7089
7670
  : []),
7671
+ // Truncation message
7672
+ ...(isTruncated
7673
+ ? [
7674
+ m('li.search-select-truncation-info', {
7675
+ style: {
7676
+ fontStyle: 'italic',
7677
+ color: 'var(--mm-text-hint, #9e9e9e)',
7678
+ padding: '8px 16px',
7679
+ cursor: 'default',
7680
+ },
7681
+ }, texts.showingXofY
7682
+ .replace('{shown}', displayedOptions.length.toString())
7683
+ .replace('{total}', totalFilteredCount.toString())),
7684
+ ]
7685
+ : []),
7686
+ // Max selections reached message
7687
+ ...(isMaxSelectionsReached
7688
+ ? [
7689
+ m('li.search-select-max-info', {
7690
+ style: {
7691
+ fontStyle: 'italic',
7692
+ color: 'var(--mm-text-hint, #9e9e9e)',
7693
+ padding: '8px 16px',
7694
+ cursor: 'default',
7695
+ },
7696
+ }, texts.maxSelectionsReached.replace('{max}', maxSelectedOptions.toString())),
7697
+ ]
7698
+ : []),
7090
7699
  // Add new option item
7091
7700
  ...(showAddNew
7092
7701
  ? [
7093
7702
  m('li', {
7094
7703
  onclick: async () => {
7095
- const option = await oncreateNewOption(state.searchTerm);
7096
- toggleOption(option, attrs);
7704
+ await createAndSelectOption(attrs);
7097
7705
  },
7098
- class: state.focusedIndex === filteredOptions.length ? 'active' : '',
7706
+ class: state.focusedIndex === displayedOptions.length ? 'active' : '',
7099
7707
  onmouseover: () => {
7100
- state.focusedIndex = filteredOptions.length;
7708
+ state.focusedIndex = displayedOptions.length;
7101
7709
  },
7102
7710
  }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
7103
7711
  ]
7104
7712
  : []),
7105
7713
  // List of filtered options
7106
- ...filteredOptions.map((option, index) => m(DropdownOption, {
7107
- // key: option.id,
7714
+ ...displayedOptions.map((option, index) => m(DropdownOption(), {
7108
7715
  option,
7109
7716
  index,
7110
7717
  selectedIds,
@@ -7113,6 +7720,7 @@
7113
7720
  onMouseOver: (idx) => {
7114
7721
  state.focusedIndex = idx;
7115
7722
  },
7723
+ showCheckbox: maxSelectedOptions !== 1,
7116
7724
  })),
7117
7725
  ]),
7118
7726
  ]);
@@ -7120,6 +7728,411 @@
7120
7728
  };
7121
7729
  };
7122
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
+
7123
8136
  class Toast {
7124
8137
  constructor(options = {}) {
7125
8138
  this.options = Object.assign(Object.assign({}, Toast.defaults), options);
@@ -9723,6 +10736,7 @@
9723
10736
  // ============================================================================
9724
10737
  // All types are already exported via individual export declarations above
9725
10738
 
10739
+ exports.AnalogClock = AnalogClock;
9726
10740
  exports.AnchorItem = AnchorItem;
9727
10741
  exports.Autocomplete = Autocomplete;
9728
10742
  exports.Breadcrumb = Breadcrumb;
@@ -9739,6 +10753,7 @@
9739
10753
  exports.ColorInput = ColorInput;
9740
10754
  exports.DataTable = DataTable;
9741
10755
  exports.DatePicker = DatePicker;
10756
+ exports.DigitalClock = DigitalClock;
9742
10757
  exports.DoubleRangeSlider = DoubleRangeSlider;
9743
10758
  exports.Dropdown = Dropdown;
9744
10759
  exports.EmailInput = EmailInput;
@@ -9791,6 +10806,7 @@
9791
10806
  exports.ThemeSwitcher = ThemeSwitcher;
9792
10807
  exports.ThemeToggle = ThemeToggle;
9793
10808
  exports.TimePicker = TimePicker;
10809
+ exports.TimeRangePicker = TimeRangePicker;
9794
10810
  exports.Timeline = Timeline;
9795
10811
  exports.Toast = Toast;
9796
10812
  exports.ToastComponent = ToastComponent;
@@ -9799,19 +10815,29 @@
9799
10815
  exports.TreeView = TreeView;
9800
10816
  exports.UrlInput = UrlInput;
9801
10817
  exports.Wizard = Wizard;
10818
+ exports.addLeadingZero = addLeadingZero;
9802
10819
  exports.clearPortal = clearPortal;
9803
10820
  exports.createBreadcrumb = createBreadcrumb;
10821
+ exports.formatTime = formatTime;
10822
+ exports.generateHourOptions = generateHourOptions;
10823
+ exports.generateMinuteOptions = generateMinuteOptions;
9804
10824
  exports.getDropdownStyles = getDropdownStyles;
9805
10825
  exports.getPortalContainer = getPortalContainer;
9806
10826
  exports.initPushpins = initPushpins;
9807
10827
  exports.initTooltips = initTooltips;
9808
10828
  exports.isNumeric = isNumeric;
10829
+ exports.isTimeDisabled = isTimeDisabled;
9809
10830
  exports.isValidationError = isValidationError;
9810
10831
  exports.isValidationSuccess = isValidationSuccess;
9811
10832
  exports.padLeft = padLeft;
10833
+ exports.parseTime = parseTime;
9812
10834
  exports.range = range;
9813
10835
  exports.releasePortalContainer = releasePortalContainer;
9814
10836
  exports.renderToPortal = renderToPortal;
10837
+ exports.scrollToValue = scrollToValue;
10838
+ exports.snapToNearestItem = snapToNearestItem;
10839
+ exports.sortOptions = sortOptions;
10840
+ exports.timeToMinutes = timeToMinutes;
9815
10841
  exports.toast = toast;
9816
10842
  exports.uniqueId = uniqueId;
9817
10843
  exports.uuid4 = uuid4;