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