mithril-materialized 3.11.0 → 3.13.0

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
@@ -1639,7 +1639,7 @@
1639
1639
  const ListItem = () => {
1640
1640
  return {
1641
1641
  view: ({ attrs: { item, mode } }) => {
1642
- const { title, content = '', active, iconName, avatar, className, onclick } = item;
1642
+ const { title, content, active, iconName, avatar, className, onclick } = item;
1643
1643
  return mode === exports.CollectionMode.AVATAR
1644
1644
  ? m('li.collection-item.avatar', {
1645
1645
  className: active ? 'active' : '',
@@ -1649,12 +1649,21 @@
1649
1649
  ? m('img.circle', { src: avatar })
1650
1650
  : m('i.material-icons.circle', { className }, avatar),
1651
1651
  m('span.title', title),
1652
- m('p', m.trust(content)),
1653
- m(SecondaryContent, item),
1654
- ])
1652
+ content ? (typeof content === 'string' ? m('p', m.trust(content)) : m('p', content)) : undefined,
1653
+ iconName ? m(SecondaryContent, item) : undefined,
1654
+ ].filter(Boolean))
1655
1655
  : m('li.collection-item', {
1656
1656
  className: active ? 'active' : '',
1657
- }, iconName ? m('div', [title, m(SecondaryContent, item)]) : title);
1657
+ onclick: onclick ? () => onclick(item) : undefined,
1658
+ }, content
1659
+ ? m('div', [
1660
+ m('div', title),
1661
+ typeof content === 'string' ? m('p.secondary-text', content) : content,
1662
+ iconName ? m(SecondaryContent, item) : undefined,
1663
+ ].filter(Boolean))
1664
+ : iconName
1665
+ ? m('div', [title, m(SecondaryContent, item)])
1666
+ : title);
1658
1667
  },
1659
1668
  };
1660
1669
  };
@@ -10755,12 +10764,12 @@
10755
10764
  const RatingItem = () => {
10756
10765
  return {
10757
10766
  view: ({ attrs }) => {
10758
- const { index, displayValue, step, icons, allowHalfSteps, disabled, onclick, onmouseover } = attrs;
10767
+ const { index, displayValue, step, icons, allowHalfSteps, disabled, showTooltip, tooltipLabel, onclick, onmouseover, } = attrs;
10759
10768
  const itemValue = (index + 1) * step;
10760
10769
  // Calculate fill state based on displayValue vs itemValue
10761
10770
  const diff = displayValue - itemValue;
10762
10771
  const fillState = diff >= 0 ? 'full' : allowHalfSteps && diff >= -step / 2 ? 'half' : 'empty';
10763
- return m('.rating__item', {
10772
+ return m('.rating__item.no-select', {
10764
10773
  className: [
10765
10774
  fillState === 'full' ? 'rating__item--filled' : '',
10766
10775
  fillState === 'half' ? 'rating__item--half' : '',
@@ -10781,6 +10790,8 @@
10781
10790
  clipPath: fillState === 'half' ? 'inset(0 50% 0 0)' : undefined,
10782
10791
  },
10783
10792
  }, typeof icons.filled === 'string' ? icons.filled : m(icons.filled)),
10793
+ // Tooltip
10794
+ showTooltip && tooltipLabel && m('.rating__tooltip', tooltipLabel),
10784
10795
  ]);
10785
10796
  },
10786
10797
  };
@@ -10852,6 +10863,7 @@
10852
10863
  },
10853
10864
  // Array.from({ length: itemCount }, (_, i) => renderRatingItem(attrs, i))
10854
10865
  [...Array(itemCount)].map((_, i) => {
10866
+ var _a;
10855
10867
  const itemValue = (i + 1) * step;
10856
10868
  return m(RatingItem, {
10857
10869
  key: `rating-item-${i}`,
@@ -10861,6 +10873,8 @@
10861
10873
  icons: Object.assign(Object.assign({}, DEFAULT_ICONS), attrs.icon),
10862
10874
  allowHalfSteps: attrs.allowHalfSteps,
10863
10875
  disabled: attrs.disabled,
10876
+ showTooltip: attrs.showTooltips,
10877
+ tooltipLabel: (_a = attrs.tooltipLabels) === null || _a === void 0 ? void 0 : _a[i],
10864
10878
  onclick: () => handleItemClick(attrs, itemValue),
10865
10879
  onmouseover: () => handleItemHover(attrs, itemValue),
10866
10880
  });
@@ -10876,6 +10890,237 @@
10876
10890
  };
10877
10891
  };
