mithril-materialized 3.5.6 → 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.umd.js CHANGED
@@ -163,6 +163,76 @@
163
163
  * @example: console.log(range(5, 10)); // [5, 6, 7, 8, 9, 10]
164
164
  */
165
165
  const range = (a, b) => Array.from({ length: b - a + 1 }, (_, i) => a + i);
166
+ // Global registry for portal containers
167
+ const portalContainers = new Map();
168
+ /**
169
+ * Creates or retrieves a portal container appended to document.body.
170
+ * Uses reference counting to manage container lifecycle.
171
+ *
172
+ * @param id - Unique identifier for the portal container
173
+ * @param zIndex - Z-index for the portal container (default: 1004, above modals at 1003)
174
+ * @returns The portal container element
175
+ */
176
+ const getPortalContainer = (id, zIndex = 1004) => {
177
+ let container = portalContainers.get(id);
178
+ if (!container) {
179
+ const element = document.createElement('div');
180
+ element.id = id;
181
+ element.style.position = 'fixed';
182
+ element.style.top = '0';
183
+ element.style.left = '0';
184
+ element.style.width = '100%';
185
+ element.style.height = '100%';
186
+ element.style.pointerEvents = 'none'; // Allow clicks through to underlying elements
187
+ element.style.zIndex = zIndex.toString();
188
+ document.body.appendChild(element);
189
+ container = { element, refCount: 0 };
190
+ portalContainers.set(id, container);
191
+ }
192
+ container.refCount++;
193
+ return container.element;
194
+ };
195
+ /**
196
+ * Decrements reference count and removes portal container if no longer needed.
197
+ *
198
+ * @param id - Portal container identifier
199
+ */
200
+ const releasePortalContainer = (id) => {
201
+ const container = portalContainers.get(id);
202
+ if (container) {
203
+ container.refCount--;
204
+ if (container.refCount <= 0) {
205
+ container.element.remove();
206
+ portalContainers.delete(id);
207
+ }
208
+ }
209
+ };
210
+ /**
211
+ * Renders a Mithril vnode into a portal container using m.render().
212
+ * This allows components to render outside their parent DOM hierarchy,
213
+ * useful for modals and pickers that need to escape stacking contexts.
214
+ *
215
+ * @param containerId - Portal container identifier
216
+ * @param vnode - Mithril vnode to render
217
+ * @param zIndex - Z-index for portal container (default: 1004)
218
+ */
219
+ const renderToPortal = (containerId, vnode, zIndex = 1004) => {
220
+ const container = getPortalContainer(containerId, zIndex);
221
+ m.render(container, vnode);
222
+ };
223
+ /**
224
+ * Clears portal content and releases container reference.
225
+ * If this is the last reference, the container will be removed from the DOM.
226
+ *
227
+ * @param containerId - Portal container identifier
228
+ */
229
+ const clearPortal = (containerId) => {
230
+ const container = portalContainers.get(containerId);
231
+ if (container) {
232
+ m.render(container.element, null);
233
+ releasePortalContainer(containerId);
234
+ }
235
+ };
166
236
 
167
237
  // import './styles/input.css';
168
238
  const Mandatory = { view: ({ attrs }) => m('span.mandatory', Object.assign({}, attrs), '*') };
@@ -2191,6 +2261,129 @@
2191
2261
  }
2192
2262
  m.redraw();
2193
2263
  };
