mithril-materialized 3.5.9 → 3.6.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.js CHANGED
@@ -64,6 +64,28 @@ const uuid4 = () => {
64
64
  };
65
65
  /** Check if a string or number is numeric. @see https://stackoverflow.com/a/9716488/319711 */
66
66
  const isNumeric = (n) => !isNaN(parseFloat(n)) && isFinite(n);
67
+ /**
68
+ * Sort options array based on sorting configuration
69
+ * @param options - Array of options to sort
70
+ * @param sortConfig - Sort configuration: 'asc', 'desc', 'none', or custom comparator function
71
+ * @returns Sorted array (or original if 'none' or undefined)
72
+ */
73
+ const sortOptions = (options, sortConfig) => {
74
+ if (!sortConfig || sortConfig === 'none') {
75
+ return options;
76
+ }
77
+ const sorted = [...options]; // Create a copy to avoid mutating original
78
+ if (typeof sortConfig === 'function') {
79
+ return sorted.sort(sortConfig);
80
+ }
81
+ // Sort by label, fallback to id if no label
82
+ return sorted.sort((a, b) => {
83
+ const aLabel = (a.label || a.id.toString()).toLowerCase();
84
+ const bLabel = (b.label || b.id.toString()).toLowerCase();
85
+ const comparison = aLabel.localeCompare(bLabel);
86
+ return sortConfig === 'asc' ? comparison : -comparison;
87
+ });
88
+ };
67
89
  /**
68
90
  * Pad left, default width 2 with a '0'
69
91
  *
@@ -1411,19 +1433,18 @@ const CollapsibleItem = () => {
1411
1433
  onclick: onToggle,
1412
1434
  }, [
1413
1435
  iconName ? m('i.material-icons', iconName) : undefined,
1414
- header ? (typeof header === 'string' ? m('span', header) : header) : undefined,
1436
+ header
1437
+ ? typeof header === 'string'
1438
+ ? m('span.collapsible-header-text', header)
1439
+ : m('.collapsible-header-content', header)
1440
+ : undefined,
1415
1441
  ])
1416
1442
  : undefined,
1417
1443
  m('.collapsible-body', {
1418
1444
  style: {
1419
1445
  display: isActive ? 'block' : 'none',
1420
- transition: 'display 0.3s ease',
1421
1446
  },
1422
- }, [
1423
- m('.collapsible-body-content', {
1424
- style: { padding: '2rem' },
1425
- }, body ? (typeof body === 'string' ? m('div', { innerHTML: body }) : body) : undefined),
1426
- ]),
1447
+ }, body ? (typeof body === 'string' ? m('div', { innerHTML: body }) : body) : undefined),
1427
1448
  ]);
1428
1449
  },
1429
1450
  };
@@ -1446,7 +1467,7 @@ const Collapsible = () => {
1446
1467
  });
1447
1468
  },
1448
1469
  view: ({ attrs }) => {
1449
- const { items, accordion = true, class: c, className, style, id } = attrs;
1470
+ const { items, header, accordion = true, class: c, className, style, id } = attrs;
1450
1471
  const toggleItem = (index) => {
1451
1472
  if (accordion) {
1452
1473
  // Accordion mode: only one item can be active
@@ -1468,12 +1489,22 @@ const Collapsible = () => {
1468
1489
  }
1469
1490
  }
1470
1491
  };
1492
+ const collapsibleItems = items.map((item, index) => m(CollapsibleItem, Object.assign(Object.assign({}, item), { key: index, isActive: state.activeItems.has(index), onToggle: () => toggleItem(index) })));
1471
1493
  return items && items.length > 0
1472
- ? m('ul.collapsible', {
1473
- class: c || className,
1474
- style: Object.assign({ border: '1px solid #ddd', borderRadius: '2px', margin: '0.5rem 0 1rem 0' }, style),
1475
- id,
1476
- }, items.map((item, index) => m(CollapsibleItem, Object.assign(Object.assign({}, item), { key: index, isActive: state.activeItems.has(index), onToggle: () => toggleItem(index) }))))
1494
+ ? header
1495
+ ? m('ul.collapsible.with-header', {
1496
+ class: c || className,
1497
+ style,
1498
+ id,
1499
+ }, [
1500
+ m('li.collapsible-main-header', m('h4', typeof header === 'string' ? header : header)),
1501
+ collapsibleItems,
1502
+ ])
1503
+ : m('ul.collapsible', {
1504
+ class: c || className,
1505
+ style,
1506
+ id,
1507
+ }, collapsibleItems)
1477
1508
  : undefined;
1478
1509
  },
1479
1510
  };
@@ -1577,7 +1608,7 @@ const Collection = () => {
1577
1608
  };
1578
1609
  };
1579
1610
 
1580
- const defaultI18n$2 = {
1611
+ const defaultI18n$3 = {
1581
1612
  cancel: 'Cancel',
1582
1613
  clear: 'Clear',
1583
1614
  done: 'Ok',
@@ -1685,9 +1716,9 @@ const DatePicker = () => {
1685
1716
  else if (attrs.displayFormat) {
1686
1717
  finalFormat = attrs.displayFormat;
1687
1718
  }
1688
- 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$2, onSelect: null, onOpen: null, onClose: null }, attrs);
1719
+ 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);
1689
1720
  // Merge i18n properly
1690
- merged.i18n = Object.assign(Object.assign({}, defaultI18n$2), attrs.i18n);
1721
+ merged.i18n = Object.assign(Object.assign({}, defaultI18n$3), attrs.i18n);
1691
1722
  return merged;
1692
1723
  };
1693
1724
  const toString = (date, format) => {
@@ -2138,11 +2169,11 @@ const DatePicker = () => {
2138
2169
  prevMonth();
2139
2170
  },
2140
2171
  }, m('svg', {
2141
- fill: '#000000',
2142
2172
  height: '24',
2143
2173
  viewBox: '0 0 24 24',
2144
2174
  width: '24',
2145
2175
  xmlns: 'http://www.w3.org/2000/svg',
2176
+ style: 'fill: var(--mm-text-primary, rgba(0, 0, 0, 0.87));',
2146
2177
  }, [
2147
2178
  m('path', { d: 'M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z' }),
2148
2179
  m('path', { d: 'M0-.5h24v24H0z', fill: 'none' }),
@@ -2204,11 +2235,11 @@ const DatePicker = () => {
2204
2235
  nextMonth();
2205
2236
  },
2206
2237
  }, m('svg', {
2207
- fill: '#000000',
2208
2238
  height: '24',
2209
2239
  viewBox: '0 0 24 24',
2210
2240
  width: '24',
2211
2241
  xmlns: 'http://www.w3.org/2000/svg',
2242
+ style: 'fill: var(--mm-text-primary, rgba(0, 0, 0, 0.87));',
2212
2243
  }, [
2213
2244
  m('path', { d: 'M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z' }),
2214
2245
  m('path', { d: 'M0-.25h24v24H0z', fill: 'none' }),
@@ -3221,19 +3252,29 @@ const TextArea = () => {
3221
3252
  if (!controlled && attrs.defaultValue !== undefined) {
3222
3253
  textarea.value = String(attrs.defaultValue);
3223
3254
  }
3224
- // Height will be calculated by hidden div
3225
3255
  // Update character count state for counter component
3226
3256
  if (maxLength) {
3227
3257
  state.currentLength = textarea.value.length;
3228
3258
  }
3259
+ // Calculate initial height after DOM is fully ready
3260
+ // Use requestAnimationFrame to ensure layout is complete
3261
+ if (state.hiddenDiv) {
3262
+ requestAnimationFrame(() => {
3263
+ if (state.hiddenDiv) {
3264
+ updateHeight(textarea, state.hiddenDiv);
3265
+ m.redraw();
3266
+ }
3267
+ });
3268
+ }
3229
3269
  }, onupdate: ({ dom }) => {
3230
3270
  const textarea = dom;
3231
- if (state.height)
3232
- textarea.style.height = state.height;
3233
- // Trigger height recalculation when value changes programmatically
3271
+ // Recalculate and apply height
3234
3272
  if (state.hiddenDiv) {
3235
3273
  updateHeight(textarea, state.hiddenDiv);
3236
3274
  }
3275
+ if (state.height) {
3276
+ textarea.style.height = state.height;
3277
+ }
3237
3278
  }, onfocus: () => {
3238
3279
  state.active = true;
3239
3280
  }, oninput: (e) => {
@@ -4522,7 +4563,7 @@ const Dropdown = () => {
4522
4563
  // Create dropdown with proper positioning
4523
4564
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4524
4565
  tabindex: 0,
4525
- style: getPortalStyles(state.inputRef),
4566
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4526
4567
  oncreate: ({ dom }) => {
4527
4568
  state.dropdownRef = dom;
4528
4569
  },
@@ -4634,7 +4675,7 @@ const Dropdown = () => {
4634
4675
  onremove: () => {
4635
4676
  state.dropdownRef = null;
4636
4677
  },
4637
- style: getDropdownStyles(state.inputRef, true, items, true),
4678
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, items, true)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4638
4679
  }, items.map((item) => {
4639
4680
  if (item.divider) {
4640
4681
  return m('li.divider');
@@ -4774,7 +4815,7 @@ const FloatingActionButton = () => {
4774
4815
 
4775
4816
  /**
4776
4817
  * Pure TypeScript MaterialBox - creates an image lightbox that fills the screen when clicked
4777
- * No MaterializeCSS dependencies
4818
+ * Uses CSS classes from _materialbox.scss
4778
4819
  */
