mithril-materialized 2.0.0-beta.10 → 2.0.0-beta.11

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
@@ -416,12 +416,18 @@ const ButtonFactory = (element, defaultClassNames, type = '') => {
416
416
  return () => {
417
417
  return {
418
418
  view: ({ attrs }) => {
419
- const { modalId, tooltip, tooltipPostion, iconName, iconClass, label, className, attr } = attrs, params = __rest(attrs, ["modalId", "tooltip", "tooltipPostion", "iconName", "iconClass", "label", "className", "attr"]);
420
- const cn = [modalId ? 'modal-trigger' : '', tooltip ? 'tooltipped' : '', defaultClassNames, className]
419
+ const { modalId, tooltip, tooltipPosition, tooltipPostion, // Keep for backwards compatibility
420
+ iconName, iconClass, label, className, attr, variant } = attrs, params = __rest(attrs, ["modalId", "tooltip", "tooltipPosition", "tooltipPostion", "iconName", "iconClass", "label", "className", "attr", "variant"]);
421
+ // Handle both new variant prop and legacy modalId/type
422
+ const buttonType = (variant === null || variant === void 0 ? void 0 : variant.type) || (modalId ? 'modal' : type || 'button');
423
+ const modalTarget = (variant === null || variant === void 0 ? void 0 : variant.type) === 'modal' ? variant.modalId : modalId;
424
+ const cn = [modalTarget ? 'modal-trigger' : '', tooltip ? 'tooltipped' : '', defaultClassNames, className]
421
425
  .filter(Boolean)
422
426
  .join(' ')
423
427
  .trim();
424
- return m(element, Object.assign(Object.assign(Object.assign({}, params), attr), { className: cn, href: modalId ? `#${modalId}` : undefined, 'data-position': tooltip ? tooltipPostion || 'top' : undefined, 'data-tooltip': tooltip || undefined, type }),
428
+ // Use tooltipPosition if available, fallback to legacy tooltipPostion
429
+ const position = tooltipPosition || tooltipPostion || 'top';
430
+ return m(element, Object.assign(Object.assign(Object.assign({}, params), attr), { className: cn, href: modalTarget ? `#${modalTarget}` : undefined, 'data-position': tooltip ? position : undefined, 'data-tooltip': tooltip || undefined, type: buttonType === 'modal' ? 'button' : buttonType }),
425
431
  // `${dca}${modalId ? `.modal-trigger[href=#${modalId}]` : ''}${
426
432
  // tooltip ? `.tooltipped[data-position=${tooltipPostion || 'top'}][data-tooltip=${tooltip}]` : ''
427
433
  // }${toAttributeString(attr)}`, {}
@@ -2035,248 +2041,6 @@ const DatePicker = () => {
2035
2041
  };
2036
2042
  };
2037
2043
 
2038
- /** Pure TypeScript Dropdown component - no Materialize dependencies */
2039
- const Dropdown = () => {
2040
- const state = {
2041
- isOpen: false,
2042
- initialValue: undefined,
2043
- id: '',
2044
- focusedIndex: -1,
2045
- inputRef: null,
2046
- dropdownRef: null,
2047
- };
2048
- const handleKeyDown = (e, items, onchange) => {
2049
- const availableItems = items.filter((item) => !item.divider && !item.disabled);
2050
- switch (e.key) {
2051
- case 'ArrowDown':
2052
- e.preventDefault();
2053
- if (!state.isOpen) {
2054
- state.isOpen = true;
2055
- state.focusedIndex = 0;
2056
- }
2057
- else {
2058
- state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
2059
- }
2060
- break;
2061
- case 'ArrowUp':
2062
- e.preventDefault();
2063
- if (state.isOpen) {
2064
- state.focusedIndex = Math.max(state.focusedIndex - 1, 0);
2065
- }
2066
- break;
2067
- case 'Enter':
2068
- case ' ':
2069
- e.preventDefault();
2070
- if (state.isOpen && state.focusedIndex >= 0 && state.focusedIndex < availableItems.length) {
2071
- const selectedItem = availableItems[state.focusedIndex];
2072
- const value = (selectedItem.id || selectedItem.label);
2073
- state.initialValue = value;
2074
- state.isOpen = false;
2075
- state.focusedIndex = -1;
2076
- if (onchange)
2077
- onchange(value);
2078
- }
2079
- else if (!state.isOpen) {
2080
- state.isOpen = true;
2081
- state.focusedIndex = 0;
2082
- }
2083
- break;
2084
- case 'Escape':
2085
- e.preventDefault();
2086
- state.isOpen = false;
2087
- state.focusedIndex = -1;
2088
- break;
2089
- }
2090
- };
2091
- return {
2092
- oninit: ({ attrs: { id = uniqueId(), initialValue, checkedId } }) => {
2093
- state.id = id;
2094
- state.initialValue = initialValue || checkedId;
2095
- // Mithril will handle click events through the component structure
2096
- },
2097
- view: ({ attrs: { key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12' }, }) => {
2098
- const { initialValue } = state;
2099
- const selectedItem = initialValue
2100
- ? items.filter((i) => (i.id ? i.id === initialValue : i.label === initialValue)).shift()
2101
- : undefined;
2102
- const title = selectedItem ? selectedItem.label : label || 'Select';
2103
- const availableItems = items.filter((item) => !item.divider && !item.disabled);
2104
- return m('.dropdown-wrapper.input-field', { className, key, style }, [
2105
- iconName ? m('i.material-icons.prefix', iconName) : undefined,
2106
- m(HelperText, { helperText }),
2107
- m('.select-wrapper', {
2108
- onclick: disabled
2109
- ? undefined
2110
- : () => {
2111
- state.isOpen = !state.isOpen;
2112
- state.focusedIndex = state.isOpen ? 0 : -1;
2113
- },
2114
- onkeydown: disabled ? undefined : (e) => handleKeyDown(e, items, onchange),
2115
- tabindex: disabled ? -1 : 0,
2116
- 'aria-expanded': state.isOpen ? 'true' : 'false',
2117
- 'aria-haspopup': 'listbox',
2118
- role: 'combobox',
2119
- }, [
2120
- m('input[type=text][readonly=true].select-dropdown.dropdown-trigger', {
2121
- id: state.id,
2122
- value: title,
2123
- oncreate: ({ dom }) => {
2124
- state.inputRef = dom;
2125
- },
2126
- onclick: (e) => {
2127
- e.preventDefault();
2128
- e.stopPropagation();
2129
- if (!disabled) {
2130
- state.isOpen = !state.isOpen;
2131
- state.focusedIndex = state.isOpen ? 0 : -1;
2132
- }
2133
- },
2134
- }),
2135
- // Dropdown Menu using Select component's positioning logic
2136
- state.isOpen &&
2137
- m('ul.dropdown-content.select-dropdown', {
2138
- tabindex: 0,
2139
- role: 'listbox',
2140
- 'aria-labelledby': state.id,
2141
- oncreate: ({ dom }) => {
2142
- state.dropdownRef = dom;
2143
- },
2144
- onremove: () => {
2145
- state.dropdownRef = null;
2146
- },
2147
- style: getDropdownStyles(state.inputRef, true, items.map((item) => (Object.assign(Object.assign({}, item), {
2148
- // Convert dropdown items to format expected by getDropdownStyles
2149
- group: undefined }))), true),
2150
- }, items.map((item, index) => {
2151
- if (item.divider) {
2152
- return m('li.divider', {
2153
- key: `divider-${index}`,
2154
- });
2155
- }
2156
- const itemIndex = availableItems.indexOf(item);
2157
- const isFocused = itemIndex === state.focusedIndex;
2158
- return m('li', Object.assign({ key: item.id || `item-${index}`, class: [
2159
- item.disabled ? 'disabled' : '',
2160
- isFocused ? 'focused' : '',
2161
- (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
2162
- ]
2163
- .filter(Boolean)
2164
- .join(' ') }, (item.disabled
2165
- ? {}
2166
- : {
2167
- onclick: (e) => {
2168
- e.stopPropagation();
2169
- const value = (item.id || item.label);
2170
- state.initialValue = value;
2171
- state.isOpen = false;
2172
- state.focusedIndex = -1;
2173
- if (onchange)
2174
- onchange(value);
2175
- },
2176
- })), m('span', {
2177
- style: {
2178
- display: 'flex',
2179
- alignItems: 'center',
2180
- padding: '14px 16px',
2181
- },
2182
- }, [
2183
- item.iconName
2184
- ? m('i.material-icons', {
2185
- style: { marginRight: '32px' },
2186
- }, item.iconName)
2187
- : undefined,
2188
- item.label,
2189
- ]));
2190
- })),
2191
- m(MaterialIcon, {
2192
- name: 'caret',
2193
- direction: 'down',
2194
- class: 'caret',
2195
- }),
2196
- ]),
2197
- ]);
2198
- },
2199
- };
2200
- };
2201
-
2202
- /**
2203
- * Floating Action Button
2204
- */
2205
- const FloatingActionButton = () => {
2206
- const state = {
2207
- isOpen: false,
2208
- };
2209
- const handleClickOutside = (e) => {
2210
- const target = e.target;
2211
- if (!target.closest('.fixed-action-btn')) {
2212
- state.isOpen = false;
2213
- }
2214
- };
2215
- return {
2216
- oncreate: () => {
2217
- document.addEventListener('click', handleClickOutside);
2218
- },
2219
- onremove: () => {
2220
- document.removeEventListener('click', handleClickOutside);
2221
- },
2222
- view: ({ attrs: { className, iconName, iconClass, position, style = position === 'left' || position === 'inline-left'
2223
- ? 'position: absolute; display: inline-block; left: 24px;'
2224
- : position === 'right' || position === 'inline-right'
2225
- ? 'position: absolute; display: inline-block; right: 24px;'
2226
- : undefined, buttons, direction = 'top', hoverEnabled = true, }, }) => {
2227
- const fabClasses = [
2228
- 'fixed-action-btn',
2229
- direction ? `direction-${direction}` : '',
2230
- state.isOpen ? 'active' : '',
2231
- // hoverEnabled ? 'hover-enabled' : '',
2232
- ]
2233
- .filter(Boolean)
2234
- .join(' ');
2235
- return m('div', {
2236
- style: position === 'inline-right' || position === 'inline-left' ? 'position: relative; height: 70px;' : undefined,
2237
- }, m(`.${fabClasses}`, {
2238
- style,
2239
- onclick: (e) => {
2240
- e.stopPropagation();
2241
- if (buttons && buttons.length > 0) {
2242
- state.isOpen = !state.isOpen;
2243
- }
2244
- },
2245
- onmouseover: hoverEnabled
2246
- ? () => {
2247
- if (buttons && buttons.length > 0) {
2248
- state.isOpen = true;
2249
- }
2250
- }
2251
- : undefined,
2252
- onmouseleave: hoverEnabled
2253
- ? () => {
2254
- state.isOpen = false;
2255
- }
2256
- : undefined,
2257
- }, [
2258
- m('a.btn-floating.btn-large', {
2259
- className,
2260
- }, m('i.material-icons', { className: iconClass }, iconName)),
2261
- buttons &&
2262
- buttons.length > 0 &&
2263
- m('ul', buttons.map((button, index) => m('li', m(`a.btn-floating.${button.className || 'red'}`, {
2264
- style: {
2265
- opacity: state.isOpen ? '1' : '0',
2266
- transform: state.isOpen ? 'scale(1)' : 'scale(0.4)',
2267
- transition: `all 0.3s ease ${index * 40}ms`,
2268
- },
2269
- onclick: (e) => {
2270
- e.stopPropagation();
2271
- if (button.onClick)
2272
- button.onClick(e);
2273
- },
2274
- }, m('i.material-icons', { className: button.iconClass }, button.iconName))))),
2275
- ]));
2276
- },
2277
- };
2278
- };
2279
-
2280
2044
  /** Character counter component that tracks text length against maxLength */
2281
2045
  const CharacterCounter = () => {
2282
2046
  return {
@@ -2450,7 +2214,8 @@ const InputField = (type, defaultClass = '') => () => {
2450
2214
  : false;
2451
2215
  return m('.input-field', { className: cn, style }, [
2452
2216
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
2453
- m('input.validate', Object.assign(Object.assign({}, params), { type, tabindex: 0, id,
2217
+ m('input.validate', Object.assign(Object.assign({}, params), { type, tabindex: 0, id,
2218
+ placeholder,
2454
2219
  // attributes,
2455
2220
  oncreate: ({ dom }) => {
2456
2221
  const input = (state.inputElement = dom);
@@ -2585,80 +2350,1007 @@ const InputField = (type, defaultClass = '') => () => {
2585
2350
  ]);
2586
2351
  },
2587
2352
  };
2588
- };
2589
- /** Component for entering some text */
2590
- const TextInput = InputField('text');
2591
- /** Component for entering a password */
2592
- const PasswordInput = InputField('password');
2593
- /** Component for entering a number */
2594
- const NumberInput = InputField('number');
2595
- /** Component for entering a URL */
2596
- const UrlInput = InputField('url');
2597
- /** Component for entering a color */
2598
- const ColorInput = InputField('color');
2599
- /** Component for entering a range */
2600
- const RangeInput = InputField('range', '.range-field');
2601
- /** Component for entering an email */
2602
- const EmailInput = InputField('email');
2603
- /** Component for uploading a file */
2604
- const FileInput = () => {
2605
- let canClear = false;
2606
- let i;
2353
+ };
2354
+ /** Component for entering some text */
2355
+ const TextInput = InputField('text');
2356
+ /** Component for entering a password */
2357
+ const PasswordInput = InputField('password');
2358
+ /** Component for entering a number */
2359
+ const NumberInput = InputField('number');
2360
+ /** Component for entering a URL */
2361
+ const UrlInput = InputField('url');
2362
+ /** Component for entering a color */
2363
+ const ColorInput = InputField('color');
2364
+ /** Component for entering a range */
2365
+ const RangeInput = InputField('range', '.range-field');
2366
+ /** Component for entering an email */
2367
+ const EmailInput = InputField('email');
2368
+ /** Component for uploading a file */
2369
+ const FileInput = () => {
2370
+ let canClear = false;
2371
+ let i;
2372
+ return {
2373
+ view: ({ attrs }) => {
2374
+ const { multiple, disabled, initialValue, placeholder, onchange, className = 'col s12', accept: acceptedFiles, label = 'File', } = attrs;
2375
+ const accept = acceptedFiles
2376
+ ? acceptedFiles instanceof Array
2377
+ ? acceptedFiles.join(', ')
2378
+ : acceptedFiles
2379
+ : undefined;
2380
+ return m('.file-field.input-field', {
2381
+ className: attrs.class || className,
2382
+ }, [
2383
+ m('.btn', [
2384
+ m('span', label),
2385
+ m('input[type=file]', {
2386
+ title: label,
2387
+ accept,
2388
+ multiple,
2389
+ disabled,
2390
+ onchange: onchange
2391
+ ? (e) => {
2392
+ const i = e.target;
2393
+ if (i && i.files && onchange) {
2394
+ canClear = true;
2395
+ onchange(i.files);
2396
+ }
2397
+ }
2398
+ : undefined,
2399
+ }),
2400
+ ]),
2401
+ m('.file-path-wrapper', m('input.file-path.validate[type=text]', {
2402
+ placeholder,
2403
+ oncreate: ({ dom }) => {
2404
+ i = dom;
2405
+ if (initialValue)
2406
+ i.value = initialValue;
2407
+ },
2408
+ })),
2409
+ (canClear || (i === null || i === void 0 ? void 0 : i.value)) &&
2410
+ m('a.waves-effect.waves-teal.btn-flat', {
2411
+ style: {
2412
+ float: 'right',
2413
+ position: 'relative',
2414
+ top: '-3rem',
2415
+ padding: 0,
2416
+ },
2417
+ onclick: () => {
2418
+ canClear = false;
2419
+ i.value = '';
2420
+ onchange && onchange({});
2421
+ },
2422
+ }, m(MaterialIcon, {
2423
+ name: 'close',
2424
+ className: 'close',
2425
+ })),
2426
+ ]);
2427
+ },
2428
+ };
2429
+ };
2430
+
2431
+ /** Component to show a check box */
2432
+ const InputCheckbox = () => {
2433
+ return {
2434
+ view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
2435
+ const checkboxId = inputId || uniqueId();
2436
+ return m(`p`, { className, style }, m('label', { for: checkboxId }, [
2437
+ m('input[type=checkbox][tabindex=0]', {
2438
+ id: checkboxId,
2439
+ checked,
2440
+ disabled,
2441
+ onclick: onchange
2442
+ ? (e) => {
2443
+ if (e.target && typeof e.target.checked !== 'undefined') {
2444
+ onchange(e.target.checked);
2445
+ }
2446
+ }
2447
+ : undefined,
2448
+ }),
2449
+ label ? (typeof label === 'string' ? m('span', label) : label) : undefined,
2450
+ ]), description && m(HelperText, { className: 'input-checkbox-desc', helperText: description }));
2451
+ },
2452
+ };
2453
+ };
2454
+ /** A list of checkboxes */
2455
+ const Options = () => {
2456
+ const state = {};
2457
+ const isChecked = (id) => state.checkedIds.indexOf(id) >= 0;
2458
+ const selectAll = (options, callback) => {
2459
+ const allIds = options.map((option) => option.id);
2460
+ state.checkedIds = [...allIds];
2461
+ if (callback)
2462
+ callback(allIds);
2463
+ };
2464
+ const selectNone = (callback) => {
2465
+ state.checkedIds = [];
2466
+ if (callback)
2467
+ callback([]);
2468
+ };
2469
+ return {
2470
+ oninit: ({ attrs: { initialValue, checkedId, id } }) => {
2471
+ const iv = checkedId || initialValue;
2472
+ state.checkedId = checkedId;
2473
+ state.checkedIds = iv ? (iv instanceof Array ? [...iv] : [iv]) : [];
2474
+ state.componentId = id || uniqueId();
2475
+ },
2476
+ view: ({ attrs: { label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, onchange: callback, }, }) => {
2477
+ const onchange = callback
2478
+ ? (propId, checked) => {
2479
+ const checkedIds = state.checkedIds.filter((i) => i !== propId);
2480
+ if (checked) {
2481
+ checkedIds.push(propId);
2482
+ }
2483
+ state.checkedIds = checkedIds;
2484
+ callback(checkedIds);
2485
+ }
2486
+ : undefined;
2487
+ const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim();
2488
+ const optionsContent = layout === 'horizontal'
2489
+ ? m('div.grid-container', options.map((option) => m(InputCheckbox, {
2490
+ disabled: disabled || option.disabled,
2491
+ label: option.label,
2492
+ onchange: onchange ? (v) => onchange(option.id, v) : undefined,
2493
+ className: option.className || checkboxClass,
2494
+ checked: isChecked(option.id),
2495
+ description: option.description,
2496
+ inputId: `${state.componentId}-${option.id}`,
2497
+ })))
2498
+ : options.map((option) => m(InputCheckbox, {
2499
+ disabled: disabled || option.disabled,
2500
+ label: option.label,
2501
+ onchange: onchange ? (v) => onchange(option.id, v) : undefined,
2502
+ className: option.className || checkboxClass,
2503
+ checked: isChecked(option.id),
2504
+ description: option.description,
2505
+ inputId: `${state.componentId}-${option.id}`,
2506
+ }));
2507
+ return m('div', { id: state.componentId, className: cn, style }, [
2508
+ label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
2509
+ showSelectAll &&
2510
+ m('div.select-all-controls', { style: 'margin-bottom: 10px;' }, [
2511
+ m('a', {
2512
+ href: '#',
2513
+ onclick: (e) => {
2514
+ e.preventDefault();
2515
+ selectAll(options, callback);
2516
+ },
2517
+ style: 'margin-right: 15px;',
2518
+ }, 'Select All'),
2519
+ m('a', {
2520
+ href: '#',
2521
+ onclick: (e) => {
2522
+ e.preventDefault();
2523
+ selectNone(callback);
2524
+ },
2525
+ }, 'Select None'),
2526
+ ]),
2527
+ description && m(HelperText, { helperText: description }),
2528
+ m('form', { action: '#' }, optionsContent),
2529
+ ]);
2530
+ },
2531
+ };
2532
+ };
2533
+
2534
+ const GlobalSearch = () => {
2535
+ return {
2536
+ view: ({ attrs }) => {
2537
+ const { searchPlaceholder, onSearch, i18n } = attrs;
2538
+ return m('.datatable-global-search', m(TextInput, {
2539
+ className: 'datatable-search',
2540
+ label: (i18n === null || i18n === void 0 ? void 0 : i18n.search) || 'Search',
2541
+ placeholder: searchPlaceholder || (i18n === null || i18n === void 0 ? void 0 : i18n.searchPlaceholder) || 'Search table...',
2542
+ oninput: onSearch,
2543
+ }));
2544
+ },
2545
+ };
2546
+ };
2547
+ const TableHeader = () => {
2548
+ return {
2549
+ view: ({ attrs }) => {
2550
+ const { columns, selection, sort, allSelected, helpers } = attrs;
2551
+ return m('thead', m('tr', [
2552
+ // Selection column header
2553
+ ...(selection && selection.mode !== 'none'
2554
+ ? [
2555
+ m('th.selection-checkbox', [
2556
+ selection.mode === 'multiple' &&
2557
+ m(InputCheckbox, {
2558
+ checked: allSelected,
2559
+ onchange: helpers.handleSelectAll,
2560
+ className: '',
2561
+ }),
2562
+ ]),
2563
+ ]
2564
+ : []),
2565
+ // Regular columns
2566
+ ...columns.map((column) => {
2567
+ const isSorted = (sort === null || sort === void 0 ? void 0 : sort.column) === column.key;
2568
+ const sortDirection = isSorted ? sort.direction : null;
2569
+ return m('th', {
2570
+ class: [
2571
+ column.headerClassName,
2572
+ column.sortable ? 'sortable' : '',
2573
+ isSorted ? `sorted-${sortDirection}` : '',
2574
+ ]
2575
+ .filter(Boolean)
2576
+ .join(' '),
2577
+ style: column.width ? { width: column.width } : undefined,
2578
+ onclick: column.sortable ? () => helpers.handleSort(column.key) : undefined,
2579
+ }, [
2580
+ column.title,
2581
+ column.sortable &&
2582
+ m('.sort-indicators', [
2583
+ m('span.sort-icon.sort-asc', {
2584
+ className: isSorted && sortDirection === 'asc' ? 'active' : '',
2585
+ }, '▲'),
2586
+ m('span.sort-icon.sort-desc', {
2587
+ className: isSorted && sortDirection === 'desc' ? 'active' : '',
2588
+ }, '▼'),
2589
+ ]),
2590
+ ]);
2591
+ }),
2592
+ ]));
2593
+ },
2594
+ };
2595
+ };
2596
+ const TableRow = () => {
2597
+ return {
2598
+ view: ({ attrs }) => {
2599
+ const { row, index, columns, selection, onRowClick, onRowDoubleClick, getRowClassName, helpers, data } = attrs;
2600
+ // Calculate the original data index for the row key
2601
+ const originalIndex = data.findIndex((originalRow) => originalRow === row);
2602
+ const rowKey = (selection === null || selection === void 0 ? void 0 : selection.getRowKey(row, originalIndex)) || String(originalIndex);
2603
+ const isSelected = (selection === null || selection === void 0 ? void 0 : selection.selectedKeys.includes(rowKey)) || false;
2604
+ return m('tr', {
2605
+ class: [getRowClassName ? getRowClassName(row, index) : '', isSelected ? 'selected' : '']
2606
+ .filter(Boolean)
2607
+ .join(' ') || undefined,
2608
+ onclick: onRowClick ? (e) => onRowClick(row, index, e) : undefined,
2609
+ ondblclick: onRowDoubleClick ? (e) => onRowDoubleClick(row, index, e) : undefined,
2610
+ }, [
2611
+ // Selection column
2612
+ selection &&
2613
+ selection.mode !== 'none' &&
2614
+ m('td.selection-checkbox', [
2615
+ m(InputCheckbox, {
2616
+ checked: isSelected,
2617
+ onchange: (checked) => helpers.handleSelectionChange(rowKey, checked),
2618
+ className: '',
2619
+ }),
2620
+ ]),
2621
+ columns.map((column) => {
2622
+ const value = helpers.getCellValue(row, column);
2623
+ let cellContent;
2624
+ if (column.cellRenderer) {
2625
+ cellContent = m(column.cellRenderer, {
2626
+ value,
2627
+ row,
2628
+ index,
2629
+ column,
2630
+ });
2631
+ }
2632
+ else if (column.render) {
2633
+ // Backward compatibility with deprecated render function
2634
+ cellContent = column.render(value, row, index);
2635
+ }
2636
+ else {
2637
+ cellContent = String(value || '');
2638
+ }
2639
+ return m('td', {
2640
+ class: [column.className, column.align ? `align-${column.align}` : ''].filter(Boolean).join(' ') ||
2641
+ undefined,
2642
+ }, cellContent);
2643
+ }),
2644
+ ]);
2645
+ },
2646
+ };
2647
+ };
2648
+ /**
2649
+ * Standalone Pagination Controls component
2650
+ *
2651
+ * Provides navigation controls for paginated data with customizable text labels.
2652
+ * Includes first page, previous page, next page, last page buttons and page info display.
2653
+ * Can be used independently of DataTable for any paginated content.
2654
+ *
2655
+ * @example
2656
+ * ```typescript
2657
+ * m(PaginationControls, {
2658
+ * pagination: { page: 0, pageSize: 10, total: 100 },
2659
+ * onPaginationChange: (newPagination) => console.log('Page changed:', newPagination),
2660
+ * i18n: { showing: 'Showing', to: 'to', of: 'of', entries: 'entries', page: 'Page' }
2661
+ * })
2662
+ * ```
2663
+ */
2664
+ const PaginationControls = () => {
2665
+ return {
2666
+ view: ({ attrs }) => {
2667
+ const { pagination, onPaginationChange, i18n } = attrs;
2668
+ if (!pagination)
2669
+ return null;
2670
+ const { page, pageSize, total } = pagination;
2671
+ const totalPages = Math.ceil(total / pageSize);
2672
+ const startItem = page * pageSize + 1;
2673
+ const endItem = Math.min((page + 1) * pageSize, total);
2674
+ const showingText = (i18n === null || i18n === void 0 ? void 0 : i18n.showing) || 'Showing';
2675
+ const toText = (i18n === null || i18n === void 0 ? void 0 : i18n.to) || 'to';
2676
+ const ofText = (i18n === null || i18n === void 0 ? void 0 : i18n.of) || 'of';
2677
+ const entriesText = (i18n === null || i18n === void 0 ? void 0 : i18n.entries) || 'entries';
2678
+ const pageText = (i18n === null || i18n === void 0 ? void 0 : i18n.page) || 'Page';
2679
+ return m('.datatable-pagination', [
2680
+ m('.pagination-info', `${showingText} ${startItem} ${toText} ${endItem} ${ofText} ${total} ${entriesText}`),
2681
+ m('.pagination-controls', [
2682
+ m('button.btn-flat', {
2683
+ disabled: page === 0,
2684
+ onclick: () => onPaginationChange(Object.assign(Object.assign({}, pagination), { page: 0 })),
2685
+ }, '⏮'),
2686
+ m('button.btn-flat', {
2687
+ disabled: page === 0,
2688
+ onclick: () => onPaginationChange(Object.assign(Object.assign({}, pagination), { page: page - 1 })),
2689
+ }, '◀'),
2690
+ m('span.page-info', `${pageText} ${page + 1} ${ofText} ${totalPages}`),
2691
+ m('button.btn-flat', {
2692
+ disabled: page >= totalPages - 1,
2693
+ onclick: () => onPaginationChange(Object.assign(Object.assign({}, pagination), { page: page + 1 })),
2694
+ }, '▶'),
2695
+ m('button.btn-flat', {
2696
+ disabled: page >= totalPages - 1,
2697
+ onclick: () => onPaginationChange(Object.assign(Object.assign({}, pagination), { page: totalPages - 1 })),
2698
+ }, '⏭'),
2699
+ ]),
2700
+ ]);
2701
+ },
2702
+ };
2703
+ };
2704
+ const TableContent = () => {
2705
+ return {
2706
+ view: ({ attrs: contentAttrs }) => {
2707
+ const { processedData, tableClasses, columns, selection, internalSort, allSelected, someSelected, helpers, onRowClick, onRowDoubleClick, getRowClassName, data, } = contentAttrs;
2708
+ return m('table', {
2709
+ class: tableClasses,
2710
+ }, m(TableHeader(), {
2711
+ columns,
2712
+ selection,
2713
+ sort: internalSort,
2714
+ allSelected,
2715
+ someSelected,
2716
+ helpers,
2717
+ }), m('tbody', processedData.map((row, index) => m(TableRow(), {
2718
+ key: (selection === null || selection === void 0 ? void 0 : selection.getRowKey(row, data.findIndex((originalRow) => originalRow === row))) || index,
2719
+ row,
2720
+ index,
2721
+ columns,
2722
+ selection,
2723
+ onRowClick,
2724
+ onRowDoubleClick,
2725
+ getRowClassName,
2726
+ helpers,
2727
+ data,
2728
+ }))));
2729
+ },
2730
+ };
2731
+ };
2732
+ /**
2733
+ * A comprehensive data table component with sorting, filtering, pagination, and selection capabilities.
2734
+ *
2735
+ * @template T The type of data objects displayed in each row
2736
+ *
2737
+ * @description
2738
+ * The DataTable component provides a feature-rich interface for displaying and interacting with tabular data.
2739
+ * It supports both controlled and uncontrolled modes for all interactive features.
2740
+ *
2741
+ * **Key Features:**
2742
+ * - Sorting: Click column headers to sort data ascending/descending
2743
+ * - Filtering: Global search across filterable columns
2744
+ * - Pagination: Navigate through large datasets with customizable page sizes
2745
+ * - Selection: Single or multiple row selection with callbacks
2746
+ * - Custom rendering: Use cellRenderer for complex cell content
2747
+ * - Responsive: Adapts to different screen sizes
2748
+ * - Internationalization: Customize all UI text
2749
+ * - Accessibility: Proper ARIA attributes and keyboard navigation
2750
+ *
2751
+ * @example Basic usage
2752
+ * ```typescript
2753
+ * interface User { id: number; name: string; email: string; }
2754
+ * const users: User[] = [...];
2755
+ * const columns: DataTableColumn<User>[] = [
2756
+ * { key: 'name', title: 'Name', field: 'name', sortable: true, filterable: true },
2757
+ * { key: 'email', title: 'Email', field: 'email', sortable: true, filterable: true }
2758
+ * ];
2759
+ *
2760
+ * return m(DataTable<User>, { data: users, columns });
2761
+ * ```
2762
+ *
2763
+ * @example Advanced usage with all features
2764
+ * ```typescript
2765
+ * return m(DataTable<User>, {
2766
+ * data: users,
2767
+ * columns,
2768
+ * title: 'User Management',
2769
+ * striped: true,
2770
+ * hoverable: true,
2771
+ * height: 400,
2772
+ *
2773
+ * // Pagination
2774
+ * pagination: { page: 0, pageSize: 10, total: users.length },
2775
+ * onPaginationChange: (pagination) => console.log('Page changed:', pagination),
2776
+ *
2777
+ * // Selection
2778
+ * selection: {
2779
+ * mode: 'multiple',
2780
+ * selectedKeys: [],
2781
+ * getRowKey: (user) => String(user.id),
2782
+ * onSelectionChange: (keys, selectedUsers) => console.log('Selection:', selectedUsers)
2783
+ * },
2784
+ *
2785
+ * // Search
2786
+ * enableGlobalSearch: true,
2787
+ * searchPlaceholder: 'Search users...',
2788
+ *
2789
+ * // Events
2790
+ * onRowClick: (user, index, event) => console.log('Clicked:', user),
2791
+ * onRowDoubleClick: (user) => editUser(user),
2792
+ *
2793
+ * // Styling
2794
+ * getRowClassName: (user) => user.active ? '' : 'inactive-row'
2795
+ * });
2796
+ * ```
2797
+ *
2798
+ * @returns A Mithril component that renders the data table
2799
+ */
2800
+ const DataTable = () => {
2801
+ const state = {
2802
+ internalSort: undefined,
2803
+ internalFilter: undefined,
2804
+ internalPagination: undefined,
2805
+ processedData: [],
2806
+ tableId: '',
2807
+ // Performance optimization caches
2808
+ lastProcessedHash: ''};
2809
+ // Helper functions
2810
+ const quickDataHash = (data) => {
2811
+ if (data.length === 0)
2812
+ return '0';
2813
+ if (data.length === 1)
2814
+ return '1';
2815
+ // Sample first, middle, and last items for quick hash
2816
+ const first = JSON.stringify(data[0]);
2817
+ const middle = data.length > 2 ? JSON.stringify(data[Math.floor(data.length / 2)]) : '';
2818
+ const last = JSON.stringify(data[data.length - 1]);
2819
+ return `${data.length}-${first.length}-${middle.length}-${last.length}`;
2820
+ };
2821
+ const getDataHash = (attrs) => {
2822
+ const { data, sort, filter, pagination } = attrs;
2823
+ const { internalSort, internalFilter, internalPagination } = state;
2824
+ const hashInputs = {
2825
+ dataLength: data.length,
2826
+ dataHash: quickDataHash(data),
2827
+ sort: sort || internalSort,
2828
+ filter: filter || internalFilter,
2829
+ pagination: pagination || internalPagination,
2830
+ };
2831
+ return JSON.stringify(hashInputs);
2832
+ };
2833
+ const getCellValue = (row, column) => {
2834
+ if (column.field) {
2835
+ return row[column.field];
2836
+ }
2837
+ return row;
2838
+ };
2839
+ const applyFiltering = (data, filter, columns) => {
2840
+ var _a;
2841
+ if (!filter.searchTerm && !filter.columnFilters)
2842
+ return data;
2843
+ const filterableColumns = columns.filter((col) => col.filterable);
2844
+ if (filterableColumns.length === 0 && !filter.searchTerm)
2845
+ return data;
2846
+ const searchTerm = (_a = filter.searchTerm) === null || _a === void 0 ? void 0 : _a.toLowerCase();
2847
+ const hasColumnFilters = filter.columnFilters &&
2848
+ Object.keys(filter.columnFilters).some((key) => {
2849
+ const value = filter.columnFilters[key];
2850
+ return value !== null && value !== undefined && value !== '';
2851
+ });
2852
+ return data.filter((row) => {
2853
+ // Global search
2854
+ if (searchTerm) {
2855
+ const matchesGlobal = filterableColumns.some((column) => {
2856
+ const value = getCellValue(row, column);
2857
+ if (value == null)
2858
+ return false;
2859
+ return String(value).toLowerCase().includes(searchTerm);
2860
+ });
2861
+ if (!matchesGlobal)
2862
+ return false;
2863
+ }
2864
+ // Column-specific filters
2865
+ if (hasColumnFilters) {
2866
+ const matchesColumnFilters = Object.entries(filter.columnFilters).every(([columnKey, filterValue]) => {
2867
+ if (filterValue === null || filterValue === undefined || filterValue === '')
2868
+ return true;
2869
+ const column = columns.find((col) => col.key === columnKey);
2870
+ if (!column)
2871
+ return true;
2872
+ const value = getCellValue(row, column);
2873
+ if (value == null)
2874
+ return false;
2875
+ return String(value).toLowerCase().includes(String(filterValue).toLowerCase());
2876
+ });
2877
+ if (!matchesColumnFilters)
2878
+ return false;
2879
+ }
2880
+ return true;
2881
+ });
2882
+ };
2883
+ const applySorting = (data, sort, columns) => {
2884
+ const column = columns.find((col) => col.key === sort.column);
2885
+ if (!column || !column.sortable)
2886
+ return data;
2887
+ const multiplier = sort.direction === 'asc' ? 1 : -1;
2888
+ return [...data].sort((a, b) => {
2889
+ const aValue = getCellValue(a, column);
2890
+ const bValue = getCellValue(b, column);
2891
+ // Handle null/undefined values
2892
+ if (aValue == null && bValue == null)
2893
+ return 0;
2894
+ if (aValue == null)
2895
+ return -1 * multiplier;
2896
+ if (bValue == null)
2897
+ return 1 * multiplier;
2898
+ // Type-specific comparisons
2899
+ const aType = typeof aValue;
2900
+ const bType = typeof bValue;
2901
+ if (aType === bType) {
2902
+ if (aType === 'number') {
2903
+ return (aValue - bValue) * multiplier;
2904
+ }
2905
+ if (aType === 'boolean') {
2906
+ return (aValue === bValue ? 0 : aValue ? 1 : -1) * multiplier;
2907
+ }
2908
+ if (aValue instanceof Date && bValue instanceof Date) {
2909
+ return (aValue.getTime() - bValue.getTime()) * multiplier;
2910
+ }
2911
+ }
2912
+ // Fallback to string comparison
2913
+ return String(aValue).localeCompare(String(bValue)) * multiplier;
2914
+ });
2915
+ };
2916
+ const processData = (attrs) => {
2917
+ const { data } = attrs;
2918
+ const { internalSort, internalFilter, internalPagination } = state;
2919
+ let processedData = [...data];
2920
+ // Apply filtering
2921
+ if (internalFilter) {
2922
+ processedData = applyFiltering(processedData, internalFilter, attrs.columns);
2923
+ }
2924
+ // Apply sorting
2925
+ if (internalSort) {
2926
+ processedData = applySorting(processedData, internalSort, attrs.columns);
2927
+ }
2928
+ // Update total count for pagination
2929
+ if (internalPagination) {
2930
+ state.internalPagination = Object.assign(Object.assign({}, internalPagination), { total: processedData.length });
2931
+ }
2932
+ // Apply pagination
2933
+ if (internalPagination) {
2934
+ const { page, pageSize } = internalPagination;
2935
+ const start = page * pageSize;
2936
+ const end = start + pageSize;
2937
+ processedData = processedData.slice(start, end);
2938
+ }
2939
+ state.processedData = processedData;
2940
+ };
2941
+ // Create stable helper functions that don't get recreated on every render
2942
+ const createHelpers = (attrs) => ({
2943
+ getCellValue,
2944
+ handleSort: (columnKey) => {
2945
+ var _a;
2946
+ const column = attrs.columns.find((col) => col.key === columnKey);
2947
+ if (!column || !column.sortable)
2948
+ return;
2949
+ const currentSort = state.internalSort;
2950
+ let newSort;
2951
+ if ((currentSort === null || currentSort === void 0 ? void 0 : currentSort.column) === columnKey) {
2952
+ // Toggle direction
2953
+ if (currentSort.direction === 'asc') {
2954
+ newSort = { column: columnKey, direction: 'desc' };
2955
+ }
2956
+ else {
2957
+ newSort = { column: columnKey, direction: 'asc' };
2958
+ }
2959
+ }
2960
+ else {
2961
+ // New column sort
2962
+ newSort = { column: columnKey, direction: 'asc' };
2963
+ }
2964
+ state.internalSort = newSort;
2965
+ (_a = attrs.onSortChange) === null || _a === void 0 ? void 0 : _a.call(attrs, newSort);
2966
+ },
2967
+ handleGlobalSearch: (searchTerm) => {
2968
+ var _a;
2969
+ const newFilter = Object.assign(Object.assign({}, state.internalFilter), { searchTerm });
2970
+ state.internalFilter = newFilter;
2971
+ // Reset pagination to first page when filtering
2972
+ if (state.internalPagination) {
2973
+ state.internalPagination = Object.assign(Object.assign({}, state.internalPagination), { page: 0 });
2974
+ }
2975
+ (_a = attrs.onFilterChange) === null || _a === void 0 ? void 0 : _a.call(attrs, newFilter);
2976
+ },
2977
+ handleSelectionChange: (rowKey, selected) => {
2978
+ var _a, _b;
2979
+ if (!attrs.selection)
2980
+ return;
2981
+ let newSelectedKeys;
2982
+ if (attrs.selection.mode === 'single') {
2983
+ newSelectedKeys = selected ? [rowKey] : [];
2984
+ }
2985
+ else if (attrs.selection.mode === 'multiple') {
2986
+ if (selected) {
2987
+ newSelectedKeys = [...attrs.selection.selectedKeys, rowKey];
2988
+ }
2989
+ else {
2990
+ newSelectedKeys = attrs.selection.selectedKeys.filter((key) => key !== rowKey);
2991
+ }
2992
+ }
2993
+ else {
2994
+ return; // No selection mode
2995
+ }
2996
+ // Get selected rows
2997
+ const selectedRows = attrs.data.filter((row, index) => {
2998
+ const key = attrs.selection.getRowKey(row, index);
2999
+ return newSelectedKeys.includes(key);
3000
+ });
3001
+ (_b = (_a = attrs.selection).onSelectionChange) === null || _b === void 0 ? void 0 : _b.call(_a, newSelectedKeys, selectedRows);
3002
+ },
3003
+ handleSelectAll: (selected) => {
3004
+ var _a, _b;
3005
+ if (!attrs.selection || attrs.selection.mode !== 'multiple')
3006
+ return;
3007
+ let newSelectedKeys;
3008
+ if (selected) {
3009
+ // Select all visible rows
3010
+ newSelectedKeys = state.processedData.map((row) => {
3011
+ const originalIndex = attrs.data.findIndex((originalRow) => originalRow === row);
3012
+ return attrs.selection.getRowKey(row, originalIndex);
3013
+ });
3014
+ }
3015
+ else {
3016
+ newSelectedKeys = [];
3017
+ }
3018
+ const selectedRows = attrs.data.filter((row, index) => {
3019
+ const key = attrs.selection.getRowKey(row, index);
3020
+ return newSelectedKeys.includes(key);
3021
+ });
3022
+ (_b = (_a = attrs.selection).onSelectionChange) === null || _b === void 0 ? void 0 : _b.call(_a, newSelectedKeys, selectedRows);
3023
+ },
3024
+ });
3025
+ return {
3026
+ oninit(vnodeInit) {
3027
+ const { sort, filter, pagination } = vnodeInit.attrs;
3028
+ state.tableId = uniqueId();
3029
+ state.internalSort = sort || undefined;
3030
+ state.internalFilter = filter || { searchTerm: '', columnFilters: {} };
3031
+ state.internalPagination = pagination || undefined;
3032
+ processData(vnodeInit.attrs);
3033
+ },
3034
+ onbeforeupdate(vnodeUpdate) {
3035
+ // Only reprocess data if inputs have changed
3036
+ const currentHash = getDataHash(vnodeUpdate.attrs);
3037
+ if (currentHash !== state.lastProcessedHash) {
3038
+ processData(vnodeUpdate.attrs);
3039
+ state.lastProcessedHash = currentHash;
3040
+ }
3041
+ },
3042
+ view(vnodeView) {
3043
+ const attrs = vnodeView.attrs;
3044
+ const { loading, emptyMessage, striped, hoverable, responsive, centered, className, id, title, height, enableGlobalSearch, searchPlaceholder, selection, columns, onRowClick, onRowDoubleClick, getRowClassName, data, onPaginationChange, i18n, } = attrs;
3045
+ const { processedData, tableId, internalSort, internalPagination } = state;
3046
+ if (loading) {
3047
+ return m('.datatable-loading', [
3048
+ m('.preloader-wrapper.small.active', m('.spinner-layer.spinner-blue-only', m('.circle-clipper.left', m('.circle')))),
3049
+ m('p', (i18n === null || i18n === void 0 ? void 0 : i18n.loading) || 'Loading...'),
3050
+ ]);
3051
+ }
3052
+ // Create stable helpers object using the factory function
3053
+ const helpers = createHelpers(attrs);
3054
+ // Calculate selection state for "select all" checkbox
3055
+ let allSelected = false;
3056
+ let someSelected = false;
3057
+ if (selection && selection.mode === 'multiple') {
3058
+ const visibleRowKeys = processedData.map((row) => {
3059
+ const originalIndex = data.findIndex((originalRow) => originalRow === row);
3060
+ return selection.getRowKey(row, originalIndex);
3061
+ });
3062
+ const selectedVisibleKeys = visibleRowKeys.filter((key) => selection.selectedKeys.includes(key));
3063
+ allSelected = visibleRowKeys.length > 0 && selectedVisibleKeys.length === visibleRowKeys.length;
3064
+ someSelected = selectedVisibleKeys.length > 0 && selectedVisibleKeys.length < visibleRowKeys.length;
3065
+ }
3066
+ const tableClasses = [
3067
+ 'datatable',
3068
+ striped ? 'striped' : '',
3069
+ hoverable ? 'highlight' : '',
3070
+ responsive ? 'responsive-table' : '',
3071
+ centered ? 'centered' : '',
3072
+ className || '',
3073
+ ]
3074
+ .filter(Boolean)
3075
+ .join(' ');
3076
+ return m('.datatable-container', {
3077
+ id: id || tableId,
3078
+ }, title && m('h5.datatable-title', title), enableGlobalSearch &&
3079
+ m(GlobalSearch, {
3080
+ searchPlaceholder,
3081
+ onSearch: helpers.handleGlobalSearch,
3082
+ i18n,
3083
+ }), m('.datatable-wrapper', {
3084
+ style: {
3085
+ maxHeight: height ? `${height}px` : undefined,
3086
+ overflowY: height ? 'auto' : undefined,
3087
+ },
3088
+ }, processedData.length === 0
3089
+ ? m('.datatable-empty', emptyMessage || (i18n === null || i18n === void 0 ? void 0 : i18n.noDataAvailable) || 'No data available')
3090
+ : m(TableContent(), {
3091
+ processedData,
3092
+ height,
3093
+ tableClasses,
3094
+ columns,
3095
+ selection,
3096
+ internalSort,
3097
+ allSelected,
3098
+ someSelected,
3099
+ helpers,
3100
+ onRowClick,
3101
+ onRowDoubleClick,
3102
+ getRowClassName,
3103
+ data,
3104
+ })), m(PaginationControls, {
3105
+ pagination: internalPagination,
3106
+ onPaginationChange: (pagination) => {
3107
+ state.internalPagination = pagination;
3108
+ onPaginationChange === null || onPaginationChange === void 0 ? void 0 : onPaginationChange(pagination);
3109
+ },
3110
+ i18n,
3111
+ }));
3112
+ },
3113
+ };
3114
+ };
3115
+
3116
+ /** Pure TypeScript Dropdown component - no Materialize dependencies */
3117
+ const Dropdown = () => {
3118
+ const state = {
3119
+ isOpen: false,
3120
+ initialValue: undefined,
3121
+ id: '',
3122
+ focusedIndex: -1,
3123
+ inputRef: null,
3124
+ dropdownRef: null,
3125
+ };
3126
+ const handleKeyDown = (e, items, onchange) => {
3127
+ const availableItems = items.filter((item) => !item.divider && !item.disabled);
3128
+ switch (e.key) {
3129
+ case 'ArrowDown':
3130
+ e.preventDefault();
3131
+ if (!state.isOpen) {
3132
+ state.isOpen = true;
3133
+ state.focusedIndex = 0;
3134
+ }
3135
+ else {
3136
+ state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
3137
+ }
3138
+ break;
3139
+ case 'ArrowUp':
3140
+ e.preventDefault();
3141
+ if (state.isOpen) {
3142
+ state.focusedIndex = Math.max(state.focusedIndex - 1, 0);
3143
+ }
3144
+ break;
3145
+ case 'Enter':
3146
+ case ' ':
3147
+ e.preventDefault();
3148
+ if (state.isOpen && state.focusedIndex >= 0 && state.focusedIndex < availableItems.length) {
3149
+ const selectedItem = availableItems[state.focusedIndex];
3150
+ const value = (selectedItem.id || selectedItem.label);
3151
+ state.initialValue = value;
3152
+ state.isOpen = false;
3153
+ state.focusedIndex = -1;
3154
+ if (onchange)
3155
+ onchange(value);
3156
+ }
3157
+ else if (!state.isOpen) {
3158
+ state.isOpen = true;
3159
+ state.focusedIndex = 0;
3160
+ }
3161
+ break;
3162
+ case 'Escape':
3163
+ e.preventDefault();
3164
+ state.isOpen = false;
3165
+ state.focusedIndex = -1;
3166
+ break;
3167
+ }
3168
+ };
3169
+ return {
3170
+ oninit: ({ attrs: { id = uniqueId(), initialValue, checkedId } }) => {
3171
+ state.id = id;
3172
+ state.initialValue = initialValue || checkedId;
3173
+ // Mithril will handle click events through the component structure
3174
+ },
3175
+ view: ({ attrs: { key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12' }, }) => {
3176
+ const { initialValue } = state;
3177
+ const selectedItem = initialValue
3178
+ ? items.filter((i) => (i.id ? i.id === initialValue : i.label === initialValue)).shift()
3179
+ : undefined;
3180
+ const title = selectedItem ? selectedItem.label : label || 'Select';
3181
+ const availableItems = items.filter((item) => !item.divider && !item.disabled);
3182
+ return m('.dropdown-wrapper.input-field', { className, key, style }, [
3183
+ iconName ? m('i.material-icons.prefix', iconName) : undefined,
3184
+ m(HelperText, { helperText }),
3185
+ m('.select-wrapper', {
3186
+ onclick: disabled
3187
+ ? undefined
3188
+ : () => {
3189
+ state.isOpen = !state.isOpen;
3190
+ state.focusedIndex = state.isOpen ? 0 : -1;
3191
+ },
3192
+ onkeydown: disabled ? undefined : (e) => handleKeyDown(e, items, onchange),
3193
+ tabindex: disabled ? -1 : 0,
3194
+ 'aria-expanded': state.isOpen ? 'true' : 'false',
3195
+ 'aria-haspopup': 'listbox',
3196
+ role: 'combobox',
3197
+ }, [
3198
+ m('input[type=text][readonly=true].select-dropdown.dropdown-trigger', {
3199
+ id: state.id,
3200
+ value: title,
3201
+ oncreate: ({ dom }) => {
3202
+ state.inputRef = dom;
3203
+ },
3204
+ onclick: (e) => {
3205
+ e.preventDefault();
3206
+ e.stopPropagation();
3207
+ if (!disabled) {
3208
+ state.isOpen = !state.isOpen;
3209
+ state.focusedIndex = state.isOpen ? 0 : -1;
3210
+ }
3211
+ },
3212
+ }),
3213
+ // Dropdown Menu using Select component's positioning logic
3214
+ state.isOpen &&
3215
+ m('ul.dropdown-content.select-dropdown', {
3216
+ tabindex: 0,
3217
+ role: 'listbox',
3218
+ 'aria-labelledby': state.id,
3219
+ oncreate: ({ dom }) => {
3220
+ state.dropdownRef = dom;
3221
+ },
3222
+ onremove: () => {
3223
+ state.dropdownRef = null;
3224
+ },
3225
+ style: getDropdownStyles(state.inputRef, true, items.map((item) => (Object.assign(Object.assign({}, item), {
3226
+ // Convert dropdown items to format expected by getDropdownStyles
3227
+ group: undefined }))), true),
3228
+ }, items.map((item, index) => {
3229
+ if (item.divider) {
3230
+ return m('li.divider', {
3231
+ key: `divider-${index}`,
3232
+ });
3233
+ }
3234
+ const itemIndex = availableItems.indexOf(item);
3235
+ const isFocused = itemIndex === state.focusedIndex;
3236
+ return m('li', Object.assign({ key: item.id || `item-${index}`, class: [
3237
+ item.disabled ? 'disabled' : '',
3238
+ isFocused ? 'focused' : '',
3239
+ (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
3240
+ ]
3241
+ .filter(Boolean)
3242
+ .join(' ') }, (item.disabled
3243
+ ? {}
3244
+ : {
3245
+ onclick: (e) => {
3246
+ e.stopPropagation();
3247
+ const value = (item.id || item.label);
3248
+ state.initialValue = value;
3249
+ state.isOpen = false;
3250
+ state.focusedIndex = -1;
3251
+ if (onchange)
3252
+ onchange(value);
3253
+ },
3254
+ })), m('span', {
3255
+ style: {
3256
+ display: 'flex',
3257
+ alignItems: 'center',
3258
+ padding: '14px 16px',
3259
+ },
3260
+ }, [
3261
+ item.iconName
3262
+ ? m('i.material-icons', {
3263
+ style: { marginRight: '32px' },
3264
+ }, item.iconName)
3265
+ : undefined,
3266
+ item.label,
3267
+ ]));
3268
+ })),
3269
+ m(MaterialIcon, {
3270
+ name: 'caret',
3271
+ direction: 'down',
3272
+ class: 'caret',
3273
+ }),
3274
+ ]),
3275
+ ]);
3276
+ },
3277
+ };
3278
+ };
3279
+
3280
+ /**
3281
+ * Floating Action Button
3282
+ */
3283
+ const FloatingActionButton = () => {
3284
+ const state = {
3285
+ isOpen: false,
3286
+ };
3287
+ const handleClickOutside = (e) => {
3288
+ const target = e.target;
3289
+ if (!target.closest('.fixed-action-btn')) {
3290
+ state.isOpen = false;
3291
+ }
3292
+ };
2607
3293
  return {
2608
- view: ({ attrs }) => {
2609
- const { multiple, disabled, initialValue, placeholder, onchange, className = 'col s12', accept: acceptedFiles, label = 'File', } = attrs;
2610
- const accept = acceptedFiles
2611
- ? acceptedFiles instanceof Array
2612
- ? acceptedFiles.join(', ')
2613
- : acceptedFiles
2614
- : undefined;
2615
- return m('.file-field.input-field', {
2616
- className: attrs.class || className,
3294
+ oncreate: () => {
3295
+ document.addEventListener('click', handleClickOutside);
3296
+ },
3297
+ onremove: () => {
3298
+ document.removeEventListener('click', handleClickOutside);
3299
+ },
3300
+ view: ({ attrs: { className, iconName, iconClass, position, style = position === 'left' || position === 'inline-left'
3301
+ ? 'position: absolute; display: inline-block; left: 24px;'
3302
+ : position === 'right' || position === 'inline-right'
3303
+ ? 'position: absolute; display: inline-block; right: 24px;'
3304
+ : undefined, buttons, direction = 'top', hoverEnabled = true, }, }) => {
3305
+ const fabClasses = [
3306
+ 'fixed-action-btn',
3307
+ direction ? `direction-${direction}` : '',
3308
+ state.isOpen ? 'active' : '',
3309
+ // hoverEnabled ? 'hover-enabled' : '',
3310
+ ]
3311
+ .filter(Boolean)
3312
+ .join(' ');
3313
+ return m('div', {
3314
+ style: position === 'inline-right' || position === 'inline-left' ? 'position: relative; height: 70px;' : undefined,
3315
+ }, m(`.${fabClasses}`, {
3316
+ style,
3317
+ onclick: (e) => {
3318
+ e.stopPropagation();
3319
+ if (buttons && buttons.length > 0) {
3320
+ state.isOpen = !state.isOpen;
3321
+ }
3322
+ },
3323
+ onmouseover: hoverEnabled
3324
+ ? () => {
3325
+ if (buttons && buttons.length > 0) {
3326
+ state.isOpen = true;
3327
+ }
3328
+ }
3329
+ : undefined,
3330
+ onmouseleave: hoverEnabled
3331
+ ? () => {
3332
+ state.isOpen = false;
3333
+ }
3334
+ : undefined,
2617
3335
  }, [
2618
- m('.btn', [
2619
- m('span', label),
2620
- m('input[type=file]', {
2621
- title: label,
2622
- accept,
2623
- multiple,
2624
- disabled,
2625
- onchange: onchange
2626
- ? (e) => {
2627
- const i = e.target;
2628
- if (i && i.files && onchange) {
2629
- canClear = true;
2630
- onchange(i.files);
2631
- }
2632
- }
2633
- : undefined,
2634
- }),
2635
- ]),
2636
- m('.file-path-wrapper', m('input.file-path.validate[type=text]', {
2637
- placeholder,
2638
- oncreate: ({ dom }) => {
2639
- i = dom;
2640
- if (initialValue)
2641
- i.value = initialValue;
2642
- },
2643
- })),
2644
- (canClear || (i === null || i === void 0 ? void 0 : i.value)) &&
2645
- m('a.waves-effect.waves-teal.btn-flat', {
3336
+ m('a.btn-floating.btn-large', {
3337
+ className,
3338
+ }, m('i.material-icons', { className: iconClass }, iconName)),
3339
+ buttons &&
3340
+ buttons.length > 0 &&
3341
+ m('ul', buttons.map((button, index) => m('li', m(`a.btn-floating.${button.className || 'red'}`, {
2646
3342
  style: {
2647
- float: 'right',
2648
- position: 'relative',
2649
- top: '-3rem',
2650
- padding: 0,
3343
+ opacity: state.isOpen ? '1' : '0',
3344
+ transform: state.isOpen ? 'scale(1)' : 'scale(0.4)',
3345
+ transition: `all 0.3s ease ${index * 40}ms`,
2651
3346
  },
2652
- onclick: () => {
2653
- canClear = false;
2654
- i.value = '';
2655
- onchange && onchange({});
3347
+ onclick: (e) => {
3348
+ e.stopPropagation();
3349
+ if (button.onClick)
3350
+ button.onClick(e);
2656
3351
  },
2657
- }, m(MaterialIcon, {
2658
- name: 'close',
2659
- className: 'close',
2660
- })),
2661
- ]);
3352
+ }, m('i.material-icons', { className: button.iconClass }, button.iconName))))),
3353
+ ]));
2662
3354
  },
2663
3355
  };
2664
3356
  };
@@ -2944,21 +3636,23 @@ const ModalPanel = () => {
2944
3636
  role: 'dialog',
2945
3637
  'aria-labelledby': `${id}-title`,
2946
3638
  'aria-describedby': description ? `${id}-desc` : undefined,
2947
- style: {
2948
- display: state.isOpen ? 'block' : 'none',
2949
- position: 'fixed',
3639
+ style: Object.assign(Object.assign({ display: state.isOpen ? 'flex' : 'none', position: 'fixed' }, (bottomSheet ? {
3640
+ // Bottom sheet positioning
3641
+ top: 'auto',
3642
+ bottom: '0',
3643
+ left: '0',
3644
+ right: '0',
3645
+ transform: 'none',
3646
+ maxWidth: '100%',
3647
+ borderRadius: '8px 8px 0 0',
3648
+ } : {
3649
+ // Regular modal positioning
2950
3650
  top: '50%',
2951
3651
  left: '50%',
2952
3652
  transform: 'translate(-50%, -50%)',
2953
- backgroundColor: '#fff',
2954
- borderRadius: '4px',
2955
3653
  maxWidth: '75%',
2956
- maxHeight: '85%',
2957
- overflow: 'auto',
2958
- zIndex: '1003',
2959
- padding: '0',
2960
- boxShadow: '0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12), 0 11px 15px -7px rgba(0,0,0,0.20)',
2961
- },
3654
+ borderRadius: '4px',
3655
+ })), { backgroundColor: 'var(--mm-modal-background, #fff)', maxHeight: '85%', overflow: 'auto', zIndex: '1003', padding: '0', flexDirection: 'column', boxShadow: '0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12), 0 11px 15px -7px rgba(0,0,0,0.20)' }),
2962
3656
  onclick: (e) => e.stopPropagation(), // Prevent backdrop click when clicking inside modal
2963
3657
  }, [
2964
3658
  // Close button
@@ -2978,7 +3672,12 @@ const ModalPanel = () => {
2978
3672
  }, '×'),
2979
3673
  // Modal content
2980
3674
  m('.modal-content', {
2981
- style: { padding: '24px', paddingTop: showCloseButton ? '48px' : '24px' },
3675
+ style: {
3676
+ padding: '24px',
3677
+ paddingTop: showCloseButton ? '48px' : '24px',
3678
+ minHeight: 'auto',
3679
+ flex: '1 1 auto',
3680
+ },
2982
3681
  }, [
2983
3682
  m('h4', { id: `${id}-title`, style: { margin: '0 0 20px 0' } }, title),
2984
3683
  description &&
@@ -2990,7 +3689,7 @@ const ModalPanel = () => {
2990
3689
  m('.modal-footer', {
2991
3690
  style: {
2992
3691
  padding: '4px 6px',
2993
- borderTop: '1px solid rgba(160,160,160,0.2)',
3692
+ borderTop: '1px solid var(--mm-border-color, rgba(160,160,160,0.2))',
2994
3693
  textAlign: 'right',
2995
3694
  },
2996
3695
  }, buttons.map((buttonProps) => m(FlatButton, Object.assign(Object.assign({}, buttonProps), { className: `modal-close ${buttonProps.className || ''}`, onclick: (e) => {
@@ -3004,109 +3703,6 @@ const ModalPanel = () => {
3004
3703
  };
3005
3704
  };
3006
3705
 
3007
- /** Component to show a check box */
3008
- const InputCheckbox = () => {
3009
- return {
3010
- view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3011
- const checkboxId = inputId || uniqueId();
3012
- return m(`p`, { className, style }, m('label', { for: checkboxId }, [
3013
- m('input[type=checkbox][tabindex=0]', {
3014
- id: checkboxId,
3015
- checked,
3016
- disabled,
3017
- onclick: onchange
3018
- ? (e) => {
3019
- if (e.target && typeof e.target.checked !== 'undefined') {
3020
- onchange(e.target.checked);
3021
- }
3022
- }
3023
- : undefined,
3024
- }),
3025
- label ? (typeof label === 'string' ? m('span', label) : label) : undefined,
3026
- ]), description && m(HelperText, { className: 'input-checkbox-desc', helperText: description }));
3027
- },
3028
- };
3029
- };
3030
- /** A list of checkboxes */
3031
- const Options = () => {
3032
- const state = {};
3033
- const isChecked = (id) => state.checkedIds.indexOf(id) >= 0;
3034
- const selectAll = (options, callback) => {
3035
- const allIds = options.map((option) => option.id);
3036
- state.checkedIds = [...allIds];
3037
- if (callback)
3038
- callback(allIds);
3039
- };
3040
- const selectNone = (callback) => {
3041
- state.checkedIds = [];
3042
- if (callback)
3043
- callback([]);
3044
- };
3045
- return {
3046
- oninit: ({ attrs: { initialValue, checkedId, id } }) => {
3047
- const iv = checkedId || initialValue;
3048
- state.checkedId = checkedId;
3049
- state.checkedIds = iv ? (iv instanceof Array ? [...iv] : [iv]) : [];
3050
- state.componentId = id || uniqueId();
3051
- },
3052
- view: ({ attrs: { label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, onchange: callback, }, }) => {
3053
- const onchange = callback
3054
- ? (propId, checked) => {
3055
- const checkedIds = state.checkedIds.filter((i) => i !== propId);
3056
- if (checked) {
3057
- checkedIds.push(propId);
3058
- }
3059
- state.checkedIds = checkedIds;
3060
- callback(checkedIds);
3061
- }
3062
- : undefined;
3063
- const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim();
3064
- const optionsContent = layout === 'horizontal'
3065
- ? m('div.grid-container', options.map((option) => m(InputCheckbox, {
3066
- disabled: disabled || option.disabled,
3067
- label: option.label,
3068
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3069
- className: option.className || checkboxClass,
3070
- checked: isChecked(option.id),
3071
- description: option.description,
3072
- inputId: `${state.componentId}-${option.id}`,
3073
- })))
3074
- : options.map((option) => m(InputCheckbox, {
3075
- disabled: disabled || option.disabled,
3076
- label: option.label,
3077
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3078
- className: option.className || checkboxClass,
3079
- checked: isChecked(option.id),
3080
- description: option.description,
3081
- inputId: `${state.componentId}-${option.id}`,
3082
- }));
3083
- return m('div', { id: state.componentId, className: cn, style }, [
3084
- label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
3085
- showSelectAll &&
3086
- m('div.select-all-controls', { style: 'margin-bottom: 10px;' }, [
3087
- m('a', {
3088
- href: '#',
3089
- onclick: (e) => {
3090
- e.preventDefault();
3091
- selectAll(options, callback);
3092
- },
3093
- style: 'margin-right: 15px;',
3094
- }, 'Select All'),
3095
- m('a', {
3096
- href: '#',
3097
- onclick: (e) => {
3098
- e.preventDefault();
3099
- selectNone(callback);
3100
- },
3101
- }, 'Select None'),
3102
- ]),
3103
- description && m(HelperText, { helperText: description }),
3104
- m('form', { action: '#' }, optionsContent),
3105
- ]);
3106
- },
3107
- };
3108
- };
3109
-
3110
3706
  const PaginationItem = () => ({
3111
3707
  view: ({ attrs: { title, href, active, disabled } }) => m('li', { className: active ? 'active' : disabled ? 'disabled' : 'waves-effect' }, typeof title === 'number' ? m(m.route.Link, { href }, title) : title),
3112
3708
  });
@@ -3371,12 +3967,15 @@ const TimePicker = () => {
3371
3967
  };
3372
3968
  const updateTimeFromInput = (inputValue) => {
3373
3969
  let value = ((inputValue || options.defaultTime || '') + '').split(':');
3970
+ let amPmWasProvided = false;
3374
3971
  if (options.twelveHour && value.length > 1) {
3375
3972
  if (value[1].toUpperCase().indexOf('AM') > -1) {
3376
3973
  state.amOrPm = 'AM';
3974
+ amPmWasProvided = true;
3377
3975
  }
3378
3976
  else if (value[1].toUpperCase().indexOf('PM') > -1) {
3379
3977
  state.amOrPm = 'PM';
3978
+ amPmWasProvided = true;
3380
3979
  }
3381
3980
  value[1] = value[1].replace('AM', '').replace('PM', '').trim();
3382
3981
  }
@@ -3385,21 +3984,33 @@ const TimePicker = () => {
3385
3984
  value = [now.getHours().toString(), now.getMinutes().toString()];
3386
3985
  if (options.twelveHour) {
3387
3986
  state.amOrPm = parseInt(value[0]) >= 12 ? 'PM' : 'AM';
3987
+ amPmWasProvided = false; // For 'now', we need to do conversion
3388
3988
  }
3389
3989
  }
3390
3990
  let hours = +value[0] || 0;
3391
3991
  let minutes = +value[1] || 0;
3392
- // Handle 24-hour to 12-hour conversion if needed
3393
- if (options.twelveHour && hours >= 12) {
3394
- state.amOrPm = 'PM';
3395
- if (hours > 12) {
3396
- hours = hours - 12;
3992
+ if (options.twelveHour) {
3993
+ if (!amPmWasProvided) {
3994
+ // No AM/PM was provided, assume this is 24-hour format input - convert it
3995
+ if (hours >= 12) {
3996
+ state.amOrPm = 'PM';
3997
+ if (hours > 12) {
3998
+ hours = hours - 12;
3999
+ }
4000
+ }
4001
+ else {
4002
+ state.amOrPm = 'AM';
4003
+ if (hours === 0) {
4004
+ hours = 12;
4005
+ }
4006
+ }
3397
4007
  }
3398
- }
3399
- else if (options.twelveHour && hours < 12) {
3400
- state.amOrPm = 'AM';
3401
- if (hours === 0) {
3402
- hours = 12;
4008
+ else {
4009
+ // AM/PM was provided, hours are already in 12-hour format
4010
+ // Just handle midnight/noon edge cases
4011
+ if (hours === 0 && state.amOrPm === 'AM') {
4012
+ hours = 12;
4013
+ }
3403
4014
  }
3404
4015
  }
3405
4016
  state.hours = hours;
@@ -6185,4 +6796,25 @@ const Stepper = () => {
6185
6796
  };
6186
6797
  };
6187
6798
 
6188
- export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DatePicker, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, InputCheckbox, Label, LargeButton, ListItem, Mandatory, MaterialBox, ModalPanel, NumberInput, Options, Pagination, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Toast, ToastComponent, Tooltip, TooltipComponent, UrlInput, Wizard, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, padLeft, range, toast, uniqueId, uuid4 };
6799
+ /**
6800
+ * @fileoverview Core TypeScript utility types for mithril-materialized library
6801
+ * These types improve type safety and developer experience across all components
6802
+ */
6803
+ /**
6804
+ * Type guard to check if validation result indicates success
6805
+ * @param result - The validation result to check
6806
+ * @returns True if validation passed
6807
+ */
6808
+ const isValidationSuccess = (result) => result === true || result === '';
6809
+ /**
6810
+ * Type guard to check if validation result indicates an error
6811
+ * @param result - The validation result to check
6812
+ * @returns True if validation failed
6813
+ */
6814
+ const isValidationError = (result) => !isValidationSuccess(result);
6815
+ // ============================================================================
6816
+ // EXPORTS
6817
+ // ============================================================================
6818
+ // All types are already exported via individual export declarations above
6819
+
6820
+ export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, InputCheckbox, Label, LargeButton, ListItem, Mandatory, MaterialBox, ModalPanel, NumberInput, Options, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Toast, ToastComponent, Tooltip, TooltipComponent, UrlInput, Wizard, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, toast, uniqueId, uuid4 };