2264
+ const handleKeyDown = (e) => {
2265
+ if (e.key === 'Escape' && state.isOpen) {
2266
+ state.isOpen = false;
2267
+ const options = mergeOptions({});
2268
+ if (options.onClose)
2269
+ options.onClose();
2270
+ clearPortal(state.portalContainerId);
2271
+ m.redraw();
2272
+ }
2273
+ };
2274
+ const renderPickerToPortal = (attrs) => {
2275
+ const options = mergeOptions(attrs);
2276
+ const pickerModal = m('.datepicker-modal-wrapper', {
2277
+ style: {
2278
+ position: 'fixed',
2279
+ top: '0',
2280
+ left: '0',
2281
+ width: '100%',
2282
+ height: '100%',
2283
+ pointerEvents: 'auto',
2284
+ display: 'flex',
2285
+ alignItems: 'center',
2286
+ justifyContent: 'center',
2287
+ },
2288
+ }, [
2289
+ // Modal overlay
2290
+ m('.modal-overlay', {
2291
+ style: {
2292
+ position: 'absolute',
2293
+ top: '0',
2294
+ left: '0',
2295
+ width: '100%',
2296
+ height: '100%',
2297
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2298
+ zIndex: '1002',
2299
+ },
2300
+ onclick: () => {
2301
+ state.isOpen = false;
2302
+ if (options.onClose)
2303
+ options.onClose();
2304
+ m.redraw();
2305
+ },
2306
+ }),
2307
+ // Modal content
2308
+ m('.modal.datepicker-modal.open', {
2309
+ id: `modal-${state.id}`,
2310
+ tabindex: 0,
2311
+ style: {
2312
+ position: 'relative',
2313
+ zIndex: '1003',
2314
+ display: 'block',
2315
+ opacity: 1,
2316
+ top: 'auto',
2317
+ transform: 'scaleX(1) scaleY(1)',
2318
+ margin: '0 auto',
2319
+ },
2320
+ }, [
2321
+ m('.modal-content.datepicker-container', {
2322
+ onclick: (e) => {
2323
+ const target = e.target;
2324
+ if (!target.closest('.select-wrapper') && !target.closest('.dropdown-content')) {
2325
+ state.monthDropdownOpen = false;
2326
+ state.yearDropdownOpen = false;
2327
+ }
2328
+ },
2329
+ }, [
2330
+ m(DateDisplay, { options }),
2331
+ m('.datepicker-calendar-container', [
2332
+ m('.datepicker-calendar', [
2333
+ m(DateControls, {
2334
+ options,
2335
+ randId: `datepicker-title-${Math.random().toString(36).slice(2)}`,
2336
+ }),
2337
+ m(Calendar, {
2338
+ year: state.calendars[0].year,
2339
+ month: state.calendars[0].month,
2340
+ options,
2341
+ }),
2342
+ ]),
2343
+ m('.datepicker-footer', [
2344
+ options.showClearBtn &&
2345
+ m('button.btn-flat.datepicker-clear.waves-effect', {
2346
+ type: 'button',
2347
+ onclick: () => {
2348
+ setDate(null, false, options);
2349
+ state.isOpen = false;
2350
+ },
2351
+ }, options.i18n.clear),
2352
+ m('button.btn-flat.datepicker-cancel.waves-effect', {
2353
+ type: 'button',
2354
+ onclick: () => {
2355
+ state.isOpen = false;
2356
+ if (options.onClose)
2357
+ options.onClose();
2358
+ },
2359
+ }, options.i18n.cancel),
2360
+ m('button.btn-flat.datepicker-done.waves-effect', {
2361
+ type: 'button',
2362
+ onclick: () => {
2363
+ state.isOpen = false;
2364
+ if (options.dateRange) {
2365
+ if (state.startDate && state.endDate && attrs.onchange) {
2366
+ const startStr = formatDate(state.startDate, 'yyyy-mm-dd', options);
2367
+ const endStr = formatDate(state.endDate, 'yyyy-mm-dd', options);
2368
+ attrs.onchange(`${startStr} - ${endStr}`);
2369
+ }
2370
+ }
2371
+ else {
2372
+ if (state.date && attrs.onchange) {
2373
+ attrs.onchange(toString(state.date, 'yyyy-mm-dd'));
2374
+ }
2375
+ }
2376
+ if (options.onClose)
2377
+ options.onClose();
2378
+ },
2379
+ }, options.i18n.done),
2380
+ ]),
2381
+ ]),
2382
+ ]),
2383
+ ]),
2384
+ ]);
2385
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
2386
+ };
2194
2387
  return {
2195
2388
  oninit: (vnode) => {
2196
2389
  const attrs = vnode.attrs;
@@ -2206,6 +2399,7 @@
2206
2399
  calendars: [{ month: 0, year: 0 }],
2207
2400
  monthDropdownOpen: false,
2208
2401
  yearDropdownOpen: false,
2402
+ portalContainerId: `datepicker-portal-${uniqueId()}`,
2209
2403
  formats: {
2210
2404
  d: () => { var _a; return ((_a = state.date) === null || _a === void 0 ? void 0 : _a.getDate()) || 0; },
2211
2405
  dd: () => {
@@ -2259,10 +2453,26 @@
2259
2453
  }
2260
2454
  // Add document click listener to close dropdowns
2261
2455
  document.addEventListener('click', handleDocumentClick);
2456
+ // Add ESC key listener
2457
+ document.addEventListener('keydown', handleKeyDown);
2262
2458
  },
2263
2459
  onremove: () => {
2264
- // Clean up event listener
2460
+ // Clean up event listeners
2265
2461
  document.removeEventListener('click', handleDocumentClick);
2462
+ document.removeEventListener('keydown', handleKeyDown);
2463
+ // Clean up portal if picker was open
2464
+ if (state.isOpen) {
2465
+ clearPortal(state.portalContainerId);
2466
+ }
2467
+ },
2468
+ onupdate: (vnode) => {
2469
+ // Render to portal when picker is open, clear when closed
2470
+ if (state.isOpen) {
2471
+ renderPickerToPortal(vnode.attrs);
2472
+ }
2473
+ else {
2474
+ clearPortal(state.portalContainerId);
2475
+ }
2266
2476
  },
2267
2477
  view: (vnode) => {
2268
2478
  const attrs = vnode.attrs;
@@ -2378,93 +2588,7 @@
2378
2588
  }, label || dateLabel),
2379
2589
  // Helper text
2380
2590
  helperText && m('span.helper-text', helperText),
2381
- // Modal datepicker
2382
- state.isOpen && [
2383
- m('.modal.datepicker-modal.open', {
2384
- id: `modal-${state.id}`,
2385
- tabindex: 0,
2386
- style: {
2387
- zIndex: 1003,
2388
- display: 'block',
2389
- opacity: 1,
2390
- top: '10%',
2391
- transform: 'scaleX(1) scaleY(1)',
2392
- },
2393
- }, [
2394
- m('.modal-content.datepicker-container', {
2395
- onclick: (e) => {
2396
- // Close dropdowns when clicking anywhere in the modal content
2397
- const target = e.target;
2398
- if (!target.closest('.select-wrapper') && !target.closest('.dropdown-content')) {
2399
- state.monthDropdownOpen = false;
2400
- state.yearDropdownOpen = false;
2401
- }
2402
- },
2403
- }, [
2404
- m(DateDisplay, { options }),
2405
- m('.datepicker-calendar-container', [
2406
- m('.datepicker-calendar', [
2407
- m(DateControls, { options, randId: `datepicker-title-${Math.random().toString(36).slice(2)}` }),
2408
- m(Calendar, { year: state.calendars[0].year, month: state.calendars[0].month, options }),
2409
- ]),
2410
- m('.datepicker-footer', [
2411
- options.showClearBtn &&
2412
- m('button.btn-flat.datepicker-clear.waves-effect', {
2413
- type: 'button',
2414
- style: '',
2415
- onclick: () => {
2416
- setDate(null, false, options);
2417
- state.isOpen = false;
2418
- },
2419
- }, options.i18n.clear),
2420
- m('button.btn-flat.datepicker-cancel.waves-effect', {
2421
- type: 'button',
2422
- onclick: () => {
2423
- state.isOpen = false;
2424
- if (options.onClose)
2425
- options.onClose();
2426
- },
2427
- }, options.i18n.cancel),
2428
- m('button.btn-flat.datepicker-done.waves-effect', {
2429
- type: 'button',
2430
- onclick: () => {
2431
- state.isOpen = false;
2432
- if (options.dateRange) {
2433
- // Range mode
2434
- if (state.startDate && state.endDate && onchange) {
2435
- const startStr = toString(state.startDate, 'yyyy-mm-dd');
2436
- const endStr = toString(state.endDate, 'yyyy-mm-dd');
2437
- onchange(`${startStr} - ${endStr}`);
2438
- }
2439
- }
2440
- else {
2441
- // Single date mode
2442
- if (state.date && onchange) {
2443
- onchange(toString(state.date, 'yyyy-mm-dd')); // Always return ISO format
2444
- }
2445
- }
2446
- if (options.onClose)
2447
- options.onClose();
2448
- },
2449
- }, options.i18n.done),
2450
- ]),
2451
- ]),
2452
- ]),
2453
- ]),
2454
- // Modal overlay
2455
- m('.modal-overlay', {
2456
- style: {
2457
- zIndex: 1002,
2458
- display: 'block',
2459
- opacity: 0.5,
2460
- },
2461
- onclick: () => {
2462
- state.isOpen = false;
2463
- if (options.onClose)
2464
- options.onClose();
2465
- },
2466
- }),
2467
- ],
2591
+ // Modal is now rendered via portal in onupdate hook
2468
2592
  ]);
