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.esm.js CHANGED
@@ -62,6 +62,28 @@ const uuid4 = () => {
62
62
  };
63
63
  /** Check if a string or number is numeric. @see https://stackoverflow.com/a/9716488/319711 */
64
64
  const isNumeric = (n) => !isNaN(parseFloat(n)) && isFinite(n);
65
+ /**
66
+ * Sort options array based on sorting configuration
67
+ * @param options - Array of options to sort
68
+ * @param sortConfig - Sort configuration: 'asc', 'desc', 'none', or custom comparator function
69
+ * @returns Sorted array (or original if 'none' or undefined)
70
+ */
71
+ const sortOptions = (options, sortConfig) => {
72
+ if (!sortConfig || sortConfig === 'none') {
73
+ return options;
74
+ }
75
+ const sorted = [...options]; // Create a copy to avoid mutating original
76
+ if (typeof sortConfig === 'function') {
77
+ return sorted.sort(sortConfig);
78
+ }
79
+ // Sort by label, fallback to id if no label
80
+ return sorted.sort((a, b) => {
81
+ const aLabel = (a.label || a.id.toString()).toLowerCase();
82
+ const bLabel = (b.label || b.id.toString()).toLowerCase();
83
+ const comparison = aLabel.localeCompare(bLabel);
84
+ return sortConfig === 'asc' ? comparison : -comparison;
85
+ });
86
+ };
65
87
  /**
66
88
  * Pad left, default width 2 with a '0'
67
89
  *
@@ -1409,19 +1431,18 @@ const CollapsibleItem = () => {
1409
1431
  onclick: onToggle,
1410
1432
  }, [
1411
1433
  iconName ? m('i.material-icons', iconName) : undefined,
1412
- header ? (typeof header === 'string' ? m('span', header) : header) : undefined,
1434
+ header
1435
+ ? typeof header === 'string'
1436
+ ? m('span.collapsible-header-text', header)
1437
+ : m('.collapsible-header-content', header)
1438
+ : undefined,
1413
1439
  ])
1414
1440
  : undefined,
1415
1441
  m('.collapsible-body', {
1416
1442
  style: {
1417
1443
  display: isActive ? 'block' : 'none',
1418
- transition: 'display 0.3s ease',
1419
1444
  },
1420
- }, [
1421
- m('.collapsible-body-content', {
1422
- style: { padding: '2rem' },
1423
- }, body ? (typeof body === 'string' ? m('div', { innerHTML: body }) : body) : undefined),
1424
- ]),
1445
+ }, body ? (typeof body === 'string' ? m('div', { innerHTML: body }) : body) : undefined),
1425
1446
  ]);
1426
1447
  },
1427
1448
  };
@@ -1444,7 +1465,7 @@ const Collapsible = () => {
1444
1465
  });
1445
1466
  },
1446
1467
  view: ({ attrs }) => {
1447
- const { items, accordion = true, class: c, className, style, id } = attrs;
1468
+ const { items, header, accordion = true, class: c, className, style, id } = attrs;
1448
1469
  const toggleItem = (index) => {
1449
1470
  if (accordion) {
1450
1471
  // Accordion mode: only one item can be active
@@ -1466,12 +1487,22 @@ const Collapsible = () => {
1466
1487
  }
1467
1488
  }
1468
1489
  };
1490
+ const collapsibleItems = items.map((item, index) => m(CollapsibleItem, Object.assign(Object.assign({}, item), { key: index, isActive: state.activeItems.has(index), onToggle: () => toggleItem(index) })));
1469
1491
  return items && items.length > 0
1470
- ? m('ul.collapsible', {
1471
- class: c || className,
1472
- style: Object.assign({ border: '1px solid #ddd', borderRadius: '2px', margin: '0.5rem 0 1rem 0' }, style),
1473
- id,
1474
- }, items.map((item, index) => m(CollapsibleItem, Object.assign(Object.assign({}, item), { key: index, isActive: state.activeItems.has(index), onToggle: () => toggleItem(index) }))))
1492
+ ? header
1493
+ ? m('ul.collapsible.with-header', {
1494
+ class: c || className,
1495
+ style,
1496
+ id,
1497
+ }, [
1498
+ m('li.collapsible-main-header', m('h4', typeof header === 'string' ? header : header)),
1499
+ collapsibleItems,
1500
+ ])
1501
+ : m('ul.collapsible', {
1502
+ class: c || className,
1503
+ style,
1504
+ id,
1505
+ }, collapsibleItems)
1475
1506
  : undefined;
1476
1507
  },
1477
1508
  };
@@ -1575,7 +1606,7 @@ const Collection = () => {
1575
1606
  };
1576
1607
  };
1577
1608
 
1578
- const defaultI18n$2 = {
1609
+ const defaultI18n$3 = {
1579
1610
  cancel: 'Cancel',
1580
1611
  clear: 'Clear',
1581
1612
  done: 'Ok',
@@ -1683,9 +1714,9 @@ const DatePicker = () => {
1683
1714
  else if (attrs.displayFormat) {
1684
1715
  finalFormat = attrs.displayFormat;
1685
1716
  }
1686
- 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);
1717
+ 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);
1687
1718
  // Merge i18n properly
1688
- merged.i18n = Object.assign(Object.assign({}, defaultI18n$2), attrs.i18n);
1719
+ merged.i18n = Object.assign(Object.assign({}, defaultI18n$3), attrs.i18n);
1689
1720
  return merged;
1690
1721
  };
1691
1722
  const toString = (date, format) => {
@@ -2136,11 +2167,11 @@ const DatePicker = () => {
2136
2167
  prevMonth();
2137
2168
  },
2138
2169
  }, m('svg', {
2139
- fill: '#000000',
2140
2170
  height: '24',
2141
2171
  viewBox: '0 0 24 24',
2142
2172
  width: '24',
2143
2173
  xmlns: 'http://www.w3.org/2000/svg',
2174
+ style: 'fill: var(--mm-text-primary, rgba(0, 0, 0, 0.87));',
2144
2175
  }, [
2145
2176
  m('path', { d: 'M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z' }),
2146
2177
  m('path', { d: 'M0-.5h24v24H0z', fill: 'none' }),
@@ -2202,11 +2233,11 @@ const DatePicker = () => {
2202
2233
  nextMonth();
2203
2234
  },
2204
2235
  }, m('svg', {
2205
- fill: '#000000',
2206
2236
  height: '24',
2207
2237
  viewBox: '0 0 24 24',
2208
2238
  width: '24',
2209
2239
  xmlns: 'http://www.w3.org/2000/svg',
2240
+ style: 'fill: var(--mm-text-primary, rgba(0, 0, 0, 0.87));',
2210
2241
  }, [
2211
2242
  m('path', { d: 'M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z' }),
2212
2243
  m('path', { d: 'M0-.25h24v24H0z', fill: 'none' }),
@@ -3219,19 +3250,29 @@ const TextArea = () => {
3219
3250
  if (!controlled && attrs.defaultValue !== undefined) {
3220
3251
  textarea.value = String(attrs.defaultValue);
3221
3252
  }
3222
- // Height will be calculated by hidden div
3223
3253
  // Update character count state for counter component
3224
3254
  if (maxLength) {
3225
3255
  state.currentLength = textarea.value.length;
3226
3256
  }
3257
+ // Calculate initial height after DOM is fully ready
3258
+ // Use requestAnimationFrame to ensure layout is complete
3259
+ if (state.hiddenDiv) {
3260
+ requestAnimationFrame(() => {
3261
+ if (state.hiddenDiv) {
3262
+ updateHeight(textarea, state.hiddenDiv);
3263
+ m.redraw();
3264
+ }
3265
+ });
3266
+ }
3227
3267
  }, onupdate: ({ dom }) => {
3228
3268
  const textarea = dom;
3229
- if (state.height)
3230
- textarea.style.height = state.height;
3231
- // Trigger height recalculation when value changes programmatically
3269
+ // Recalculate and apply height
3232
3270
  if (state.hiddenDiv) {
3233
3271
  updateHeight(textarea, state.hiddenDiv);
3234
3272
  }
3273
+ if (state.height) {
3274
+ textarea.style.height = state.height;
3275
+ }
3235
3276
  }, onfocus: () => {
3236
3277
  state.active = true;
3237
3278
  }, oninput: (e) => {
@@ -4520,7 +4561,7 @@ const Dropdown = () => {
4520
4561
  // Create dropdown with proper positioning
4521
4562
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4522
4563
  tabindex: 0,
4523
- style: getPortalStyles(state.inputRef),
4564
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4524
4565
  oncreate: ({ dom }) => {
4525
4566
  state.dropdownRef = dom;
4526
4567
  },
@@ -4632,7 +4673,7 @@ const Dropdown = () => {
4632
4673
  onremove: () => {
4633
4674
  state.dropdownRef = null;
4634
4675
  },
4635
- style: getDropdownStyles(state.inputRef, true, items, true),
4676
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, items, true)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4636
4677
  }, items.map((item) => {
4637
4678
  if (item.divider) {
4638
4679
  return m('li.divider');
@@ -4772,7 +4813,7 @@ const FloatingActionButton = () => {
4772
4813
 
4773
4814
  /**
4774
4815
  * Pure TypeScript MaterialBox - creates an image lightbox that fills the screen when clicked
4775
- * No MaterializeCSS dependencies
4816
+ * Uses CSS classes from _materialbox.scss
4776
4817
  */
