mithril-materialized 3.5.5 → 3.5.7

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
@@ -161,6 +161,76 @@ const getDropdownStyles = (inputRef, overlap = false, options, isDropDown = fals
161
161
  * @example: console.log(range(5, 10)); // [5, 6, 7, 8, 9, 10]
162
162
  */
163
163
  const range = (a, b) => Array.from({ length: b - a + 1 }, (_, i) => a + i);
164
+ // Global registry for portal containers
165
+ const portalContainers = new Map();
166
+ /**
167
+ * Creates or retrieves a portal container appended to document.body.
168
+ * Uses reference counting to manage container lifecycle.
169
+ *
170
+ * @param id - Unique identifier for the portal container
171
+ * @param zIndex - Z-index for the portal container (default: 1004, above modals at 1003)
172
+ * @returns The portal container element
173
+ */
174
+ const getPortalContainer = (id, zIndex = 1004) => {
175
+ let container = portalContainers.get(id);
176
+ if (!container) {
177
+ const element = document.createElement('div');
178
+ element.id = id;
179
+ element.style.position = 'fixed';
180
+ element.style.top = '0';
181
+ element.style.left = '0';
182
+ element.style.width = '100%';
183
+ element.style.height = '100%';
184
+ element.style.pointerEvents = 'none'; // Allow clicks through to underlying elements
185
+ element.style.zIndex = zIndex.toString();
186
+ document.body.appendChild(element);
187
+ container = { element, refCount: 0 };
188
+ portalContainers.set(id, container);
189
+ }
190
+ container.refCount++;
191
+ return container.element;
192
+ };
193
+ /**
194
+ * Decrements reference count and removes portal container if no longer needed.
195
+ *
196
+ * @param id - Portal container identifier
197
+ */
198
+ const releasePortalContainer = (id) => {
199
+ const container = portalContainers.get(id);
200
+ if (container) {
201
+ container.refCount--;
202
+ if (container.refCount <= 0) {
203
+ container.element.remove();
204
+ portalContainers.delete(id);
205
+ }
206
+ }
207
+ };
208
+ /**
209
+ * Renders a Mithril vnode into a portal container using m.render().
210
+ * This allows components to render outside their parent DOM hierarchy,
211
+ * useful for modals and pickers that need to escape stacking contexts.
212
+ *
213
+ * @param containerId - Portal container identifier
214
+ * @param vnode - Mithril vnode to render
215
+ * @param zIndex - Z-index for portal container (default: 1004)
216
+ */
217
+ const renderToPortal = (containerId, vnode, zIndex = 1004) => {
218
+ const container = getPortalContainer(containerId, zIndex);
219
+ m.render(container, vnode);
220
+ };
221
+ /**
222
+ * Clears portal content and releases container reference.
223
+ * If this is the last reference, the container will be removed from the DOM.
224
+ *
225
+ * @param containerId - Portal container identifier
226
+ */
227
+ const clearPortal = (containerId) => {
228
+ const container = portalContainers.get(containerId);
229
+ if (container) {
230
+ m.render(container.element, null);
231
+ releasePortalContainer(containerId);
232
+ }
233
+ };
164
234
 
165
235
  // import './styles/input.css';
166
236
  const Mandatory = { view: ({ attrs }) => m('span.mandatory', Object.assign({}, attrs), '*') };
@@ -2189,6 +2259,129 @@ const DatePicker = () => {
2189
2259
  }
2190
2260
  m.redraw();
2191
2261
  };
