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.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
  *
@@ -1413,19 +1435,18 @@
1413
1435
  onclick: onToggle,
1414
1436
  }, [
1415
1437
  iconName ? m('i.material-icons', iconName) : undefined,
1416
- header ? (typeof header === 'string' ? m('span', header) : header) : undefined,
1438
+ header
1439
+ ? typeof header === 'string'
1440
+ ? m('span.collapsible-header-text', header)
1441
+ : m('.collapsible-header-content', header)
1442
+ : undefined,
1417
1443
  ])
1418
1444
  : undefined,
1419
1445
  m('.collapsible-body', {
1420
1446
  style: {
1421
1447
  display: isActive ? 'block' : 'none',
1422
- transition: 'display 0.3s ease',
1423
1448
  },
1424
- }, [
1425
- m('.collapsible-body-content', {
1426
- style: { padding: '2rem' },
1427
- }, body ? (typeof body === 'string' ? m('div', { innerHTML: body }) : body) : undefined),
1428
- ]),
1449
+ }, body ? (typeof body === 'string' ? m('div', { innerHTML: body }) : body) : undefined),
1429
1450
  ]);
1430
1451
  },
1431
1452
  };
@@ -1448,7 +1469,7 @@
1448
1469
  });
1449
1470
  },
1450
1471
  view: ({ attrs }) => {
1451
- const { items, accordion = true, class: c, className, style, id } = attrs;
1472
+ const { items, header, accordion = true, class: c, className, style, id } = attrs;
1452
1473
  const toggleItem = (index) => {
1453
1474
  if (accordion) {
1454
1475
  // Accordion mode: only one item can be active
@@ -1470,12 +1491,22 @@
1470
1491
  }
1471
1492
  }
1472
1493
  };
1494
+ const collapsibleItems = items.map((item, index) => m(CollapsibleItem, Object.assign(Object.assign({}, item), { key: index, isActive: state.activeItems.has(index), onToggle: () => toggleItem(index) })));
1473
1495
  return items && items.length > 0
1474
- ? m('ul.collapsible', {
1475
- class: c || className,
1476
- style: Object.assign({ border: '1px solid #ddd', borderRadius: '2px', margin: '0.5rem 0 1rem 0' }, style),
1477
- id,
1478
- }, items.map((item, index) => m(CollapsibleItem, Object.assign(Object.assign({}, item), { key: index, isActive: state.activeItems.has(index), onToggle: () => toggleItem(index) }))))
1496
+ ? header
1497
+ ? m('ul.collapsible.with-header', {
1498
+ class: c || className,
1499
+ style,
1500
+ id,
1501
+ }, [
1502
+ m('li.collapsible-main-header', m('h4', typeof header === 'string' ? header : header)),
1503
+ collapsibleItems,
1504
+ ])
1505
+ : m('ul.collapsible', {
1506
+ class: c || className,
1507
+ style,
1508
+ id,
1509
+ }, collapsibleItems)
1479
1510
  : undefined;
1480
1511
  },
1481
1512
  };
@@ -1579,7 +1610,7 @@
1579
1610
  };
1580
1611
  };
1581
1612
 
1582
- const defaultI18n$2 = {
1613
+ const defaultI18n$3 = {
1583
1614
  cancel: 'Cancel',
1584
1615
  clear: 'Clear',
1585
1616
  done: 'Ok',
@@ -1687,9 +1718,9 @@
1687
1718
  else if (attrs.displayFormat) {
1688
1719
  finalFormat = attrs.displayFormat;
1689
1720
  }
1690
- 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);
1721
+ const merged = Object.assign({ autoClose: false, format: finalFormat, parse: null, defaultDate: null, setDefaultDate: false, disableWeekends: false, disableDayFn: null, firstDay: 0, minDate: null, maxDate: null, yearRange, showClearBtn: false, showWeekNumbers: false, weekNumbering: 'iso', i18n: defaultI18n$3, onSelect: null, onOpen: null, onClose: null }, attrs);
1691
1722
  // Merge i18n properly
1692
- merged.i18n = Object.assign(Object.assign({}, defaultI18n$2), attrs.i18n);
1723
+ merged.i18n = Object.assign(Object.assign({}, defaultI18n$3), attrs.i18n);
1693
1724
  return merged;
1694
1725
  };
