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