2262
+ const handleKeyDown = (e) => {
2263
+ if (e.key === 'Escape' && state.isOpen) {
2264
+ state.isOpen = false;
2265
+ const options = mergeOptions({});
2266
+ if (options.onClose)
2267
+ options.onClose();
2268
+ clearPortal(state.portalContainerId);
2269
+ m.redraw();
2270
+ }
2271
+ };
2272
+ const renderPickerToPortal = (attrs) => {
2273
+ const options = mergeOptions(attrs);
2274
+ const pickerModal = m('.datepicker-modal-wrapper', {
2275
+ style: {
2276
+ position: 'fixed',
2277
+ top: '0',
2278
+ left: '0',
2279
+ width: '100%',
2280
+ height: '100%',
2281
+ pointerEvents: 'auto',
2282
+ display: 'flex',
2283
+ alignItems: 'center',
2284
+ justifyContent: 'center',
2285
+ },
2286
+ }, [
2287
+ // Modal overlay
2288
+ m('.modal-overlay', {
2289
+ style: {
2290
+ position: 'absolute',
2291
+ top: '0',
2292
+ left: '0',
2293
+ width: '100%',
2294
+ height: '100%',
2295
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2296
+ zIndex: '1002',
2297
+ },
2298
+ onclick: () => {
2299
+ state.isOpen = false;
2300
+ if (options.onClose)
2301
+ options.onClose();
2302
+ m.redraw();
2303
+ },
2304
+ }),
2305
+ // Modal content
2306
+ m('.modal.datepicker-modal.open', {
2307
+ id: `modal-${state.id}`,
2308
+ tabindex: 0,
2309
+ style: {
2310
+ position: 'relative',
2311
+ zIndex: '1003',
2312
+ display: 'block',
2313
+ opacity: 1,
2314
+ top: 'auto',
2315
+ transform: 'scaleX(1) scaleY(1)',
2316
+ margin: '0 auto',
2317
+ },
2318
+ }, [
2319
+ m('.modal-content.datepicker-container', {
2320
+ onclick: (e) => {
2321
+ const target = e.target;
2322
+ if (!target.closest('.select-wrapper') && !target.closest('.dropdown-content')) {
2323
+ state.monthDropdownOpen = false;
2324
+ state.yearDropdownOpen = false;
2325
+ }
2326
+ },
2327
+ }, [
2328
+ m(DateDisplay, { options }),
2329
+ m('.datepicker-calendar-container', [
2330
+ m('.datepicker-calendar', [
2331
+ m(DateControls, {
2332
+ options,
2333
+ randId: `datepicker-title-${Math.random().toString(36).slice(2)}`,
2334
+ }),
2335
+ m(Calendar, {
2336
+ year: state.calendars[0].year,
2337
+ month: state.calendars[0].month,
2338
+ options,
2339
+ }),
2340
+ ]),
2341
+ m('.datepicker-footer', [
2342
+ options.showClearBtn &&
2343
+ m('button.btn-flat.datepicker-clear.waves-effect', {
2344
+ type: 'button',
2345
+ onclick: () => {
2346
+ setDate(null, false, options);
2347
+ state.isOpen = false;
2348
+ },
2349
+ }, options.i18n.clear),
2350
+ m('button.btn-flat.datepicker-cancel.waves-effect', {
2351
+ type: 'button',
2352
+ onclick: () => {
2353
+ state.isOpen = false;
2354
+ if (options.onClose)
2355
+ options.onClose();
2356
+ },
2357
+ }, options.i18n.cancel),
2358
+ m('button.btn-flat.datepicker-done.waves-effect', {
2359
+ type: 'button',
2360
+ onclick: () => {
2361
+ state.isOpen = false;
2362
+ if (options.dateRange) {
2363
+ if (state.startDate && state.endDate && attrs.onchange) {
2364
+ const startStr = formatDate(state.startDate, 'yyyy-mm-dd', options);
2365
+ const endStr = formatDate(state.endDate, 'yyyy-mm-dd', options);
2366
+ attrs.onchange(`${startStr} - ${endStr}`);
2367
+ }
2368
+ }
2369
+ else {
2370
+ if (state.date && attrs.onchange) {
2371
+ attrs.onchange(toString(state.date, 'yyyy-mm-dd'));
2372
+ }
2373
+ }
2374
+ if (options.onClose)
2375
+ options.onClose();
2376
+ },
2377
+ }, options.i18n.done),
2378
+ ]),
2379
+ ]),
2380
+ ]),
2381
+ ]),
2382
+ ]);
2383
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
2384
+ };
2192
2385
  return {
2193
2386
  oninit: (vnode) => {
2194
2387
  const attrs = vnode.attrs;
@@ -2204,6 +2397,7 @@ const DatePicker = () => {
2204
2397
  calendars: [{ month: 0, year: 0 }],
2205
2398
  monthDropdownOpen: false,
2206
2399
  yearDropdownOpen: false,
2400
+ portalContainerId: `datepicker-portal-${uniqueId()}`,
2207
2401
  formats: {
2208
2402
  d: () => { var _a; return ((_a = state.date) === null || _a === void 0 ? void 0 : _a.getDate()) || 0; },
2209
2403
  dd: () => {
@@ -2257,10 +2451,26 @@ const DatePicker = () => {
2257
2451
  }
2258
2452
  // Add document click listener to close dropdowns
2259
2453
  document.addEventListener('click', handleDocumentClick);
2454
+ // Add ESC key listener
2455
+ document.addEventListener('keydown', handleKeyDown);
2260
2456
  },
2261
2457
  onremove: () => {
2262
- // Clean up event listener
2458
+ // Clean up event listeners
2263
2459
  document.removeEventListener('click', handleDocumentClick);
2460
+ document.removeEventListener('keydown', handleKeyDown);
2461
+ // Clean up portal if picker was open
2462
+ if (state.isOpen) {
2463
+ clearPortal(state.portalContainerId);
2464
+ }
2465
+ },
2466
+ onupdate: (vnode) => {
2467
+ // Render to portal when picker is open, clear when closed
2468
+ if (state.isOpen) {
2469
+ renderPickerToPortal(vnode.attrs);
2470
+ }
2471
+ else {
2472
+ clearPortal(state.portalContainerId);
2473
+ }
2264
2474
  },
2265
2475
  view: (vnode) => {
2266
2476
  const attrs = vnode.attrs;
@@ -2376,93 +2586,7 @@ const DatePicker = () => {
2376
2586
  }, label || dateLabel),
2377
2587
  // Helper text
2378
2588
  helperText && m('span.helper-text', helperText),
2379
- // Modal datepicker
2380
- state.isOpen && [
2381
- m('.modal.datepicker-modal.open', {
2382
- id: `modal-${state.id}`,
2383
- tabindex: 0,
2384
- style: {
2385
- zIndex: 1003,
2386
- display: 'block',
2387
- opacity: 1,
2388
- top: '10%',
2389
- transform: 'scaleX(1) scaleY(1)',
2390
- },
2391
- }, [
2392
- m('.modal-content.datepicker-container', {
2393
- onclick: (e) => {
2394
- // Close dropdowns when clicking anywhere in the modal content
2395
- const target = e.target;
2396
- if (!target.closest('.select-wrapper') && !target.closest('.dropdown-content')) {
2397
- state.monthDropdownOpen = false;
2398
- state.yearDropdownOpen = false;
2399
- }
2400
- },
2401
- }, [
2402
- m(DateDisplay, { options }),
2403
- m('.datepicker-calendar-container', [
2404
- m('.datepicker-calendar', [
2405
- m(DateControls, { options, randId: `datepicker-title-${Math.random().toString(36).slice(2)}` }),
2406
- m(Calendar, { year: state.calendars[0].year, month: state.calendars[0].month, options }),
2407
- ]),
2408
- m('.datepicker-footer', [
2409
- options.showClearBtn &&
2410
- m('button.btn-flat.datepicker-clear.waves-effect', {
2411
- type: 'button',
2412
- style: '',
2413
- onclick: () => {
2414
- setDate(null, false, options);
2415
- state.isOpen = false;
2416
- },
2417
- }, options.i18n.clear),
2418
- m('button.btn-flat.datepicker-cancel.waves-effect', {
2419
- type: 'button',
2420
- onclick: () => {
2421
- state.isOpen = false;
2422
- if (options.onClose)
2423
- options.onClose();
2424
- },
2425
- }, options.i18n.cancel),
2426
- m('button.btn-flat.datepicker-done.waves-effect', {
2427
- type: 'button',
2428
- onclick: () => {
2429
- state.isOpen = false;
2430
- if (options.dateRange) {
2431
- // Range mode
2432
- if (state.startDate && state.endDate && onchange) {
2433
- const startStr = toString(state.startDate, 'yyyy-mm-dd');
2434
- const endStr = toString(state.endDate, 'yyyy-mm-dd');
2435
- onchange(`${startStr} - ${endStr}`);
2436
- }
2437
- }
2438
- else {
2439
- // Single date mode
2440
- if (state.date && onchange) {
2441
- onchange(toString(state.date, 'yyyy-mm-dd')); // Always return ISO format
2442
- }
2443
- }
2444
- if (options.onClose)
2445
- options.onClose();
2446
- },
2447
- }, options.i18n.done),
2448
- ]),
2449
- ]),
2450
- ]),
2451
- ]),
2452
- // Modal overlay
2453
- m('.modal-overlay', {
2454
- style: {
2455
- zIndex: 1002,
2456
- display: 'block',
2457
- opacity: 0.5,
2458
- },
2459
- onclick: () => {
2460
- state.isOpen = false;
2461
- if (options.onClose)
2462
- options.onClose();
2463
- },
2464
- }),
2465
- ],
2589
+ // Modal is now rendered via portal in onupdate hook
2466
2590
  ]);
2467
2591
  },
2468
2592
  };
