mithril-materialized 3.5.2 → 3.5.4

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
@@ -3618,9 +3618,7 @@
3618
3618
  const OptionsList = {
3619
3619
  view: ({ attrs: { options, layout } }) => {
3620
3620
  const optionElements = options.map(({ component, props, key }) => m(component, Object.assign(Object.assign({}, props), { key })));
3621
- return layout === 'horizontal'
3622
- ? m('div.grid-container', optionElements)
3623
- : optionElements;
3621
+ return layout === 'horizontal' ? m('div.grid-container', optionElements) : optionElements;
3624
3622
  },
3625
3623
  };
3626
3624
  /** A list of checkboxes */
@@ -3646,7 +3644,7 @@
3646
3644
  oninit: ({ attrs }) => {
3647
3645
  state.componentId = attrs.id || uniqueId();
3648
3646
  },
3649
- view: ({ attrs: { checkedId, label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, selectAllText = 'Select All', selectNoneText = 'Select None', onchange, }, }) => {
3647
+ view: ({ attrs: { checkedId, label, options = [], description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, selectAllText = 'Select All', selectNoneText = 'Select None', onchange, }, }) => {
3650
3648
  // Derive checked IDs from props
3651
3649
  const checkedIds = checkedId !== undefined ? (Array.isArray(checkedId) ? checkedId : [checkedId]) : [];
3652
3650
  const isChecked = (id) => checkedIds.includes(id);
@@ -7816,6 +7814,66 @@
7816
7814
  };
7817
7815
  };
7818
7816
 