2469
2593
  },
2470
2594
  };
@@ -5673,6 +5797,66 @@
5673
5797
  },
5674
5798
  };
5675
5799
  };
5800
+ const handleKeyDown = (e) => {
5801
+ if (e.key === 'Escape' && state.isOpen) {
5802
+ close();
5803
+ clearPortal(state.portalContainerId);
5804
+ m.redraw();
5805
+ }
5806
+ };
5807
+ const renderPickerToPortal = (attrs) => {
5808
+ const { showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel' } = attrs;
5809
+ const pickerModal = m('.timepicker-modal-wrapper', {
5810
+ style: {
5811
+ position: 'fixed',
5812
+ top: '0',
5813
+ left: '0',
5814
+ width: '100%',
5815
+ height: '100%',
5816
+ pointerEvents: 'auto',
5817
+ display: 'flex',
5818
+ alignItems: 'center',
5819
+ justifyContent: 'center',
5820
+ },
5821
+ }, [
5822
+ // Modal overlay
5823
+ m('.modal-overlay', {
5824
+ style: {
5825
+ position: 'absolute',
5826
+ top: '0',
5827
+ left: '0',
5828
+ width: '100%',
5829
+ height: '100%',
5830
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
5831
+ zIndex: '1002',
5832
+ },
5833
+ onclick: () => {
5834
+ close();
5835
+ m.redraw();
5836
+ },
5837
+ }),
5838
+ // Modal content
5839
+ m('.modal.timepicker-modal.open', {
5840
+ style: {
5841
+ position: 'relative',
5842
+ zIndex: '1003',
5843
+ display: 'block',
5844
+ opacity: 1,
5845
+ top: 'auto',
5846
+ transform: 'scaleX(1) scaleY(1)',
5847
+ margin: '0 auto',
5848
+ },
5849
+ }, [
5850
+ m(TimepickerModal, {
5851
+ showClearBtn,
5852
+ clearLabel,
5853
+ closeLabel,
5854
+ doneLabel: 'OK',
5855
+ }),
5856
+ ]),
5857
+ ]);
5858
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
5859
+ };
5676
5860
  return {
5677
5861
  oninit: (vnode) => {
5678
5862
  const attrs = vnode.attrs;
@@ -5689,11 +5873,14 @@
5689
5873
  y0: 0,
5690
5874
  dx: 0,
5691
5875
  dy: 0,
5876
+ portalContainerId: `timepicker-portal-${uniqueId()}`,
5692
5877
  };
5693
5878
  // Handle value after options are set
5694
5879
  if (attrs.defaultValue) {
5695
5880
  updateTimeFromInput(attrs.defaultValue);
5696
5881
  }
5882
+ // Add ESC key listener
5883
+ document.addEventListener('keydown', handleKeyDown);
5697
5884
  },
5698
5885
  onremove: () => {
5699
5886
  // Cleanup
@@ -5707,6 +5894,21 @@
5707
5894
  document.removeEventListener('touchmove', handleDocumentClickMove);
5708
5895
  document.removeEventListener('mouseup', handleDocumentClickEnd);
5709
5896
  document.removeEventListener('touchend', handleDocumentClickEnd);
5897
+ document.removeEventListener('keydown', handleKeyDown);
5898
+ // Clean up portal if picker was open
5899
+ if (state.isOpen) {
5900
+ clearPortal(state.portalContainerId);
5901
+ }
5902
+ },
5903
+ onupdate: (vnode) => {
5904
+ const { useModal = true } = vnode.attrs;
5905
+ // Only render to portal when using modal mode
5906
+ if (useModal && state.isOpen) {
5907
+ renderPickerToPortal(vnode.attrs);
5908
+ }
5909
+ else {
5910
+ clearPortal(state.portalContainerId);
5911
+ }
5710
5912
  },
5711
5913
  view: ({ attrs }) => {
5712
5914
  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;
@@ -5803,29 +6005,7 @@
5803
6005
  }, label),
