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