mithril-materialized 3.5.6 → 3.5.8

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
  };
@@ -3593,7 +3717,7 @@ const InputCheckbox = () => {
3593
3717
  view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3594
3718
  if (!checkboxId)
3595
3719
  checkboxId = inputId || uniqueId();
3596
- return m(`p`, { className, style }, m('label', { for: checkboxId }, [
3720
+ return m('div', { className, style }, m('label', { for: checkboxId }, [
3597
3721
  m('input[type=checkbox][tabindex=0]', {
3598
3722
  className: disabled ? 'disabled' : undefined,
3599
3723
  id: checkboxId,
@@ -5671,6 +5795,66 @@ const TimePicker = () => {
5671
5795
  },
5672
5796
  };
5673
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
+ };
5674
5858
  return {
5675
5859
  oninit: (vnode) => {
5676
5860
  const attrs = vnode.attrs;
@@ -5687,11 +5871,14 @@ const TimePicker = () => {
5687
5871
  y0: 0,
5688
5872
  dx: 0,
5689
5873
  dy: 0,
5874
+ portalContainerId: `timepicker-portal-${uniqueId()}`,
5690
5875
  };
5691
5876
  // Handle value after options are set
5692
5877
  if (attrs.defaultValue) {
5693
5878
  updateTimeFromInput(attrs.defaultValue);
5694
5879
  }
5880
+ // Add ESC key listener
5881
+ document.addEventListener('keydown', handleKeyDown);
5695
5882
  },
5696
5883
  onremove: () => {
5697
5884
  // Cleanup
@@ -5705,6 +5892,21 @@ const TimePicker = () => {
5705
5892
  document.removeEventListener('touchmove', handleDocumentClickMove);
5706
5893
  document.removeEventListener('mouseup', handleDocumentClickEnd);
5707
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
+ }
5708
5910
  },
5709
5911
  view: ({ attrs }) => {
5710
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;
@@ -5801,29 +6003,7 @@ const TimePicker = () => {
5801
6003
  }, label),
5802
6004
  // Helper text
5803
6005
  helperText && m('span.helper-text', helperText),
5804
- // Modal timepicker
5805
- useModal &&
5806
- state.isOpen && [
5807
- // Modal overlay
5808
- m('.modal-overlay', {
5809
- style: {
5810
- zIndex: 1002,
5811
- display: 'block',
5812
- opacity: 0.5,
5813
- },
5814
- onclick: () => close(),
5815
- }),
5816
- // Modal content
5817
- m('.modal.timepicker-modal.open', {
5818
- style: {
5819
- zIndex: 1003,
5820
- display: 'block',
5821
- opacity: 1,
5822
- top: '10%',
5823
- transform: 'scaleX(1) scaleY(1)',
5824
- },
5825
- }, m(TimepickerModal, { showClearBtn, clearLabel, closeLabel, doneLabel: 'OK' })),
5826
- ],
6006
+ // Modal is now rendered via portal in onupdate hook
5827
6007
  ]);
5828
6008
  },
5829
6009
  };
@@ -9622,8 +9802,10 @@ exports.TooltipComponent = TooltipComponent;
9622
9802
  exports.TreeView = TreeView;
9623
9803
  exports.UrlInput = UrlInput;
9624
9804
  exports.Wizard = Wizard;
9805
+ exports.clearPortal = clearPortal;
9625
9806
  exports.createBreadcrumb = createBreadcrumb;
9626
9807
  exports.getDropdownStyles = getDropdownStyles;
9808
+ exports.getPortalContainer = getPortalContainer;
9627
9809
  exports.initPushpins = initPushpins;
9628
9810
  exports.initTooltips = initTooltips;
9629
9811
  exports.isNumeric = isNumeric;
@@ -9631,6 +9813,8 @@ exports.isValidationError = isValidationError;
9631
9813
  exports.isValidationSuccess = isValidationSuccess;
9632
9814
  exports.padLeft = padLeft;
9633
9815
  exports.range = range;
9816
+ exports.releasePortalContainer = releasePortalContainer;
9817
+ exports.renderToPortal = renderToPortal;
9634
9818
  exports.toast = toast;
9635
9819
  exports.uniqueId = uniqueId;
9636
9820
  exports.uuid4 = uuid4;