@@ -3302,8 +3426,8 @@ const InputField = (type, defaultClass = '') => () => {
3302
3426
  const shouldValidate = !isNonInteractive && (validate || type === 'email' || type === 'url' || isNumeric);
3303
3427
  return m('.input-field', { className: cn, style }, [
3304
3428
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
3305
- m('input', Object.assign(Object.assign({ class: shouldValidate ? 'validate' : undefined }, params), { type, tabindex: 0, id,
3306
- placeholder, value: controlled ? value : undefined, class: type === 'range' && attrs.vertical ? 'range-slider vertical' : undefined, style: type === 'range' && attrs.vertical
3429
+ m('input', Object.assign(Object.assign({ class: type === 'range' && attrs.vertical ? 'range-slider vertical' : shouldValidate ? 'validate' : undefined }, params), { type, tabindex: 0, id,
3430
+ placeholder, value: controlled ? value : undefined, style: type === 'range' && attrs.vertical
3307
3431
  ? {
3308
3432
  height: attrs.height || '200px',
3309
3433
  width: '6px',
@@ -4896,7 +5020,7 @@ const ModalPanel = () => {
4896
5020
  closeModal(attrs);
4897
5021
  }
4898
5022
  }
4899
- const { id, title, description, fixedFooter, bottomSheet, buttons, richContent, className, showCloseButton = true, closeOnBackdropClick = true, } = attrs;
5023
+ const { id, title, description, fixedFooter, bottomSheet, buttons, richContent, className, showCloseButton = true, closeOnBackdropClick = true, closeOnButtonClick = true, } = attrs;
4900
5024
  const modalClasses = [
4901
5025
  'modal',
4902
5026
  className || '',
@@ -4993,7 +5117,7 @@ const ModalPanel = () => {
4993
5117
  }, buttons.map((buttonProps) => m(FlatButton, Object.assign(Object.assign({}, buttonProps), { className: `modal-close ${buttonProps.className || ''}`, onclick: (e) => {
4994
5118
  if (buttonProps.onclick)
4995
5119
  buttonProps.onclick(e);
4996
- closeModal(attrs);
5120
+ closeOnButtonClick && closeModal(attrs);
4997
5121
  } })))),
4998
5122
  ]),
4999
5123
  ]);
@@ -5171,6 +5295,7 @@ const defaultOptions = {
5171
5295
  autoClose: false,
5172
5296
  twelveHour: true,
5173
5297
  vibrate: true,
5298
+ roundBy5: false,
5174
5299
  onOpen: () => { },
5175
5300
  onOpenStart: () => { },
5176
5301
  onOpenEnd: () => { },
@@ -5222,7 +5347,7 @@ const TimePicker = () => {
5222
5347
  const clickPos = getPos(e);
5223
5348
  state.dx = clickPos.x - state.x0;
5224
5349
  state.dy = clickPos.y - state.y0;
5225
- setHand(state.dx, state.dy, false);
5350
+ setHand(state.dx, state.dy, options.roundBy5);
5226
5351
  document.addEventListener('mousemove', handleDocumentClickMove);
5227
5352
  document.addEventListener('touchmove', handleDocumentClickMove);
5228
5353
  document.addEventListener('mouseup', handleDocumentClickEnd);
@@ -5234,7 +5359,7 @@ const TimePicker = () => {
5234
5359
  const x = clickPos.x - state.x0;
5235
5360
  const y = clickPos.y - state.y0;
5236
5361
  state.moved = true;
5237
- setHand(x, y, false);
5362
+ setHand(x, y, options.roundBy5);
5238
5363
  m.redraw();
5239
5364
  };
5240
5365
  const handleDocumentClickEnd = (e) => {
@@ -5670,6 +5795,66 @@ const TimePicker = () => {
5670
5795
  },
5671
5796
  };
5672
5797
  };
5798
+ const handleKeyDown = (e) => {
5799
+ if (e.key === 'Escape' && state.isOpen) {
5800
+ close();
5801
+ clearPortal(state.portalContainerId);
5802
+ m.redraw();
5803
+ }
5804
+ };
5805
+ const renderPickerToPortal = (attrs) => {
5806
+ const { showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel' } = attrs;
5807
+ const pickerModal = m('.timepicker-modal-wrapper', {
5808
+ style: {
5809
+ position: 'fixed',
5810
+ top: '0',
5811
+ left: '0',
5812
+ width: '100%',
5813
+ height: '100%',
5814
+ pointerEvents: 'auto',
5815
+ display: 'flex',
5816
+ alignItems: 'center',
5817
+ justifyContent: 'center',
5818
+ },
5819
+ }, [
5820
+ // Modal overlay
5821
+ m('.modal-overlay', {
5822
+ style: {
5823
+ position: 'absolute',
5824
+ top: '0',
5825
+ left: '0',
5826
+ width: '100%',
5827
+ height: '100%',
5828
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
5829
+ zIndex: '1002',
5830
+ },
5831
+ onclick: () => {
5832
+ close();
5833
+ m.redraw();
5834
+ },
5835
+ }),
5836
+ // Modal content
5837
+ m('.modal.timepicker-modal.open', {
5838
+ style: {
5839
+ position: 'relative',
5840
+ zIndex: '1003',
5841
+ display: 'block',
5842
+ opacity: 1,
5843
+ top: 'auto',
5844
+ transform: 'scaleX(1) scaleY(1)',
5845
+ margin: '0 auto',
5846
+ },
5847
+ }, [
5848
+ m(TimepickerModal, {
5849
+ showClearBtn,
5850
+ clearLabel,
5851
+ closeLabel,
5852
+ doneLabel: 'OK',
5853
+ }),
5854
+ ]),
5855
+ ]);
5856
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
5857
+ };
5673
5858
  return {
5674
5859
  oninit: (vnode) => {
5675
5860
  const attrs = vnode.attrs;
@@ -5686,11 +5871,14 @@ const TimePicker = () => {
5686
5871
  y0: 0,
5687
5872
  dx: 0,
5688
5873
  dy: 0,
5874
+ portalContainerId: `timepicker-portal-${uniqueId()}`,
5689
5875
  };
5690
5876
  // Handle value after options are set
5691
5877
  if (attrs.defaultValue) {
5692
5878
  updateTimeFromInput(attrs.defaultValue);
5693
5879
  }
5880
+ // Add ESC key listener
5881
+ document.addEventListener('keydown', handleKeyDown);
5694
5882
  },
5695
5883
  onremove: () => {
5696
5884
  // Cleanup
@@ -5704,6 +5892,21 @@ const TimePicker = () => {
5704
5892
  document.removeEventListener('touchmove', handleDocumentClickMove);
5705
5893
  document.removeEventListener('mouseup', handleDocumentClickEnd);
5706
5894
  document.removeEventListener('touchend', handleDocumentClickEnd);
5895
+ document.removeEventListener('keydown', handleKeyDown);
5896
+ // Clean up portal if picker was open
5897
+ if (state.isOpen) {
5898
+ clearPortal(state.portalContainerId);
5899
+ }
5900
+ },
5901
+ onupdate: (vnode) => {
5902
+ const { useModal = true } = vnode.attrs;
5903
+ // Only render to portal when using modal mode
5904
+ if (useModal && state.isOpen) {
5905
+ renderPickerToPortal(vnode.attrs);
5906
+ }
5907
+ else {
5908
+ clearPortal(state.portalContainerId);
5909
+ }
5707
5910
  },
5708
5911
  view: ({ attrs }) => {
5709
5912
  const { id = state.id, label, placeholder, disabled, readonly, required, iconName, helperText, onchange, oninput, useModal = true, showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel', twelveHour, className: cn1, class: cn2, } = attrs;
@@ -5800,29 +6003,7 @@ const TimePicker = () => {
5800
6003
  }, label),
5801
6004
  // Helper text
5802
6005
  helperText && m('span.helper-text', helperText),
5803
- // Modal timepicker
5804
- useModal &&
5805
- state.isOpen && [
5806
- // Modal overlay
5807
- m('.modal-overlay', {
5808
- style: {
5809
- zIndex: 1002,
5810
- display: 'block',
5811
- opacity: 0.5,
5812
- },
5813
- onclick: () => close(),
5814
- }),
5815
- // Modal content
5816
- m('.modal.timepicker-modal.open', {
5817
- style: {
5818
- zIndex: 1003,
5819
- display: 'block',
5820
- opacity: 1,
5821
- top: '10%',
5822
- transform: 'scaleX(1) scaleY(1)',
5823
- },
5824
- }, m(TimepickerModal, { showClearBtn, clearLabel, closeLabel, doneLabel: 'OK' })),
5825
- ],
6006
+ // Modal is now rendered via portal in onupdate hook
5826
6007
  ]);
5827
6008
  },
5828
6009
  };
@@ -9621,8 +9802,10 @@ exports.TooltipComponent = TooltipComponent;
9621
9802
  exports.TreeView = TreeView;
9622
9803
  exports.UrlInput = UrlInput;
9623
9804
  exports.Wizard = Wizard;
9805
+ exports.clearPortal = clearPortal;
9624
9806
  exports.createBreadcrumb = createBreadcrumb;
9625
9807
  exports.getDropdownStyles = getDropdownStyles;
9808
+ exports.getPortalContainer = getPortalContainer;
9626
9809
  exports.initPushpins = initPushpins;
9627
9810
  exports.initTooltips = initTooltips;
9628
9811
  exports.isNumeric = isNumeric;
@@ -9630,6 +9813,8 @@ exports.isValidationError = isValidationError;
9630
9813
  exports.isValidationSuccess = isValidationSuccess;
9631
9814
  exports.padLeft = padLeft;
9632
9815
  exports.range = range;
9816
+ exports.releasePortalContainer = releasePortalContainer;
9817
+ exports.renderToPortal = renderToPortal;
9633
9818
  exports.toast = toast;
9634
9819
  exports.uniqueId = uniqueId;
9635
9820
  exports.uuid4 = uuid4;