mithril-materialized 3.3.8 → 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/dist/index.umd.js CHANGED
@@ -232,8 +232,6 @@
232
232
  if (attrs.onAutocomplete) {
233
233
  attrs.onAutocomplete(suggestion.key);
234
234
  }
235
- // Force redraw to update label state
236
- m.redraw();
237
235
  };
238
236
  const handleKeydown = (e, attrs) => {
239
237
  if (!state.isOpen)
@@ -312,7 +310,7 @@
312
310
  const id = attrs.id || state.id;
313
311
  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"]);
314
312
  const controlled = isControlled(attrs);
315
- const currentValue = controlled ? (attrs.value || '') : state.internalValue;
313
+ const currentValue = controlled ? attrs.value || '' : state.internalValue;
316
314
  const cn = newRow ? className + ' clear' : className;
317
315
  // Update suggestions when input changes
318
316
  state.suggestions = filterSuggestions(currentValue, data, limit, minLength);
@@ -327,7 +325,7 @@
327
325
  style,
328
326
  }, [
329
327
  iconName ? m('i.material-icons.prefix', iconName) : '',
330
- m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: controlled ? currentValue : undefined, oncreate: (vnode) => {
328
+ m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: currentValue, oncreate: (vnode) => {
331
329
  state.inputElement = vnode.dom;
332
330
  // Set initial value for uncontrolled mode
333
331
  if (!controlled && attrs.defaultValue) {
@@ -363,14 +361,10 @@
363
361
  }
364
362
  }, onblur: (e) => {
365
363
  state.isActive = false;
366
- // Delay closing to allow clicks on suggestions
367
- setTimeout(() => {
368
- if (!e.relatedTarget || !e.relatedTarget.closest('.autocomplete-content')) {
369
- state.isOpen = false;
370
- state.selectedIndex = -1;
371
- m.redraw();
372
- }
373
- }, 150);
364
+ if (!e.relatedTarget || !e.relatedTarget.closest('.autocomplete-content')) {
365
+ state.isOpen = false;
366
+ state.selectedIndex = -1;
367
+ }
374
368
  } })),
375
369
  // Autocomplete dropdown
376
370
  state.isOpen &&
@@ -386,7 +380,6 @@
386
380
  },
387
381
  onmouseover: () => {
388
382
  state.selectedIndex = index;
389
- m.redraw();
390
383
  },
391
384
  }, [
392
385
  // Check if value contains image URL or icon
@@ -436,6 +429,103 @@
436
429
  },
437
430
  });
438
431
 