4777
4818
  const MaterialBox = () => {
4778
4819
  const state = {
@@ -4788,21 +4829,11 @@ const MaterialBox = () => {
4788
4829
  state.originalImage = img;
4789
4830
  if (attrs.onOpenStart)
4790
4831
  attrs.onOpenStart();
4832
+ const inDuration = attrs.inDuration || 275;
4791
4833
  // Create overlay
4792
4834
  const overlay = document.createElement('div');
4793
4835
  overlay.className = 'materialbox-overlay';
4794
- overlay.style.cssText = `
4795
- position: fixed;
4796
- top: 0;
4797
- left: 0;
4798
- right: 0;
4799
- bottom: 0;
4800
- background-color: rgba(0, 0, 0, 0.85);
4801
- z-index: 1000;
4802
- opacity: 0;
4803
- transition: opacity ${attrs.inDuration || 275}ms ease;
4804
- cursor: zoom-out;
4805
- `;
4836
+ overlay.style.transition = `opacity ${inDuration}ms ease`;
4806
4837
  // Create enlarged image
4807
4838
  const enlargedImg = document.createElement('img');
4808
4839
  enlargedImg.src = img.src;
@@ -4823,36 +4854,18 @@ const MaterialBox = () => {
4823
4854
  finalWidth = maxHeight * aspectRatio;
4824
4855
  }
4825
4856
  // Set initial position and size (same as original image)
4826
- enlargedImg.style.cssText = `
4827
- position: fixed;
4828
- top: ${imgRect.top}px;
4829
- left: ${imgRect.left}px;
4830
- width: ${imgRect.width}px;
4831
- height: ${imgRect.height}px;
4832
- transition: all ${attrs.inDuration || 275}ms ease;
4833
- cursor: zoom-out;
4834
- max-width: none;
4835
- z-index: 1001;
4836
- `;
4857
+ enlargedImg.style.top = `${imgRect.top}px`;
4858
+ enlargedImg.style.left = `${imgRect.left}px`;
4859
+ enlargedImg.style.width = `${imgRect.width}px`;
4860
+ enlargedImg.style.height = `${imgRect.height}px`;
4861
+ enlargedImg.style.transition = `all ${inDuration}ms ease`;
4837
4862
  // Add caption if provided
4838
4863
  let caption = null;
4839
4864
  if (attrs.caption) {
4840
4865
  caption = document.createElement('div');
4841
4866
  caption.className = 'materialbox-caption';
4842
4867
  caption.textContent = attrs.caption;
4843
- caption.style.cssText = `
4844
- position: fixed;
4845
- bottom: 20px;
4846
- left: 50%;
4847
- transform: translateX(-50%);
4848
- color: white;
4849
- font-size: 16px;
4850
- text-align: center;
4851
- opacity: 0;
4852
- transition: opacity ${attrs.inDuration || 275}ms ease ${attrs.inDuration || 275}ms;
4853
- z-index: 1002;
4854
- pointer-events: none;
4855
- `;
4868
+ caption.style.transition = `opacity ${inDuration}ms ease ${inDuration}ms`;
4856
4869
  }
4857
4870
  // Add to DOM
4858
4871
  document.body.appendChild(overlay);
@@ -4887,7 +4900,7 @@ const MaterialBox = () => {
4887
4900
  setTimeout(() => {
4888
4901
  if (attrs.onOpenEnd)
4889
4902
  attrs.onOpenEnd();
4890
- }, attrs.inDuration || 275);
4903
+ }, inDuration);
4891
4904
  };
4892
4905
  const closeBox = (attrs) => {
4893
4906
  if (!state.isOpen || !state.originalImage || !state.overlay || !state.overlayImage)
@@ -4944,8 +4957,14 @@ const MaterialBox = () => {
4944
4957
  },
4945
4958
  view: ({ attrs }) => {
4946
4959
  const { src, alt, width, height, caption, className, style } = attrs, otherAttrs = __rest(attrs, ["src", "alt", "width", "height", "caption", "className", "style"]);
4960
+ // Build style attribute - handle both string and object styles
4961
+ let imgStyle = style || {};
4962
+ if (typeof style !== 'string') {
4963
+ // If style is an object or undefined, add default styles as object
4964
+ imgStyle = Object.assign({ cursor: 'zoom-in', transition: 'opacity 200ms ease' }, (style || {}));
4965
+ }
4947
4966
  return m('img.materialboxed', Object.assign(Object.assign({}, otherAttrs), { src, alt: alt || '', width,
4948
- height, className: ['materialboxed', className].filter(Boolean).join(' ') || undefined, style: Object.assign({ cursor: 'zoom-in', transition: 'opacity 200ms ease' }, style), onclick: (e) => {
4967
+ height, className: ['materialboxed', className].filter(Boolean).join(' ') || undefined, style: imgStyle, onclick: (e) => {
4949
4968
  e.preventDefault();
4950
4969
  openBox(e.target, attrs);
4951
4970
  } }));
@@ -5275,6 +5294,11 @@ const Parallax = () => {
5275
5294
  };
5276
5295
  };
5277
5296
 
5297
+ const defaultI18n$2 = {
5298
+ cancel: 'Cancel',
5299
+ clear: 'Clear',
5300
+ done: 'Ok',
5301
+ };
5278
5302
  const defaultOptions = {
5279
5303
  dialRadius: 135,
5280
5304
  outerRadius: 105,
@@ -5285,11 +5309,7 @@ const defaultOptions = {
5285
5309
  defaultTime: 'now',
5286
5310
  fromNow: 0,
5287
5311
  showClearBtn: false,
5288
- i18n: {
5289
- cancel: 'Cancel',
5290
- clear: 'Clear',
5291
- done: 'Ok',
5292
- },
5312
+ i18n: defaultI18n$2,
5293
5313
  autoClose: false,
5294
5314
  twelveHour: true,
5295
5315
  vibrate: true,
@@ -5439,7 +5459,7 @@ const TimePicker = () => {
5439
5459
  state.hours = hours;
5440
5460
  state.minutes = minutes;
5441
5461
  if (state.spanHours) {
5442
- state.spanHours.innerHTML = state.hours.toString();
5462
+ state.spanHours.innerHTML = addLeadingZero(state.hours);
5443
5463
  }
5444
5464
  if (state.spanMinutes) {
5445
5465
  state.spanMinutes.innerHTML = addLeadingZero(state.minutes);
@@ -5547,7 +5567,7 @@ const TimePicker = () => {
5547
5567
  }
5548
5568
  state[state.currentView] = value;
5549
5569
  if (isHours && state.spanHours) {
5550
- state.spanHours.innerHTML = value.toString();
5570
+ state.spanHours.innerHTML = addLeadingZero(value);
5551
5571
  }
5552
5572
  else if (!isHours && state.spanMinutes) {
5553
5573
  state.spanMinutes.innerHTML = addLeadingZero(value);
@@ -5681,7 +5701,7 @@ const TimePicker = () => {
5681
5701
  const TimepickerModal = () => {
5682
5702
  return {
5683
5703
  view: ({ attrs }) => {
5684
- const { showClearBtn, clearLabel, closeLabel, doneLabel } = attrs;
5704
+ const { i18n, showClearBtn } = attrs;
5685
5705
  return [
5686
5706
  m('.modal-content.timepicker-container', [
5687
5707
  m('.timepicker-digital-display', [
@@ -5693,7 +5713,7 @@ const TimePicker = () => {
5693
5713
  oncreate: (vnode) => {
5694
5714
  state.spanHours = vnode.dom;
5695
5715
  },
5696
- }, state.hours.toString()),
5716
+ }, addLeadingZero(state.hours)),
5697
5717
  ':',
5698
5718
  m('span.timepicker-span-minutes', {
5699
5719
  class: state.currentView === 'minutes' ? 'text-primary' : '',
@@ -5773,18 +5793,18 @@ const TimePicker = () => {
5773
5793
  tabindex: options.twelveHour ? '3' : '1',
5774
5794
  style: showClearBtn ? '' : 'visibility: hidden;',
5775
5795
  onclick: () => clear(),
5776
- }, clearLabel),
5796
+ }, i18n.clear),
5777
5797
  m('.confirmation-btns', [
5778
5798
  m('button.btn-flat.timepicker-close.waves-effect', {
5779
5799
  type: 'button',
5780
5800
  tabindex: options.twelveHour ? '3' : '1',
5781
5801
  onclick: () => close(),
5782
- }, closeLabel),
5802
+ }, i18n.cancel),
5783
5803
  m('button.btn-flat.timepicker-close.waves-effect', {
5784
5804
  type: 'button',
5785
5805
  tabindex: options.twelveHour ? '3' : '1',
5786
5806
  onclick: () => done(),
5787
- }, doneLabel),
5807
+ }, i18n.done),
5788
5808
  ]),
5789
5809
  ]),
5790
5810
  ]),
@@ -5801,7 +5821,6 @@ const TimePicker = () => {
5801
5821
  }
5802
5822
  };
5803
5823
  const renderPickerToPortal = (attrs) => {
5804
- const { showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel' } = attrs;
5805
5824
  const pickerModal = m('.timepicker-modal-wrapper', {
5806
5825
  style: {
5807
5826
  position: 'fixed',
@@ -5844,10 +5863,8 @@ const TimePicker = () => {
5844
5863
  },
5845
5864
  }, [
5846
5865
  m(TimepickerModal, {
5847
- showClearBtn,
5848
- clearLabel,
5849
- closeLabel,
5850
- doneLabel: 'OK',
5866
+ i18n: options.i18n,
5867
+ showClearBtn: options.showClearBtn,
5851
5868
  }),
5852
5869
  ]),
5853
5870
  ]);
@@ -5907,7 +5924,7 @@ const TimePicker = () => {
5907
5924
  }
5908
5925
  },
5909
5926
  view: ({ attrs }) => {
5910
- 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;
5927
+ const { id = state.id, label, placeholder, disabled, readonly, required, iconName, helperText, onchange, oninput, useModal = true, twelveHour, className: cn1, class: cn2, } = attrs;
5911
5928
  const className = cn1 || cn2 || 'col s12';
5912
5929
  // Format time value for display
5913
5930
  const formatTime = (hours, minutes, use12Hour) => {
@@ -6353,7 +6370,7 @@ const Select = () => {
6353
6370
  // Render ungrouped options first
6354
6371
  attrs.options
6355
6372
  .filter((option) => !option.group)
6356
- .map((option) => m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === attrs.options.indexOf(option) ? 'focused' : '' }, (option.disabled
6373
+ .map((option) => m('li', Object.assign({ class: `${option.disabled ? 'disabled' : ''}${isSelected(option.id, selectedIds) ? ' selected' : ''}${state.focusedIndex === attrs.options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
6357
6374
  ? {}
6358
6375
  : {
6359
6376
  onclick: () => {
@@ -6430,7 +6447,7 @@ const Select = () => {
6430
6447
  // Create dropdown with proper positioning
6431
6448
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6432
6449
  tabindex: 0,
6433
- style: getPortalStyles(state.inputRef),
6450
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6434
6451
  oncreate: ({ dom }) => {
6435
6452
  state.dropdownRef = dom;
6436
6453
  },
@@ -6499,7 +6516,8 @@ const Select = () => {
6499
6516
  selectedIds = state.internalSelectedIds;
6500
6517
  }
6501
6518
  const finalClassName = newRow ? `${className} clear` : className;
6502
- const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6519
+ const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
6520
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6503
6521
  // Update portal dropdown when inside modal
6504
6522
  if (state.isInsideModal) {
6505
6523
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -6544,7 +6562,7 @@ const Select = () => {
6544
6562
  onremove: () => {
6545
6563
  state.dropdownRef = null;
6546
6564
  },
6547
- style: getDropdownStyles(state.inputRef, true, options),
6565
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, options)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6548
6566
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder)),
6549
6567
  m(MaterialIcon, {
6550
6568
  name: 'caret',
@@ -6794,7 +6812,7 @@ const SelectedChip = {
6794
6812
  ]),
6795
6813
  };
6796
6814
  const DropdownOption = {
6797
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6815
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
6798
6816
  const checkboxId = `search-select-option-${option.id}`;
6799
6817
  const optionLabel = option.label || option.id.toString();
6800
6818
  return m('li', {
@@ -6811,11 +6829,12 @@ const DropdownOption = {
6811
6829
  }
6812
6830
  },
6813
6831
  }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6814
- m('input', {
6815
- type: 'checkbox',
6816
- id: checkboxId,
6817
- checked: selectedIds.includes(option.id),
6818
- }),
6832
+ showCheckbox &&
6833
+ m('input', {
6834
+ type: 'checkbox',
6835
+ id: checkboxId,
6836
+ checked: selectedIds.includes(option.id),
6837
+ }),
6819
6838
  m('span', optionLabel),
6820
6839
  ]));
6821
6840
  },
@@ -6833,6 +6852,7 @@ const SearchSelect = () => {
6833
6852
  dropdownRef: null,
6834
6853
  focusedIndex: -1,
6835
6854
  internalSelectedIds: [],
6855
+ createdOptions: [],
6836
6856
  };
6837
6857
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
6838
6858
  const componentId = uniqueId();
@@ -6875,7 +6895,10 @@ const SearchSelect = () => {
6875
6895
  // Handle add new option
6876
6896
  return 'addNew';
6877
6897
  }
6878
- else if (state.focusedIndex < filteredOptions.length) ;
6898
+ else if (state.focusedIndex < filteredOptions.length) {
6899
+ // This will be handled in the view method where attrs are available
6900
+ return 'selectOption';
6901
+ }
6879
6902
  }
6880
6903
  break;
6881
6904
  case 'Escape':
@@ -6886,11 +6909,22 @@ const SearchSelect = () => {
6886
6909
  }
6887
6910
  return null;
6888
6911
  };
6912
+ // Create new option and add to state
6913
+ const createAndSelectOption = async (attrs) => {
6914
+ if (!attrs.oncreateNewOption || !state.searchTerm)
6915
+ return;
6916
+ const newOption = await attrs.oncreateNewOption(state.searchTerm);
6917
+ // Store the created option internally
6918
+ state.createdOptions.push(newOption);
6919
+ // Select the new option
6920
+ toggleOption(newOption, attrs);
6921
+ };
6889
6922
  // Toggle option selection
6890
6923
  const toggleOption = (option, attrs) => {
6891
6924
  if (option.disabled)
6892
6925
  return;
6893
6926
  const controlled = isControlled(attrs);
6927
+ const { maxSelectedOptions } = attrs;
6894
6928
  // Get current selected IDs from props or internal state
6895
6929
  const currentSelectedIds = controlled
6896
6930
  ? attrs.checkedId !== undefined
@@ -6899,9 +6933,29 @@ const SearchSelect = () => {
6899
6933
  : [attrs.checkedId]
6900
6934
  : []
6901
6935
  : state.internalSelectedIds;
6902
- const newIds = currentSelectedIds.includes(option.id)
6903
- ? currentSelectedIds.filter((id) => id !== option.id)
6904
- : [...currentSelectedIds, option.id];
6936
+ const isSelected = currentSelectedIds.includes(option.id);
6937
+ let newIds;
6938
+ if (isSelected) {
6939
+ // Remove if already selected
6940
+ newIds = currentSelectedIds.filter((id) => id !== option.id);
6941
+ }
6942
+ else {
6943
+ // Check if we've reached the max selection limit
6944
+ if (maxSelectedOptions && currentSelectedIds.length >= maxSelectedOptions) {
6945
+ // If max=1, replace the selection
6946
+ if (maxSelectedOptions === 1) {
6947
+ newIds = [option.id];
6948
+ }
6949
+ else {
6950
+ // Otherwise, don't add more
6951
+ return;
6952
+ }
6953
+ }
6954
+ else {
6955
+ // Add to selection
6956
+ newIds = [...currentSelectedIds, option.id];
6957
+ }
6958
+ }
6905
6959
  // Update internal state for uncontrolled mode
6906
6960
  if (!controlled) {
6907
6961
  state.internalSelectedIds = newIds;
@@ -6963,21 +7017,32 @@ const SearchSelect = () => {
6963
7017
  : [attrs.checkedId]
6964
7018
  : []
6965
7019
  : state.internalSelectedIds;
6966
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
7020
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
6967
7021
  // Use i18n values if provided, otherwise use defaults
6968
7022
  const texts = {
6969
7023
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6970
7024
  addNewPrefix: i18n.addNewPrefix || '+',
7025
+ showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7026
+ maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
6971
7027
  };
7028
+ // Check if max selections is reached
7029
+ const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7030
+ // Merge provided options with internally created options
7031
+ const allOptions = [...options, ...state.createdOptions];
6972
7032
  // Get selected options for display
6973
- const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
7033
+ const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7034
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6974
7035
  // Safely filter options
6975
- const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
7036
+ const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6976
7037
  !selectedIds.includes(option.id));
7038
+ // Apply display limit if configured
7039
+ const totalFilteredCount = filteredOptions.length;
7040
+ const displayedOptions = maxDisplayedOptions ? filteredOptions.slice(0, maxDisplayedOptions) : filteredOptions;
7041
+ const isTruncated = maxDisplayedOptions && totalFilteredCount > maxDisplayedOptions;
6977
7042
  // Check if we should show the "add new option" element
6978
7043
  const showAddNew = oncreateNewOption &&
6979
7044
  state.searchTerm &&
6980
- !filteredOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
7045
+ !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
6981
7046
  // Render the dropdown
6982
7047
  return m('.input-field.multi-select-dropdown', { className }, [
6983
7048
  m('.chips.chips-initial.chips-container', {
@@ -7050,7 +7115,7 @@ const SearchSelect = () => {
7050
7115
  onremove: () => {
7051
7116
  state.dropdownRef = null;
7052
7117
  },
7053
- style: getDropdownStyles(state.inputRef),
7118
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
7054
7119
  }, [
7055
7120
  m('li', // Search Input
7056
7121
  {
@@ -7070,41 +7135,65 @@ const SearchSelect = () => {
7070
7135
  state.focusedIndex = -1; // Reset focus when typing
7071
7136
  },
7072
7137
  onkeydown: async (e) => {
7073
- const result = handleKeyDown(e, filteredOptions, !!showAddNew);
7138
+ const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7074
7139
  if (result === 'addNew' && oncreateNewOption) {
7075
- const option = await oncreateNewOption(state.searchTerm);
7076
- toggleOption(option, attrs);
7140
+ await createAndSelectOption(attrs);
7077
7141
  }
7078
- else if (e.key === 'Enter' &&
7079
- state.focusedIndex >= 0 &&
7080
- state.focusedIndex < filteredOptions.length) {
7081
- toggleOption(filteredOptions[state.focusedIndex], attrs);
7142
+ else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
7143
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
7082
7144
  }
7083
7145
  },
7084
7146
  class: 'search-select-input',
7085
7147
  }),
7086
7148
  ]),
7087
7149
  // No options found message or list of options
7088
- ...(filteredOptions.length === 0 && !showAddNew
7150
+ ...(displayedOptions.length === 0 && !showAddNew
7089
7151
  ? [m('li.search-select-no-options', texts.noOptionsFound)]
7090
7152
  : []),
7153
+ // Truncation message
7154
+ ...(isTruncated
7155
+ ? [
7156
+ m('li.search-select-truncation-info', {
7157
+ style: {
7158
+ fontStyle: 'italic',
7159
+ color: 'var(--mm-text-hint, #9e9e9e)',
7160
+ padding: '8px 16px',
7161
+ cursor: 'default',
7162
+ },
7163
+ }, texts.showingXofY
7164
+ .replace('{shown}', displayedOptions.length.toString())
7165
+ .replace('{total}', totalFilteredCount.toString())),
7166
+ ]
7167
+ : []),
7168
+ // Max selections reached message
7169
+ ...(isMaxSelectionsReached
7170
+ ? [
7171
+ m('li.search-select-max-info', {
7172
+ style: {
7173
+ fontStyle: 'italic',
7174
+ color: 'var(--mm-text-hint, #9e9e9e)',
7175
+ padding: '8px 16px',
7176
+ cursor: 'default',
7177
+ },
7178
+ }, texts.maxSelectionsReached.replace('{max}', maxSelectedOptions.toString())),
7179
+ ]
7180
+ : []),
7091
7181
  // Add new option item
7092
7182
  ...(showAddNew
7093
7183
  ? [
7094
7184
  m('li', {
7095
7185
  onclick: async () => {
7096
- const option = await oncreateNewOption(state.searchTerm);
7097
- toggleOption(option, attrs);
7186
+ await createAndSelectOption(attrs);
7098
7187
  },
7099
- class: state.focusedIndex === filteredOptions.length ? 'active' : '',
7188
+ class: state.focusedIndex === displayedOptions.length ? 'active' : '',
7100
7189
  onmouseover: () => {
7101
- state.focusedIndex = filteredOptions.length;
7190
+ state.focusedIndex = displayedOptions.length;
7102
7191
  },
7103
7192
  }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
7104
7193
  ]
7105
7194
  : []),
7106
7195
  // List of filtered options
7107
- ...filteredOptions.map((option, index) => m(DropdownOption, {
7196
+ ...displayedOptions.map((option, index) => m(DropdownOption, {
7108
7197
  // key: option.id,
7109
7198
  option,
7110
7199
  index,
@@ -7114,6 +7203,7 @@ const SearchSelect = () => {
7114
7203
  onMouseOver: (idx) => {
7115
7204
  state.focusedIndex = idx;
7116
7205
  },
7206
+ showCheckbox: maxSelectedOptions !== 1,
7117
7207
  })),
7118
7208
  ]),
7119
7209
  ]);
@@ -9724,4 +9814,4 @@ const isValidationError = (result) => !isValidationSuccess(result);
9724
9814
  // ============================================================================
9725
9815
  // All types are already exported via individual export declarations above
9726
9816
 
9727
- export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, DoubleRangeSlider, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, IconButton, ImageList, InputCheckbox, Label, LargeButton, ListItem, Mandatory, Masonry, MaterialBox, MaterialIcon, ModalPanel, NumberInput, Options, OptionsList, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, Rating, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SingleRangeSlider, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Timeline, Toast, ToastComponent, Tooltip, TooltipComponent, TreeView, UrlInput, Wizard, clearPortal, createBreadcrumb, getDropdownStyles, getPortalContainer, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, releasePortalContainer, renderToPortal, toast, uniqueId, uuid4 };
9817
+ export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, DoubleRangeSlider, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, IconButton, ImageList, InputCheckbox, Label, LargeButton, ListItem, Mandatory, Masonry, MaterialBox, MaterialIcon, ModalPanel, NumberInput, Options, OptionsList, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, Rating, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SingleRangeSlider, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Timeline, Toast, ToastComponent, Tooltip, TooltipComponent, TreeView, UrlInput, Wizard, clearPortal, createBreadcrumb, getDropdownStyles, getPortalContainer, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, releasePortalContainer, renderToPortal, sortOptions, toast, uniqueId, uuid4 };