1695
1726
  const toString = (date, format) => {
@@ -2140,11 +2171,11 @@
2140
2171
  prevMonth();
2141
2172
  },
2142
2173
  }, m('svg', {
2143
- fill: '#000000',
2144
2174
  height: '24',
2145
2175
  viewBox: '0 0 24 24',
2146
2176
  width: '24',
2147
2177
  xmlns: 'http://www.w3.org/2000/svg',
2178
+ style: 'fill: var(--mm-text-primary, rgba(0, 0, 0, 0.87));',
2148
2179
  }, [
2149
2180
  m('path', { d: 'M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z' }),
2150
2181
  m('path', { d: 'M0-.5h24v24H0z', fill: 'none' }),
@@ -2206,11 +2237,11 @@
2206
2237
  nextMonth();
2207
2238
  },
2208
2239
  }, m('svg', {
2209
- fill: '#000000',
2210
2240
  height: '24',
2211
2241
  viewBox: '0 0 24 24',
2212
2242
  width: '24',
2213
2243
  xmlns: 'http://www.w3.org/2000/svg',
2244
+ style: 'fill: var(--mm-text-primary, rgba(0, 0, 0, 0.87));',
2214
2245
  }, [
2215
2246
  m('path', { d: 'M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z' }),
2216
2247
  m('path', { d: 'M0-.25h24v24H0z', fill: 'none' }),
@@ -3223,19 +3254,29 @@
3223
3254
  if (!controlled && attrs.defaultValue !== undefined) {
3224
3255
  textarea.value = String(attrs.defaultValue);
3225
3256
  }
3226
- // Height will be calculated by hidden div
3227
3257
  // Update character count state for counter component
3228
3258
  if (maxLength) {
3229
3259
  state.currentLength = textarea.value.length;
3230
3260
  }
3261
+ // Calculate initial height after DOM is fully ready
3262
+ // Use requestAnimationFrame to ensure layout is complete
3263
+ if (state.hiddenDiv) {
3264
+ requestAnimationFrame(() => {
3265
+ if (state.hiddenDiv) {
3266
+ updateHeight(textarea, state.hiddenDiv);
3267
+ m.redraw();
3268
+ }
3269
+ });
3270
+ }
3231
3271
  }, onupdate: ({ dom }) => {
3232
3272
  const textarea = dom;
3233
- if (state.height)
3234
- textarea.style.height = state.height;
3235
- // Trigger height recalculation when value changes programmatically
3273
+ // Recalculate and apply height
3236
3274
  if (state.hiddenDiv) {
3237
3275
  updateHeight(textarea, state.hiddenDiv);
3238
3276
  }
3277
+ if (state.height) {
3278
+ textarea.style.height = state.height;
3279
+ }
3239
3280
  }, onfocus: () => {
3240
3281
  state.active = true;
3241
3282
  }, oninput: (e) => {
@@ -4524,7 +4565,7 @@
4524
4565
  // Create dropdown with proper positioning
4525
4566
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4526
4567
  tabindex: 0,
4527
- style: getPortalStyles(state.inputRef),
4568
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4528
4569
  oncreate: ({ dom }) => {
4529
4570
  state.dropdownRef = dom;
4530
4571
  },
@@ -4636,7 +4677,7 @@
4636
4677
  onremove: () => {
4637
4678
  state.dropdownRef = null;
4638
4679
  },
4639
- style: getDropdownStyles(state.inputRef, true, items, true),
4680
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, items, true)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4640
4681
  }, items.map((item) => {
4641
4682
  if (item.divider) {
4642
4683
  return m('li.divider');
@@ -4776,7 +4817,7 @@
4776
4817
 
4777
4818
  /**
4778
4819
  * Pure TypeScript MaterialBox - creates an image lightbox that fills the screen when clicked
4779
- * No MaterializeCSS dependencies
4820
+ * Uses CSS classes from _materialbox.scss
4780
4821
  */
4781
4822
  const MaterialBox = () => {
4782
4823
  const state = {
@@ -4792,21 +4833,11 @@
4792
4833
  state.originalImage = img;
4793
4834
  if (attrs.onOpenStart)
4794
4835
  attrs.onOpenStart();
4836
+ const inDuration = attrs.inDuration || 275;
4795
4837
  // Create overlay
4796
4838
  const overlay = document.createElement('div');
4797
4839
  overlay.className = 'materialbox-overlay';
4798
- overlay.style.cssText = `
4799
- position: fixed;
4800
- top: 0;
4801
- left: 0;
4802
- right: 0;
4803
- bottom: 0;
4804
- background-color: rgba(0, 0, 0, 0.85);
4805
- z-index: 1000;
4806
- opacity: 0;
4807
- transition: opacity ${attrs.inDuration || 275}ms ease;
4808
- cursor: zoom-out;
4809
- `;
4840
+ overlay.style.transition = `opacity ${inDuration}ms ease`;
4810
4841
  // Create enlarged image
4811
4842
  const enlargedImg = document.createElement('img');
4812
4843
  enlargedImg.src = img.src;
@@ -4827,36 +4858,18 @@
4827
4858
  finalWidth = maxHeight * aspectRatio;
4828
4859
  }
4829
4860
  // Set initial position and size (same as original image)
4830
- enlargedImg.style.cssText = `
4831
- position: fixed;
4832
- top: ${imgRect.top}px;
4833
- left: ${imgRect.left}px;
4834
- width: ${imgRect.width}px;
4835
- height: ${imgRect.height}px;
4836
- transition: all ${attrs.inDuration || 275}ms ease;
4837
- cursor: zoom-out;
4838
- max-width: none;
4839
- z-index: 1001;
4840
- `;
4861
+ enlargedImg.style.top = `${imgRect.top}px`;
4862
+ enlargedImg.style.left = `${imgRect.left}px`;
4863
+ enlargedImg.style.width = `${imgRect.width}px`;
4864
+ enlargedImg.style.height = `${imgRect.height}px`;
4865
+ enlargedImg.style.transition = `all ${inDuration}ms ease`;
4841
4866
  // Add caption if provided
4842
4867
  let caption = null;
4843
4868
  if (attrs.caption) {
4844
4869
  caption = document.createElement('div');
4845
4870
  caption.className = 'materialbox-caption';
4846
4871
  caption.textContent = attrs.caption;
4847
- caption.style.cssText = `
4848
- position: fixed;
4849
- bottom: 20px;
4850
- left: 50%;
4851
- transform: translateX(-50%);
4852
- color: white;
4853
- font-size: 16px;
4854
- text-align: center;
4855
- opacity: 0;
4856
- transition: opacity ${attrs.inDuration || 275}ms ease ${attrs.inDuration || 275}ms;
4857
- z-index: 1002;
4858
- pointer-events: none;
4859
- `;
4872
+ caption.style.transition = `opacity ${inDuration}ms ease ${inDuration}ms`;
4860
4873
  }
4861
4874
  // Add to DOM
4862
4875
  document.body.appendChild(overlay);
@@ -4891,7 +4904,7 @@
4891
4904
  setTimeout(() => {
4892
4905
  if (attrs.onOpenEnd)
4893
4906
  attrs.onOpenEnd();
4894
- }, attrs.inDuration || 275);
4907
+ }, inDuration);
4895
4908
  };
4896
4909
  const closeBox = (attrs) => {
4897
4910
  if (!state.isOpen || !state.originalImage || !state.overlay || !state.overlayImage)
@@ -4948,8 +4961,14 @@
4948
4961
  },
4949
4962
  view: ({ attrs }) => {
4950
4963
  const { src, alt, width, height, caption, className, style } = attrs, otherAttrs = __rest(attrs, ["src", "alt", "width", "height", "caption", "className", "style"]);
4964
+ // Build style attribute - handle both string and object styles
4965
+ let imgStyle = style || {};
4966
+ if (typeof style !== 'string') {
4967
+ // If style is an object or undefined, add default styles as object
4968
+ imgStyle = Object.assign({ cursor: 'zoom-in', transition: 'opacity 200ms ease' }, (style || {}));
4969
+ }
4951
4970
  return m('img.materialboxed', Object.assign(Object.assign({}, otherAttrs), { src, alt: alt || '', width,
4952
- height, className: ['materialboxed', className].filter(Boolean).join(' ') || undefined, style: Object.assign({ cursor: 'zoom-in', transition: 'opacity 200ms ease' }, style), onclick: (e) => {
4971
+ height, className: ['materialboxed', className].filter(Boolean).join(' ') || undefined, style: imgStyle, onclick: (e) => {
4953
4972
  e.preventDefault();
4954
4973
  openBox(e.target, attrs);
4955
4974
  } }));
@@ -5279,6 +5298,11 @@
5279
5298
  };
5280
5299
  };
5281
5300
 
5301
+ const defaultI18n$2 = {
5302
+ cancel: 'Cancel',
5303
+ clear: 'Clear',
5304
+ done: 'Ok',
5305
+ };
5282
5306
  const defaultOptions = {
5283
5307
  dialRadius: 135,
5284
5308
  outerRadius: 105,
@@ -5289,11 +5313,7 @@
5289
5313
  defaultTime: 'now',
5290
5314
  fromNow: 0,
5291
5315
  showClearBtn: false,
5292
- i18n: {
5293
- cancel: 'Cancel',
5294
- clear: 'Clear',
5295
- done: 'Ok',
5296
- },
5316
+ i18n: defaultI18n$2,
5297
5317
  autoClose: false,
5298
5318
  twelveHour: true,
5299
5319
  vibrate: true,
@@ -5443,7 +5463,7 @@
5443
5463
  state.hours = hours;
5444
5464
  state.minutes = minutes;
5445
5465
  if (state.spanHours) {
5446
- state.spanHours.innerHTML = state.hours.toString();
5466
+ state.spanHours.innerHTML = addLeadingZero(state.hours);
5447
5467
  }
5448
5468
  if (state.spanMinutes) {
5449
5469
  state.spanMinutes.innerHTML = addLeadingZero(state.minutes);
@@ -5551,7 +5571,7 @@
5551
5571
  }
5552
5572
  state[state.currentView] = value;
5553
5573
  if (isHours && state.spanHours) {
5554
- state.spanHours.innerHTML = value.toString();
5574
+ state.spanHours.innerHTML = addLeadingZero(value);
5555
5575
  }
5556
5576
  else if (!isHours && state.spanMinutes) {
5557
5577
  state.spanMinutes.innerHTML = addLeadingZero(value);
@@ -5685,7 +5705,7 @@
5685
5705
  const TimepickerModal = () => {
5686
5706
  return {
5687
5707
  view: ({ attrs }) => {
5688
- const { showClearBtn, clearLabel, closeLabel, doneLabel } = attrs;
5708
+ const { i18n, showClearBtn } = attrs;
5689
5709
  return [
5690
5710
  m('.modal-content.timepicker-container', [
5691
5711
  m('.timepicker-digital-display', [
@@ -5697,7 +5717,7 @@
5697
5717
  oncreate: (vnode) => {
5698
5718
  state.spanHours = vnode.dom;
5699
5719
  },
5700
- }, state.hours.toString()),
5720
+ }, addLeadingZero(state.hours)),
5701
5721
  ':',
5702
5722
  m('span.timepicker-span-minutes', {
5703
5723
  class: state.currentView === 'minutes' ? 'text-primary' : '',
@@ -5777,18 +5797,18 @@
5777
5797
  tabindex: options.twelveHour ? '3' : '1',
5778
5798
  style: showClearBtn ? '' : 'visibility: hidden;',
5779
5799
  onclick: () => clear(),
5780
- }, clearLabel),
5800
+ }, i18n.clear),
5781
5801
  m('.confirmation-btns', [
5782
5802
  m('button.btn-flat.timepicker-close.waves-effect', {
5783
5803
  type: 'button',
5784
5804
  tabindex: options.twelveHour ? '3' : '1',
5785
5805
  onclick: () => close(),
5786
- }, closeLabel),
5806
+ }, i18n.cancel),
5787
5807
  m('button.btn-flat.timepicker-close.waves-effect', {
5788
5808
  type: 'button',
5789
5809
  tabindex: options.twelveHour ? '3' : '1',
5790
5810
  onclick: () => done(),
5791
- }, doneLabel),
5811
+ }, i18n.done),
5792
5812
  ]),
5793
5813
  ]),