432
+ /*!
433
+ * Waves Effect for Mithril Materialized
434
+ * Based on Waves v0.6.4 by Alfiana E. Sibuea
435
+ * Adapted for TypeScript and Mithril integration
436
+ */
437
+ class WavesEffect {
438
+ static offset(elem) {
439
+ const rect = elem.getBoundingClientRect();
440
+ return {
441
+ top: rect.top + window.pageYOffset,
442
+ left: rect.left + window.pageXOffset
443
+ };
444
+ }
445
+ static createRipple(e, element) {
446
+ // Disable right click
447
+ if (e.button === 2) {
448
+ return;
449
+ }
450
+ // Create ripple element
451
+ const ripple = document.createElement('div');
452
+ ripple.className = 'waves-ripple';
453
+ // Get click position relative to element
454
+ const pos = this.offset(element);
455
+ const relativeY = e.pageY - pos.top;
456
+ const relativeX = e.pageX - pos.left;
457
+ // Calculate scale based on element size
458
+ const scale = (element.clientWidth / 100) * 10;
459
+ // Set initial ripple position and style
460
+ ripple.style.cssText = `
461
+ top: ${relativeY}px;
462
+ left: ${relativeX}px;
463
+ transform: scale(0);
464
+ opacity: 1;
465
+ `;
466
+ // Add ripple to element
467
+ element.appendChild(ripple);
468
+ // Force reflow and animate
469
+ ripple.offsetHeight;
470
+ ripple.style.transform = `scale(${scale})`;
471
+ ripple.style.opacity = '1';
472
+ // Store reference for cleanup
473
+ ripple.setAttribute('data-created', Date.now().toString());
474
+ }
475
+ static removeRipples(element) {
476
+ const ripples = element.querySelectorAll('.waves-ripple');
477
+ ripples.forEach((ripple) => {
478
+ const created = parseInt(ripple.getAttribute('data-created') || '0');
479
+ const age = Date.now() - created;
480
+ const fadeOut = () => {
481
+ ripple.style.opacity = '0';
482
+ setTimeout(() => {
483
+ if (ripple.parentNode) {
484
+ ripple.parentNode.removeChild(ripple);
485
+ }
486
+ }, this.duration);
487
+ };
488
+ if (age >= 350) {
489
+ fadeOut();
490
+ }
491
+ else {
492
+ setTimeout(fadeOut, 350 - age);
493
+ }
494
+ });
495
+ }
496
+ }
497
+ WavesEffect.duration = 750;
498
+ WavesEffect.onMouseDown = (e) => {
499
+ const element = e.currentTarget;
500
+ if (element && element.classList.contains('waves-effect')) {
501
+ WavesEffect.createRipple(e, element);
502
+ }
503
+ };
504
+ WavesEffect.onMouseUp = (e) => {
505
+ const element = e.currentTarget;
506
+ if (element && element.classList.contains('waves-effect')) {
507
+ WavesEffect.removeRipples(element);
508
+ }
509
+ };
510
+ WavesEffect.onMouseLeave = (e) => {
511
+ const element = e.currentTarget;
512
+ if (element && element.classList.contains('waves-effect')) {
513
+ WavesEffect.removeRipples(element);
514
+ }
515
+ };
516
+ WavesEffect.onTouchStart = (e) => {
517
+ const element = e.currentTarget;
518
+ if (element && element.classList.contains('waves-effect')) {
519
+ WavesEffect.createRipple(e, element);
520
+ }
521
+ };
522
+ WavesEffect.onTouchEnd = (e) => {
523
+ const element = e.currentTarget;
524
+ if (element && element.classList.contains('waves-effect')) {
525
+ WavesEffect.removeRipples(element);
526
+ }
527
+ };
528
+
439
529
  /**
440
530
  * A factory to create new buttons.
441
531
  *
@@ -449,13 +539,18 @@
449
539
  iconName, iconClass, label, className, variant } = attrs, params = __rest(attrs, ["tooltip", "tooltipPosition", "tooltipPostion", "iconName", "iconClass", "label", "className", "variant"]);
450
540
  // Use variant or fallback to factory type
451
541
  const buttonType = variant || type || 'button';
452
- const cn = [tooltip ? 'tooltipped' : '', defaultClassNames, className]
453
- .filter(Boolean)
454
- .join(' ')
455
- .trim();
542
+ const cn = [tooltip ? 'tooltipped' : '', defaultClassNames, className].filter(Boolean).join(' ').trim();
456
543
  // Use tooltipPosition if available, fallback to legacy tooltipPostion
457
544
  const position = tooltipPosition || tooltipPostion || 'top';
458
- return m(element, Object.assign(Object.assign({}, params), { className: cn, 'data-position': tooltip ? position : undefined, 'data-tooltip': tooltip || undefined, type: buttonType }), iconName ? m(Icon, { iconName, className: iconClass || 'left' }) : undefined, label ? label : undefined);
545
+ // Add waves effect event handlers if waves-effect class is present
546
+ const wavesHandlers = cn.includes('waves-effect') ? {
547
+ onmousedown: WavesEffect.onMouseDown,
548
+ onmouseup: WavesEffect.onMouseUp,
549
+ onmouseleave: WavesEffect.onMouseLeave,
550
+ ontouchstart: WavesEffect.onTouchStart,
551
+ ontouchend: WavesEffect.onTouchEnd
552
+ } : {};
553
+ return m(element, Object.assign(Object.assign(Object.assign({}, params), wavesHandlers), { className: cn, 'data-position': tooltip ? position : undefined, 'data-tooltip': tooltip || undefined, type: buttonType }), iconName ? m(Icon, { iconName, className: iconClass || 'left' }) : undefined, label ? label : undefined);
459
554
  },
460
555
  };
461
556
  };
@@ -464,6 +559,7 @@
464
559
  const LargeButton = ButtonFactory('a', 'waves-effect waves-light btn-large', 'button');
465
560
  const SmallButton = ButtonFactory('a', 'waves-effect waves-light btn-small', 'button');
466
561
  const FlatButton = ButtonFactory('a', 'waves-effect waves-teal btn-flat', 'button');
562
+ const IconButton = ButtonFactory('button', 'btn-flat btn-icon waves-effect waves-teal', 'button');
467
563
  const RoundIconButton = ButtonFactory('button', 'btn-floating btn-large waves-effect waves-light', 'button');
468
564
  const SubmitButton = ButtonFactory('button', 'btn waves-effect waves-light', 'submit');
469
565
 
@@ -900,7 +996,7 @@
900
996
  };
901
997
  const rotation = (_a = rotationMap[direction]) !== null && _a !== void 0 ? _a : 0;
902
998
  const transform = rotation ? `rotate(${rotation}deg)` : undefined;
903
- return m('svg', Object.assign(Object.assign({}, props), { style: Object.assign({ transform }, style), height: '1lh', width: '24', viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' }), iconPaths[name].map((d) => m('path', {
999
+ return m('svg', Object.assign(Object.assign({}, props), { style: Object.assign({ transform }, style), height: '24px', width: '24px', viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' }), iconPaths[name].map((d) => m('path', {
904
1000
  d,
905
1001
  fill: d.includes('M0 0h24v24H0z') ? 'none' : 'currentColor',
906
1002
  })));
@@ -2384,36 +2480,52 @@
2384
2480
  return null;
2385
2481
  }
2386
2482
  };
2483
+ const isControlled = (attrs) => {
2484
+ return attrs.value !== undefined && typeof attrs.oninput === 'function';
2485
+ };
2486
+ const isRangeControlled = (attrs) => {
2487
+ return (attrs.minValue !== undefined || attrs.maxValue !== undefined) && typeof attrs.oninput === 'function';
2488
+ };
2387
2489
  const initRangeState = (state, attrs) => {
2388
- const { min = 0, max = 100, value, minValue, maxValue } = attrs;
2490
+ const { min = 0, max = 100, value, minValue, maxValue, defaultValue } = attrs;
2389
2491
  // Initialize single range value
2390
- if (value !== undefined) {
2391
- const currentValue = value;
2492
+ if (isControlled(attrs)) {
2493
+ // Always use value from props in controlled mode
2494
+ state.singleValue = value !== undefined ? value : min;
2495
+ }
2496
+ else {
2497
+ // Use internal state for uncontrolled mode
2392
2498
  if (state.singleValue === undefined) {
2393
- state.singleValue = currentValue;
2499
+ state.singleValue = defaultValue !== undefined ? defaultValue : value !== undefined ? value : min;
2394
2500
  }
2395
- if (state.lastValue !== value && !state.hasUserInteracted) {
2501
+ // Only update internal state if props changed and user hasn't interacted
2502
+ if (state.lastValue !== value && !state.hasUserInteracted && value !== undefined) {
2396
2503
  state.singleValue = value;
2397
2504
  state.lastValue = value;
2398
2505
  }
2399
2506
  }
2400
- else if (state.singleValue === undefined) {
2401
- state.singleValue = min;
2402
- }
2403
2507
  // Initialize range values
2404
- const currentMinValue = minValue !== undefined ? minValue : min;
2405
- const currentMaxValue = maxValue !== undefined ? maxValue : max;
2406
- if (state.rangeMinValue === undefined || state.rangeMaxValue === undefined) {
2407
- state.rangeMinValue = currentMinValue;
2408
- state.rangeMaxValue = currentMaxValue;
2508
+ if (isRangeControlled(attrs)) {
2509
+ // Always use values from props in controlled mode
2510
+ state.rangeMinValue = minValue !== undefined ? minValue : min;
2511
+ state.rangeMaxValue = maxValue !== undefined ? maxValue : max;
2409
2512
  }
2410
- if (!state.hasUserInteracted &&
2411
- ((minValue !== undefined && state.lastMinValue !== minValue) ||
2412
- (maxValue !== undefined && state.lastMaxValue !== maxValue))) {
2413
- state.rangeMinValue = currentMinValue;
2414
- state.rangeMaxValue = currentMaxValue;
2415
- state.lastMinValue = minValue;
2416
- state.lastMaxValue = maxValue;
2513
+ else {
2514
+ // Use internal state for uncontrolled mode
2515
+ const currentMinValue = minValue !== undefined ? minValue : min;
2516
+ const currentMaxValue = maxValue !== undefined ? maxValue : max;
2517
+ if (state.rangeMinValue === undefined || state.rangeMaxValue === undefined) {
2518
+ state.rangeMinValue = currentMinValue;
2519
+ state.rangeMaxValue = currentMaxValue;
2520
+ }
2521
+ if (!state.hasUserInteracted &&
2522
+ ((minValue !== undefined && state.lastMinValue !== minValue) ||
2523
+ (maxValue !== undefined && state.lastMaxValue !== maxValue))) {
2524
+ state.rangeMinValue = currentMinValue;
2525
+ state.rangeMaxValue = currentMaxValue;
2526
+ state.lastMinValue = minValue;
2527
+ state.lastMaxValue = maxValue;
2528
+ }
2417
2529
  }
2418
2530
  // Initialize active thumb if not set
2419
2531
  if (state.activeThumb === null) {
@@ -2430,15 +2542,18 @@
2430
2542
  minValue = maxValue;
2431
2543
  if (maxValue < minValue)
2432
2544
  maxValue = minValue;
2433
- state.rangeMinValue = minValue;
2434
- state.rangeMaxValue = maxValue;
2545
+ // Only update internal state for uncontrolled mode
2546
+ if (!isRangeControlled(attrs)) {
2547
+ state.rangeMinValue = minValue;
2548
+ state.rangeMaxValue = maxValue;
2549
+ }
2435
2550
  state.hasUserInteracted = true;
2436
- // Call oninput for immediate feedback or onchange for final changes
2551
+ // Call appropriate handler based on interaction type, not control mode
2437
2552
  if (immediate && attrs.oninput) {
2438
- attrs.oninput(minValue, maxValue);
2553
+ attrs.oninput(minValue, maxValue); // Immediate feedback during drag
2439
2554
  }
2440
- else if (!immediate && attrs.onchange) {
2441
- attrs.onchange(minValue, maxValue);
2555
+ if (!immediate && attrs.onchange) {
2556
+ attrs.onchange(minValue, maxValue); // Final value on interaction end (blur/mouseup)
2442
2557
  }
2443
2558
  };
2444
2559
  // Single Range Slider Component
@@ -2472,19 +2587,24 @@
2472
2587
  : tooltipPos
2473
2588
  : tooltipPos;
2474
2589
  const updateSingleValue = (newValue, immediate = false) => {
2475
- state.singleValue = newValue;
2590
+ // Only update internal state for uncontrolled mode
2591
+ if (!isControlled(attrs)) {
2592
+ state.singleValue = newValue;
2593
+ }
2476
2594
  state.hasUserInteracted = true;
2595
+ // Call appropriate handler based on interaction type, not control mode
2477
2596
  if (immediate && oninput) {
2478
- oninput(newValue);
2597
+ oninput(newValue); // Immediate feedback during drag
2479
2598
  }
2480
- else if (!immediate && onchange) {
2481
- onchange(newValue);
2599
+ if (!immediate && onchange) {
2600
+ onchange(newValue); // Final value on interaction end (blur/mouseup)
2482
2601
  }
2483
2602
  };
2484
2603
  const handleMouseDown = (e) => {
2485
2604
  if (disabled)
2486
2605
  return;
2487
2606
  e.preventDefault();
2607
+ e.stopPropagation();
2488
2608
  state.isDragging = true;
2489
2609
  if (finalValueDisplay === 'auto') {
2490
2610
  m.redraw();
@@ -2559,6 +2679,11 @@
2559
2679
  updateSingleValue(newValue, false);
2560
2680
  }
2561
2681
  },
2682
+ onblur: () => {
2683
+ if (disabled || !onchange)
2684
+ return;
2685
+ onchange(state.singleValue);
2686
+ },
2562
2687
  }, [
2563
2688
  m(`.track.${orientation}`),
2564
2689
  m(`.range-progress.${orientation}`, { style: progressStyle }),
@@ -2617,6 +2742,7 @@
2617
2742
  if (disabled)
2618
2743
  return;
2619
2744
  e.preventDefault();
2745
+ e.stopPropagation();
2620
2746
  state.isDragging = true;
2621
2747
  state.activeThumb = thumb;
2622
2748
  if (finalValueDisplay === 'auto') {
@@ -2707,6 +2833,11 @@
2707
2833
  maxThumb.focus();
2708
2834
  }
2709
2835
  },
2836
+ onblur: () => {
2837
+ if (disabled || !attrs.onchange)
2838
+ return;
2839
+ attrs.onchange(state.rangeMinValue, state.rangeMaxValue);
2840
+ },
2710
2841
  }, [
2711
2842
  m(`.track.${orientation}`),
2712
2843
  m(`.range.${orientation}`, { style: rangeStyle }),
@@ -2912,13 +3043,13 @@
2912
3043
  overflowWrap: 'break-word',
2913
3044
  },
2914
3045
  oncreate: ({ dom }) => {
2915
- const hiddenDiv = state.hiddenDiv = dom;
3046
+ const hiddenDiv = (state.hiddenDiv = dom);
2916
3047
  if (state.textarea) {
2917
3048
  updateHeight(state.textarea, hiddenDiv);
2918
3049
  }
2919
3050
  },
2920
3051
  onupdate: ({ dom }) => {
2921
- const hiddenDiv = state.hiddenDiv = dom;
3052
+ const hiddenDiv = (state.hiddenDiv = dom);
2922
3053
  if (state.textarea) {
2923
3054
  updateHeight(state.textarea, hiddenDiv);
2924
3055
  }
@@ -3031,8 +3162,7 @@
3031
3162
  isDragging: false,
3032
3163
  activeThumb: null,
3033
3164
  };
3034
- const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' &&
3035
- (typeof attrs.oninput === 'function' || typeof attrs.onchange === 'function');
3165
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
3036
3166
  const getValue = (target) => {
3037
3167
  const val = target.value;
3038
3168
  return (val ? (type === 'number' || type === 'range' ? +val : val) : val);
@@ -3082,7 +3212,7 @@
3082
3212
  const isNonInteractive = attrs.readonly || attrs.disabled;
3083
3213
  // Warn developer for improper controlled usage
3084
3214
  if (attrs.value !== undefined && !controlled && !isNonInteractive) {
3085
- console.warn(`${type} input received 'value' prop without 'oninput' or 'onchange' handler. ` +
3215
+ console.warn(`${type} input with label '${attrs.label}' received 'value' prop without 'oninput' handler. ` +
3086
3216
  `Use 'defaultValue' for uncontrolled components or add an event handler for controlled components.`);
3087
3217
  }
3088
3218
  // Initialize internal value if not in controlled mode
@@ -4109,14 +4239,12 @@
4109
4239
  inputRef: null,
4110
4240
  dropdownRef: null,
4111
4241
  internalCheckedId: undefined,
4242
+ isInsideModal: false,
4112
4243
  };
4113
4244
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
4114
- const closeDropdown = (e) => {
4115
- const target = e.target;
4116
- if (!target.closest('.dropdown-wrapper.input-field')) {
4117
- state.isOpen = false;
4118
- m.redraw();
4119
- }
4245
+ const closeDropdown = () => {
4246
+ state.isOpen = false;
4247
+ m.redraw(); // Needed to remove the dropdown options list (potentially added to document root)
4120
4248
  };
4121
4249
  const handleKeyDown = (e, items) => {
4122
4250
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -4161,6 +4289,83 @@
4161
4289
  return undefined;
4162
4290
  }
4163
4291
  };
4292
+ const getPortalStyles = (inputRef) => {
4293
+ if (!inputRef)
4294
+ return {};
4295
+ const rect = inputRef.getBoundingClientRect();
4296
+ const viewportHeight = window.innerHeight;
4297
+ const spaceBelow = viewportHeight - rect.bottom;
4298
+ const spaceAbove = rect.top;
4299
+ // Choose whether to show above or below based on available space
4300
+ const showAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
4301
+ return {
4302
+ position: 'fixed',
4303
+ top: showAbove ? 'auto' : `${rect.bottom}px`,
4304
+ bottom: showAbove ? `${viewportHeight - rect.top}px` : 'auto',
4305
+ left: `${rect.left}px`,
4306
+ width: `${rect.width}px`,
4307
+ zIndex: 10000,
4308
+ maxHeight: showAbove ? `${spaceAbove - 20}px` : `${spaceBelow - 20}px`,
4309
+ overflow: 'auto',
4310
+ display: 'block',
4311
+ opacity: 1,
4312
+ };
4313
+ };
4314
+ const updatePortalDropdown = (items, selectedLabel, onSelectItem) => {
4315
+ if (!state.isInsideModal)
4316
+ return;
4317
+ // Clean up existing portal
4318
+ const existingPortal = document.getElementById(`${state.id}-dropdown`);
4319
+ if (existingPortal) {
4320
+ existingPortal.remove();
4321
+ }
4322
+ if (!state.isOpen || !state.inputRef)
4323
+ return;
4324
+ // Create portal element
4325
+ const portalElement = document.createElement('div');
4326
+ portalElement.id = `${state.id}-dropdown`;
4327
+ document.body.appendChild(portalElement);
4328
+ // Create dropdown content
4329
+ const availableItems = items.filter((item) => !item.divider && !item.disabled);
4330
+ const dropdownContent = items.map((item) => {
4331
+ if (item.divider) {
4332
+ return m('li.divider');
4333
+ }
4334
+ const itemIndex = availableItems.indexOf(item);
4335
+ const isSelected = selectedLabel === item.label;
4336
+ const isFocused = state.focusedIndex === itemIndex;
4337
+ return m('li', {
4338
+ class: `${isSelected ? 'selected' : ''} ${isFocused ? 'focused' : ''}${item.disabled ? ' disabled' : ''}`,
4339
+ onclick: item.disabled ? undefined : () => onSelectItem(item),
4340
+ }, m('span', {
4341
+ style: {
4342
+ display: 'flex',
4343
+ alignItems: 'center',
4344
+ padding: '14px 16px',
4345
+ },
4346
+ }, [
4347
+ item.iconName
4348
+ ? m('i.material-icons', {
4349
+ style: { marginRight: '32px' },
4350
+ }, item.iconName)
4351
+ : undefined,
4352
+ item.label,
4353
+ ]));
4354
+ });
4355
+ // Create dropdown with proper positioning
4356
+ const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4357
+ tabindex: 0,
4358
+ style: getPortalStyles(state.inputRef),
4359
+ oncreate: ({ dom }) => {
4360
+ state.dropdownRef = dom;
4361
+ },
4362
+ onremove: () => {
4363
+ state.dropdownRef = null;
4364
+ },
4365
+ }, dropdownContent);
4366
+ // Render to portal
4367
+ m.render(portalElement, dropdownVnode);
4368
+ };
4164
4369
  return {
4165
4370
  oninit: ({ attrs }) => {
4166
4371
  var _a;
@@ -4172,9 +4377,18 @@
4172
4377
  // Add global click listener to close dropdown
4173
4378
  document.addEventListener('click', closeDropdown);
4174
4379
  },
4380
+ oncreate: ({ dom }) => {
4381
+ // Detect if component is inside a modal
4382
+ state.isInsideModal = !!dom.closest('.modal');
4383
+ },
4175
4384
  onremove: () => {
4176
4385
  // Cleanup global listener
4177
4386
  document.removeEventListener('click', closeDropdown);
4387
+ // Cleanup portal
4388
+ const portalElement = document.getElementById(`${state.id}-dropdown`);
4389
+ if (portalElement) {
4390
+ portalElement.remove();
4391
+ }
4178
4392
  },
4179
4393
  view: ({ attrs }) => {
4180
4394
  const { checkedId, key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12', } = attrs;
@@ -4197,6 +4411,16 @@
4197
4411
  : undefined;
4198
4412
  const title = selectedItem ? selectedItem.label : label || 'Select';
4199
4413
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
4414
+ // Update portal dropdown when inside modal
4415
+ if (state.isInsideModal) {
4416
+ updatePortalDropdown(items, title, (item) => {
4417
+ if (item.id) {
4418
+ state.isOpen = false;
4419
+ state.focusedIndex = -1;
4420
+ handleSelection(item.id);
4421
+ }
4422
+ });
4423
+ }
4200
4424
  return m('.dropdown-wrapper.input-field', { className, key, style }, [
4201
4425
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
4202
4426
  m(HelperText, { helperText }),
@@ -4230,8 +4454,9 @@
4230
4454
  }
4231
4455
  },
4232
4456
  }),
4233
- // Dropdown Menu using Select component's positioning logic
4457
+ // Dropdown Menu - render inline only when NOT inside modal
4234
4458
  state.isOpen &&
4459
+ !state.isInsideModal &&
4235
4460
  m('ul.dropdown-content.select-dropdown', {
4236
4461
  tabindex: 0,
4237
4462
  role: 'listbox',
@@ -4348,12 +4573,17 @@
4348
4573
  }
4349
4574
  : undefined,
4350
4575
  }, [
4351
- m('a.btn-floating.btn-large', {
4576
+ m('a.btn-floating.btn-large.waves-effect.waves-light', {
4352
4577
  className,
4578
+ onmousedown: WavesEffect.onMouseDown,
4579
+ onmouseup: WavesEffect.onMouseUp,
4580
+ onmouseleave: WavesEffect.onMouseLeave,
4581
+ ontouchstart: WavesEffect.onTouchStart,
4582
+ ontouchend: WavesEffect.onTouchEnd,
4353
4583
  }, m('i.material-icons', { className: iconClass }, iconName)),
4354
4584
  buttons &&
4355
4585
  buttons.length > 0 &&
4356
- m('ul', buttons.map((button, index) => m('li', m(`a.btn-floating.${button.className || 'red'}`, {
4586
+ m('ul', buttons.map((button, index) => m('li', m(`a.btn-floating.waves-effect.waves-light.${button.className || 'red'}`, {
4357
4587
  style: {
4358
4588
  opacity: state.isOpen ? '1' : '0',
4359
4589
  transform: state.isOpen ? 'scale(1)' : 'scale(0.4)',
@@ -4364,6 +4594,11 @@
4364
4594
  if (button.onclick)
4365
4595
  button.onclick(e);
4366
4596
  },
4597
+ onmousedown: WavesEffect.onMouseDown,
4598
+ onmouseup: WavesEffect.onMouseUp,
4599
+ onmouseleave: WavesEffect.onMouseLeave,
4600
+ ontouchstart: WavesEffect.onTouchStart,
4601
+ ontouchend: WavesEffect.onTouchEnd,
4367
4602
  }, m('i.material-icons', { className: button.iconClass }, button.iconName))))),
4368
4603
  ]));
4369
4604
  },
@@ -4671,7 +4906,7 @@
4671
4906
  maxWidth: '75%',
4672
4907
  borderRadius: '4px',
4673
4908
  })), { 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)' }),
4674
- onclick: (e) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
4909
+ // onclick: (e: Event) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
4675
4910
  }, [
4676
4911
  // Close button
4677
4912
  showCloseButton &&
@@ -5767,6 +6002,7 @@
5767
6002
  dropdownRef: null,
5768
6003
  internalSelectedIds: [],
5769
6004
  isInsideModal: false,
6005
+ isMultiple: false,
5770
6006
  };
5771
6007
  const isControlled = (attrs) => attrs.checkedId !== undefined && attrs.onchange !== undefined;
5772
6008
  const isSelected = (id, selectedIds) => {
@@ -5849,10 +6085,18 @@
5849
6085
  }
5850
6086
  };
5851
6087
  const closeDropdown = (e) => {
6088
+ console.log('select closeDropdown called');
6089
+ if (!state.isMultiple) {
6090
+ state.isOpen = false;
6091
+ return;
6092
+ }
5852
6093
  const target = e.target;
5853
- if (!target.closest('.input-field.select-space')) {
6094
+ // When inside modal, check both the select component AND the portaled dropdown
6095
+ const isClickInsideSelect = target.closest('.input-field.select-space');
6096
+ const isClickInsidePortalDropdown = state.isInsideModal && state.dropdownRef && (state.dropdownRef.contains(target) || target === state.dropdownRef);
6097
+ if (!isClickInsideSelect && !isClickInsidePortalDropdown) {
6098
+ console.log('select closeDropdown called: set state');
5854
6099
  state.isOpen = false;
5855
- m.redraw();
5856
6100
  }
5857
6101
  };
5858
6102
  const getPortalStyles = (inputRef) => {
@@ -5860,13 +6104,21 @@
5860
6104
  return {};
5861
6105
  const rect = inputRef.getBoundingClientRect();
5862
6106
  const viewportHeight = window.innerHeight;
6107
+ const spaceBelow = viewportHeight - rect.bottom;
6108
+ const spaceAbove = rect.top;
6109
+ // Choose whether to show above or below based on available space
6110
+ const showAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
5863
6111
  return {
5864
6112
  position: 'fixed',
5865
- top: `${rect.bottom}px`,
6113
+ top: showAbove ? 'auto' : `${rect.bottom}px`,
6114
+ bottom: showAbove ? `${viewportHeight - rect.top}px` : 'auto',
5866
6115
  left: `${rect.left}px`,
5867
6116
  width: `${rect.width}px`,
5868
6117
  zIndex: 10000, // Higher than modal z-index
5869
- maxHeight: `${viewportHeight - rect.bottom - 20}px`, // Leave 20px margin from bottom
6118
+ maxHeight: showAbove ? `${spaceAbove - 20}px` : `${spaceBelow - 20}px`, // Leave 20px margin
6119
+ overflow: 'auto',
6120
+ display: 'block',
6121
+ opacity: 1,
5870
6122
  };
5871
6123
  };
5872
6124
  const renderDropdownContent = (attrs, selectedIds, multiple, placeholder) => [
@@ -5874,15 +6126,10 @@
5874
6126
  // Render ungrouped options first
5875
6127
  attrs.options
5876
6128
  .filter((option) => !option.group)
5877
- .map((option) => m('li', Object.assign({ key: option.id, class: option.disabled
5878
- ? 'disabled'
5879
- : state.focusedIndex === attrs.options.indexOf(option)
5880
- ? 'focused'
5881
- : '' }, (option.disabled
6129
+ .map((option) => m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === attrs.options.indexOf(option) ? 'focused' : '' }, (option.disabled
5882
6130
  ? {}
5883
6131
  : {
5884
- onclick: (e) => {
5885
- e.stopPropagation();
6132
+ onclick: () => {
5886
6133
  toggleOption(option.id, multiple, attrs);
5887
6134
  },
5888
6135
  })), [
@@ -5912,8 +6159,8 @@
5912
6159
  return groups;
5913
6160
  }, {}))
5914
6161
  .map(([groupName, groupOptions]) => [
5915
- m('li.optgroup', { key: `group-${groupName}`, tabindex: 0 }, m('span', groupName)),
5916
- ...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
6162
+ m('li.optgroup', { tabindex: 0 }, m('span', groupName)),
6163
+ ...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
5917
6164
  ? {}
5918
6165
  : {
5919
6166
  onclick: (e) => {
@@ -5940,23 +6187,20 @@
5940
6187
  .reduce((acc, val) => acc.concat(val), []),
5941
6188
  ];
5942
6189
  const updatePortalDropdown = (attrs, selectedIds, multiple, placeholder) => {
5943
- var _a;
5944
6190
  if (!state.isInsideModal)
5945
6191
  return;
5946
- let portalElement = document.getElementById(state.dropdownId);
5947
- if (!state.isOpen) {
5948
- // Clean up portal when dropdown is closed
5949
- if (portalElement) {
5950
- m.render(portalElement, []);
5951
- (_a = portalElement.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(portalElement);
5952
- }
5953
- return;
5954
- }
5955
- if (!portalElement) {
5956
- portalElement = document.createElement('div');
5957
- portalElement.id = state.dropdownId;
5958
- document.body.appendChild(portalElement);
6192
+ // Clean up existing portal
6193
+ const existingPortal = document.getElementById(state.dropdownId);
6194
+ if (existingPortal) {
6195
+ existingPortal.remove();
5959
6196
  }
6197
+ if (!state.isOpen || !state.inputRef)
6198
+ return;
6199
+ // Create portal element
6200
+ const portalElement = document.createElement('div');
6201
+ portalElement.id = state.dropdownId;
6202
+ document.body.appendChild(portalElement);
6203
+ // Create dropdown with proper positioning
5960
6204
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
5961
6205
  tabindex: 0,
5962
6206
  style: getPortalStyles(state.inputRef),
@@ -5967,6 +6211,7 @@
5967
6211
  state.dropdownRef = null;
5968
6212
  },
5969
6213
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder));
6214
+ // Render to portal
5970
6215
  m.render(portalElement, dropdownVnode);
5971
6216
  };
5972
6217
  return {
@@ -6009,7 +6254,8 @@
6009
6254
  view: ({ attrs }) => {
6010
6255
  var _a;
6011
6256
  const controlled = isControlled(attrs);
6012
- const { disabled } = attrs;
6257
+ const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, disabled, } = attrs;
6258
+ state.isMultiple = multiple;
6013
6259
  // Get selected IDs from props or internal state
6014
6260
  let selectedIds;
6015
6261
  if (controlled) {
@@ -6025,7 +6271,6 @@
6025
6271
  // Interactive uncontrolled: use internal state
6026
6272
  selectedIds = state.internalSelectedIds;
6027
6273
  }
6028
- const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, } = attrs;
6029
6274
  const finalClassName = newRow ? `${className} clear` : className;
6030
6275
  const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6031
6276
  // Update portal dropdown when inside modal
@@ -6062,7 +6307,8 @@
6062
6307
  },
6063
6308
  }),
6064
6309
  // Dropdown Menu - render inline only when NOT inside modal
6065
- state.isOpen && !state.isInsideModal &&
6310
+ state.isOpen &&
6311
+ !state.isInsideModal &&
6066
6312
  m('ul.dropdown-content.select-dropdown', {
6067
6313
  tabindex: 0,
6068
6314
  oncreate: ({ dom }) => {
@@ -6220,7 +6466,6 @@
6220
6466
  }
6221
6467
  state.isDragging = false;
6222
6468
  state.translateX = 0;
6223
- // m.redraw();
6224
6469
  };
6225
6470
  /** Initialize active tab - selectedTabId takes precedence, next active property or first available tab */