5804
6006
  // Helper text
5805
6007
  helperText && m('span.helper-text', helperText),
5806
- // Modal timepicker
5807
- useModal &&
5808
- state.isOpen && [
5809
- // Modal overlay
5810
- m('.modal-overlay', {
5811
- style: {
5812
- zIndex: 1002,
5813
- display: 'block',
5814
- opacity: 0.5,
5815
- },
5816
- onclick: () => close(),
5817
- }),
5818
- // Modal content
5819
- m('.modal.timepicker-modal.open', {
5820
- style: {
5821
- zIndex: 1003,
5822
- display: 'block',
5823
- opacity: 1,
5824
- top: '10%',
5825
- transform: 'scaleX(1) scaleY(1)',
5826
- },
5827
- }, m(TimepickerModal, { showClearBtn, clearLabel, closeLabel, doneLabel: 'OK' })),
5828
- ],
6008
+ // Modal is now rendered via portal in onupdate hook
5829
6009
  ]);
5830
6010
  },
5831
6011
  };
@@ -9624,8 +9804,10 @@
9624
9804
  exports.TreeView = TreeView;
9625
9805
  exports.UrlInput = UrlInput;
9626
9806
  exports.Wizard = Wizard;
9807
+ exports.clearPortal = clearPortal;
9627
9808
  exports.createBreadcrumb = createBreadcrumb;