4779
4820
  const MaterialBox = () => {
4780
4821
  const state = {
@@ -4790,21 +4831,11 @@ const MaterialBox = () => {
4790
4831
  state.originalImage = img;
4791
4832
  if (attrs.onOpenStart)
4792
4833
  attrs.onOpenStart();
4834
+ const inDuration = attrs.inDuration || 275;
4793
4835
  // Create overlay
4794
4836
  const overlay = document.createElement('div');
4795
4837
  overlay.className = 'materialbox-overlay';
4796
- overlay.style.cssText = `
4797
- position: fixed;
4798
- top: 0;
4799
- left: 0;
4800
- right: 0;
4801
- bottom: 0;
4802
- background-color: rgba(0, 0, 0, 0.85);
4803
- z-index: 1000;
4804
- opacity: 0;
4805
- transition: opacity ${attrs.inDuration || 275}ms ease;
4806
- cursor: zoom-out;
4807
- `;
4838
+ overlay.style.transition = `opacity ${inDuration}ms ease`;
4808
4839
  // Create enlarged image
4809
4840
  const enlargedImg = document.createElement('img');
4810
4841
  enlargedImg.src = img.src;
@@ -4825,36 +4856,18 @@ const MaterialBox = () => {
4825
4856
  finalWidth = maxHeight * aspectRatio;
4826
4857
  }
4827
4858
  // Set initial position and size (same as original image)
4828
- enlargedImg.style.cssText = `
4829
- position: fixed;
4830
- top: ${imgRect.top}px;
4831
- left: ${imgRect.left}px;
4832
- width: ${imgRect.width}px;
4833
- height: ${imgRect.height}px;
4834
- transition: all ${attrs.inDuration || 275}ms ease;
4835
- cursor: zoom-out;
4836
- max-width: none;
4837
- z-index: 1001;
4838
- `;
4859
+ enlargedImg.style.top = `${imgRect.top}px`;
4860
+ enlargedImg.style.left = `${imgRect.left}px`;
4861
+ enlargedImg.style.width = `${imgRect.width}px`;
4862
+ enlargedImg.style.height = `${imgRect.height}px`;
4863
+ enlargedImg.style.transition = `all ${inDuration}ms ease`;
4839
4864
  // Add caption if provided
4840
4865
  let caption = null;
4841
4866
  if (attrs.caption) {
4842
4867
  caption = document.createElement('div');
4843
4868
  caption.className = 'materialbox-caption';
4844
4869
  caption.textContent = attrs.caption;
4845
- caption.style.cssText = `
4846
- position: fixed;
4847
- bottom: 20px;
4848
- left: 50%;
4849
- transform: translateX(-50%);
4850
- color: white;
4851
- font-size: 16px;
4852
- text-align: center;
4853
- opacity: 0;
4854
- transition: opacity ${attrs.inDuration || 275}ms ease ${attrs.inDuration || 275}ms;
4855
- z-index: 1002;
4856
- pointer-events: none;
4857
- `;
4870
+ caption.style.transition = `opacity ${inDuration}ms ease ${inDuration}ms`;
4858
4871
  }
4859
4872
  // Add to DOM
4860
4873
  document.body.appendChild(overlay);
@@ -4889,7 +4902,7 @@ const MaterialBox = () => {
4889
4902
  setTimeout(() => {
4890
4903
  if (attrs.onOpenEnd)
4891
4904
  attrs.onOpenEnd();
4892
- }, attrs.inDuration || 275);
4905
+ }, inDuration);
4893
4906
  };
4894
4907
  const closeBox = (attrs) => {
4895
4908
  if (!state.isOpen || !state.originalImage || !state.overlay || !state.overlayImage)
@@ -4946,8 +4959,14 @@ const MaterialBox = () => {
4946
4959
  },
4947
4960
  view: ({ attrs }) => {
4948
4961
  const { src, alt, width, height, caption, className, style } = attrs, otherAttrs = __rest(attrs, ["src", "alt", "width", "height", "caption", "className", "style"]);
4962
+ // Build style attribute - handle both string and object styles
4963
+ let imgStyle = style || {};
4964
+ if (typeof style !== 'string') {
4965
+ // If style is an object or undefined, add default styles as object
4966
+ imgStyle = Object.assign({ cursor: 'zoom-in', transition: 'opacity 200ms ease' }, (style || {}));
4967
+ }
4949
4968
  return m('img.materialboxed', Object.assign(Object.assign({}, otherAttrs), { src, alt: alt || '', width,
4950
- height, className: ['materialboxed', className].filter(Boolean).join(' ') || undefined, style: Object.assign({ cursor: 'zoom-in', transition: 'opacity 200ms ease' }, style), onclick: (e) => {
4969
+ height, className: ['materialboxed', className].filter(Boolean).join(' ') || undefined, style: imgStyle, onclick: (e) => {
4951
4970
  e.preventDefault();
4952
4971
  openBox(e.target, attrs);
4953
4972
  } }));
@@ -5277,6 +5296,11 @@ const Parallax = () => {
5277
5296
  };
5278
5297
  };
5279
5298
 
5299
+ const defaultI18n$2 = {
5300
+ cancel: 'Cancel',
5301
+ clear: 'Clear',
5302
+ done: 'Ok',
5303
+ };
5280
5304
  const defaultOptions = {
5281
5305
  dialRadius: 135,
5282
5306
  outerRadius: 105,
@@ -5287,11 +5311,7 @@ const defaultOptions = {
5287
5311
  defaultTime: 'now',
5288
5312
  fromNow: 0,
5289
5313
  showClearBtn: false,
5290
- i18n: {
5291
- cancel: 'Cancel',
5292
- clear: 'Clear',
5293
- done: 'Ok',
5294
- },
5314
+ i18n: defaultI18n$2,
5295
5315
  autoClose: false,
5296
5316
  twelveHour: true,
5297
5317
  vibrate: true,
@@ -5441,7 +5461,7 @@ const TimePicker = () => {
5441
5461
  state.hours = hours;
5442
5462
  state.minutes = minutes;
5443
5463
  if (state.spanHours) {
5444
- state.spanHours.innerHTML = state.hours.toString();
5464
+ state.spanHours.innerHTML = addLeadingZero(state.hours);
5445
5465
  }
5446
5466
  if (state.spanMinutes) {
5447
5467
  state.spanMinutes.innerHTML = addLeadingZero(state.minutes);
@@ -5549,7 +5569,7 @@ const TimePicker = () => {
5549
5569
  }
5550
5570
  state[state.currentView] = value;
5551
5571
  if (isHours && state.spanHours) {
5552
- state.spanHours.innerHTML = value.toString();
5572
+ state.spanHours.innerHTML = addLeadingZero(value);
5553
5573
  }
5554
5574
  else if (!isHours && state.spanMinutes) {
5555
5575
  state.spanMinutes.innerHTML = addLeadingZero(value);
@@ -5683,7 +5703,7 @@ const TimePicker = () => {
5683
5703
  const TimepickerModal = () => {
5684
5704
  return {
5685
5705
  view: ({ attrs }) => {
5686
- const { showClearBtn, clearLabel, closeLabel, doneLabel } = attrs;
5706
+ const { i18n, showClearBtn } = attrs;
5687
5707
  return [
5688
5708
  m('.modal-content.timepicker-container', [
5689
5709
  m('.timepicker-digital-display', [
@@ -5695,7 +5715,7 @@ const TimePicker = () => {
5695
5715
  oncreate: (vnode) => {
5696
5716
  state.spanHours = vnode.dom;
5697
5717
  },
5698
- }, state.hours.toString()),
5718
+ }, addLeadingZero(state.hours)),
5699
5719
  ':',
5700
5720
  m('span.timepicker-span-minutes', {
5701
5721
  class: state.currentView === 'minutes' ? 'text-primary' : '',
@@ -5775,18 +5795,18 @@ const TimePicker = () => {
5775
5795
  tabindex: options.twelveHour ? '3' : '1',
5776
5796
  style: showClearBtn ? '' : 'visibility: hidden;',
5777
5797
  onclick: () => clear(),
5778
- }, clearLabel),
5798
+ }, i18n.clear),
5779
5799
  m('.confirmation-btns', [
5780
5800
  m('button.btn-flat.timepicker-close.waves-effect', {
5781
5801
  type: 'button',
5782
5802
  tabindex: options.twelveHour ? '3' : '1',
5783
5803
  onclick: () => close(),
5784
- }, closeLabel),
5804
+ }, i18n.cancel),
5785
5805
  m('button.btn-flat.timepicker-close.waves-effect', {
5786
5806
  type: 'button',
5787
5807
  tabindex: options.twelveHour ? '3' : '1',
5788
5808
  onclick: () => done(),
5789
- }, doneLabel),
5809
+ }, i18n.done),
5790
5810
  ]),
5791
5811
  ]),
5792
5812
  ]),
@@ -5803,7 +5823,6 @@ const TimePicker = () => {
5803
5823
  }
5804
5824
  };
5805
5825
  const renderPickerToPortal = (attrs) => {
5806
- const { showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel' } = attrs;
5807
5826
  const pickerModal = m('.timepicker-modal-wrapper', {
5808
5827
  style: {
5809
5828
  position: 'fixed',
@@ -5846,10 +5865,8 @@ const TimePicker = () => {
5846
5865
  },
5847
5866
  }, [
5848
5867
  m(TimepickerModal, {
5849
- showClearBtn,
5850
- clearLabel,
5851
- closeLabel,
5852
- doneLabel: 'OK',
5868
+ i18n: options.i18n,
5869
+ showClearBtn: options.showClearBtn,
5853
5870
  }),
5854
5871
  ]),
5855
5872
  ]);
@@ -5909,7 +5926,7 @@ const TimePicker = () => {
5909
5926
  }
5910
5927
  },
5911
5928
  view: ({ attrs }) => {
5912
- const { id = state.id, label, placeholder, disabled, readonly, required, iconName, helperText, onchange, oninput, useModal = true, showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel', twelveHour, className: cn1, class: cn2, } = attrs;
5929
+ const { id = state.id, label, placeholder, disabled, readonly, required, iconName, helperText, onchange, oninput, useModal = true, twelveHour, className: cn1, class: cn2, } = attrs;
5913
5930
  const className = cn1 || cn2 || 'col s12';
5914
5931
  // Format time value for display
5915
5932
  const formatTime = (hours, minutes, use12Hour) => {
@@ -6355,7 +6372,7 @@ const Select = () => {
6355
6372
  // Render ungrouped options first
6356
6373
  attrs.options
6357
6374
  .filter((option) => !option.group)
6358
- .map((option) => m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === attrs.options.indexOf(option) ? 'focused' : '' }, (option.disabled
6375
+ .map((option) => m('li', Object.assign({ class: `${option.disabled ? 'disabled' : ''}${isSelected(option.id, selectedIds) ? ' selected' : ''}${state.focusedIndex === attrs.options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
6359
6376
  ? {}
6360
6377
  : {
6361
6378
  onclick: () => {
@@ -6432,7 +6449,7 @@ const Select = () => {
6432
6449
  // Create dropdown with proper positioning
6433
6450
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6434
6451
  tabindex: 0,
6435
- style: getPortalStyles(state.inputRef),
6452
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6436
6453
  oncreate: ({ dom }) => {
6437
6454
  state.dropdownRef = dom;
6438
6455
  },
@@ -6501,7 +6518,8 @@ const Select = () => {
6501
6518
  selectedIds = state.internalSelectedIds;
6502
6519
  }
6503
6520
  const finalClassName = newRow ? `${className} clear` : className;
6504
- const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6521
+ const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
6522
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6505
6523
  // Update portal dropdown when inside modal
6506
6524
  if (state.isInsideModal) {
6507
6525
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -6546,7 +6564,7 @@ const Select = () => {
6546
6564
  onremove: () => {
6547
6565
  state.dropdownRef = null;
6548
6566
  },
6549
- style: getDropdownStyles(state.inputRef, true, options),
6567
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, options)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6550
6568
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder)),
6551
6569
  m(MaterialIcon, {
6552
6570
  name: 'caret',
@@ -6796,7 +6814,7 @@ const SelectedChip = {
6796
6814
  ]),
6797
6815
  };
6798
6816
  const DropdownOption = {
6799
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6817
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
6800
6818
  const checkboxId = `search-select-option-${option.id}`;
6801
6819
  const optionLabel = option.label || option.id.toString();
6802
6820
  return m('li', {
@@ -6813,11 +6831,12 @@ const DropdownOption = {
6813
6831
  }
6814
6832
  },
6815
6833
  }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6816
- m('input', {
6817
- type: 'checkbox',
6818
- id: checkboxId,
6819
- checked: selectedIds.includes(option.id),
6820
- }),
6834
+ showCheckbox &&
6835
+ m('input', {
6836
+ type: 'checkbox',
6837
+ id: checkboxId,
6838
+ checked: selectedIds.includes(option.id),
6839
+ }),
6821
6840
  m('span', optionLabel),
6822
6841
  ]));
6823
6842
  },
@@ -6835,6 +6854,7 @@ const SearchSelect = () => {
6835
6854
  dropdownRef: null,
6836
6855
  focusedIndex: -1,
6837
6856
  internalSelectedIds: [],
6857
+ createdOptions: [],
6838
6858
  };
6839
6859
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
6840
6860
  const componentId = uniqueId();
@@ -6877,7 +6897,10 @@ const SearchSelect = () => {
6877
6897
  // Handle add new option
6878
6898
  return 'addNew';
6879
6899
  }
6880
- else if (state.focusedIndex < filteredOptions.length) ;
6900
+ else if (state.focusedIndex < filteredOptions.length) {
6901
+ // This will be handled in the view method where attrs are available
6902
+ return 'selectOption';
6903
+ }
6881
6904
  }
6882
6905
  break;
6883
6906
  case 'Escape':
@@ -6888,11 +6911,22 @@ const SearchSelect = () => {
6888
6911
  }
6889
6912
  return null;
6890
6913
  };
6914
+ // Create new option and add to state
6915
+ const createAndSelectOption = async (attrs) => {
6916
+ if (!attrs.oncreateNewOption || !state.searchTerm)
6917
+ return;
6918
+ const newOption = await attrs.oncreateNewOption(state.searchTerm);
6919
+ // Store the created option internally
6920
+ state.createdOptions.push(newOption);
6921
+ // Select the new option
6922
+ toggleOption(newOption, attrs);
6923
+ };
6891
6924
  // Toggle option selection
6892
6925
  const toggleOption = (option, attrs) => {
6893
6926
  if (option.disabled)
6894
6927
  return;
6895
6928
  const controlled = isControlled(attrs);
6929
+ const { maxSelectedOptions } = attrs;
6896
6930
  // Get current selected IDs from props or internal state
6897
6931
  const currentSelectedIds = controlled
6898
6932
  ? attrs.checkedId !== undefined
@@ -6901,9 +6935,29 @@ const SearchSelect = () => {
6901
6935
  : [attrs.checkedId]
6902
6936
  : []
6903
6937
  : state.internalSelectedIds;
6904
- const newIds = currentSelectedIds.includes(option.id)
6905
- ? currentSelectedIds.filter((id) => id !== option.id)
6906
- : [...currentSelectedIds, option.id];
6938
+ const isSelected = currentSelectedIds.includes(option.id);
6939
+ let newIds;
6940
+ if (isSelected) {
6941
+ // Remove if already selected
6942
+ newIds = currentSelectedIds.filter((id) => id !== option.id);
6943
+ }
6944
+ else {
6945
+ // Check if we've reached the max selection limit
6946
+ if (maxSelectedOptions && currentSelectedIds.length >= maxSelectedOptions) {
6947
+ // If max=1, replace the selection
6948
+ if (maxSelectedOptions === 1) {
6949
+ newIds = [option.id];
6950
+ }
6951
+ else {
6952
+ // Otherwise, don't add more
6953
+ return;
6954
+ }
6955
+ }
6956
+ else {
6957
+ // Add to selection
6958
+ newIds = [...currentSelectedIds, option.id];
6959
+ }
6960
+ }
6907
6961
  // Update internal state for uncontrolled mode
6908
6962
  if (!controlled) {
6909
6963
  state.internalSelectedIds = newIds;
@@ -6965,21 +7019,32 @@ const SearchSelect = () => {
6965
7019
  : [attrs.checkedId]
6966
7020
  : []
6967
7021
  : state.internalSelectedIds;
6968
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
7022
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
6969
7023
  // Use i18n values if provided, otherwise use defaults
6970
7024
  const texts = {
6971
7025
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6972
7026
  addNewPrefix: i18n.addNewPrefix || '+',
7027
+ showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7028
+ maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
6973
7029
  };
7030
+ // Check if max selections is reached
7031
+ const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7032
+ // Merge provided options with internally created options
7033
+ const allOptions = [...options, ...state.createdOptions];
6974
7034
  // Get selected options for display
6975
- const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
7035
+ const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7036
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6976
7037
  // Safely filter options
6977
- const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
7038
+ const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6978
7039
  !selectedIds.includes(option.id));
7040
+ // Apply display limit if configured
7041
+ const totalFilteredCount = filteredOptions.length;
7042
+ const displayedOptions = maxDisplayedOptions ? filteredOptions.slice(0, maxDisplayedOptions) : filteredOptions;
7043
+ const isTruncated = maxDisplayedOptions && totalFilteredCount > maxDisplayedOptions;
6979
7044
  // Check if we should show the "add new option" element
6980
7045
  const showAddNew = oncreateNewOption &&
6981
7046
  state.searchTerm &&
6982
- !filteredOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
7047
+ !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
6983
7048
  // Render the dropdown
6984
7049
  return m('.input-field.multi-select-dropdown', { className }, [
6985
7050
  m('.chips.chips-initial.chips-container', {
@@ -7052,7 +7117,7 @@ const SearchSelect = () => {
7052
7117
  onremove: () => {
7053
7118
  state.dropdownRef = null;
7054
7119
  },
7055
- style: getDropdownStyles(state.inputRef),
7120
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
7056
7121
  }, [
7057
7122
  m('li', // Search Input
7058
7123
  {
@@ -7072,41 +7137,65 @@ const SearchSelect = () => {
7072
7137
  state.focusedIndex = -1; // Reset focus when typing
7073
7138
  },
7074
7139
  onkeydown: async (e) => {
7075
- const result = handleKeyDown(e, filteredOptions, !!showAddNew);
7140
+ const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7076
7141
  if (result === 'addNew' && oncreateNewOption) {
7077
- const option = await oncreateNewOption(state.searchTerm);
7078
- toggleOption(option, attrs);
7142
+ await createAndSelectOption(attrs);
7079
7143
  }
7080
- else if (e.key === 'Enter' &&
7081
- state.focusedIndex >= 0 &&
7082
- state.focusedIndex < filteredOptions.length) {
7083
- toggleOption(filteredOptions[state.focusedIndex], attrs);
7144
+ else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
7145
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
7084
7146
  }
7085
7147
  },
7086
7148
  class: 'search-select-input',
7087
7149
  }),
7088
7150
  ]),
7089
7151
  // No options found message or list of options
7090
- ...(filteredOptions.length === 0 && !showAddNew
7152
+ ...(displayedOptions.length === 0 && !showAddNew
7091
7153
  ? [m('li.search-select-no-options', texts.noOptionsFound)]
7092
7154
  : []),
7155
+ // Truncation message
7156
+ ...(isTruncated
7157
+ ? [
7158
+ m('li.search-select-truncation-info', {
7159
+ style: {
7160
+ fontStyle: 'italic',
7161
+ color: 'var(--mm-text-hint, #9e9e9e)',
7162
+ padding: '8px 16px',
7163
+ cursor: 'default',
7164
+ },
7165
+ }, texts.showingXofY
7166
+ .replace('{shown}', displayedOptions.length.toString())
7167
+ .replace('{total}', totalFilteredCount.toString())),
7168
+ ]
7169
+ : []),
7170
+ // Max selections reached message
7171
+ ...(isMaxSelectionsReached
7172
+ ? [
7173
+ m('li.search-select-max-info', {
7174
+ style: {
7175
+ fontStyle: 'italic',
7176
+ color: 'var(--mm-text-hint, #9e9e9e)',
7177
+ padding: '8px 16px',
7178
+ cursor: 'default',
7179
+ },
7180
+ }, texts.maxSelectionsReached.replace('{max}', maxSelectedOptions.toString())),
7181
+ ]
7182
+ : []),
7093
7183
  // Add new option item
7094
7184
  ...(showAddNew
7095
7185
  ? [
7096
7186
  m('li', {
7097
7187
  onclick: async () => {
7098
- const option = await oncreateNewOption(state.searchTerm);
7099
- toggleOption(option, attrs);
7188
+ await createAndSelectOption(attrs);
7100
7189
  },
7101
- class: state.focusedIndex === filteredOptions.length ? 'active' : '',
7190
+ class: state.focusedIndex === displayedOptions.length ? 'active' : '',
7102
7191
  onmouseover: () => {
7103
- state.focusedIndex = filteredOptions.length;
7192
+ state.focusedIndex = displayedOptions.length;
7104
7193
  },
7105
7194
  }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
7106
7195
  ]
7107
7196
  : []),
7108
7197
  // List of filtered options
7109
- ...filteredOptions.map((option, index) => m(DropdownOption, {
7198
+ ...displayedOptions.map((option, index) => m(DropdownOption, {
7110
7199
  // key: option.id,
7111
7200
  option,
7112
7201
  index,
@@ -7116,6 +7205,7 @@ const SearchSelect = () => {
7116
7205
  onMouseOver: (idx) => {
7117
7206
  state.focusedIndex = idx;
7118
7207
  },
7208
+ showCheckbox: maxSelectedOptions !== 1,
7119
7209
  })),
7120
7210
  ]),
7121
7211
  ]);
@@ -9815,6 +9905,7 @@ exports.padLeft = padLeft;
9815
9905
  exports.range = range;
9816
9906
  exports.releasePortalContainer = releasePortalContainer;
9817
9907
  exports.renderToPortal = renderToPortal;
9908
+ exports.sortOptions = sortOptions;
9818
9909
  exports.toast = toast;
9819
9910
  exports.uniqueId = uniqueId;
9820
9911
  exports.uuid4 = uuid4;