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.esm.js CHANGED
@@ -159,6 +159,76 @@ const getDropdownStyles = (inputRef, overlap = false, options, isDropDown = fals
159
159
  * @example: console.log(range(5, 10)); // [5, 6, 7, 8, 9, 10]
160
160
  */
161
161
  const range = (a, b) => Array.from({ length: b - a + 1 }, (_, i) => a + i);
162
+ // Global registry for portal containers
163
+ const portalContainers = new Map();
164
+ /**
165
+ * Creates or retrieves a portal container appended to document.body.
166
+ * Uses reference counting to manage container lifecycle.
167
+ *
168
+ * @param id - Unique identifier for the portal container
169
+ * @param zIndex - Z-index for the portal container (default: 1004, above modals at 1003)
170
+ * @returns The portal container element
171
+ */
172
+ const getPortalContainer = (id, zIndex = 1004) => {
173
+ let container = portalContainers.get(id);
174
+ if (!container) {
175
+ const element = document.createElement('div');
176
+ element.id = id;
177
+ element.style.position = 'fixed';
178
+ element.style.top = '0';
179
+ element.style.left = '0';
180
+ element.style.width = '100%';
181
+ element.style.height = '100%';
182
+ element.style.pointerEvents = 'none'; // Allow clicks through to underlying elements
183
+ element.style.zIndex = zIndex.toString();
184
+ document.body.appendChild(element);
185
+ container = { element, refCount: 0 };
186
+ portalContainers.set(id, container);
187
+ }
188
+ container.refCount++;
189
+ return container.element;
190
+ };
191
+ /**
192
+ * Decrements reference count and removes portal container if no longer needed.
193
+ *
194
+ * @param id - Portal container identifier
195
+ */
196
+ const releasePortalContainer = (id) => {
197
+ const container = portalContainers.get(id);
198
+ if (container) {
199
+ container.refCount--;
200
+ if (container.refCount <= 0) {
201
+ container.element.remove();
202
+ portalContainers.delete(id);
203
+ }
204
+ }
205
+ };
206
+ /**
207
+ * Renders a Mithril vnode into a portal container using m.render().
208
+ * This allows components to render outside their parent DOM hierarchy,
209
+ * useful for modals and pickers that need to escape stacking contexts.
210
+ *
211
+ * @param containerId - Portal container identifier
212
+ * @param vnode - Mithril vnode to render
213
+ * @param zIndex - Z-index for portal container (default: 1004)
214
+ */
215
+ const renderToPortal = (containerId, vnode, zIndex = 1004) => {
216
+ const container = getPortalContainer(containerId, zIndex);
217
+ m.render(container, vnode);
218
+ };
219
+ /**
220
+ * Clears portal content and releases container reference.
221
+ * If this is the last reference, the container will be removed from the DOM.
222
+ *
223
+ * @param containerId - Portal container identifier
224
+ */
225
+ const clearPortal = (containerId) => {
226
+ const container = portalContainers.get(containerId);
227
+ if (container) {
228
+ m.render(container.element, null);
229
+ releasePortalContainer(containerId);
230
+ }
231
+ };
162
232
 
163
233
  // import './styles/input.css';
164
234
  const Mandatory = { view: ({ attrs }) => m('span.mandatory', Object.assign({}, attrs), '*') };
@@ -2187,6 +2257,129 @@ const DatePicker = () => {
2187
2257
  }
2188
2258
  m.redraw();
2189
2259
  };