5794
5814
  ]),
@@ -5805,7 +5825,6 @@
5805
5825
  }
5806
5826
  };
5807
5827
  const renderPickerToPortal = (attrs) => {
5808
- const { showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel' } = attrs;
5809
5828
  const pickerModal = m('.timepicker-modal-wrapper', {
5810
5829
  style: {
5811
5830
  position: 'fixed',
@@ -5848,10 +5867,8 @@
5848
5867
  },
5849
5868
  }, [
5850
5869
  m(TimepickerModal, {
5851
- showClearBtn,
5852
- clearLabel,
5853
- closeLabel,
5854
- doneLabel: 'OK',
5870
+ i18n: options.i18n,
5871
+ showClearBtn: options.showClearBtn,
5855
5872
  }),
5856
5873
  ]),
5857
5874
  ]);
@@ -5911,7 +5928,7 @@
5911
5928
  }
5912
5929
  },
5913
5930
  view: ({ attrs }) => {
5914
- 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;
5931
+ const { id = state.id, label, placeholder, disabled, readonly, required, iconName, helperText, onchange, oninput, useModal = true, twelveHour, className: cn1, class: cn2, } = attrs;
5915
5932
  const className = cn1 || cn2 || 'col s12';
5916
5933
  // Format time value for display
5917
5934
  const formatTime = (hours, minutes, use12Hour) => {
@@ -6357,7 +6374,7 @@
6357
6374
  // Render ungrouped options first
6358
6375
  attrs.options
6359
6376
  .filter((option) => !option.group)
6360
- .map((option) => m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === attrs.options.indexOf(option) ? 'focused' : '' }, (option.disabled
6377
+ .map((option) => m('li', Object.assign({ class: `${option.disabled ? 'disabled' : ''}${isSelected(option.id, selectedIds) ? ' selected' : ''}${state.focusedIndex === attrs.options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
6361
6378
  ? {}
6362
6379
  : {
6363
6380
  onclick: () => {
@@ -6434,7 +6451,7 @@
6434
6451
  // Create dropdown with proper positioning
6435
6452
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6436
6453
  tabindex: 0,
6437
- style: getPortalStyles(state.inputRef),
6454
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6438
6455
  oncreate: ({ dom }) => {
6439
6456
  state.dropdownRef = dom;
6440
6457
  },
@@ -6503,7 +6520,8 @@
6503
6520
  selectedIds = state.internalSelectedIds;
6504
6521
  }
6505
6522
  const finalClassName = newRow ? `${className} clear` : className;
6506
- const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6523
+ const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
6524
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6507
6525
  // Update portal dropdown when inside modal
6508
6526
  if (state.isInsideModal) {
6509
6527
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -6548,7 +6566,7 @@
6548
6566
  onremove: () => {
6549
6567
  state.dropdownRef = null;
6550
6568
  },
6551
- style: getDropdownStyles(state.inputRef, true, options),
6569
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, options)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6552
6570
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder)),
6553
6571
  m(MaterialIcon, {
6554
6572
  name: 'caret',
@@ -6798,7 +6816,7 @@
6798
6816
  ]),
6799
6817
  };
6800
6818
  const DropdownOption = {
6801
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6819
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
6802
6820
  const checkboxId = `search-select-option-${option.id}`;
6803
6821
  const optionLabel = option.label || option.id.toString();
6804
6822
  return m('li', {
@@ -6815,11 +6833,12 @@
6815
6833
  }
6816
6834
  },
6817
6835
  }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6818
- m('input', {
6819
- type: 'checkbox',
6820
- id: checkboxId,
6821
- checked: selectedIds.includes(option.id),
6822
- }),
6836
+ showCheckbox &&
6837
+ m('input', {
6838
+ type: 'checkbox',
6839
+ id: checkboxId,
6840
+ checked: selectedIds.includes(option.id),
6841
+ }),
6823
6842
  m('span', optionLabel),
6824
6843
  ]));
6825
6844
  },
@@ -6837,6 +6856,7 @@
6837
6856
  dropdownRef: null,
6838
6857
  focusedIndex: -1,
6839
6858
  internalSelectedIds: [],
6859
+ createdOptions: [],
6840
6860
  };
6841
6861
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
6842
6862
  const componentId = uniqueId();
@@ -6879,7 +6899,10 @@
6879
6899
  // Handle add new option
6880
6900
  return 'addNew';
6881
6901
  }
6882
- else if (state.focusedIndex < filteredOptions.length) ;
6902
+ else if (state.focusedIndex < filteredOptions.length) {
6903
+ // This will be handled in the view method where attrs are available
6904
+ return 'selectOption';
6905
+ }
6883
6906
  }
6884
6907
  break;
6885
6908
  case 'Escape':
@@ -6890,11 +6913,22 @@
6890
6913
  }
6891
6914
  return null;
6892
6915
  };
6916
+ // Create new option and add to state
6917
+ const createAndSelectOption = async (attrs) => {
6918
+ if (!attrs.oncreateNewOption || !state.searchTerm)
6919
+ return;
6920
+ const newOption = await attrs.oncreateNewOption(state.searchTerm);
6921
+ // Store the created option internally
6922
+ state.createdOptions.push(newOption);
6923
+ // Select the new option
6924
+ toggleOption(newOption, attrs);
6925
+ };
6893
6926
  // Toggle option selection
6894
6927
  const toggleOption = (option, attrs) => {
6895
6928
  if (option.disabled)
6896
6929
  return;
6897
6930
  const controlled = isControlled(attrs);
6931
+ const { maxSelectedOptions } = attrs;
6898
6932
  // Get current selected IDs from props or internal state
6899
6933
  const currentSelectedIds = controlled
6900
6934
  ? attrs.checkedId !== undefined
@@ -6903,9 +6937,29 @@
6903
6937
  : [attrs.checkedId]
6904
6938
  : []
6905
6939
  : state.internalSelectedIds;
6906
- const newIds = currentSelectedIds.includes(option.id)
6907
- ? currentSelectedIds.filter((id) => id !== option.id)
6908
- : [...currentSelectedIds, option.id];
6940
+ const isSelected = currentSelectedIds.includes(option.id);
6941
+ let newIds;
6942
+ if (isSelected) {
6943
+ // Remove if already selected
6944
+ newIds = currentSelectedIds.filter((id) => id !== option.id);
6945
+ }
6946
+ else {
6947
+ // Check if we've reached the max selection limit
6948
+ if (maxSelectedOptions && currentSelectedIds.length >= maxSelectedOptions) {
6949
+ // If max=1, replace the selection
6950
+ if (maxSelectedOptions === 1) {
6951
+ newIds = [option.id];
6952
+ }
6953
+ else {
6954
+ // Otherwise, don't add more
6955
+ return;
6956
+ }
6957
+ }
6958
+ else {
6959
+ // Add to selection
6960
+ newIds = [...currentSelectedIds, option.id];
6961
+ }
6962
+ }
6909
6963
  // Update internal state for uncontrolled mode
6910
6964
  if (!controlled) {
6911
6965
  state.internalSelectedIds = newIds;
@@ -6967,21 +7021,32 @@
6967
7021
  : [attrs.checkedId]
6968
7022
  : []
6969
7023
  : state.internalSelectedIds;
6970
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
7024
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
6971
7025
  // Use i18n values if provided, otherwise use defaults
6972
7026
  const texts = {
6973
7027
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6974
7028
  addNewPrefix: i18n.addNewPrefix || '+',
7029
+ showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7030
+ maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
6975
7031
  };
7032
+ // Check if max selections is reached
7033
+ const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7034
+ // Merge provided options with internally created options
7035
+ const allOptions = [...options, ...state.createdOptions];
6976
7036
  // Get selected options for display
6977
- const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
7037
+ const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7038
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6978
7039
  // Safely filter options
6979
- const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
7040
+ const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6980
7041
  !selectedIds.includes(option.id));
7042
+ // Apply display limit if configured
7043
+ const totalFilteredCount = filteredOptions.length;
7044
+ const displayedOptions = maxDisplayedOptions ? filteredOptions.slice(0, maxDisplayedOptions) : filteredOptions;
7045
+ const isTruncated = maxDisplayedOptions && totalFilteredCount > maxDisplayedOptions;
6981
7046
  // Check if we should show the "add new option" element
6982
7047
  const showAddNew = oncreateNewOption &&
6983
7048
  state.searchTerm &&
6984
- !filteredOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
7049
+ !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
6985
7050
  // Render the dropdown
6986
7051
  return m('.input-field.multi-select-dropdown', { className }, [
6987
7052
  m('.chips.chips-initial.chips-container', {
@@ -7054,7 +7119,7 @@
7054
7119
  onremove: () => {
7055
7120
  state.dropdownRef = null;
7056
7121
  },
7057
- style: getDropdownStyles(state.inputRef),
7122
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
7058
7123
  }, [
7059
7124
  m('li', // Search Input
7060
7125
  {
@@ -7074,41 +7139,65 @@
7074
7139
  state.focusedIndex = -1; // Reset focus when typing
7075
7140
  },
7076
7141
  onkeydown: async (e) => {
7077
- const result = handleKeyDown(e, filteredOptions, !!showAddNew);
7142
+ const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7078
7143
  if (result === 'addNew' && oncreateNewOption) {
7079
- const option = await oncreateNewOption(state.searchTerm);
7080
- toggleOption(option, attrs);
7144
+ await createAndSelectOption(attrs);
7081
7145
  }
7082
- else if (e.key === 'Enter' &&
7083
- state.focusedIndex >= 0 &&
7084
- state.focusedIndex < filteredOptions.length) {
7085
- toggleOption(filteredOptions[state.focusedIndex], attrs);
7146
+ else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
7147
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
7086
7148
  }
7087
7149
  },
7088
7150
  class: 'search-select-input',
7089
7151
  }),
7090
7152
  ]),
7091
7153
  // No options found message or list of options
7092
- ...(filteredOptions.length === 0 && !showAddNew
7154
+ ...(displayedOptions.length === 0 && !showAddNew
7093
7155
  ? [m('li.search-select-no-options', texts.noOptionsFound)]
7094
7156
  : []),
7157
+ // Truncation message
7158
+ ...(isTruncated
7159
+ ? [
7160
+ m('li.search-select-truncation-info', {
7161
+ style: {
7162
+ fontStyle: 'italic',
7163
+ color: 'var(--mm-text-hint, #9e9e9e)',
7164
+ padding: '8px 16px',
7165
+ cursor: 'default',
7166
+ },
7167
+ }, texts.showingXofY
7168
+ .replace('{shown}', displayedOptions.length.toString())
7169
+ .replace('{total}', totalFilteredCount.toString())),
7170
+ ]
7171
+ : []),
7172
+ // Max selections reached message
7173
+ ...(isMaxSelectionsReached
7174
+ ? [
7175
+ m('li.search-select-max-info', {
7176
+ style: {
7177
+ fontStyle: 'italic',
7178
+ color: 'var(--mm-text-hint, #9e9e9e)',
7179
+ padding: '8px 16px',
7180
+ cursor: 'default',
7181
+ },
7182
+ }, texts.maxSelectionsReached.replace('{max}', maxSelectedOptions.toString())),
7183
+ ]
7184
+ : []),
7095
7185
  // Add new option item
7096
7186
  ...(showAddNew
7097
7187
  ? [
7098
7188
  m('li', {
7099
7189
  onclick: async () => {
7100
- const option = await oncreateNewOption(state.searchTerm);
7101
- toggleOption(option, attrs);
7190
+ await createAndSelectOption(attrs);
7102
7191
  },
7103
- class: state.focusedIndex === filteredOptions.length ? 'active' : '',
7192
+ class: state.focusedIndex === displayedOptions.length ? 'active' : '',
7104
7193
  onmouseover: () => {
7105
- state.focusedIndex = filteredOptions.length;
7194
+ state.focusedIndex = displayedOptions.length;
7106
7195
  },
7107
7196
  }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
7108
7197
  ]
7109
7198
  : []),
7110
7199
  // List of filtered options
7111
- ...filteredOptions.map((option, index) => m(DropdownOption, {
7200
+ ...displayedOptions.map((option, index) => m(DropdownOption, {
7112
7201
  // key: option.id,
7113
7202
  option,
7114
7203
  index,
@@ -7118,6 +7207,7 @@
7118
7207
  onMouseOver: (idx) => {
7119
7208
  state.focusedIndex = idx;
7120
7209
  },
7210
+ showCheckbox: maxSelectedOptions !== 1,
7121
7211
  })),
7122
7212
  ]),
7123
7213
  ]);
@@ -9817,6 +9907,7 @@
9817
9907
  exports.range = range;
9818
9908
  exports.releasePortalContainer = releasePortalContainer;
9819
9909
  exports.renderToPortal = renderToPortal;
9910
+ exports.sortOptions = sortOptions;
9820
9911
  exports.toast = toast;
9821
9912
  exports.uniqueId = uniqueId;
9822
9913
  exports.uuid4 = uuid4;