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