2260
+ const handleKeyDown = (e) => {
2261
+ if (e.key === 'Escape' && state.isOpen) {
2262
+ state.isOpen = false;
2263
+ const options = mergeOptions({});
2264
+ if (options.onClose)
2265
+ options.onClose();
2266
+ clearPortal(state.portalContainerId);
2267
+ m.redraw();
2268
+ }
2269
+ };
2270
+ const renderPickerToPortal = (attrs) => {
2271
+ const options = mergeOptions(attrs);
2272
+ const pickerModal = m('.datepicker-modal-wrapper', {
2273
+ style: {
2274
+ position: 'fixed',
2275
+ top: '0',
2276
+ left: '0',
2277
+ width: '100%',
2278
+ height: '100%',
2279
+ pointerEvents: 'auto',
2280
+ display: 'flex',
2281
+ alignItems: 'center',
2282
+ justifyContent: 'center',
2283
+ },
2284
+ }, [
2285
+ // Modal overlay
2286
+ m('.modal-overlay', {
2287
+ style: {
2288
+ position: 'absolute',
2289
+ top: '0',
2290
+ left: '0',
2291
+ width: '100%',
2292
+ height: '100%',
2293
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2294
+ zIndex: '1002',
2295
+ },
2296
+ onclick: () => {
2297
+ state.isOpen = false;
2298
+ if (options.onClose)
2299
+ options.onClose();
2300
+ m.redraw();
2301
+ },
2302
+ }),
2303
+ // Modal content
2304
+ m('.modal.datepicker-modal.open', {
2305
+ id: `modal-${state.id}`,
2306
+ tabindex: 0,
2307
+ style: {
2308
+ position: 'relative',
2309
+ zIndex: '1003',
2310
+ display: 'block',
2311
+ opacity: 1,
2312
+ top: 'auto',
2313
+ transform: 'scaleX(1) scaleY(1)',
2314
+ margin: '0 auto',
2315
+ },
2316
+ }, [
2317
+ m('.modal-content.datepicker-container', {
2318
+ onclick: (e) => {
2319
+ const target = e.target;
2320
+ if (!target.closest('.select-wrapper') && !target.closest('.dropdown-content')) {
2321
+ state.monthDropdownOpen = false;
2322
+ state.yearDropdownOpen = false;
2323
+ }
2324
+ },
2325
+ }, [
2326
+ m(DateDisplay, { options }),
2327
+ m('.datepicker-calendar-container', [
2328
+ m('.datepicker-calendar', [
2329
+ m(DateControls, {
2330
+ options,
2331
+ randId: `datepicker-title-${Math.random().toString(36).slice(2)}`,
2332
+ }),
2333
+ m(Calendar, {
2334
+ year: state.calendars[0].year,
2335
+ month: state.calendars[0].month,
2336
+ options,
2337
+ }),
2338
+ ]),
2339
+ m('.datepicker-footer', [
2340
+ options.showClearBtn &&
2341
+ m('button.btn-flat.datepicker-clear.waves-effect', {
2342
+ type: 'button',
2343
+ onclick: () => {
2344
+ setDate(null, false, options);
2345
+ state.isOpen = false;
2346
+ },
2347
+ }, options.i18n.clear),
2348
+ m('button.btn-flat.datepicker-cancel.waves-effect', {
2349
+ type: 'button',
2350
+ onclick: () => {
2351
+ state.isOpen = false;
2352
+ if (options.onClose)
2353
+ options.onClose();
2354
+ },
2355
+ }, options.i18n.cancel),
2356
+ m('button.btn-flat.datepicker-done.waves-effect', {
2357
+ type: 'button',
2358
+ onclick: () => {
2359
+ state.isOpen = false;
2360
+ if (options.dateRange) {
2361
+ if (state.startDate && state.endDate && attrs.onchange) {
2362
+ const startStr = formatDate(state.startDate, 'yyyy-mm-dd', options);
2363
+ const endStr = formatDate(state.endDate, 'yyyy-mm-dd', options);
2364
+ attrs.onchange(`${startStr} - ${endStr}`);
2365
+ }
2366
+ }
2367
+ else {
2368
+ if (state.date && attrs.onchange) {
2369
+ attrs.onchange(toString(state.date, 'yyyy-mm-dd'));
2370
+ }
2371
+ }
2372
+ if (options.onClose)
2373
+ options.onClose();
2374
+ },
2375
+ }, options.i18n.done),
2376
+ ]),
2377
+ ]),
2378
+ ]),
2379
+ ]),
2380
+ ]);
2381
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
2382
+ };
2190
2383
  return {
2191
2384
  oninit: (vnode) => {
2192
2385
  const attrs = vnode.attrs;
@@ -2202,6 +2395,7 @@ const DatePicker = () => {
2202
2395
  calendars: [{ month: 0, year: 0 }],
2203
2396
  monthDropdownOpen: false,
2204
2397
  yearDropdownOpen: false,
2398
+ portalContainerId: `datepicker-portal-${uniqueId()}`,
2205
2399
  formats: {
2206
2400
  d: () => { var _a; return ((_a = state.date) === null || _a === void 0 ? void 0 : _a.getDate()) || 0; },
2207
2401
  dd: () => {
@@ -2255,10 +2449,26 @@ const DatePicker = () => {
2255
2449
  }
2256
2450
  // Add document click listener to close dropdowns
2257
2451
  document.addEventListener('click', handleDocumentClick);
2452
+ // Add ESC key listener
2453
+ document.addEventListener('keydown', handleKeyDown);
2258
2454
  },
2259
2455
  onremove: () => {
2260
- // Clean up event listener
2456
+ // Clean up event listeners
2261
2457
  document.removeEventListener('click', handleDocumentClick);
2458
+ document.removeEventListener('keydown', handleKeyDown);
2459
+ // Clean up portal if picker was open
2460
+ if (state.isOpen) {
2461
+ clearPortal(state.portalContainerId);
2462
+ }
2463
+ },
2464
+ onupdate: (vnode) => {
2465
+ // Render to portal when picker is open, clear when closed
2466
+ if (state.isOpen) {
2467
+ renderPickerToPortal(vnode.attrs);
2468
+ }
2469
+ else {
2470
+ clearPortal(state.portalContainerId);
2471
+ }
2262
2472
  },
2263
2473
  view: (vnode) => {
2264
2474
  const attrs = vnode.attrs;
@@ -2374,93 +2584,7 @@ const DatePicker = () => {
2374
2584
  }, label || dateLabel),
2375
2585
  // Helper text
2376
2586
  helperText && m('span.helper-text', helperText),
2377
- // Modal datepicker
2378
- state.isOpen && [
2379
- m('.modal.datepicker-modal.open', {
2380
- id: `modal-${state.id}`,
2381
- tabindex: 0,
2382
- style: {
2383
- zIndex: 1003,
2384
- display: 'block',
2385
- opacity: 1,
2386
- top: '10%',
2387
- transform: 'scaleX(1) scaleY(1)',
2388
- },
2389
- }, [
2390
- m('.modal-content.datepicker-container', {
2391
- onclick: (e) => {
2392
- // Close dropdowns when clicking anywhere in the modal content
2393
- const target = e.target;
2394
- if (!target.closest('.select-wrapper') && !target.closest('.dropdown-content')) {
2395
- state.monthDropdownOpen = false;
2396
- state.yearDropdownOpen = false;
2397
- }
2398
- },
2399
- }, [
2400
- m(DateDisplay, { options }),
2401
- m('.datepicker-calendar-container', [
2402
- m('.datepicker-calendar', [
2403
- m(DateControls, { options, randId: `datepicker-title-${Math.random().toString(36).slice(2)}` }),
2404
- m(Calendar, { year: state.calendars[0].year, month: state.calendars[0].month, options }),
2405
- ]),
2406
- m('.datepicker-footer', [
2407
- options.showClearBtn &&
2408
- m('button.btn-flat.datepicker-clear.waves-effect', {
2409
- type: 'button',
2410
- style: '',
2411
- onclick: () => {
2412
- setDate(null, false, options);
2413
- state.isOpen = false;
2414
- },
2415
- }, options.i18n.clear),
2416
- m('button.btn-flat.datepicker-cancel.waves-effect', {
2417
- type: 'button',
2418
- onclick: () => {
2419
- state.isOpen = false;
2420
- if (options.onClose)
2421
- options.onClose();
2422
- },
2423
- }, options.i18n.cancel),
2424
- m('button.btn-flat.datepicker-done.waves-effect', {
2425
- type: 'button',
2426
- onclick: () => {
2427
- state.isOpen = false;
2428
- if (options.dateRange) {
2429
- // Range mode
2430
- if (state.startDate && state.endDate && onchange) {
2431
- const startStr = toString(state.startDate, 'yyyy-mm-dd');
2432
- const endStr = toString(state.endDate, 'yyyy-mm-dd');
2433
- onchange(`${startStr} - ${endStr}`);
2434
- }
2435
- }
2436
- else {
2437
- // Single date mode
2438
- if (state.date && onchange) {
2439
- onchange(toString(state.date, 'yyyy-mm-dd')); // Always return ISO format
2440
- }
2441
- }
2442
- if (options.onClose)
2443
- options.onClose();
2444
- },
2445
- }, options.i18n.done),
2446
- ]),
2447
- ]),
2448
- ]),
2449
- ]),
2450
- // Modal overlay
2451
- m('.modal-overlay', {
2452
- style: {
2453
- zIndex: 1002,
2454
- display: 'block',
2455
- opacity: 0.5,
2456
- },
2457
- onclick: () => {
2458
- state.isOpen = false;
2459
- if (options.onClose)
2460
- options.onClose();
2461
- },
2462
- }),
2463
- ],
2587
+ // Modal is now rendered via portal in onupdate hook
2464
2588
  ]);
2465
2589
  },
2466
2590
  };