10878
10892
 
10893
+ /** Create a LikertScale component */
10894
+ const LikertScale = () => {
10895
+ const state = {
10896
+ id: uniqueId(),
10897
+ groupId: uniqueId(),
10898
+ internalValue: undefined,
10899
+ isFocused: false,
10900
+ };
10901
+ const isControlled = (attrs) => typeof attrs.value !== 'undefined' && typeof attrs.onchange === 'function';
10902
+ const getCurrentValue = (attrs) => {
10903
+ var _a, _b;
10904
+ const controlled = isControlled(attrs);
10905
+ const isNonInteractive = attrs.readonly || attrs.disabled;
10906
+ if (controlled) {
10907
+ return attrs.value;
10908
+ }
10909
+ // Non-interactive components: prefer defaultValue, fallback to value
10910
+ if (isNonInteractive) {
10911
+ return (_a = attrs.defaultValue) !== null && _a !== void 0 ? _a : attrs.value;
10912
+ }
10913
+ // Interactive uncontrolled: use internal state (user can change it)
10914
+ return (_b = state.internalValue) !== null && _b !== void 0 ? _b : attrs.defaultValue;
10915
+ };
10916
+ const getLabelText = (value, min, max, getLabelFn) => {
10917
+ if (getLabelFn && value !== undefined) {
10918
+ return getLabelFn(value, min, max);
10919
+ }
10920
+ if (value === undefined) {
10921
+ return `No selection, please choose a value between ${min} and ${max}`;
10922
+ }
10923
+ return `Selected ${value} out of ${min} to ${max}`;
10924
+ };
10925
+ const getSizeClass = (size = 'medium') => {
10926
+ switch (size) {
10927
+ case 'small':
10928
+ return 'likert-scale--small';
10929
+ case 'large':
10930
+ return 'likert-scale--large';
10931
+ default:
10932
+ return 'likert-scale--medium';
10933
+ }
10934
+ };
10935
+ const getDensityClass = (density = 'standard') => {
10936
+ switch (density) {
10937
+ case 'compact':
10938
+ return 'likert-scale--compact';
10939
+ case 'comfortable':
10940
+ return 'likert-scale--comfortable';
10941
+ default:
10942
+ return 'likert-scale--standard';
10943
+ }
10944
+ };
10945
+ const getLayoutClass = (layout = 'responsive') => {
10946
+ switch (layout) {
10947
+ case 'horizontal':
10948
+ return 'likert-scale--horizontal';
10949
+ case 'vertical':
10950
+ return 'likert-scale--vertical';
10951
+ default:
10952
+ return 'likert-scale--responsive';
10953
+ }
10954
+ };
10955
+ const handleChange = (attrs, newValue) => {
10956
+ var _a;
10957
+ if (attrs.readonly || attrs.disabled)
10958
+ return;
10959
+ if (!isControlled(attrs)) {
10960
+ state.internalValue = newValue;
10961
+ }
10962
+ (_a = attrs.onchange) === null || _a === void 0 ? void 0 : _a.call(attrs, newValue);
10963
+ };
10964
+ const handleKeyDown = (attrs, e) => {
10965
+ if (attrs.readonly || attrs.disabled)
10966
+ return;
10967
+ const min = attrs.min || 1;
10968
+ const max = attrs.max || 5;
10969
+ const step = attrs.step || 1;
10970
+ const currentValue = getCurrentValue(attrs);
10971
+ let newValue = currentValue;
10972
+ switch (e.key) {
10973
+ case 'ArrowRight':
10974
+ case 'ArrowUp':
10975
+ e.preventDefault();
10976
+ newValue = currentValue !== undefined ? Math.min(max, currentValue + step) : min;
10977
+ break;
10978
+ case 'ArrowLeft':
10979
+ case 'ArrowDown':
10980
+ e.preventDefault();
10981
+ newValue = currentValue !== undefined ? Math.max(min, currentValue - step) : min;
10982
+ break;
10983
+ case 'Home':
10984
+ e.preventDefault();
10985
+ newValue = min;
10986
+ break;
10987
+ case 'End':
10988
+ e.preventDefault();
10989
+ newValue = max;
10990
+ break;
10991
+ default:
10992
+ return;
10993
+ }
10994
+ if (newValue !== currentValue) {
10995
+ handleChange(attrs, newValue);
10996
+ }
10997
+ };
10998
+ const LikertScaleItem = () => {
10999
+ return {
11000
+ view: ({ attrs }) => {
11001
+ const { value, currentValue, showNumber, showTooltip, tooltipLabel, groupId, name, disabled, readonly, onchange, } = attrs;
11002
+ const radioId = `${groupId}-${value}`;
11003
+ const isChecked = currentValue === value;
11004
+ return m('.likert-scale__item.no-select', {
11005
+ className: [
11006
+ isChecked ? 'likert-scale__item--checked' : '',
11007
+ disabled ? 'likert-scale__item--disabled' : '',
11008
+ readonly ? 'likert-scale__item--readonly' : '',
11009
+ ]
11010
+ .filter(Boolean)
11011
+ .join(' '),
11012
+ }, [
11013
+ // Number label (optional)
11014
+ showNumber && m('.likert-scale__number', value),
11015
+ // Radio button input
11016
+ m('input[type=radio].likert-scale__input', {
11017
+ id: radioId,
11018
+ name: name || groupId,
11019
+ value: value,
11020
+ checked: isChecked,
11021
+ disabled: disabled || readonly,
11022
+ onchange: () => onchange(value),
11023
+ }),
11024
+ // Label for radio button
11025
+ m('label.likert-scale__label', {
11026
+ for: radioId,
11027
+ }),
11028
+ // Tooltip (optional)
11029
+ showTooltip && tooltipLabel && m('.likert-scale__tooltip', tooltipLabel),
11030
+ ]);
11031
+ },
11032
+ };
11033
+ };
11034
+ return {
11035
+ oninit: ({ attrs }) => {
11036
+ const controlled = isControlled(attrs);
11037
+ const isNonInteractive = attrs.readonly || attrs.disabled;
11038
+ // Warn developer for improper controlled usage
11039
+ if (attrs.value !== undefined && !controlled && !isNonInteractive) {
11040
+ console.warn(`LikertScale component received 'value' prop without 'onchange' handler. ` +
11041
+ `Use 'defaultValue' for uncontrolled components or add 'onchange' for controlled components.`);
11042
+ }
11043
+ if (!controlled) {
11044
+ state.internalValue = attrs.defaultValue;
11045
+ }
11046
+ },
11047
+ view: ({ attrs }) => {
11048
+ const { min = 1, max = 5, step = 1, size = 'medium', density = 'standard', layout = 'responsive', className = '', style = {}, readonly = false, disabled = false, id = state.id, name, label, description, isMandatory, startLabel, middleLabel, endLabel, showNumbers = false, showTooltips = false, tooltipLabels, alignLabels = false } = attrs, ariaAttrs = __rest(attrs, ["min", "max", "step", "size", "density", "layout", "className", "style", "readonly", "disabled", "id", "name", "label", "description", "isMandatory", "startLabel", "middleLabel", "endLabel", "showNumbers", "showTooltips", "tooltipLabels", "alignLabels"]);
11049
+ const currentValue = getCurrentValue(attrs);
11050
+ const itemCount = Math.floor((max - min) / step) + 1;
11051
+ // Generate scale values
11052
+ const scaleValues = Array.from({ length: itemCount }, (_, i) => min + i * step);
11053
+ return m('.likert-scale', {
11054
+ className: [
11055
+ 'likert-scale',
11056
+ getSizeClass(size),
11057
+ getDensityClass(density),
11058
+ getLayoutClass(layout),
11059
+ readonly ? 'likert-scale--readonly' : '',
11060
+ disabled ? 'likert-scale--disabled' : '',
11061
+ state.isFocused ? 'likert-scale--focused' : '',
11062
+ alignLabels ? 'likert-scale--aligned' : '',
11063
+ className,
11064
+ ]
11065
+ .filter(Boolean)
11066
+ .join(' '),
11067
+ style,
11068
+ id,
11069
+ role: 'radiogroup',
11070
+ 'aria-label': ariaAttrs['aria-label'] || attrs.ariaLabel || label || `Rating scale from ${min} to ${max}`,
11071
+ 'aria-labelledby': ariaAttrs['aria-labelledby'],
11072
+ 'aria-readonly': readonly,
11073
+ 'aria-disabled': disabled,
11074
+ onkeydown: (e) => handleKeyDown(attrs, e),
11075
+ onfocus: () => {
11076
+ state.isFocused = true;
11077
+ },
11078
+ onblur: () => {
11079
+ state.isFocused = false;
11080
+ },
11081
+ tabindex: readonly || disabled ? -1 : 0,
11082
+ }, [
11083
+ // Label section (only text label, not the description)
11084
+ label &&
11085
+ m('.likert-scale__question-label', [
11086
+ m('span', label + (isMandatory ? ' *' : '')),
11087
+ description && m('.likert-scale__description', m.trust(description)),
11088
+ ]),
11089
+ // Scale section container
11090
+ m('.likert-scale__scale-container', [
11091
+ // Scale items with numbers
11092
+ m('.likert-scale__scale', scaleValues.map((value) => m(LikertScaleItem, {
11093
+ key: `likert-item-${value}`,
11094
+ value,
11095
+ currentValue,
11096
+ showNumber: showNumbers,
11097
+ showTooltip: showTooltips,
11098
+ tooltipLabel: tooltipLabels === null || tooltipLabels === void 0 ? void 0 : tooltipLabels[value - min],
11099
+ groupId: state.groupId,
11100
+ name,
11101
+ disabled,
11102
+ readonly,
11103
+ onchange: (v) => handleChange(attrs, v),
11104
+ }))),
11105
+ // Scale anchors
11106
+ (startLabel || middleLabel || endLabel) &&
11107
+ m('.likert-scale__anchors', [
11108
+ startLabel && m('.likert-scale__anchor.likert-scale__anchor--start', startLabel),
11109
+ middleLabel && m('.likert-scale__anchor.likert-scale__anchor--middle', middleLabel),
11110
+ endLabel && m('.likert-scale__anchor.likert-scale__anchor--end', endLabel),
11111
+ ]),
11112
+ ]),
11113
+ // Screen reader text
11114
+ m('.likert-scale__sr-only', {
11115
+ className: 'likert-scale__sr-only',
11116
+ 'aria-live': 'polite',
11117
+ 'aria-atomic': 'true',
11118
+ }, getLabelText(currentValue, min, max, attrs.getLabelText)),
11119
+ ]);
11120
+ },
11121
+ };
11122
+ };
11123
+
10879
11124
  /**
10880
11125
  * ToggleButton component.
10881
11126
  *
@@ -11227,6 +11472,7 @@
11227
11472
  exports.InputCheckbox = InputCheckbox;
11228
11473
  exports.Label = Label;
11229
11474
  exports.LargeButton = LargeButton;
11475
+ exports.LikertScale = LikertScale;
11230
11476
  exports.LinearProgress = LinearProgress;
11231
11477
  exports.ListItem = ListItem;
11232
11478
  exports.Mandatory = Mandatory;
@@ -0,0 +1,70 @@
1
+ import { FactoryComponent, Attributes } from 'mithril';
2
+ /** Likert scale component size options */
3
+ export type LikertScaleSize = 'small' | 'medium' | 'large';
4
+ /** Likert scale component density options */
5
+ export type LikertScaleDensity = 'compact' | 'standard' | 'comfortable';
6
+ /** Likert scale layout options */
7
+ export type LikertScaleLayout = 'horizontal' | 'vertical' | 'responsive';
8
+ /** Likert scale component attributes */
9
+ export interface LikertScaleAttrs<T extends number = number> extends Attributes {
10
+ /** Minimum scale value (default: 1) */
11
+ min?: number;
12
+ /** Maximum scale value (default: 5) */
13
+ max?: number;
14
+ /** Step size for scale increments (default: 1) */
15
+ step?: number;
16
+ /** Current value for controlled mode */
17
+ value?: T;
18
+ /** Initial value for uncontrolled mode */
19
+ defaultValue?: T;
20
+ /** Callback when value changes */
21
+ onchange?: (value: T) => void;
22
+ /** Question/prompt text */
23
+ label?: string;
24
+ /** Helper text description */
25
+ description?: string;
26
+ /** Anchor label for minimum value (e.g., "Very Unhappy") */
27
+ startLabel?: string;
28
+ /** Anchor label for middle value (optional, e.g., "Neutral") */
29
+ middleLabel?: string;
30
+ /** Anchor label for maximum value (e.g., "Very Happy") */
31
+ endLabel?: string;
32
+ /** Whether to display numeric values (default: false) */
33
+ showNumbers?: boolean;
34
+ /** Whether to show tooltips on hover (default: false) */
35
+ showTooltips?: boolean;
36
+ /** Custom tooltip labels for each value */
37
+ tooltipLabels?: string[];
38
+ /** Density variant (default: 'standard') */
39
+ density?: LikertScaleDensity;
40
+ /** Size variant (default: 'medium') */
41
+ size?: LikertScaleSize;
42
+ /** Layout mode (default: 'responsive' = horizontal on desktop, vertical on mobile) */
43
+ layout?: LikertScaleLayout;
44
+ /** HTML ID for the component */
45
+ id?: string;
46
+ /** Name for form submission */
47
+ name?: string;
48
+ /** Whether the component is disabled */
49
+ disabled?: boolean;
50
+ /** Whether the component is read-only */
51
+ readonly?: boolean;
52
+ /** If true, add a mandatory '*' after the label */
53
+ isMandatory?: boolean;
54
+ /** ARIA label for the component */
55
+ 'aria-label'?: string;
56
+ /** ARIA label for the component (camelCase alternative) */
57
+ ariaLabel?: string;
58
+ /** ARIA labelledby reference */
59
+ 'aria-labelledby'?: string;
60
+ /** Function to get label text for accessibility */
61
+ getLabelText?: (value: number, min: number, max: number) => string;
62
+ /** Class name for the container */
63
+ className?: string;
64
+ /** Additional CSS styles */
65
+ style?: any;
66
+ /** Use CSS grid for label/scale alignment in multi-question surveys (default: false) */
67
+ alignLabels?: boolean;
68
+ }
69
+ /** Create a LikertScale component */
70
+ export declare const LikertScale: FactoryComponent<LikertScaleAttrs>;
@@ -2778,6 +2778,11 @@ td, th {
2778
2778
  .collection .collection-item.active .secondary-content {
2779
2779
  color: var(--mm-text-on-primary, #fff);
2780
2780
  }
2781
+ .collection .collection-item .secondary-text {
2782
+ color: var(--mm-text-secondary, rgba(0, 0, 0, 0.54));
2783
+ font-size: 0.9rem;
2784
+ margin-top: 4px;
2785
+ }
2781
2786
  .collection a.collection-item {
2782
2787
  display: block;
2783
2788
  transition: 0.25s;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.11.0",
3
+ "version": "3.13.0",
4
4
  "description": "A materialize library for mithril.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -591,6 +591,13 @@ td, th{
591
591
  color: var(--mm-text-on-primary, #fff);
592
592
  }
593
593
  }
594
+
595
+ // Secondary text styling for BASIC mode with content
596
+ .secondary-text {
597
+ color: var(--mm-text-secondary, rgba(0, 0, 0, 0.54));
598
+ font-size: 0.9rem;
599
+ margin-top: 4px;
600
+ }
594
601
  }
595
602
  a.collection-item{
596
603
  display: block;