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.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
@@ -432,6 +425,103 @@ const Icon = () => ({
432
425
  },
433
426
  });
434
427
 
428
+ /*!
429
+ * Waves Effect for Mithril Materialized
430
+ * Based on Waves v0.6.4 by Alfiana E. Sibuea
431
+ * Adapted for TypeScript and Mithril integration
432
+ */
433
+ class WavesEffect {
434
+ static offset(elem) {
435
+ const rect = elem.getBoundingClientRect();
436
+ return {
437
+ top: rect.top + window.pageYOffset,
438
+ left: rect.left + window.pageXOffset
439
+ };
440
+ }
441
+ static createRipple(e, element) {
442
+ // Disable right click
443
+ if (e.button === 2) {
444
+ return;
445
+ }
446
+ // Create ripple element
447
+ const ripple = document.createElement('div');
448
+ ripple.className = 'waves-ripple';
449
+ // Get click position relative to element
450
+ const pos = this.offset(element);
451
+ const relativeY = e.pageY - pos.top;
452
+ const relativeX = e.pageX - pos.left;
453
+ // Calculate scale based on element size
454
+ const scale = (element.clientWidth / 100) * 10;
455
+ // Set initial ripple position and style
456
+ ripple.style.cssText = `
457
+ top: ${relativeY}px;
458
+ left: ${relativeX}px;
459
+ transform: scale(0);
460
+ opacity: 1;
461
+ `;
462
+ // Add ripple to element
463
+ element.appendChild(ripple);
464
+ // Force reflow and animate
465
+ ripple.offsetHeight;
466
+ ripple.style.transform = `scale(${scale})`;
467
+ ripple.style.opacity = '1';
468
+ // Store reference for cleanup
469
+ ripple.setAttribute('data-created', Date.now().toString());
470
+ }
471
+ static removeRipples(element) {
472
+ const ripples = element.querySelectorAll('.waves-ripple');
473
+ ripples.forEach((ripple) => {
474
+ const created = parseInt(ripple.getAttribute('data-created') || '0');
475
+ const age = Date.now() - created;
476
+ const fadeOut = () => {
477
+ ripple.style.opacity = '0';
478
+ setTimeout(() => {
479
+ if (ripple.parentNode) {
480
+ ripple.parentNode.removeChild(ripple);
481
+ }
482
+ }, this.duration);
483
+ };
484
+ if (age >= 350) {
485
+ fadeOut();
486
+ }
487
+ else {
488
+ setTimeout(fadeOut, 350 - age);
489
+ }
490
+ });
491
+ }
492
+ }
493
+ WavesEffect.duration = 750;
494
+ WavesEffect.onMouseDown = (e) => {
495
+ const element = e.currentTarget;
496
+ if (element && element.classList.contains('waves-effect')) {
497
+ WavesEffect.createRipple(e, element);
498
+ }
499
+ };
500
+ WavesEffect.onMouseUp = (e) => {
501
+ const element = e.currentTarget;
502
+ if (element && element.classList.contains('waves-effect')) {
503
+ WavesEffect.removeRipples(element);
504
+ }
505
+ };
506
+ WavesEffect.onMouseLeave = (e) => {
507
+ const element = e.currentTarget;
508
+ if (element && element.classList.contains('waves-effect')) {
509
+ WavesEffect.removeRipples(element);
510
+ }
511
+ };
512
+ WavesEffect.onTouchStart = (e) => {
513
+ const element = e.currentTarget;
514
+ if (element && element.classList.contains('waves-effect')) {
515
+ WavesEffect.createRipple(e, element);
516
+ }
517
+ };
518
+ WavesEffect.onTouchEnd = (e) => {
519
+ const element = e.currentTarget;
520
+ if (element && element.classList.contains('waves-effect')) {
521
+ WavesEffect.removeRipples(element);
522
+ }
523
+ };
524
+
435
525
  /**
436
526
  * A factory to create new buttons.
437
527
  *
@@ -445,13 +535,18 @@ const ButtonFactory = (element, defaultClassNames, type = '') => {
445
535
  iconName, iconClass, label, className, variant } = attrs, params = __rest(attrs, ["tooltip", "tooltipPosition", "tooltipPostion", "iconName", "iconClass", "label", "className", "variant"]);
446
536
  // Use variant or fallback to factory type
447
537
  const buttonType = variant || type || 'button';
448
- const cn = [tooltip ? 'tooltipped' : '', defaultClassNames, className]
449
- .filter(Boolean)
450
- .join(' ')
451
- .trim();
538
+ const cn = [tooltip ? 'tooltipped' : '', defaultClassNames, className].filter(Boolean).join(' ').trim();
452
539
  // Use tooltipPosition if available, fallback to legacy tooltipPostion
453
540
  const position = tooltipPosition || tooltipPostion || 'top';
454
- 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);
541
+ // Add waves effect event handlers if waves-effect class is present
542
+ const wavesHandlers = cn.includes('waves-effect') ? {
543
+ onmousedown: WavesEffect.onMouseDown,
544
+ onmouseup: WavesEffect.onMouseUp,
545
+ onmouseleave: WavesEffect.onMouseLeave,
546
+ ontouchstart: WavesEffect.onTouchStart,
547
+ ontouchend: WavesEffect.onTouchEnd
548
+ } : {};
549
+ 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);
455
550
  },
456
551
  };
457
552
  };
@@ -460,6 +555,7 @@ const Button = ButtonFactory('a', 'waves-effect waves-light btn', 'button');
460
555
  const LargeButton = ButtonFactory('a', 'waves-effect waves-light btn-large', 'button');
461
556
  const SmallButton = ButtonFactory('a', 'waves-effect waves-light btn-small', 'button');
462
557
  const FlatButton = ButtonFactory('a', 'waves-effect waves-teal btn-flat', 'button');
558
+ const IconButton = ButtonFactory('button', 'btn-flat btn-icon waves-effect waves-teal', 'button');
463
559
  const RoundIconButton = ButtonFactory('button', 'btn-floating btn-large waves-effect waves-light', 'button');
464
560
  const SubmitButton = ButtonFactory('button', 'btn waves-effect waves-light', 'submit');
465
561
 
@@ -896,7 +992,7 @@ const MaterialIcon = () => {
896
992
  };
897
993
  const rotation = (_a = rotationMap[direction]) !== null && _a !== void 0 ? _a : 0;
898
994
  const transform = rotation ? `rotate(${rotation}deg)` : undefined;
899
- 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', {
995
+ 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', {
900
996
  d,
901
997
  fill: d.includes('M0 0h24v24H0z') ? 'none' : 'currentColor',
902
998
  })));
@@ -2380,36 +2476,52 @@ const handleKeyboardNavigation = (key, currentValue, min, max, step) => {
2380
2476
  return null;
2381
2477
  }
2382
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
+ };
2383
2485
  const initRangeState = (state, attrs) => {
2384
- const { min = 0, max = 100, value, minValue, maxValue } = attrs;
2486
+ const { min = 0, max = 100, value, minValue, maxValue, defaultValue } = attrs;
2385
2487
  // Initialize single range value
2386
- if (value !== undefined) {
2387
- 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
2388
2494
  if (state.singleValue === undefined) {
2389
- state.singleValue = currentValue;
2495
+ state.singleValue = defaultValue !== undefined ? defaultValue : value !== undefined ? value : min;
2390
2496
  }
2391
- 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) {
2392
2499
  state.singleValue = value;
2393
2500
  state.lastValue = value;
2394
2501
  }
2395
2502
  }
2396
- else if (state.singleValue === undefined) {
2397
- state.singleValue = min;
2398
- }
2399
2503
  // Initialize range values
2400
- const currentMinValue = minValue !== undefined ? minValue : min;
2401
- const currentMaxValue = maxValue !== undefined ? maxValue : max;
2402
- if (state.rangeMinValue === undefined || state.rangeMaxValue === undefined) {
2403
- state.rangeMinValue = currentMinValue;
2404
- 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;
2405
2508
  }
2406
- if (!state.hasUserInteracted &&
2407
- ((minValue !== undefined && state.lastMinValue !== minValue) ||
2408
- (maxValue !== undefined && state.lastMaxValue !== maxValue))) {
2409
- state.rangeMinValue = currentMinValue;
2410
- state.rangeMaxValue = currentMaxValue;
2411
- state.lastMinValue = minValue;
2412
- 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
+ }
2413
2525
  }
2414
2526
  // Initialize active thumb if not set
2415
2527
  if (state.activeThumb === null) {
@@ -2426,15 +2538,18 @@ const updateRangeValues = (minValue, maxValue, attrs, state, immediate) => {
2426
2538
  minValue = maxValue;
2427
2539
  if (maxValue < minValue)
2428
2540
  maxValue = minValue;
2429
- state.rangeMinValue = minValue;
2430
- state.rangeMaxValue = maxValue;
2541
+ // Only update internal state for uncontrolled mode
2542
+ if (!isRangeControlled(attrs)) {
2543
+ state.rangeMinValue = minValue;
2544
+ state.rangeMaxValue = maxValue;
2545
+ }
2431
2546
  state.hasUserInteracted = true;
2432
- // Call oninput for immediate feedback or onchange for final changes
2547
+ // Call appropriate handler based on interaction type, not control mode
2433
2548
  if (immediate && attrs.oninput) {
2434
- attrs.oninput(minValue, maxValue);
2549
+ attrs.oninput(minValue, maxValue); // Immediate feedback during drag
2435
2550
  }
2436
- else if (!immediate && attrs.onchange) {
2437
- attrs.onchange(minValue, maxValue);
2551
+ if (!immediate && attrs.onchange) {
2552
+ attrs.onchange(minValue, maxValue); // Final value on interaction end (blur/mouseup)
2438
2553
  }
2439
2554
  };
2440
2555
  // Single Range Slider Component
@@ -2468,19 +2583,24 @@ const SingleRangeSlider = {
2468
2583
  : tooltipPos
2469
2584
  : tooltipPos;
2470
2585
  const updateSingleValue = (newValue, immediate = false) => {
2471
- state.singleValue = newValue;
2586
+ // Only update internal state for uncontrolled mode
2587
+ if (!isControlled(attrs)) {
2588
+ state.singleValue = newValue;
2589
+ }
2472
2590
  state.hasUserInteracted = true;
2591
+ // Call appropriate handler based on interaction type, not control mode
2473
2592
  if (immediate && oninput) {
2474
- oninput(newValue);
2593
+ oninput(newValue); // Immediate feedback during drag
2475
2594
  }
2476
- else if (!immediate && onchange) {
2477
- onchange(newValue);
2595
+ if (!immediate && onchange) {
2596
+ onchange(newValue); // Final value on interaction end (blur/mouseup)
2478
2597
  }
2479
2598
  };
2480
2599
  const handleMouseDown = (e) => {
2481
2600
  if (disabled)
2482
2601
  return;
2483
2602
  e.preventDefault();
2603
+ e.stopPropagation();
2484
2604
  state.isDragging = true;
2485
2605
  if (finalValueDisplay === 'auto') {
2486
2606
  m.redraw();
@@ -2555,6 +2675,11 @@ const SingleRangeSlider = {
2555
2675
  updateSingleValue(newValue, false);
2556
2676
  }
2557
2677
  },
2678
+ onblur: () => {
2679
+ if (disabled || !onchange)
2680
+ return;
2681
+ onchange(state.singleValue);
2682
+ },
2558
2683
  }, [
2559
2684
  m(`.track.${orientation}`),
2560
2685
  m(`.range-progress.${orientation}`, { style: progressStyle }),
@@ -2613,6 +2738,7 @@ const DoubleRangeSlider = {
2613
2738
  if (disabled)
2614
2739
  return;
2615
2740
  e.preventDefault();
2741
+ e.stopPropagation();
2616
2742
  state.isDragging = true;
2617
2743
  state.activeThumb = thumb;
2618
2744
  if (finalValueDisplay === 'auto') {
@@ -2703,6 +2829,11 @@ const DoubleRangeSlider = {
2703
2829
  maxThumb.focus();
2704
2830
  }
2705
2831
  },
2832
+ onblur: () => {
2833
+ if (disabled || !attrs.onchange)
2834
+ return;
2835
+ attrs.onchange(state.rangeMinValue, state.rangeMaxValue);
2836
+ },
2706
2837
  }, [
2707
2838
  m(`.track.${orientation}`),
2708
2839
  m(`.range.${orientation}`, { style: rangeStyle }),
@@ -2908,13 +3039,13 @@ const TextArea = () => {
2908
3039
  overflowWrap: 'break-word',
2909
3040
  },
2910
3041
  oncreate: ({ dom }) => {
2911
- const hiddenDiv = state.hiddenDiv = dom;
3042
+ const hiddenDiv = (state.hiddenDiv = dom);
2912
3043
  if (state.textarea) {
2913
3044
  updateHeight(state.textarea, hiddenDiv);
2914
3045
  }
2915
3046
  },
2916
3047
  onupdate: ({ dom }) => {
2917
- const hiddenDiv = state.hiddenDiv = dom;
3048
+ const hiddenDiv = (state.hiddenDiv = dom);
2918
3049
  if (state.textarea) {
2919
3050
  updateHeight(state.textarea, hiddenDiv);
2920
3051
  }
@@ -3027,8 +3158,7 @@ const InputField = (type, defaultClass = '') => () => {
3027
3158
  isDragging: false,
3028
3159
  activeThumb: null,
3029
3160
  };
3030
- const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' &&
3031
- (typeof attrs.oninput === 'function' || typeof attrs.onchange === 'function');
3161
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
3032
3162
  const getValue = (target) => {
3033
3163
  const val = target.value;
3034
3164
  return (val ? (type === 'number' || type === 'range' ? +val : val) : val);
@@ -3078,7 +3208,7 @@ const InputField = (type, defaultClass = '') => () => {
3078
3208
  const isNonInteractive = attrs.readonly || attrs.disabled;
3079
3209
  // Warn developer for improper controlled usage
3080
3210
  if (attrs.value !== undefined && !controlled && !isNonInteractive) {
3081
- 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. ` +
3082
3212
  `Use 'defaultValue' for uncontrolled components or add an event handler for controlled components.`);
3083
3213
  }
3084
3214
  // Initialize internal value if not in controlled mode
@@ -4105,14 +4235,12 @@ const Dropdown = () => {
4105
4235
  inputRef: null,
4106
4236
  dropdownRef: null,
4107
4237
  internalCheckedId: undefined,
4238
+ isInsideModal: false,
4108
4239
  };
4109
4240
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
4110
- const closeDropdown = (e) => {
4111
- const target = e.target;
4112
- if (!target.closest('.dropdown-wrapper.input-field')) {
4113
- state.isOpen = false;
4114
- m.redraw();
4115
- }
4241
+ const closeDropdown = () => {
4242
+ state.isOpen = false;
4243
+ m.redraw(); // Needed to remove the dropdown options list (potentially added to document root)
4116
4244
  };
4117
4245
  const handleKeyDown = (e, items) => {
4118
4246
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -4157,6 +4285,83 @@ const Dropdown = () => {
4157
4285
  return undefined;
4158
4286
  }
4159
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
+ };
4160
4365
  return {
4161
4366
  oninit: ({ attrs }) => {
4162
4367
  var _a;
@@ -4168,9 +4373,18 @@ const Dropdown = () => {
4168
4373
  // Add global click listener to close dropdown
4169
4374
  document.addEventListener('click', closeDropdown);
4170
4375
  },
4376
+ oncreate: ({ dom }) => {
4377
+ // Detect if component is inside a modal
4378
+ state.isInsideModal = !!dom.closest('.modal');
4379
+ },
4171
4380
  onremove: () => {
4172
4381
  // Cleanup global listener
4173
4382
  document.removeEventListener('click', closeDropdown);
4383
+ // Cleanup portal
4384
+ const portalElement = document.getElementById(`${state.id}-dropdown`);
4385
+ if (portalElement) {
4386
+ portalElement.remove();
4387
+ }
4174
4388
  },
4175
4389
  view: ({ attrs }) => {
4176
4390
  const { checkedId, key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12', } = attrs;
@@ -4193,6 +4407,16 @@ const Dropdown = () => {
4193
4407
  : undefined;
4194
4408
  const title = selectedItem ? selectedItem.label : label || 'Select';
4195
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
+ }
4196
4420
  return m('.dropdown-wrapper.input-field', { className, key, style }, [
4197
4421
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
4198
4422
  m(HelperText, { helperText }),
@@ -4226,8 +4450,9 @@ const Dropdown = () => {
4226
4450
  }
4227
4451
  },
4228
4452
  }),
4229
- // Dropdown Menu using Select component's positioning logic
4453
+ // Dropdown Menu - render inline only when NOT inside modal
4230
4454
  state.isOpen &&
4455
+ !state.isInsideModal &&
4231
4456
  m('ul.dropdown-content.select-dropdown', {
4232
4457
  tabindex: 0,
4233
4458
  role: 'listbox',
@@ -4344,12 +4569,17 @@ const FloatingActionButton = () => {
4344
4569
  }
4345
4570
  : undefined,
4346
4571
  }, [
4347
- m('a.btn-floating.btn-large', {
4572
+ m('a.btn-floating.btn-large.waves-effect.waves-light', {
4348
4573
  className,
4574
+ onmousedown: WavesEffect.onMouseDown,
4575
+ onmouseup: WavesEffect.onMouseUp,
4576
+ onmouseleave: WavesEffect.onMouseLeave,
4577
+ ontouchstart: WavesEffect.onTouchStart,
4578
+ ontouchend: WavesEffect.onTouchEnd,
4349
4579
  }, m('i.material-icons', { className: iconClass }, iconName)),
4350
4580
  buttons &&
4351
4581
  buttons.length > 0 &&
4352
- m('ul', buttons.map((button, index) => m('li', m(`a.btn-floating.${button.className || 'red'}`, {
4582
+ m('ul', buttons.map((button, index) => m('li', m(`a.btn-floating.waves-effect.waves-light.${button.className || 'red'}`, {
4353
4583
  style: {
4354
4584
  opacity: state.isOpen ? '1' : '0',
4355
4585
  transform: state.isOpen ? 'scale(1)' : 'scale(0.4)',
@@ -4360,6 +4590,11 @@ const FloatingActionButton = () => {
4360
4590
  if (button.onclick)
4361
4591
  button.onclick(e);
4362
4592
  },
4593
+ onmousedown: WavesEffect.onMouseDown,
4594
+ onmouseup: WavesEffect.onMouseUp,
4595
+ onmouseleave: WavesEffect.onMouseLeave,
4596
+ ontouchstart: WavesEffect.onTouchStart,
4597
+ ontouchend: WavesEffect.onTouchEnd,
4363
4598
  }, m('i.material-icons', { className: button.iconClass }, button.iconName))))),
4364
4599
  ]));
4365
4600
  },
@@ -4667,7 +4902,7 @@ const ModalPanel = () => {
4667
4902
  maxWidth: '75%',
4668
4903
  borderRadius: '4px',
4669
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)' }),
4670
- onclick: (e) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
4905
+ // onclick: (e: Event) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
4671
4906
  }, [
4672
4907
  // Close button
4673
4908
  showCloseButton &&
@@ -5763,6 +5998,7 @@ const Select = () => {
5763
5998
  dropdownRef: null,
5764
5999
  internalSelectedIds: [],
5765
6000
  isInsideModal: false,
6001
+ isMultiple: false,
5766
6002
  };
5767
6003
  const isControlled = (attrs) => attrs.checkedId !== undefined && attrs.onchange !== undefined;
5768
6004
  const isSelected = (id, selectedIds) => {
@@ -5845,10 +6081,18 @@ const Select = () => {
5845
6081
  }
5846
6082
  };
5847
6083
  const closeDropdown = (e) => {
6084
+ console.log('select closeDropdown called');
6085
+ if (!state.isMultiple) {
6086
+ state.isOpen = false;
6087
+ return;
6088
+ }
5848
6089
  const target = e.target;
5849
- 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');
5850
6095
  state.isOpen = false;
5851
- m.redraw();
5852
6096
  }
5853
6097
  };
5854
6098
  const getPortalStyles = (inputRef) => {
@@ -5856,13 +6100,21 @@ const Select = () => {
5856
6100
  return {};
5857
6101
  const rect = inputRef.getBoundingClientRect();
5858
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;
5859
6107
  return {
5860
6108
  position: 'fixed',
5861
- top: `${rect.bottom}px`,
6109
+ top: showAbove ? 'auto' : `${rect.bottom}px`,
6110
+ bottom: showAbove ? `${viewportHeight - rect.top}px` : 'auto',
5862
6111
  left: `${rect.left}px`,
5863
6112
  width: `${rect.width}px`,
5864
6113
  zIndex: 10000, // Higher than modal z-index
5865
- 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,
5866
6118
  };
5867
6119
  };
5868
6120
  const renderDropdownContent = (attrs, selectedIds, multiple, placeholder) => [
@@ -5870,15 +6122,10 @@ const Select = () => {
5870
6122
  // Render ungrouped options first
5871
6123
  attrs.options
5872
6124
  .filter((option) => !option.group)
5873
- .map((option) => m('li', Object.assign({ key: option.id, class: option.disabled
5874
- ? 'disabled'
5875
- : state.focusedIndex === attrs.options.indexOf(option)
5876
- ? 'focused'
5877
- : '' }, (option.disabled
6125
+ .map((option) => m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === attrs.options.indexOf(option) ? 'focused' : '' }, (option.disabled
5878
6126
  ? {}
5879
6127
  : {
5880
- onclick: (e) => {
5881
- e.stopPropagation();
6128
+ onclick: () => {
5882
6129
  toggleOption(option.id, multiple, attrs);
5883
6130
  },
5884
6131
  })), [
@@ -5908,8 +6155,8 @@ const Select = () => {
5908
6155
  return groups;
5909
6156
  }, {}))
5910
6157
  .map(([groupName, groupOptions]) => [
5911
- m('li.optgroup', { key: `group-${groupName}`, tabindex: 0 }, m('span', groupName)),
5912
- ...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
5913
6160
  ? {}
5914
6161
  : {
5915
6162
  onclick: (e) => {
@@ -5936,23 +6183,20 @@ const Select = () => {
5936
6183
  .reduce((acc, val) => acc.concat(val), []),
5937
6184
  ];
5938
6185
  const updatePortalDropdown = (attrs, selectedIds, multiple, placeholder) => {
5939
- var _a;
5940
6186
  if (!state.isInsideModal)
5941
6187
  return;
5942
- let portalElement = document.getElementById(state.dropdownId);
5943
- if (!state.isOpen) {
5944
- // Clean up portal when dropdown is closed
5945
- if (portalElement) {
5946
- m.render(portalElement, []);
5947
- (_a = portalElement.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(portalElement);
5948
- }
5949
- return;
5950
- }
5951
- if (!portalElement) {
5952
- portalElement = document.createElement('div');
5953
- portalElement.id = state.dropdownId;
5954
- document.body.appendChild(portalElement);
6188
+ // Clean up existing portal
6189
+ const existingPortal = document.getElementById(state.dropdownId);
6190
+ if (existingPortal) {
6191
+ existingPortal.remove();
5955
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
5956
6200
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
5957
6201
  tabindex: 0,
5958
6202
  style: getPortalStyles(state.inputRef),
@@ -5963,6 +6207,7 @@ const Select = () => {
5963
6207
  state.dropdownRef = null;
5964
6208
  },
5965
6209
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder));
6210
+ // Render to portal
5966
6211
  m.render(portalElement, dropdownVnode);
5967
6212
  };
5968
6213
  return {
@@ -6005,7 +6250,8 @@ const Select = () => {
6005
6250
  view: ({ attrs }) => {
6006
6251
  var _a;
6007
6252
  const controlled = isControlled(attrs);
6008
- 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;
6009
6255
  // Get selected IDs from props or internal state
6010
6256
  let selectedIds;
6011
6257
  if (controlled) {
@@ -6021,7 +6267,6 @@ const Select = () => {
6021
6267
  // Interactive uncontrolled: use internal state
6022
6268
  selectedIds = state.internalSelectedIds;
6023
6269
  }
6024
- const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, } = attrs;
6025
6270
  const finalClassName = newRow ? `${className} clear` : className;
6026
6271
  const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6027
6272
  // Update portal dropdown when inside modal
@@ -6058,7 +6303,8 @@ const Select = () => {
6058
6303
  },
6059
6304
  }),
6060
6305
  // Dropdown Menu - render inline only when NOT inside modal
6061
- state.isOpen && !state.isInsideModal &&
6306
+ state.isOpen &&
6307
+ !state.isInsideModal &&
6062
6308
  m('ul.dropdown-content.select-dropdown', {
6063
6309
  tabindex: 0,
6064
6310
  oncreate: ({ dom }) => {
@@ -6216,7 +6462,6 @@ const Tabs = () => {
6216
6462
  }
6217
6463
  state.isDragging = false;
6218
6464
  state.translateX = 0;
6219
- // m.redraw();
6220
6465
  };
6221
6466
  /** Initialize active tab - selectedTabId takes precedence, next active property or first available tab */