@@ -3591,7 +3715,7 @@ const InputCheckbox = () => {
3591
3715
  view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3592
3716
  if (!checkboxId)
3593
3717
  checkboxId = inputId || uniqueId();
3594
- return m(`p`, { className, style }, m('label', { for: checkboxId }, [
3718
+ return m('div', { className, style }, m('label', { for: checkboxId }, [
3595
3719
  m('input[type=checkbox][tabindex=0]', {
3596
3720
  className: disabled ? 'disabled' : undefined,
3597
3721
  id: checkboxId,
@@ -5669,6 +5793,66 @@ const TimePicker = () => {
5669
5793
  },
5670
5794
  };
5671
5795
  };
5796
+ const handleKeyDown = (e) => {
5797
+ if (e.key === 'Escape' && state.isOpen) {
5798
+ close();
5799
+ clearPortal(state.portalContainerId);
5800
+ m.redraw();
5801
+ }
5802
+ };
5803
+ const renderPickerToPortal = (attrs) => {
5804
+ const { showClearBtn = false, clearLabel = 'Clear', closeLabel = 'Cancel' } = attrs;
5805
+ const pickerModal = m('.timepicker-modal-wrapper', {
5806
+ style: {
5807
+ position: 'fixed',
5808
+ top: '0',
5809
+ left: '0',
5810
+ width: '100%',
5811
+ height: '100%',
5812
+ pointerEvents: 'auto',
5813
+ display: 'flex',
5814
+ alignItems: 'center',
5815
+ justifyContent: 'center',
5816
+ },
5817
+ }, [
5818
+ // Modal overlay
5819
+ m('.modal-overlay', {
5820
+ style: {
5821
+ position: 'absolute',
5822
+ top: '0',
5823
+ left: '0',
5824
+ width: '100%',
5825
+ height: '100%',
5826
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
5827
+ zIndex: '1002',
5828
+ },
5829
+ onclick: () => {
5830
+ close();
5831
+ m.redraw();
5832
+ },
5833
+ }),
5834
+ // Modal content
5835
+ m('.modal.timepicker-modal.open', {
5836
+ style: {
5837
+ position: 'relative',
5838
+ zIndex: '1003',
5839
+ display: 'block',
5840
+ opacity: 1,
5841
+ top: 'auto',
5842
+ transform: 'scaleX(1) scaleY(1)',
5843
+ margin: '0 auto',
5844
+ },
5845
+ }, [
5846
+ m(TimepickerModal, {
5847
+ showClearBtn,
5848
+ clearLabel,
5849
+ closeLabel,
5850
+ doneLabel: 'OK',
5851
+ }),
5852
+ ]),
5853
+ ]);
5854
+ renderToPortal(state.portalContainerId, pickerModal, 1004);
5855
+ };
5672
5856
  return {
5673
5857
  oninit: (vnode) => {
5674
5858
  const attrs = vnode.attrs;
@@ -5685,11 +5869,14 @@ const TimePicker = () => {
5685
5869
  y0: 0,
5686
5870
  dx: 0,
5687
5871
  dy: 0,
5872
+ portalContainerId: `timepicker-portal-${uniqueId()}`,
5688
5873
  };
5689
5874
  // Handle value after options are set
5690
5875
  if (attrs.defaultValue) {
5691
5876
  updateTimeFromInput(attrs.defaultValue);
5692
5877
  }
5878
+ // Add ESC key listener
5879
+ document.addEventListener('keydown', handleKeyDown);
5693
5880
  },
5694
5881
  onremove: () => {
5695
5882
  // Cleanup
@@ -5703,6 +5890,21 @@ const TimePicker = () => {
5703
5890
  document.removeEventListener('touchmove', handleDocumentClickMove);
5704
5891
  document.removeEventListener('mouseup', handleDocumentClickEnd);
5705
5892
  document.removeEventListener('touchend', handleDocumentClickEnd);
5893
+ document.removeEventListener('keydown', handleKeyDown);
5894
+ // Clean up portal if picker was open
5895
+ if (state.isOpen) {
5896
+ clearPortal(state.portalContainerId);
5897
+ }
5898
+ },
5899
+ onupdate: (vnode) => {
5900
+ const { useModal = true } = vnode.attrs;
5901
+ // Only render to portal when using modal mode
5902
+ if (useModal && state.isOpen) {
5903
+ renderPickerToPortal(vnode.attrs);
5904
+ }
5905
+ else {
5906
+ clearPortal(state.portalContainerId);
5907
+ }
5706
5908
  },
5707
5909
  view: ({ attrs }) => {
5708
5910
  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;
@@ -5799,29 +6001,7 @@ const TimePicker = () => {
5799
6001
  }, label),
5800
6002
  // Helper text
5801
6003
  helperText && m('span.helper-text', helperText),
5802
- // Modal timepicker
5803
- useModal &&
5804
- state.isOpen && [
5805
- // Modal overlay
5806
- m('.modal-overlay', {
5807
- style: {
5808
- zIndex: 1002,
5809
- display: 'block',
5810
- opacity: 0.5,
5811
- },
5812
- onclick: () => close(),
5813
- }),
5814
- // Modal content
5815
- m('.modal.timepicker-modal.open', {
5816
- style: {
5817
- zIndex: 1003,
5818
- display: 'block',
5819
- opacity: 1,
5820
- top: '10%',
5821
- transform: 'scaleX(1) scaleY(1)',
5822
- },
5823
- }, m(TimepickerModal, { showClearBtn, clearLabel, closeLabel, doneLabel: 'OK' })),
5824
- ],
6004
+ // Modal is now rendered via portal in onupdate hook
5825
6005
  ]);
5826
6006
  },
5827
6007
  };
@@ -9544,4 +9724,4 @@ const isValidationError = (result) => !isValidationSuccess(result);
9544
9724
  // ============================================================================
9545
9725
  // All types are already exported via individual export declarations above
9546
9726
 
9547
- 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 };
9727
+ 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, clearPortal, createBreadcrumb, getDropdownStyles, getPortalContainer, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, releasePortalContainer, renderToPortal, toast, uniqueId, uuid4 };