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