mithril-materialized 3.4.0 → 3.4.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Mithril.js component library inspired by [materialize-css](https://materializecss.com) design principles, [available on npm](https://www.npmjs.com/package/mithril-materialized). This library provides you with ready-to-use Mithril components that follow Material Design guidelines, with **no external JavaScript dependencies**.
4
4
 
5
- ## 🚀 v3.3 - Latest Release
5
+ ## 🚀 v3.4 - Latest Release
6
6
 
7
7
  The current stable release that provides a complete Mithril.js Material Design component library with no external JavaScript dependencies.
8
8
 
package/dist/core.css CHANGED
@@ -3167,9 +3167,6 @@ body.keyboard-focused .select-dropdown.dropdown-content li:focus {
3167
3167
  .select-dropdown.dropdown-content li:hover {
3168
3168
  background-color: var(--mm-dropdown-hover, rgba(0, 0, 0, 0.08));
3169
3169
  }
3170
- .select-dropdown.dropdown-content li.selected {
3171
- background-color: var(--mm-dropdown-selected, rgba(0, 0, 0, 0.03));
3172
- }
3173
3170
 
3174
3171
  .prefix ~ .select-wrapper {
3175
3172
  margin-left: 3rem;
package/dist/forms.css CHANGED
@@ -1412,9 +1412,6 @@ body.keyboard-focused .select-dropdown.dropdown-content li:focus {
1412
1412
  .select-dropdown.dropdown-content li:hover {
1413
1413
  background-color: var(--mm-dropdown-hover, rgba(0, 0, 0, 0.08));
1414
1414
  }
1415
- .select-dropdown.dropdown-content li.selected {
1416
- background-color: var(--mm-dropdown-selected, rgba(0, 0, 0, 0.03));
1417
- }
1418
1415
 
1419
1416
  .prefix ~ .select-wrapper {
1420
1417
  margin-left: 3rem;
package/dist/index.css CHANGED
@@ -5632,6 +5632,7 @@ body.keyboard-focused .dropdown-content li:focus {
5632
5632
  font-size: 16px;
5633
5633
  line-height: 32px;
5634
5634
  padding-left: 8px;
5635
+ min-height: 1lh;
5635
5636
  }
5636
5637
 
5637
5638
  .chips {
@@ -6846,9 +6847,6 @@ body.keyboard-focused .select-dropdown.dropdown-content li:focus {
6846
6847
  .select-dropdown.dropdown-content li:hover {
6847
6848
  background-color: var(--mm-dropdown-hover, rgba(0, 0, 0, 0.08));
6848
6849
  }
6849
- .select-dropdown.dropdown-content li.selected {
6850
- background-color: var(--mm-dropdown-selected, rgba(0, 0, 0, 0.03));
6851
- }
6852
6850
 
6853
6851
  .prefix ~ .select-wrapper {
6854
6852
  margin-left: 3rem;
package/dist/index.esm.js CHANGED
@@ -228,8 +228,6 @@ const Autocomplete = () => {
228
228
  if (attrs.onAutocomplete) {
229
229
  attrs.onAutocomplete(suggestion.key);
230
230
  }
231
- // Force redraw to update label state
232
- m.redraw();
233
231
  };
234
232
  const handleKeydown = (e, attrs) => {
235
233
  if (!state.isOpen)
@@ -308,7 +306,7 @@ const Autocomplete = () => {
308
306
  const id = attrs.id || state.id;
309
307
  const { label, helperText, onchange, newRow, className = 'col s12', style, iconName, isMandatory, data = {}, limit = Infinity, minLength = 1 } = attrs, params = __rest(attrs, ["label", "helperText", "onchange", "newRow", "className", "style", "iconName", "isMandatory", "data", "limit", "minLength"]);
310
308
  const controlled = isControlled(attrs);
311
- const currentValue = controlled ? (attrs.value || '') : state.internalValue;
309
+ const currentValue = controlled ? attrs.value || '' : state.internalValue;
312
310
  const cn = newRow ? className + ' clear' : className;
313
311
  // Update suggestions when input changes
314
312
  state.suggestions = filterSuggestions(currentValue, data, limit, minLength);
@@ -323,7 +321,7 @@ const Autocomplete = () => {
323
321
  style,
324
322
  }, [
325
323
  iconName ? m('i.material-icons.prefix', iconName) : '',
326
- m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: controlled ? currentValue : undefined, oncreate: (vnode) => {
324
+ m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: currentValue, oncreate: (vnode) => {
327
325
  state.inputElement = vnode.dom;
328
326
  // Set initial value for uncontrolled mode
329
327
  if (!controlled && attrs.defaultValue) {
@@ -359,14 +357,10 @@ const Autocomplete = () => {
359
357
  }
360
358
  }, onblur: (e) => {
361
359
  state.isActive = false;
362
- // Delay closing to allow clicks on suggestions
363
- setTimeout(() => {
364
- if (!e.relatedTarget || !e.relatedTarget.closest('.autocomplete-content')) {
365
- state.isOpen = false;
366
- state.selectedIndex = -1;
367
- m.redraw();
368
- }
369
- }, 150);
360
+ if (!e.relatedTarget || !e.relatedTarget.closest('.autocomplete-content')) {
361
+ state.isOpen = false;
362
+ state.selectedIndex = -1;
363
+ }
370
364
  } })),
371
365
  // Autocomplete dropdown
372
366
  state.isOpen &&
@@ -382,7 +376,6 @@ const Autocomplete = () => {
382
376
  },
383
377
  onmouseover: () => {
384
378
  state.selectedIndex = index;
385
- m.redraw();
386
379
  },
387
380
  }, [
388
381
  // Check if value contains image URL or icon
@@ -2483,36 +2476,52 @@ const handleKeyboardNavigation = (key, currentValue, min, max, step) => {
2483
2476
  return null;
2484
2477
  }
2485
2478
  };
2479
+ const isControlled = (attrs) => {
2480
+ return attrs.value !== undefined && typeof attrs.oninput === 'function';
2481
+ };
2482
+ const isRangeControlled = (attrs) => {
2483
+ return (attrs.minValue !== undefined || attrs.maxValue !== undefined) && typeof attrs.oninput === 'function';
2484
+ };
2486
2485
  const initRangeState = (state, attrs) => {
2487
- const { min = 0, max = 100, value, minValue, maxValue } = attrs;
2486
+ const { min = 0, max = 100, value, minValue, maxValue, defaultValue } = attrs;
2488
2487
  // Initialize single range value
2489
- if (value !== undefined) {
2490
- const currentValue = value;
2488
+ if (isControlled(attrs)) {
2489
+ // Always use value from props in controlled mode
2490
+ state.singleValue = value !== undefined ? value : min;
2491
+ }
2492
+ else {
2493
+ // Use internal state for uncontrolled mode
2491
2494
  if (state.singleValue === undefined) {
2492
- state.singleValue = currentValue;
2495
+ state.singleValue = defaultValue !== undefined ? defaultValue : value !== undefined ? value : min;
2493
2496
  }
2494
- if (state.lastValue !== value && !state.hasUserInteracted) {
2497
+ // Only update internal state if props changed and user hasn't interacted
2498
+ if (state.lastValue !== value && !state.hasUserInteracted && value !== undefined) {
2495
2499
  state.singleValue = value;
2496
2500
  state.lastValue = value;
2497
2501
  }
2498
2502
  }
2499
- else if (state.singleValue === undefined) {
2500
- state.singleValue = min;
2501
- }
2502
2503
  // Initialize range values
2503
- const currentMinValue = minValue !== undefined ? minValue : min;
2504
- const currentMaxValue = maxValue !== undefined ? maxValue : max;
2505
- if (state.rangeMinValue === undefined || state.rangeMaxValue === undefined) {
2506
- state.rangeMinValue = currentMinValue;
2507
- state.rangeMaxValue = currentMaxValue;
2504
+ if (isRangeControlled(attrs)) {
2505
+ // Always use values from props in controlled mode
2506
+ state.rangeMinValue = minValue !== undefined ? minValue : min;
2507
+ state.rangeMaxValue = maxValue !== undefined ? maxValue : max;
2508
2508
  }
2509
- if (!state.hasUserInteracted &&
2510
- ((minValue !== undefined && state.lastMinValue !== minValue) ||
2511
- (maxValue !== undefined && state.lastMaxValue !== maxValue))) {
2512
- state.rangeMinValue = currentMinValue;
2513
- state.rangeMaxValue = currentMaxValue;
2514
- state.lastMinValue = minValue;
2515
- state.lastMaxValue = maxValue;
2509
+ else {
2510
+ // Use internal state for uncontrolled mode
2511
+ const currentMinValue = minValue !== undefined ? minValue : min;
2512
+ const currentMaxValue = maxValue !== undefined ? maxValue : max;
2513
+ if (state.rangeMinValue === undefined || state.rangeMaxValue === undefined) {
2514
+ state.rangeMinValue = currentMinValue;
2515
+ state.rangeMaxValue = currentMaxValue;
2516
+ }
2517
+ if (!state.hasUserInteracted &&
2518
+ ((minValue !== undefined && state.lastMinValue !== minValue) ||
2519
+ (maxValue !== undefined && state.lastMaxValue !== maxValue))) {
2520
+ state.rangeMinValue = currentMinValue;
2521
+ state.rangeMaxValue = currentMaxValue;
2522
+ state.lastMinValue = minValue;
2523
+ state.lastMaxValue = maxValue;
2524
+ }
2516
2525
  }
2517
2526
  // Initialize active thumb if not set
2518
2527
  if (state.activeThumb === null) {
@@ -2529,15 +2538,18 @@ const updateRangeValues = (minValue, maxValue, attrs, state, immediate) => {
2529
2538
  minValue = maxValue;
2530
2539
  if (maxValue < minValue)
2531
2540
  maxValue = minValue;
2532
- state.rangeMinValue = minValue;
2533
- state.rangeMaxValue = maxValue;
2541
+ // Only update internal state for uncontrolled mode
2542
+ if (!isRangeControlled(attrs)) {
2543
+ state.rangeMinValue = minValue;
2544
+ state.rangeMaxValue = maxValue;
2545
+ }
2534
2546
  state.hasUserInteracted = true;
2535
- // Call oninput for immediate feedback or onchange for final changes
2547
+ // Call appropriate handler based on interaction type, not control mode
2536
2548
  if (immediate && attrs.oninput) {
2537
- attrs.oninput(minValue, maxValue);
2549
+ attrs.oninput(minValue, maxValue); // Immediate feedback during drag
2538
2550
  }
2539
- else if (!immediate && attrs.onchange) {
2540
- attrs.onchange(minValue, maxValue);
2551
+ if (!immediate && attrs.onchange) {
2552
+ attrs.onchange(minValue, maxValue); // Final value on interaction end (blur/mouseup)
2541
2553
  }
2542
2554
  };
2543
2555
  // Single Range Slider Component
@@ -2571,19 +2583,24 @@ const SingleRangeSlider = {
2571
2583
  : tooltipPos
2572
2584
  : tooltipPos;
2573
2585
  const updateSingleValue = (newValue, immediate = false) => {
2574
- state.singleValue = newValue;
2586
+ // Only update internal state for uncontrolled mode
2587
+ if (!isControlled(attrs)) {
2588
+ state.singleValue = newValue;
2589
+ }
2575
2590
  state.hasUserInteracted = true;
2591
+ // Call appropriate handler based on interaction type, not control mode
2576
2592
  if (immediate && oninput) {
2577
- oninput(newValue);
2593
+ oninput(newValue); // Immediate feedback during drag
2578
2594
  }
2579
- else if (!immediate && onchange) {
2580
- onchange(newValue);
2595
+ if (!immediate && onchange) {
2596
+ onchange(newValue); // Final value on interaction end (blur/mouseup)
2581
2597
  }
2582
2598
  };
2583
2599
  const handleMouseDown = (e) => {
2584
2600
  if (disabled)
2585
2601
  return;
2586
2602
  e.preventDefault();
2603
+ e.stopPropagation();
2587
2604
  state.isDragging = true;
2588
2605
  if (finalValueDisplay === 'auto') {
2589
2606
  m.redraw();
@@ -2658,6 +2675,11 @@ const SingleRangeSlider = {
2658
2675
  updateSingleValue(newValue, false);
2659
2676
  }
2660
2677
  },
2678
+ onblur: () => {
2679
+ if (disabled || !onchange)
2680
+ return;
2681
+ onchange(state.singleValue);
2682
+ },
2661
2683
  }, [
2662
2684
  m(`.track.${orientation}`),
2663
2685
  m(`.range-progress.${orientation}`, { style: progressStyle }),
@@ -2716,6 +2738,7 @@ const DoubleRangeSlider = {
2716
2738
  if (disabled)
2717
2739
  return;
2718
2740
  e.preventDefault();
2741
+ e.stopPropagation();
2719
2742
  state.isDragging = true;
2720
2743
  state.activeThumb = thumb;
2721
2744
  if (finalValueDisplay === 'auto') {
@@ -2806,6 +2829,11 @@ const DoubleRangeSlider = {
2806
2829
  maxThumb.focus();
2807
2830
  }
2808
2831
  },
2832
+ onblur: () => {
2833
+ if (disabled || !attrs.onchange)
2834
+ return;
2835
+ attrs.onchange(state.rangeMinValue, state.rangeMaxValue);
2836
+ },
2809
2837
  }, [
2810
2838
  m(`.track.${orientation}`),
2811
2839
  m(`.range.${orientation}`, { style: rangeStyle }),
@@ -3011,13 +3039,13 @@ const TextArea = () => {
3011
3039
  overflowWrap: 'break-word',
3012
3040
  },
3013
3041
  oncreate: ({ dom }) => {
3014
- const hiddenDiv = state.hiddenDiv = dom;
3042
+ const hiddenDiv = (state.hiddenDiv = dom);
3015
3043
  if (state.textarea) {
3016
3044
  updateHeight(state.textarea, hiddenDiv);
3017
3045
  }
3018
3046
  },
3019
3047
  onupdate: ({ dom }) => {
3020
- const hiddenDiv = state.hiddenDiv = dom;
3048
+ const hiddenDiv = (state.hiddenDiv = dom);
3021
3049
  if (state.textarea) {
3022
3050
  updateHeight(state.textarea, hiddenDiv);
3023
3051
  }
@@ -3130,8 +3158,7 @@ const InputField = (type, defaultClass = '') => () => {
3130
3158
  isDragging: false,
3131
3159
  activeThumb: null,
3132
3160
  };
3133
- const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' &&
3134
- (typeof attrs.oninput === 'function' || typeof attrs.onchange === 'function');
3161
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
3135
3162
  const getValue = (target) => {
3136
3163
  const val = target.value;
3137
3164
  return (val ? (type === 'number' || type === 'range' ? +val : val) : val);
@@ -3181,7 +3208,7 @@ const InputField = (type, defaultClass = '') => () => {
3181
3208
  const isNonInteractive = attrs.readonly || attrs.disabled;
3182
3209
  // Warn developer for improper controlled usage
3183
3210
  if (attrs.value !== undefined && !controlled && !isNonInteractive) {
3184
- console.warn(`${type} input received 'value' prop without 'oninput' or 'onchange' handler. ` +
3211
+ console.warn(`${type} input with label '${attrs.label}' received 'value' prop without 'oninput' handler. ` +
3185
3212
  `Use 'defaultValue' for uncontrolled components or add an event handler for controlled components.`);
3186
3213
  }
3187
3214
  // Initialize internal value if not in controlled mode
@@ -4208,14 +4235,12 @@ const Dropdown = () => {
4208
4235
  inputRef: null,
4209
4236
  dropdownRef: null,
4210
4237
  internalCheckedId: undefined,
4238
+ isInsideModal: false,
4211
4239
  };
4212
4240
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
4213
- const closeDropdown = (e) => {
4214
- const target = e.target;
4215
- if (!target.closest('.dropdown-wrapper.input-field')) {
4216
- state.isOpen = false;
4217
- m.redraw();
4218
- }
4241
+ const closeDropdown = () => {
4242
+ state.isOpen = false;
4243
+ m.redraw(); // Needed to remove the dropdown options list (potentially added to document root)
4219
4244
  };
4220
4245
  const handleKeyDown = (e, items) => {
4221
4246
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -4260,6 +4285,83 @@ const Dropdown = () => {
4260
4285
  return undefined;
4261
4286
  }
4262
4287
  };
4288
+ const getPortalStyles = (inputRef) => {
4289
+ if (!inputRef)
4290
+ return {};
4291
+ const rect = inputRef.getBoundingClientRect();
4292
+ const viewportHeight = window.innerHeight;
4293
+ const spaceBelow = viewportHeight - rect.bottom;
4294
+ const spaceAbove = rect.top;
4295
+ // Choose whether to show above or below based on available space
4296
+ const showAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
4297
+ return {
4298
+ position: 'fixed',
4299
+ top: showAbove ? 'auto' : `${rect.bottom}px`,
4300
+ bottom: showAbove ? `${viewportHeight - rect.top}px` : 'auto',
4301
+ left: `${rect.left}px`,
4302
+ width: `${rect.width}px`,
4303
+ zIndex: 10000,
4304
+ maxHeight: showAbove ? `${spaceAbove - 20}px` : `${spaceBelow - 20}px`,
4305
+ overflow: 'auto',
4306
+ display: 'block',
4307
+ opacity: 1,
4308
+ };
4309
+ };
4310
+ const updatePortalDropdown = (items, selectedLabel, onSelectItem) => {
4311
+ if (!state.isInsideModal)
4312
+ return;
4313
+ // Clean up existing portal
4314
+ const existingPortal = document.getElementById(`${state.id}-dropdown`);
4315
+ if (existingPortal) {
4316
+ existingPortal.remove();
4317
+ }
4318
+ if (!state.isOpen || !state.inputRef)
4319
+ return;
4320
+ // Create portal element
4321
+ const portalElement = document.createElement('div');
4322
+ portalElement.id = `${state.id}-dropdown`;
4323
+ document.body.appendChild(portalElement);
4324
+ // Create dropdown content
4325
+ const availableItems = items.filter((item) => !item.divider && !item.disabled);
4326
+ const dropdownContent = items.map((item) => {
4327
+ if (item.divider) {
4328
+ return m('li.divider');
4329
+ }
4330
+ const itemIndex = availableItems.indexOf(item);
4331
+ const isSelected = selectedLabel === item.label;
4332
+ const isFocused = state.focusedIndex === itemIndex;
4333
+ return m('li', {
4334
+ class: `${isSelected ? 'selected' : ''} ${isFocused ? 'focused' : ''}${item.disabled ? ' disabled' : ''}`,
4335
+ onclick: item.disabled ? undefined : () => onSelectItem(item),
4336
+ }, m('span', {
4337
+ style: {
4338
+ display: 'flex',
4339
+ alignItems: 'center',
4340
+ padding: '14px 16px',
4341
+ },
4342
+ }, [
4343
+ item.iconName
4344
+ ? m('i.material-icons', {
4345
+ style: { marginRight: '32px' },
4346
+ }, item.iconName)
4347
+ : undefined,
4348
+ item.label,
4349
+ ]));
4350
+ });
4351
+ // Create dropdown with proper positioning
4352
+ const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4353
+ tabindex: 0,
4354
+ style: getPortalStyles(state.inputRef),
4355
+ oncreate: ({ dom }) => {
4356
+ state.dropdownRef = dom;
4357
+ },
4358
+ onremove: () => {
4359
+ state.dropdownRef = null;
4360
+ },
4361
+ }, dropdownContent);
4362
+ // Render to portal
4363
+ m.render(portalElement, dropdownVnode);
4364
+ };
4263
4365
  return {
4264
4366
  oninit: ({ attrs }) => {
4265
4367
  var _a;
@@ -4271,9 +4373,18 @@ const Dropdown = () => {
4271
4373
  // Add global click listener to close dropdown
4272
4374
  document.addEventListener('click', closeDropdown);
4273
4375
  },
4376
+ oncreate: ({ dom }) => {
4377
+ // Detect if component is inside a modal
4378
+ state.isInsideModal = !!dom.closest('.modal');
4379
+ },
4274
4380
  onremove: () => {
4275
4381
  // Cleanup global listener
4276
4382
  document.removeEventListener('click', closeDropdown);
4383
+ // Cleanup portal
4384
+ const portalElement = document.getElementById(`${state.id}-dropdown`);
4385
+ if (portalElement) {
4386
+ portalElement.remove();
4387
+ }
4277
4388
  },
4278
4389
  view: ({ attrs }) => {
4279
4390
  const { checkedId, key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12', } = attrs;
@@ -4296,6 +4407,16 @@ const Dropdown = () => {
4296
4407
  : undefined;
4297
4408
  const title = selectedItem ? selectedItem.label : label || 'Select';
4298
4409
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
4410
+ // Update portal dropdown when inside modal
4411
+ if (state.isInsideModal) {
4412
+ updatePortalDropdown(items, title, (item) => {
4413
+ if (item.id) {
4414
+ state.isOpen = false;
4415
+ state.focusedIndex = -1;
4416
+ handleSelection(item.id);
4417
+ }
4418
+ });
4419
+ }
4299
4420
  return m('.dropdown-wrapper.input-field', { className, key, style }, [
4300
4421
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
4301
4422
  m(HelperText, { helperText }),
@@ -4329,8 +4450,9 @@ const Dropdown = () => {
4329
4450
  }
4330
4451
  },
4331
4452
  }),
4332
- // Dropdown Menu using Select component's positioning logic
4453
+ // Dropdown Menu - render inline only when NOT inside modal
4333
4454
  state.isOpen &&
4455
+ !state.isInsideModal &&
4334
4456
  m('ul.dropdown-content.select-dropdown', {
4335
4457
  tabindex: 0,
4336
4458
  role: 'listbox',
@@ -4780,7 +4902,7 @@ const ModalPanel = () => {
4780
4902
  maxWidth: '75%',
4781
4903
  borderRadius: '4px',
4782
4904
  })), { backgroundColor: 'var(--mm-modal-background, #fff)', maxHeight: '85%', overflow: 'auto', zIndex: '1003', padding: '0', flexDirection: 'column', boxShadow: '0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12), 0 11px 15px -7px rgba(0,0,0,0.20)' }),
4783
- onclick: (e) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
4905
+ // onclick: (e: Event) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
4784
4906
  }, [
4785
4907
  // Close button
4786
4908
  showCloseButton &&
@@ -5876,6 +5998,7 @@ const Select = () => {
5876
5998
  dropdownRef: null,
5877
5999
  internalSelectedIds: [],
5878
6000
  isInsideModal: false,
6001
+ isMultiple: false,
5879
6002
  };
5880
6003
  const isControlled = (attrs) => attrs.checkedId !== undefined && attrs.onchange !== undefined;
5881
6004
  const isSelected = (id, selectedIds) => {
@@ -5958,10 +6081,18 @@ const Select = () => {
5958
6081
  }
5959
6082
  };
5960
6083
  const closeDropdown = (e) => {
6084
+ console.log('select closeDropdown called');
6085
+ if (!state.isMultiple) {
6086
+ state.isOpen = false;
6087
+ return;
6088
+ }
5961
6089
  const target = e.target;
5962
- if (!target.closest('.input-field.select-space')) {
6090
+ // When inside modal, check both the select component AND the portaled dropdown
6091
+ const isClickInsideSelect = target.closest('.input-field.select-space');
6092
+ const isClickInsidePortalDropdown = state.isInsideModal && state.dropdownRef && (state.dropdownRef.contains(target) || target === state.dropdownRef);
6093
+ if (!isClickInsideSelect && !isClickInsidePortalDropdown) {
6094
+ console.log('select closeDropdown called: set state');
5963
6095
  state.isOpen = false;
5964
- m.redraw();
5965
6096
  }
5966
6097
  };
5967
6098
  const getPortalStyles = (inputRef) => {
@@ -5969,13 +6100,21 @@ const Select = () => {
5969
6100
  return {};
5970
6101
  const rect = inputRef.getBoundingClientRect();
5971
6102
  const viewportHeight = window.innerHeight;
6103
+ const spaceBelow = viewportHeight - rect.bottom;
6104
+ const spaceAbove = rect.top;
6105
+ // Choose whether to show above or below based on available space
6106
+ const showAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
5972
6107
  return {
5973
6108
  position: 'fixed',
5974
- top: `${rect.bottom}px`,
6109
+ top: showAbove ? 'auto' : `${rect.bottom}px`,
6110
+ bottom: showAbove ? `${viewportHeight - rect.top}px` : 'auto',
5975
6111
  left: `${rect.left}px`,
5976
6112
  width: `${rect.width}px`,
5977
6113
  zIndex: 10000, // Higher than modal z-index
5978
- maxHeight: `${viewportHeight - rect.bottom - 20}px`, // Leave 20px margin from bottom
6114
+ maxHeight: showAbove ? `${spaceAbove - 20}px` : `${spaceBelow - 20}px`, // Leave 20px margin
6115
+ overflow: 'auto',
6116
+ display: 'block',
6117
+ opacity: 1,
5979
6118
  };
5980
6119
  };
5981
6120
  const renderDropdownContent = (attrs, selectedIds, multiple, placeholder) => [
@@ -5983,15 +6122,10 @@ const Select = () => {
5983
6122
  // Render ungrouped options first
5984
6123
  attrs.options
5985
6124
  .filter((option) => !option.group)
5986
- .map((option) => m('li', Object.assign({ key: option.id, class: option.disabled
5987
- ? 'disabled'
5988
- : state.focusedIndex === attrs.options.indexOf(option)
5989
- ? 'focused'
5990
- : '' }, (option.disabled
6125
+ .map((option) => m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === attrs.options.indexOf(option) ? 'focused' : '' }, (option.disabled
5991
6126
  ? {}
5992
6127
  : {
5993
- onclick: (e) => {
5994
- e.stopPropagation();
6128
+ onclick: () => {
5995
6129
  toggleOption(option.id, multiple, attrs);
5996
6130
  },
5997
6131
  })), [
@@ -6021,8 +6155,8 @@ const Select = () => {
6021
6155
  return groups;
6022
6156
  }, {}))
6023
6157
  .map(([groupName, groupOptions]) => [
6024
- m('li.optgroup', { key: `group-${groupName}`, tabindex: 0 }, m('span', groupName)),
6025
- ...groupOptions.map((option) => m('li', Object.assign({ key: option.id, class: `optgroup-option${option.disabled ? ' disabled' : ''}${isSelected(option.id, selectedIds) ? ' selected' : ''}${state.focusedIndex === attrs.options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
6158
+ m('li.optgroup', { tabindex: 0 }, m('span', groupName)),
6159
+ ...groupOptions.map((option) => m('li', Object.assign({ class: `optgroup-option${option.disabled ? ' disabled' : ''}${isSelected(option.id, selectedIds) ? ' selected' : ''}${state.focusedIndex === attrs.options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
6026
6160
  ? {}
6027
6161
  : {
6028
6162
  onclick: (e) => {
@@ -6049,23 +6183,20 @@ const Select = () => {
6049
6183
  .reduce((acc, val) => acc.concat(val), []),
6050
6184
  ];
6051
6185
  const updatePortalDropdown = (attrs, selectedIds, multiple, placeholder) => {
6052
- var _a;
6053
6186
  if (!state.isInsideModal)
6054
6187
  return;
6055
- let portalElement = document.getElementById(state.dropdownId);
6056
- if (!state.isOpen) {
6057
- // Clean up portal when dropdown is closed
6058
- if (portalElement) {
6059
- m.render(portalElement, []);
6060
- (_a = portalElement.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(portalElement);
6061
- }
6062
- return;
6063
- }
6064
- if (!portalElement) {
6065
- portalElement = document.createElement('div');
6066
- portalElement.id = state.dropdownId;
6067
- document.body.appendChild(portalElement);
6188
+ // Clean up existing portal
6189
+ const existingPortal = document.getElementById(state.dropdownId);
6190
+ if (existingPortal) {
6191
+ existingPortal.remove();
6068
6192
  }
6193
+ if (!state.isOpen || !state.inputRef)
6194
+ return;
6195
+ // Create portal element
6196
+ const portalElement = document.createElement('div');
6197
+ portalElement.id = state.dropdownId;
6198
+ document.body.appendChild(portalElement);
6199
+ // Create dropdown with proper positioning
6069
6200
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6070
6201
  tabindex: 0,
6071
6202
  style: getPortalStyles(state.inputRef),
@@ -6076,6 +6207,7 @@ const Select = () => {
6076
6207
  state.dropdownRef = null;
6077
6208
  },
6078
6209
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder));
6210
+ // Render to portal
6079
6211
  m.render(portalElement, dropdownVnode);
6080
6212
  };
6081
6213
  return {
@@ -6118,7 +6250,8 @@ const Select = () => {
6118
6250
  view: ({ attrs }) => {
6119
6251
  var _a;
6120
6252
  const controlled = isControlled(attrs);
6121
- const { disabled } = attrs;
6253
+ const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, disabled, } = attrs;
6254
+ state.isMultiple = multiple;
6122
6255
  // Get selected IDs from props or internal state
6123
6256
  let selectedIds;
6124
6257
  if (controlled) {
@@ -6134,7 +6267,6 @@ const Select = () => {
6134
6267
  // Interactive uncontrolled: use internal state
6135
6268
  selectedIds = state.internalSelectedIds;
6136
6269
  }
6137
- const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, } = attrs;
6138
6270
  const finalClassName = newRow ? `${className} clear` : className;
6139
6271
  const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6140
6272
  // Update portal dropdown when inside modal
@@ -6171,7 +6303,8 @@ const Select = () => {
6171
6303
  },
6172
6304
  }),
6173
6305
  // Dropdown Menu - render inline only when NOT inside modal
6174
- state.isOpen && !state.isInsideModal &&
6306
+ state.isOpen &&
6307
+ !state.isInsideModal &&
6175
6308
  m('ul.dropdown-content.select-dropdown', {
6176
6309
  tabindex: 0,
6177
6310
  oncreate: ({ dom }) => {
@@ -6329,7 +6462,6 @@ const Tabs = () => {
6329
6462
  }
6330
6463
  state.isDragging = false;
6331
6464
  state.translateX = 0;
6332
- // m.redraw();
6333
6465
  };
6334
6466
  /** Initialize active tab - selectedTabId takes precedence, next active property or first available tab */
6335
6467
  const setActiveTabId = (anchoredTabs, selectedTabId) => {
@@ -6356,7 +6488,6 @@ const Tabs = () => {
6356
6488
  },
6357
6489
  oncreate: () => {
6358
6490
  updateIndicator();
6359
- m.redraw();
6360
6491
  },
6361
6492
  view: ({ attrs }) => {
6362
6493
  const { tabWidth, tabs, className, style, swipeable = false } = attrs;
@@ -6490,7 +6621,6 @@ const SearchSelect = () => {
6490
6621
  else {
6491
6622
  // Click outside, close dropdown
6492
6623
  state.isOpen = false;
6493
- m.redraw();
6494
6624
  }
6495
6625
  };
6496
6626
  // Handle keyboard navigation
@@ -6725,9 +6855,7 @@ const SearchSelect = () => {
6725
6855
  ]),
6726
6856
  // No options found message or list of options
6727
6857
  ...(filteredOptions.length === 0 && !showAddNew
6728
- ? [
6729
- m('li.search-select-no-options', texts.noOptionsFound),
6730
- ]
6858
+ ? [m('li.search-select-no-options', texts.noOptionsFound)]
6731
6859
  : []),
6732
6860
  // Add new option item
6733
6861
  ...(showAddNew