mithril-materialized 3.4.5 → 3.5.1

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
@@ -963,6 +963,18 @@ const iconPaths = {
963
963
  'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z', // chevron down
964
964
  'M0 0h24v24H0z', // background
965
965
  ],
966
+ chevron_left: [
967
+ 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z', // chevron left
968
+ 'M0 0h24v24H0z', // background
969
+ ],
970
+ chevron_right: [
971
+ 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', // chevron right
972
+ 'M0 0h24v24H0z', // background
973
+ ],
974
+ menu: [
975
+ 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z', // hamburger menu
976
+ 'M0 0h24v24H0z', // background
977
+ ],
966
978
  expand: [
967
979
  'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', // plus
968
980
  'M0 0h24v24H0z', // background
@@ -971,6 +983,18 @@ const iconPaths = {
971
983
  'M19 13H5v-2h14v2z', // minus
972
984
  'M0 0h24v24H0z', // background
973
985
  ],
986
+ check: [
987
+ 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', // checkmark
988
+ 'M0 0h24v24H0z', // background
989
+ ],
990
+ radio_checked: [
991
+ 'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z', // radio button checked
992
+ 'M0 0h24v24H0z', // background
993
+ ],
994
+ radio_unchecked: [
995
+ 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z', // radio button unchecked
996
+ 'M0 0h24v24H0z', // background
997
+ ],
974
998
  light_mode: [
975
999
  'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5M2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1m18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1m0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1M5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41zm12.4 12.4a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41zm1.06-11a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0zM7.05 18.4a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0z',
976
1000
  'M0 0h24v24H0z', // background
@@ -3272,9 +3296,11 @@ const InputField = (type, defaultClass = '') => () => {
3272
3296
  ? true
3273
3297
  : false;
3274
3298
  const rangeType = type === 'range' && !attrs.minmax;
3299
+ // Only add validate class if input is interactive and validation is needed
3300
+ const shouldValidate = !isNonInteractive && (validate || type === 'email' || type === 'url' || isNumeric);
3275
3301
  return m('.input-field', { className: cn, style }, [
3276
3302
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
3277
- m('input.validate', Object.assign(Object.assign({}, params), { type, tabindex: 0, id,
3303
+ m('input', Object.assign(Object.assign({ class: shouldValidate ? 'validate' : undefined }, params), { type, tabindex: 0, id,
3278
3304
  placeholder, value: controlled ? value : undefined, class: type === 'range' && attrs.vertical ? 'range-slider vertical' : undefined, style: type === 'range' && attrs.vertical
3279
3305
  ? {
3280
3306
  height: attrs.height || '200px',
@@ -3355,6 +3381,17 @@ const InputField = (type, defaultClass = '') => () => {
3355
3381
  state.active = false;
3356
3382
  const target = e.target;
3357
3383
  state.hasInteracted = true;
3384
+ // Skip validation for readonly/disabled inputs
3385
+ if (attrs.readonly || attrs.disabled) {
3386
+ // Call original onblur if provided
3387
+ if (attrs.onblur) {
3388
+ attrs.onblur(e);
3389
+ }
3390
+ if (onchange && state.inputElement) {
3391
+ onchange(getValue(state.inputElement));
3392
+ }
3393
+ return;
3394
+ }
3358
3395
  if (target && validate) {
3359
3396
  const value = getValue(target);
3360
3397
  // Only validate if user has entered some text
@@ -7797,12 +7834,27 @@ const Sidenav = () => {
7797
7834
  document.body.style.overflow = isOpen && mode === 'overlay' ? 'hidden' : '';
7798
7835
  }
7799
7836
  };
7837
+ const toggleExpanded = (attrs) => {
7838
+ const newExpandedState = !(attrs.isExpanded !== false);
7839
+ if (attrs.onExpandChange) {
7840
+ attrs.onExpandChange(newExpandedState);
7841
+ }
7842
+ };
7843
+ const toggleHamburger = (attrs) => {
7844
+ const newOpenState = !state.isOpen;
7845
+ if (attrs.onToggle) {
7846
+ attrs.onToggle(newOpenState);
7847
+ }
7848
+ };
7800
7849
  return {
7801
7850
  oninit: ({ attrs }) => {
7802
7851
  state = {
7803
7852
  id: attrs.id || uniqueId(),
7804
7853
  isOpen: attrs.isOpen || false,
7805
7854
  isAnimating: false,
7855
+ isExpanded: attrs.isExpanded !== false,
7856
+ activeItemIndex: null,
7857
+ selectedSubmenuItems: new Map(),
7806
7858
  };
7807
7859
  // Set up keyboard listener
7808
7860
  if (typeof document !== 'undefined' && attrs.closeOnEscape !== false) {
@@ -7831,12 +7883,16 @@ const Sidenav = () => {
7831
7883
  }
7832
7884
  },
7833
7885
  view: ({ attrs, children }) => {
7834
- const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false, } = attrs;
7886
+ const { position = 'left', mode = 'overlay', width = 300, className = '', showBackdrop = true, animationDuration = 300, fixed = false, showHamburger = false, expandable = false, } = attrs;
7835
7887
  const isOpen = state.isOpen;
7888
+ const collapsedWidth = 60;
7889
+ const isExpanded = attrs.isExpanded !== false;
7890
+ const currentWidth = expandable && !isExpanded ? collapsedWidth : width;
7836
7891
  return [
7837
- // Backdrop (using existing materialize class)
7892
+ // Backdrop (using existing materialize class) - only for overlay mode
7838
7893
  showBackdrop &&
7839
7894
  mode === 'overlay' &&
7895
+ !fixed &&
7840
7896
  m('.sidenav-overlay', {
7841
7897
  style: {
7842
7898
  display: isOpen ? 'block' : 'none',
@@ -7847,49 +7903,206 @@ const Sidenav = () => {
7847
7903
  // Sidenav (using existing materialize structure)
7848
7904
  m('ul.sidenav', {
7849
7905
  id: state.id,
7850
- class: [position === 'right' ? 'right-aligned' : '', fixed ? 'sidenav-fixed' : '', className]
7906
+ class: [
7907
+ position === 'right' ? 'right-aligned' : '',
7908
+ fixed ? 'sidenav-fixed' : '',
7909
+ expandable && !isExpanded ? 'sidenav-collapsed' : '',
7910
+ className,
7911
+ ]
7851
7912
  .filter(Boolean)
7852
7913
  .join(' ') || undefined,
7853
7914
  style: {
7854
- width: `${width}px`,
7915
+ width: `${currentWidth}px`,
7855
7916
  transform: isOpen ? 'translateX(0)' : position === 'left' ? 'translateX(-105%)' : 'translateX(105%)',
7856
7917
  'transition-duration': `${animationDuration}ms`,
7918
+ 'transition-property': 'transform, width',
7857
7919
  },
7858
- }, children),
7920
+ }, [
7921
+ // Hamburger toggle button (inside sidenav, at the top)
7922
+ showHamburger &&
7923
+ m('li.sidenav-hamburger-item', {
7924
+ style: {
7925
+ display: 'flex',
7926
+ 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
7927
+ 'align-items': 'center',
7928
+ padding: '12px 16px',
7929
+ cursor: 'pointer',
7930
+ 'border-bottom': '1px solid rgba(0,0,0,0.1)',
7931
+ },
7932
+ onclick: () => toggleHamburger(attrs),
7933
+ }, m(MaterialIcon, {
7934
+ name: 'menu',
7935
+ style: { width: '24px', height: '24px' },
7936
+ })),
7937
+ // Expand/collapse toggle button (if expandable, right below hamburger)
7938
+ expandable &&
7939
+ m('li.sidenav-expand-toggle', {
7940
+ style: {
7941
+ display: 'flex',
7942
+ 'justify-content': position === 'right' ? 'flex-end' : 'flex-start',
7943
+ 'align-items': 'center',
7944
+ padding: '12px 16px',
7945
+ cursor: 'pointer',
7946
+ 'border-bottom': '1px solid rgba(0,0,0,0.1)',
7947
+ },
7948
+ onclick: () => toggleExpanded(attrs),
7949
+ }, m(MaterialIcon, {
7950
+ name: position === 'right'
7951
+ ? (isExpanded ? 'chevron_right' : 'chevron_left')
7952
+ : (isExpanded ? 'chevron_left' : 'chevron_right'),
7953
+ style: { width: '24px', height: '24px' },
7954
+ })),
7955
+ // Children (menu items) - inject internal props
7956
+ Array.isArray(children)
7957
+ ? children.map((child) => {
7958
+ if (child && typeof child === 'object' && 'tag' in child) {
7959
+ // Clone the vnode and add internal props
7960
+ return Object.assign(Object.assign({}, child), { attrs: Object.assign(Object.assign({}, child.attrs), { _isExpanded: isExpanded, _position: position }) });
7961
+ }
7962
+ return child;
7963
+ })
7964
+ : children,
7965
+ ]),
7859
7966
  ];
7860
7967
  },
7861
7968
  };
7862
7969
  };
7970
+ /**
7971
+ * Sidenav Submenu Item Component
7972
+ */
7973
+ const NavbarSubItem = () => {
7974
+ return {
7975
+ view: ({ attrs }) => {
7976
+ const { text, icon, selected = false, value, onSelect, mode, isExpanded, position = 'left' } = attrs;
7977
+ const handleClick = () => {
7978
+ if (onSelect) {
7979
+ onSelect(value !== undefined ? value : text, !selected);
7980
+ }
7981
+ };
7982
+ const isRightAligned = position === 'right';
7983
+ const submenuContent = isRightAligned
7984
+ ? [
7985
+ // Right-aligned: text on left, icons on right
7986
+ isExpanded && m('span', { style: { 'flex': '1', 'text-align': 'left' } }, text),
7987
+ icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px' } }, icon),
7988
+ m(MaterialIcon, {
7989
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
7990
+ style: {
7991
+ width: '18px',
7992
+ height: '18px',
7993
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
7994
+ },
7995
+ }),
7996
+ ]
7997
+ : [
7998
+ // Left-aligned: indicator on left, text and icon on right
7999
+ m(MaterialIcon, {
8000
+ name: mode === 'checkbox' ? (selected ? 'check' : 'close') : selected ? 'radio_checked' : 'radio_unchecked',
8001
+ style: {
8002
+ width: '18px',
8003
+ height: '18px',
8004
+ opacity: mode === 'checkbox' && !selected ? '0.3' : '1',
8005
+ },
8006
+ }),
8007
+ icon && isExpanded && m('i.material-icons', { style: { 'font-size': '18px', 'margin-left': '8px' } }, icon),
8008
+ isExpanded && m('span', { style: { 'margin-left': icon ? '8px' : '8px' } }, text),
8009
+ ];
8010
+ return m('li.sidenav-subitem', {
8011
+ class: selected ? 'selected' : '',
8012
+ style: {
8013
+ padding: isExpanded ? '8px 16px 8px 48px' : '8px 16px',
8014
+ cursor: 'pointer',
8015
+ display: 'flex',
8016
+ 'align-items': 'center',
8017
+ gap: '8px',
8018
+ 'font-size': '0.9em',
8019
+ 'justify-content': isRightAligned ? 'space-between' : 'flex-start',
8020
+ },
8021
+ onclick: handleClick,
8022
+ }, submenuContent);
8023
+ },
8024
+ };
8025
+ };
7863
8026
  /**
7864
8027
  * Sidenav Item Component
7865
8028
  * Individual items for the sidenav menu
7866
8029
  */
7867
8030
  const SidenavItem = () => {
8031
+ let isSubmenuOpen = false;
7868
8032
  return {
7869
8033
  view: ({ attrs, children }) => {
7870
- const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, } = attrs;
8034
+ const { text, icon, active = false, disabled = false, onclick, href, className = '', divider = false, subheader = false, submenu = [], submenuMode = 'checkbox', } = attrs;
7871
8035
  if (divider) {
7872
8036
  return m('li.divider');
7873
8037
  }
7874
8038
  if (subheader) {
7875
8039
  return m('li.subheader', text || children);
7876
8040
  }
7877
- const itemClasses = [active ? 'active' : '', disabled ? 'disabled' : '', className].filter(Boolean).join(' ') || undefined;
7878
- const content = [icon && m('i.material-icons', icon), text || children];
7879
- if (href && !disabled) {
7880
- return m('li', { class: itemClasses }, [
8041
+ const hasSubmenu = submenu && submenu.length > 0;
8042
+ const itemClasses = [
8043
+ active ? 'active' : '',
8044
+ disabled ? 'disabled' : '',
8045
+ hasSubmenu ? 'has-submenu' : '',
8046
+ className,
8047
+ ]
8048
+ .filter(Boolean)
8049
+ .join(' ') || undefined;
8050
+ const handleMainClick = (e) => {
8051
+ e.preventDefault();
8052
+ if (hasSubmenu) {
8053
+ isSubmenuOpen = active ? !isSubmenuOpen : true;
8054
+ }
8055
+ if (onclick && !disabled) {
8056
+ onclick(e);
8057
+ }
8058
+ };
8059
+ // Get internal props passed from parent Sidenav
8060
+ const isExpanded = attrs._isExpanded !== false;
8061
+ const position = attrs._position || 'left';
8062
+ const isRightAligned = position === 'right';
8063
+ // In expanded mode, icons are at the outside edge
8064
+ // In collapsed mode, icons are centered
8065
+ const content = isRightAligned
8066
+ ? [
8067
+ // Right-aligned: text on left, icon on right
8068
+ isExpanded && m('span.sidenav-item-text', { style: { 'flex': '1', 'text-align': 'left', 'margin-right': '8px' } }, text || children),
8069
+ m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8070
+ ]
8071
+ : [
8072
+ // Left-aligned: icon on left, text on right
8073
+ m('i.material-icons', { style: { 'min-width': '24px', 'width': '24px' } }, icon || ''),
8074
+ isExpanded && m('span.sidenav-item-text', { style: { 'margin-left': '8px', 'flex': '1' } }, text || children),
8075
+ ];
8076
+ const linkStyle = {
8077
+ display: 'flex',
8078
+ 'align-items': 'center',
8079
+ padding: isExpanded ? '12px 16px' : '12px 18px',
8080
+ 'justify-content': isExpanded ? (isRightAligned ? 'flex-end' : 'flex-start') : 'center',
8081
+ };
8082
+ const mainItem = href && !disabled
8083
+ ? m('li', { class: itemClasses }, [
7881
8084
  m('a', {
7882
8085
  href,
7883
- onclick: disabled ? undefined : onclick,
8086
+ onclick: handleMainClick,
8087
+ style: linkStyle,
8088
+ }, content),
8089
+ ])
8090
+ : m('li', { class: itemClasses }, [
8091
+ m('a', {
8092
+ onclick: handleMainClick,
8093
+ href: '#!',
8094
+ style: linkStyle,
7884
8095
  }, content),
7885
8096
  ]);
8097
+ // Return main item with submenu if applicable
8098
+ if (hasSubmenu && active && isSubmenuOpen) {
8099
+ return [
8100
+ mainItem,
8101
+ submenu.map((subItem) => m(NavbarSubItem, Object.assign(Object.assign({}, subItem), { mode: submenuMode, isExpanded,
8102
+ position }))),
8103
+ ];
7886
8104
  }
7887
- return m('li', { class: itemClasses }, [
7888
- m('a', {
7889
- onclick: disabled ? undefined : onclick,
7890
- href: '#!',
7891
- }, content),
7892
- ]);
8105
+ return mainItem;
7893
8106
  },
7894
8107
  };
7895
8108
  };