9628
9809
  exports.getDropdownStyles = getDropdownStyles;
9810
+ exports.getPortalContainer = getPortalContainer;
9629
9811
  exports.initPushpins = initPushpins;
9630
9812
  exports.initTooltips = initTooltips;
9631
9813
  exports.isNumeric = isNumeric;
@@ -9633,6 +9815,8 @@
9633
9815
  exports.isValidationSuccess = isValidationSuccess;
9634
9816
  exports.padLeft = padLeft;
9635
9817
  exports.range = range;
9818
+ exports.releasePortalContainer = releasePortalContainer;
9819
+ exports.renderToPortal = renderToPortal;
9636
9820
  exports.toast = toast;
9637
9821
  exports.uniqueId = uniqueId;
9638
9822
  exports.uuid4 = uuid4;
package/dist/utils.d.ts CHANGED
@@ -41,3 +41,35 @@ export declare const getDropdownStyles: (inputRef?: HTMLElement | null, overlap?
41
41
  * @example: console.log(range(5, 10)); // [5, 6, 7, 8, 9, 10]
42
42
  */
43
43
  export declare const range: (a: number, b: number) => number[];
44
+ /**
45
+ * Creates or retrieves a portal container appended to document.body.
46
+ * Uses reference counting to manage container lifecycle.
47
+ *
48
+ * @param id - Unique identifier for the portal container
49
+ * @param zIndex - Z-index for the portal container (default: 1004, above modals at 1003)
50
+ * @returns The portal container element
51
+ */
52
+ export declare const getPortalContainer: (id: string, zIndex?: number) => HTMLElement;
53
+ /**
54
+ * Decrements reference count and removes portal container if no longer needed.
55
+ *
56
+ * @param id - Portal container identifier
57
+ */
58
+ export declare const releasePortalContainer: (id: string) => void;
59
+ /**
60
+ * Renders a Mithril vnode into a portal container using m.render().
61
+ * This allows components to render outside their parent DOM hierarchy,
62
+ * useful for modals and pickers that need to escape stacking contexts.
63
+ *
64
+ * @param containerId - Portal container identifier
65
+ * @param vnode - Mithril vnode to render
66
+ * @param zIndex - Z-index for portal container (default: 1004)
67
+ */
68
+ export declare const renderToPortal: (containerId: string, vnode: any, zIndex?: number) => void;
69
+ /**
70
+ * Clears portal content and releases container reference.
71
+ * If this is the last reference, the container will be removed from the DOM.
72
+ *
73
+ * @param containerId - Portal container identifier
74
+ */
75
+ export declare const clearPortal: (containerId: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.5.6",
3
+ "version": "3.5.7",
4
4
  "description": "A materialize library for mithril.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",