6222
6467
  const setActiveTabId = (anchoredTabs, selectedTabId) => {
@@ -6243,7 +6488,6 @@ const Tabs = () => {
6243
6488
  },
6244
6489
  oncreate: () => {
6245
6490
  updateIndicator();
6246
- m.redraw();
6247
6491
  },
6248
6492
  view: ({ attrs }) => {
6249
6493
  const { tabWidth, tabs, className, style, swipeable = false } = attrs;
@@ -6377,7 +6621,6 @@ const SearchSelect = () => {
6377
6621
  else {
6378
6622
  // Click outside, close dropdown
6379
6623
  state.isOpen = false;
6380
- m.redraw();
6381
6624
  }
6382
6625
  };
6383
6626
  // Handle keyboard navigation
@@ -6612,9 +6855,7 @@ const SearchSelect = () => {
6612
6855
  ]),
6613
6856
  // No options found message or list of options
6614
6857
  ...(filteredOptions.length === 0 && !showAddNew
6615
- ? [
6616
- m('li.search-select-no-options', texts.noOptionsFound),
6617
- ]
6858
+ ? [m('li.search-select-no-options', texts.noOptionsFound)]
6618
6859
  : []),
6619
6860
  // Add new option item
6620
6861
  ...(showAddNew
@@ -8988,4 +9229,4 @@ const isValidationError = (result) => !isValidationSuccess(result);
8988
9229
  // ============================================================================
8989
9230
  // All types are already exported via individual export declarations above
8990
9231
 
8991
- 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, 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, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, toast, uniqueId, uuid4 };
9232
+ 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, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, toast, uniqueId, uuid4 };