6226
6471
  const setActiveTabId = (anchoredTabs, selectedTabId) => {
@@ -6247,7 +6492,6 @@
6247
6492
  },
6248
6493
  oncreate: () => {
6249
6494
  updateIndicator();
6250
- m.redraw();
6251
6495
  },
6252
6496
  view: ({ attrs }) => {
6253
6497
  const { tabWidth, tabs, className, style, swipeable = false } = attrs;
@@ -6381,7 +6625,6 @@
6381
6625
  else {
6382
6626
  // Click outside, close dropdown
6383
6627
  state.isOpen = false;
6384
- m.redraw();
6385
6628
  }
6386
6629
  };
6387
6630
  // Handle keyboard navigation
@@ -6616,9 +6859,7 @@
6616
6859
  ]),
6617
6860
  // No options found message or list of options
6618
6861
  ...(filteredOptions.length === 0 && !showAddNew
6619
- ? [
6620
- m('li.search-select-no-options', texts.noOptionsFound),
6621
- ]
6862
+ ? [m('li.search-select-no-options', texts.noOptionsFound)]
6622
6863
  : []),
6623
6864
  // Add new option item
6624
6865
  ...(showAddNew
@@ -9017,6 +9258,7 @@
9017
9258
  exports.FloatingActionButton = FloatingActionButton;
9018
9259
  exports.HelperText = HelperText;
9019
9260
  exports.Icon = Icon;
9261
+ exports.IconButton = IconButton;
9020
9262
  exports.ImageList = ImageList;
9021
9263
  exports.InputCheckbox = InputCheckbox;
9022
9264
  exports.Label = Label;