7817
+ // List of MaterialIcon SVG icons that are available
7818
+ const materialIconSvgNames = [
7819
+ 'caret', 'close', 'chevron', 'chevron_left', 'chevron_right', 'menu',
7820
+ 'expand', 'collapse', 'check', 'radio_checked', 'radio_unchecked',
7821
+ 'light_mode', 'dark_mode'
7822
+ ];
7823
+ /**
7824
+ * Helper function to render icons based on IconDefinition type
7825
+ */
7826
+ const renderIcon = (icon, style) => {
7827
+ if (!icon)
7828
+ return null;
7829
+ if (typeof icon === 'string') {
7830
+ // Check if this is a MaterialIcon SVG name
7831
+ if (materialIconSvgNames.includes(icon)) {
7832
+ return m(MaterialIcon, { name: icon, style });
7833
+ }
7834
+ // Fall back to Material Icons font for other icon names
7835
+ return m('i.material-icons', { style }, icon);
7836
+ }
7837
+ if (icon.type === 'svg') {
7838
+ // Inline SVG
7839
+ return m.trust(icon.content);
7840
+ }
7841
+ if (icon.type === 'image') {
7842
+ // Image URL
7843
+ return m('img', {
7844
+ src: icon.content,
7845
+ style: Object.assign(Object.assign({}, style), { width: '24px', height: '24px', objectFit: 'contain' }),
7846
+ });
7847
+ }
7848
+ return null;
7849
+ };
7850
+ /**
7851
+ * Helper function to render a single sidenav item (for header/footer items)
7852
+ */
7853
+ const renderSidenavItem = (item, isExpanded, position) => {
7854
+ const { text, icon, onclick, href, className = '' } = item;
7855
+ const isRightAligned = position === 'right';
7856
+ const content = isRightAligned
7857
+ ? [
7858
+ isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text),
7859
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
7860
+ ]
7861
+ : [
7862
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
7863
+ isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text),
7864
+ ];
7865
+ const linkStyle = {
7866
+ display: 'flex',
7867
+ 'align-items': 'center',
7868
+ padding: isExpanded ? '12px 16px' : '12px 18px',
7869
+ 'justify-content': isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
7870
+ };
7871
+ return m('li', { class: className }, m('a', {
7872
+ href: href || '#!',
7873
+ onclick: onclick,
7874
+ style: linkStyle,
7875
+ }, content));
7876
+ };
7819
7877
  /**
7820
7878
  * Sidenav Component
7821
7879
  * A responsive navigation drawer that slides in from the side
@@ -7938,6 +7996,8 @@
7938
7996
  name: 'menu',
7939
7997
  style: { width: '24px', height: '24px' },
7940
7998
  })),
7999
+ // Header item (if provided, appears before expand/collapse toggle)
8000
+ attrs.header && renderSidenavItem(attrs.header, isExpanded, position),
7941
8001
  // Expand/collapse toggle button (if expandable, right below hamburger)
7942
8002
  expandable &&
7943
8003
  m('li.sidenav-expand-toggle', {
@@ -7966,6 +8026,8 @@
7966
8026
  return child;
7967
8027
  })
7968
8028
  : children,
8029
+ // Footer item (if provided, appears at the bottom)
8030
+ attrs.footer && renderSidenavItem(attrs.footer, isExpanded, position),
7969
8031
  ]),
7970
8032
  ];
7971
8033
  },
@@ -7984,43 +8046,42 @@
7984
8046
  }
7985
8047
  };
7986
8048
  const isRightAligned = position === 'right';
8049
+ // Render indicator icon for checkbox/radio modes
8050
+ const indicatorIcon = mode !== 'none'
8051
+ ? m(MaterialIcon, {
8052
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
8053
+ style: {
8054
+ width: '18px',
8055
+ height: '18px',
8056
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
8057
+ },
8058
+ })
8059
+ : null;
7987
8060
  const submenuContent = isRightAligned
7988
8061
  ? [
7989
8062
  // Right-aligned: text on left, icons on right
7990
8063
  isExpanded && m('span', { style: { 'flex': '1', 'text-align': 'left' } }, text),
7991
- icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px' } }, icon),
7992
- m(MaterialIcon, {
7993
- name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7994
- style: {
7995
- width: '18px',
7996
- height: '18px',
7997
- opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7998
- },
7999
- }),
8064
+ icon && isExpanded && renderIcon(icon, { 'font-size': '18px' }),
8065
+ indicatorIcon,
8000
8066
  ]
8001
8067
  : [
8002
8068
  // Left-aligned: indicator on left, text and icon on right
8003
- m(MaterialIcon, {
8004
- name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
8005
- style: {
8006
- width: '18px',
8007
- height: '18px',
8008
- opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
8009
- },
8010
- }),
8011
- icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px', 'margin-left': '8px' } }, icon),
8012
- isExpanded && m('span', { style: { 'margin-left': icon ? '8px' : '8px' } }, text),
8069
+ indicatorIcon,
8070
+ icon && isExpanded && renderIcon(icon, { 'font-size': '18px', 'margin-left': indicatorIcon ? '8px' : '0' }),
8071
+ isExpanded && m('span', { style: { 'margin-left': icon || indicatorIcon ? '8px' : '0' } }, text),
8013
8072
  ];
8014
8073
  return m('li.sidenav-subitem', {
8015
8074
  class: selected ? 'selected' : '',
8016
8075
  style: {
8017
- padding: isExpanded ? '8px 16px 8px 48px' : '8px 16px',
8076
+ padding: isExpanded ? '0 16px 0 48px' : '0 16px',
8018
8077
  cursor: 'pointer',
8019
8078
  display: 'flex',
8020
8079
  'align-items': 'center',
8021
8080
  gap: '8px',
8022
8081
  'font-size': '0.9em',
8023
8082
  'justify-content': isRightAligned ? 'space-between' : 'flex-start',
8083
+ height: '48px',
8084
+ 'min-height': '48px',
8024
8085
  },
8025
8086
  onclick: handleClick,
8026
8087
  }, submenuContent);
@@ -8052,8 +8113,8 @@
8052
8113
  .filter(Boolean)
8053
8114
  .join(' ') || undefined;
8054
8115
  const handleMainClick = (e) => {
8055
- e.preventDefault();
8056
8116
  if (hasSubmenu) {
8117
+ e.preventDefault();
8057
8118
  isSubmenuOpen = active ? !isSubmenuOpen : true;
8058
8119
  }
8059
8120
  if (onclick && !disabled) {
@@ -8070,11 +8131,11 @@
8070
8131
  ? [
8071
8132
  // Right-aligned: text on left, icon on right
8072
8133
  isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
8073
- m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8134
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
8074
8135
  ]
8075
8136
  : [
8076
8137
  // Left-aligned: icon on left, text on right
8077
- m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8138
+ renderIcon(icon, { 'min-width': '24px', 'width': '24px' }),
8078
8139
  isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text || children),
8079
8140
  ];
8080
8141
  const linkStyle = {
package/dist/sidenav.d.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { FactoryComponent, Attributes } from 'mithril';
2
+ /** Icon definition - supports material icon name, inline SVG, or image URL */
3
+ export type IconDefinition = string | {
4
+ type: 'svg' | 'image';
5
+ content: string;
6
+ };
2
7
  export interface NavbarSubItemAttrs {
3
8
  /** Text content of the submenu item */
4
9
  text: string;
5
- /** Optional icon name */
6
- icon?: string;
10
+ /** Optional icon - material icon name, SVG object, or image object */
11
+ icon?: IconDefinition;
7
12
  /** Whether this submenu item is selected */
8
13
  selected?: boolean;
9
14
  /** Value for the submenu item */
@@ -46,12 +51,16 @@ export interface SidenavAttrs extends Attributes {
46
51
  isExpanded?: boolean;
47
52
  /** Callback when expand state changes */
48
53
  onExpandChange?: (expanded: boolean) => void;
54
+ /** Header item displayed before expand/collapse toggle */
55
+ header?: SidenavItemAttrs;
56
+ /** Footer item displayed at the bottom of the sidenav */
57
+ footer?: SidenavItemAttrs;
49
58
  }
50
59
  export interface SidenavItemAttrs {
51
60
  /** Text content of the item */
52
61
  text?: string;
53
- /** Icon name (material icons) */
54
- icon?: string;
62
+ /** Icon - material icon name, SVG object, or image object */
63
+ icon?: IconDefinition;
55
64
  /** Whether this item is active */
56
65
  active?: boolean;
57
66
  /** Whether this item is disabled */
@@ -68,8 +77,8 @@ export interface SidenavItemAttrs {
68
77
  subheader?: boolean;
69
78
  /** Submenu items */
70
79
  submenu?: NavbarSubItemAttrs[];
71
- /** Submenu selection mode */
72
- submenuMode?: 'checkbox' | 'radio';
80
+ /** Submenu selection mode - 'checkbox' for multi-select, 'radio' for single-select, 'none' for no indicators */
81
+ submenuMode?: 'checkbox' | 'radio' | 'none';
73
82
  /** @internal - Whether the sidenav is expanded (passed from parent) */
74
83
  _isExpanded?: boolean;
75
84
  /** @internal - Position of the sidenav (passed from parent) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "A materialize library for mithril.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -75,10 +75,10 @@
75
75
  "mithril": "^2.3.7"
76
76
  },
77
77
  "devDependencies": {
78
- "@playwright/test": "^1.55.0",
79
- "@rollup/plugin-typescript": "^12.1.4",
78
+ "@playwright/test": "^1.56.1",
79
+ "@rollup/plugin-typescript": "^12.3.0",
80
80
  "@testing-library/dom": "^10.4.1",
81
- "@testing-library/jest-dom": "^6.8.0",
81
+ "@testing-library/jest-dom": "^6.9.1",
82
82
  "@testing-library/user-event": "^14.6.1",
83
83
  "@types/jest": "^30.0.0",
84
84
  "@types/mithril": "^2.2.7",
@@ -86,16 +86,16 @@
86
86
  "concurrently": "^9.2.1",
87
87
  "express": "^5.1.0",
88
88
  "identity-obj-proxy": "^3.0.0",
89
- "jest": "^30.1.3",
90
- "jest-environment-jsdom": "^30.1.2",
89
+ "jest": "^30.2.0",
90
+ "jest-environment-jsdom": "^30.2.0",
91
91
  "js-yaml": "^4.1.0",
92
- "rimraf": "^6.0.1",
93
- "rollup": "^4.50.1",
92
+ "rimraf": "^6.1.0",
93
+ "rollup": "^4.52.5",
94
94
  "rollup-plugin-postcss": "^4.0.2",
95
- "sass": "^1.92.1",
96
- "ts-jest": "^29.4.1",
95
+ "sass": "^1.93.2",
96
+ "ts-jest": "^29.4.5",
97
97
  "tslib": "^2.8.1",
98
- "typedoc": "^0.28.12",
99
- "typescript": "^5.9.2"
98
+ "typedoc": "^0.28.14",
99
+ "typescript": "^5.9.3"
100
100
  }
101
101
  }
@@ -217,3 +217,26 @@ nav {
217
217
  height: variables.$navbar-height;
218
218
  }
219
219
  }
220
+
221
+ // Dark theme support
222
+ [data-theme="dark"] {
223
+ nav ul li.active {
224
+ background-color: rgba(38, 166, 154, 0.2);
225
+
226
+ a {
227
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
228
+ }
229
+
230
+ i, .material-icons {
231
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
232
+ }
233
+ }
234
+ }
235
+
236
+ // Make menu items non-selectable by default
237
+ nav ul li {
238
+ user-select: none;
239
+ -webkit-user-select: none;
240
+ -moz-user-select: none;
241
+ -ms-user-select: none;
242
+ }
@@ -193,14 +193,17 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
193
193
  li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
194
194
  &:hover { background-color: var(--mm-border-color, rgba(0,0,0,.05));}
195
195
 
196
- // color: var(--mm-text-primary, variables.$sidenav-font-color);
197
- color: var(--mm-nav-active-text, #fff);
196
+ color: var(--mm-text-primary, variables.$sidenav-font-color);
198
197
  display: block;
199
198
  font-size: variables.$sidenav-font-size;
200
199
  font-weight: 500;
201
200
  height: variables.$sidenav-item-height;
202
201
  line-height: variables.$sidenav-line-height;
203
202
  padding: 0 (variables.$sidenav-padding * 2);
203
+ user-select: none;
204
+ -webkit-user-select: none;
205
+ -moz-user-select: none;
206
+ -ms-user-select: none;
204
207
 
205
208
  & > i,
206
209
  & > [class^="mdi-"], li > a > [class*="mdi-"],
@@ -215,6 +218,17 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
215
218
  }
216
219
  }
217
220
 
221
+ // Active menu item styling
222
+ li.active > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
223
+ color: var(--mm-nav-active-text, #fff);
224
+ background-color: var(--mm-primary-color, #26a69a);
225
+
226
+ & > i,
227
+ & > i.material-icons {
228
+ color: var(--mm-nav-active-text, #fff);
229
+ }
230
+ }
231
+
218
232
  // Stlye btn anchors
219
233
  li > .btn, li > .btn-large, li > .btn-flat, li > .btn-floating {
220
234
  margin: 10px (variables.$sidenav-padding * 2);
@@ -452,17 +466,25 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
452
466
  .sidenav-subitem {
453
467
  list-style: none;
454
468
  transition: background-color 0.2s ease;
469
+ user-select: none;
470
+ -webkit-user-select: none;
471
+ -moz-user-select: none;
472
+ -ms-user-select: none;
455
473
 
456
474
  &:hover {
457
475
  background: var(--mm-border-color, rgba(0, 0, 0, 0.05));
458
476
  }
459
477
 
460
- // Don't show selected background - only the check/radio icon indicates selection
461
- // to avoid confusion with multiple active menu items
462
478
  &.selected {
479
+ background-color: var(--mm-primary-color-light, rgba(38, 166, 154, 0.15));
480
+
463
481
  svg {
464
482
  fill: var(--mm-primary-color, #26a69a);
465
483
  }
484
+
485
+ i.material-icons {
486
+ color: var(--mm-primary-color, #26a69a);
487
+ }
466
488
  }
467
489
 
468
490
  svg {
@@ -510,24 +532,24 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
510
532
  }
511
533
 
512
534
  li.active {
513
- background-color: rgba(255, 255, 255, 0.05);
535
+ background-color: var(--mm-primary-color, #26a69a);
514
536
 
515
537
  & > a:not(.btn):not(.btn-large):not(.btn-flat):not(.btn-floating) {
516
- color: var(--mm-nav-active-text, #fff);
538
+ color: #000;
517
539
 
518
540
  & > i,
519
541
  & > i.material-icons {
520
- color: var(--mm-nav-active-text, #fff);
542
+ color: #000;
521
543
  }
522
544
  }
523
545
  }
524
546
 
525
547
  .collapsible-body > ul:not(.collapsible) > li.active a {
526
- color: var(--mm-nav-active-text, #fff);
548
+ color: #000;
527
549
 
528
550
  i,
529
551
  i.material-icons {
530
- color: var(--mm-nav-active-text, #fff);
552
+ color: #000;
531
553
  }
532
554
  }
533
555
 
@@ -552,6 +574,19 @@ ul.sidenav.right-aligned li > a:not(.btn):not(.btn-large):not(.btn-flat):not(.bt
552
574
  i.material-icons {
553
575
  color: var(--mm-text-secondary, rgba(255, 255, 255, 0.6));
554
576
  }
577
+
578
+ &.selected {
579
+ background-color: rgba(38, 166, 154, 0.2);
580
+ color: var(--mm-text-primary, rgba(255, 255, 255, 0.87));
581
+
582
+ svg {
583
+ fill: var(--mm-primary-color, #26a69a);
584
+ }
585
+
586
+ i.material-icons {
587
+ color: var(--mm-primary-color, #26a69a);
588
+ }
589
+ }
555
590
  }
556
591
  }
557
592
 
@@ -123,7 +123,7 @@ body {
123
123
  // Navigation colors
124
124
  --mm-nav-background: #1e1e1e;
125
125
  --mm-nav-text: #ffffff;
126
- --mm-nav-active-text: #000000;
126
+ --mm-nav-active-text: #ffffff;
127
127
 
128
128
  // Modal and overlay colors
129
129
  --mm-modal-background: #2d2d2d;