mtrl 0.3.6 → 0.3.8
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/package.json +2 -2
- package/src/components/button/api.ts +16 -0
- package/src/components/button/types.ts +9 -0
- package/src/components/menu/api.ts +61 -22
- package/src/components/menu/config.ts +10 -8
- package/src/components/menu/features/anchor.ts +254 -19
- package/src/components/menu/features/controller.ts +724 -271
- package/src/components/menu/features/index.ts +11 -2
- package/src/components/menu/features/position.ts +353 -0
- package/src/components/menu/index.ts +5 -5
- package/src/components/menu/menu.ts +21 -61
- package/src/components/menu/types.ts +30 -16
- package/src/components/select/api.ts +78 -0
- package/src/components/select/config.ts +76 -0
- package/src/components/select/features.ts +331 -0
- package/src/components/select/index.ts +38 -0
- package/src/components/select/select.ts +73 -0
- package/src/components/select/types.ts +355 -0
- package/src/components/textfield/api.ts +78 -6
- package/src/components/textfield/features/index.ts +17 -0
- package/src/components/textfield/features/leading-icon.ts +127 -0
- package/src/components/textfield/features/placement.ts +149 -0
- package/src/components/textfield/features/prefix-text.ts +107 -0
- package/src/components/textfield/features/suffix-text.ts +100 -0
- package/src/components/textfield/features/supporting-text.ts +113 -0
- package/src/components/textfield/features/trailing-icon.ts +108 -0
- package/src/components/textfield/textfield.ts +51 -15
- package/src/components/textfield/types.ts +70 -0
- package/src/core/collection/adapters/base.ts +62 -0
- package/src/core/collection/collection.ts +300 -0
- package/src/core/collection/index.ts +57 -0
- package/src/core/collection/list-manager.ts +333 -0
- package/src/index.ts +4 -45
- package/src/styles/abstract/_variables.scss +18 -0
- package/src/styles/components/_button.scss +21 -5
- package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
- package/src/styles/components/_menu.scss +97 -24
- package/src/styles/components/_select.scss +272 -0
- package/src/styles/components/_textfield.scss +233 -42
- package/src/styles/main.scss +2 -1
- package/src/components/textfield/features.ts +0 -322
- package/src/core/collection/adapters/base.js +0 -26
- package/src/core/collection/collection.js +0 -259
- package/src/core/collection/list-manager.js +0 -157
- /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
// src/components/textfield/features.ts
|
|
2
|
-
import { BaseComponent, ElementComponent } from '../../core/compose/component';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Configuration for leading icon feature
|
|
6
|
-
*/
|
|
7
|
-
export interface LeadingIconConfig {
|
|
8
|
-
/**
|
|
9
|
-
* Leading icon HTML content
|
|
10
|
-
*/
|
|
11
|
-
leadingIcon?: string;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* CSS class prefix
|
|
15
|
-
*/
|
|
16
|
-
prefix?: string;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Component name
|
|
20
|
-
*/
|
|
21
|
-
componentName?: string;
|
|
22
|
-
|
|
23
|
-
[key: string]: any;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Configuration for trailing icon feature
|
|
28
|
-
*/
|
|
29
|
-
export interface TrailingIconConfig {
|
|
30
|
-
/**
|
|
31
|
-
* Trailing icon HTML content
|
|
32
|
-
*/
|
|
33
|
-
trailingIcon?: string;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* CSS class prefix
|
|
37
|
-
*/
|
|
38
|
-
prefix?: string;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Component name
|
|
42
|
-
*/
|
|
43
|
-
componentName?: string;
|
|
44
|
-
|
|
45
|
-
[key: string]: any;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Configuration for supporting text feature
|
|
50
|
-
*/
|
|
51
|
-
export interface SupportingTextConfig {
|
|
52
|
-
/**
|
|
53
|
-
* Supporting text content
|
|
54
|
-
*/
|
|
55
|
-
supportingText?: string;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Whether supporting text indicates an error
|
|
59
|
-
*/
|
|
60
|
-
error?: boolean;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* CSS class prefix
|
|
64
|
-
*/
|
|
65
|
-
prefix?: string;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Component name
|
|
69
|
-
*/
|
|
70
|
-
componentName?: string;
|
|
71
|
-
|
|
72
|
-
[key: string]: any;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Component with leading icon capabilities
|
|
77
|
-
*/
|
|
78
|
-
export interface LeadingIconComponent extends BaseComponent {
|
|
79
|
-
/**
|
|
80
|
-
* Leading icon element
|
|
81
|
-
*/
|
|
82
|
-
leadingIcon: HTMLElement | null;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Sets leading icon content
|
|
86
|
-
* @param html - HTML content for the icon
|
|
87
|
-
* @returns Component instance for chaining
|
|
88
|
-
*/
|
|
89
|
-
setLeadingIcon: (html: string) => LeadingIconComponent;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Removes leading icon
|
|
93
|
-
* @returns Component instance for chaining
|
|
94
|
-
*/
|
|
95
|
-
removeLeadingIcon: () => LeadingIconComponent;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Component with trailing icon capabilities
|
|
100
|
-
*/
|
|
101
|
-
export interface TrailingIconComponent extends BaseComponent {
|
|
102
|
-
/**
|
|
103
|
-
* Trailing icon element
|
|
104
|
-
*/
|
|
105
|
-
trailingIcon: HTMLElement | null;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Sets trailing icon content
|
|
109
|
-
* @param html - HTML content for the icon
|
|
110
|
-
* @returns Component instance for chaining
|
|
111
|
-
*/
|
|
112
|
-
setTrailingIcon: (html: string) => TrailingIconComponent;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Removes trailing icon
|
|
116
|
-
* @returns Component instance for chaining
|
|
117
|
-
*/
|
|
118
|
-
removeTrailingIcon: () => TrailingIconComponent;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Component with supporting text capabilities
|
|
123
|
-
*/
|
|
124
|
-
export interface SupportingTextComponent extends BaseComponent {
|
|
125
|
-
/**
|
|
126
|
-
* Supporting text element
|
|
127
|
-
*/
|
|
128
|
-
supportingTextElement: HTMLElement | null;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Sets supporting text content
|
|
132
|
-
* @param text - Text content
|
|
133
|
-
* @param isError - Whether text represents an error
|
|
134
|
-
* @returns Component instance for chaining
|
|
135
|
-
*/
|
|
136
|
-
setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Removes supporting text
|
|
140
|
-
* @returns Component instance for chaining
|
|
141
|
-
*/
|
|
142
|
-
removeSupportingText: () => SupportingTextComponent;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Creates and manages a leading icon for a component
|
|
147
|
-
* @param config - Configuration object with leading icon settings
|
|
148
|
-
* @returns Function that enhances a component with leading icon functionality
|
|
149
|
-
*/
|
|
150
|
-
export const withLeadingIcon = <T extends LeadingIconConfig>(config: T) =>
|
|
151
|
-
<C extends ElementComponent>(component: C): C & LeadingIconComponent => {
|
|
152
|
-
if (!config.leadingIcon) {
|
|
153
|
-
return component as C & LeadingIconComponent;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Create icon element
|
|
157
|
-
const PREFIX = config.prefix || 'mtrl';
|
|
158
|
-
const iconElement = document.createElement('span');
|
|
159
|
-
iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
|
|
160
|
-
iconElement.innerHTML = config.leadingIcon;
|
|
161
|
-
|
|
162
|
-
// Add leading icon to the component
|
|
163
|
-
component.element.appendChild(iconElement);
|
|
164
|
-
|
|
165
|
-
// Add leading-icon class to the component
|
|
166
|
-
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
167
|
-
|
|
168
|
-
// When there's a leading icon, adjust input padding
|
|
169
|
-
if (component.input) {
|
|
170
|
-
component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Add lifecycle integration if available
|
|
174
|
-
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
175
|
-
const originalDestroy = component.lifecycle.destroy;
|
|
176
|
-
component.lifecycle.destroy = () => {
|
|
177
|
-
iconElement.remove();
|
|
178
|
-
originalDestroy.call(component.lifecycle);
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
...component,
|
|
184
|
-
leadingIcon: iconElement,
|
|
185
|
-
|
|
186
|
-
setLeadingIcon(html: string) {
|
|
187
|
-
iconElement.innerHTML = html;
|
|
188
|
-
return this;
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
removeLeadingIcon() {
|
|
192
|
-
if (iconElement.parentNode) {
|
|
193
|
-
iconElement.remove();
|
|
194
|
-
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
195
|
-
if (component.input) {
|
|
196
|
-
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
197
|
-
}
|
|
198
|
-
this.leadingIcon = null;
|
|
199
|
-
}
|
|
200
|
-
return this;
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Creates and manages a trailing icon for a component
|
|
207
|
-
* @param config - Configuration object with trailing icon settings
|
|
208
|
-
* @returns Function that enhances a component with trailing icon functionality
|
|
209
|
-
*/
|
|
210
|
-
export const withTrailingIcon = <T extends TrailingIconConfig>(config: T) =>
|
|
211
|
-
<C extends ElementComponent>(component: C): C & TrailingIconComponent => {
|
|
212
|
-
if (!config.trailingIcon) {
|
|
213
|
-
return component as C & TrailingIconComponent;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Create icon element
|
|
217
|
-
const PREFIX = config.prefix || 'mtrl';
|
|
218
|
-
const iconElement = document.createElement('span');
|
|
219
|
-
iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-trailing-icon`;
|
|
220
|
-
iconElement.innerHTML = config.trailingIcon;
|
|
221
|
-
|
|
222
|
-
// Add trailing icon to the component
|
|
223
|
-
component.element.appendChild(iconElement);
|
|
224
|
-
|
|
225
|
-
// Add trailing-icon class to the component
|
|
226
|
-
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
|
|
227
|
-
|
|
228
|
-
// When there's a trailing icon, adjust input padding
|
|
229
|
-
if (component.input) {
|
|
230
|
-
component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Add lifecycle integration if available
|
|
234
|
-
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
235
|
-
const originalDestroy = component.lifecycle.destroy;
|
|
236
|
-
component.lifecycle.destroy = () => {
|
|
237
|
-
iconElement.remove();
|
|
238
|
-
originalDestroy.call(component.lifecycle);
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
...component,
|
|
244
|
-
trailingIcon: iconElement,
|
|
245
|
-
|
|
246
|
-
setTrailingIcon(html: string) {
|
|
247
|
-
iconElement.innerHTML = html;
|
|
248
|
-
return this;
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
removeTrailingIcon() {
|
|
252
|
-
if (iconElement.parentNode) {
|
|
253
|
-
iconElement.remove();
|
|
254
|
-
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
|
|
255
|
-
if (component.input) {
|
|
256
|
-
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
|
|
257
|
-
}
|
|
258
|
-
this.trailingIcon = null;
|
|
259
|
-
}
|
|
260
|
-
return this;
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Creates and manages supporting text for a component
|
|
267
|
-
* @param config - Configuration object with supporting text settings
|
|
268
|
-
* @returns Function that enhances a component with supporting text functionality
|
|
269
|
-
*/
|
|
270
|
-
export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
|
|
271
|
-
<C extends ElementComponent>(component: C): C & SupportingTextComponent => {
|
|
272
|
-
if (!config.supportingText) {
|
|
273
|
-
return component as C & SupportingTextComponent;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Create supporting text element
|
|
277
|
-
const PREFIX = config.prefix || 'mtrl';
|
|
278
|
-
const supportingElement = document.createElement('div');
|
|
279
|
-
supportingElement.className = `${PREFIX}-${config.componentName || 'textfield'}-helper`;
|
|
280
|
-
supportingElement.textContent = config.supportingText;
|
|
281
|
-
|
|
282
|
-
if (config.error) {
|
|
283
|
-
supportingElement.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`);
|
|
284
|
-
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--error`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Add supporting text to the component
|
|
288
|
-
component.element.appendChild(supportingElement);
|
|
289
|
-
|
|
290
|
-
// Add lifecycle integration if available
|
|
291
|
-
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
292
|
-
const originalDestroy = component.lifecycle.destroy;
|
|
293
|
-
component.lifecycle.destroy = () => {
|
|
294
|
-
supportingElement.remove();
|
|
295
|
-
originalDestroy.call(component.lifecycle);
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
...component,
|
|
301
|
-
supportingTextElement: supportingElement,
|
|
302
|
-
|
|
303
|
-
setSupportingText(text: string, isError = false) {
|
|
304
|
-
supportingElement.textContent = text;
|
|
305
|
-
|
|
306
|
-
// Handle error state
|
|
307
|
-
supportingElement.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`, isError);
|
|
308
|
-
component.element.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}--error`, isError);
|
|
309
|
-
|
|
310
|
-
return this;
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
removeSupportingText() {
|
|
314
|
-
if (supportingElement.parentNode) {
|
|
315
|
-
supportingElement.remove();
|
|
316
|
-
this.supportingTextElement = null;
|
|
317
|
-
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--error`);
|
|
318
|
-
}
|
|
319
|
-
return this;
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// src/core/collection/adapters/base.js
|
|
2
|
-
|
|
3
|
-
export const OPERATORS = {
|
|
4
|
-
EQ: 'eq',
|
|
5
|
-
NE: 'ne',
|
|
6
|
-
GT: 'gt',
|
|
7
|
-
GTE: 'gte',
|
|
8
|
-
LT: 'lt',
|
|
9
|
-
LTE: 'lte',
|
|
10
|
-
IN: 'in',
|
|
11
|
-
NIN: 'nin',
|
|
12
|
-
CONTAINS: 'contains',
|
|
13
|
-
STARTS_WITH: 'startsWith',
|
|
14
|
-
ENDS_WITH: 'endsWith'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const createBaseAdapter = ({ onError } = {}) => {
|
|
18
|
-
const handleError = (error, context) => {
|
|
19
|
-
onError?.(error, context)
|
|
20
|
-
throw error
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
handleError
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// src/core/collection/collection.js
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Event types for collection changes
|
|
5
|
-
*/
|
|
6
|
-
export const COLLECTION_EVENTS = {
|
|
7
|
-
CHANGE: 'change',
|
|
8
|
-
ADD: 'add',
|
|
9
|
-
UPDATE: 'update',
|
|
10
|
-
REMOVE: 'remove',
|
|
11
|
-
ERROR: 'error',
|
|
12
|
-
LOADING: 'loading'
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Query operators for filtering
|
|
17
|
-
*/
|
|
18
|
-
export const OPERATORS = {
|
|
19
|
-
EQ: 'eq',
|
|
20
|
-
NE: 'ne',
|
|
21
|
-
GT: 'gt',
|
|
22
|
-
GTE: 'gte',
|
|
23
|
-
LT: 'lt',
|
|
24
|
-
LTE: 'lte',
|
|
25
|
-
IN: 'in',
|
|
26
|
-
NIN: 'nin',
|
|
27
|
-
CONTAINS: 'contains',
|
|
28
|
-
STARTS_WITH: 'startsWith',
|
|
29
|
-
ENDS_WITH: 'endsWith'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Base Collection class providing data management interface
|
|
34
|
-
* @template T - Type of items in collection
|
|
35
|
-
*/
|
|
36
|
-
export class Collection {
|
|
37
|
-
#items = new Map()
|
|
38
|
-
#observers = new Set()
|
|
39
|
-
#query = null
|
|
40
|
-
#sort = null
|
|
41
|
-
#loading = false
|
|
42
|
-
#error = null
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Creates a new collection instance
|
|
46
|
-
* @param {Object} config - Collection configuration
|
|
47
|
-
* @param {Function} config.transform - Transform function for items
|
|
48
|
-
* @param {Function} config.validate - Validation function for items
|
|
49
|
-
*/
|
|
50
|
-
constructor (config = {}) {
|
|
51
|
-
this.transform = config.transform || (item => item)
|
|
52
|
-
this.validate = config.validate || (() => true)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Subscribe to collection changes
|
|
57
|
-
* @param {Function} observer - Observer callback
|
|
58
|
-
* @returns {Function} Unsubscribe function
|
|
59
|
-
*/
|
|
60
|
-
subscribe (observer) {
|
|
61
|
-
this.#observers.add(observer)
|
|
62
|
-
return () => this.#observers.delete(observer)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Notify observers of collection changes
|
|
67
|
-
* @param {string} event - Event type
|
|
68
|
-
* @param {*} data - Event data
|
|
69
|
-
*/
|
|
70
|
-
#notify (event, data) {
|
|
71
|
-
this.#observers.forEach(observer => observer({ event, data }))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Set loading state
|
|
76
|
-
* @param {boolean} loading - Loading state
|
|
77
|
-
*/
|
|
78
|
-
#setLoading (loading) {
|
|
79
|
-
this.#loading = loading
|
|
80
|
-
this.#notify(COLLECTION_EVENTS.LOADING, loading)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Set error state
|
|
85
|
-
* @param {Error} error - Error object
|
|
86
|
-
*/
|
|
87
|
-
#setError (error) {
|
|
88
|
-
this.#error = error
|
|
89
|
-
this.#notify(COLLECTION_EVENTS.ERROR, error)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get collection items based on current query and sort
|
|
94
|
-
* @returns {Array<T>} Collection items
|
|
95
|
-
*/
|
|
96
|
-
get items () {
|
|
97
|
-
let result = Array.from(this.#items.values())
|
|
98
|
-
|
|
99
|
-
if (this.#query) {
|
|
100
|
-
result = result.filter(this.#query)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (this.#sort) {
|
|
104
|
-
result.sort(this.#sort)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return result
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Get collection size
|
|
112
|
-
* @returns {number} Number of items
|
|
113
|
-
*/
|
|
114
|
-
get size () {
|
|
115
|
-
return this.#items.size
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get loading state
|
|
120
|
-
* @returns {boolean} Loading state
|
|
121
|
-
*/
|
|
122
|
-
get loading () {
|
|
123
|
-
return this.#loading
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get error state
|
|
128
|
-
* @returns {Error|null} Error object
|
|
129
|
-
*/
|
|
130
|
-
get error () {
|
|
131
|
-
return this.#error
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Set query filter
|
|
136
|
-
* @param {Function} queryFn - Query function
|
|
137
|
-
*/
|
|
138
|
-
query (queryFn) {
|
|
139
|
-
this.#query = queryFn
|
|
140
|
-
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Set sort function
|
|
145
|
-
* @param {Function} sortFn - Sort function
|
|
146
|
-
*/
|
|
147
|
-
sort (sortFn) {
|
|
148
|
-
this.#sort = sortFn
|
|
149
|
-
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Add items to collection
|
|
154
|
-
* @param {T|Array<T>} items - Items to add
|
|
155
|
-
* @returns {Promise<Array<T>>} Added items
|
|
156
|
-
*/
|
|
157
|
-
async add (items) {
|
|
158
|
-
try {
|
|
159
|
-
this.#setLoading(true)
|
|
160
|
-
const toAdd = Array.isArray(items) ? items : [items]
|
|
161
|
-
|
|
162
|
-
const validated = toAdd.filter(this.validate)
|
|
163
|
-
const transformed = validated.map(this.transform)
|
|
164
|
-
|
|
165
|
-
transformed.forEach(item => {
|
|
166
|
-
if (!item.id) {
|
|
167
|
-
throw new Error('Items must have an id property')
|
|
168
|
-
}
|
|
169
|
-
this.#items.set(item.id, item)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
this.#notify(COLLECTION_EVENTS.ADD, transformed)
|
|
173
|
-
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
174
|
-
|
|
175
|
-
return transformed
|
|
176
|
-
} catch (error) {
|
|
177
|
-
this.#setError(error)
|
|
178
|
-
throw error
|
|
179
|
-
} finally {
|
|
180
|
-
this.#setLoading(false)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Update items in collection
|
|
186
|
-
* @param {T|Array<T>} items - Items to update
|
|
187
|
-
* @returns {Promise<Array<T>>} Updated items
|
|
188
|
-
*/
|
|
189
|
-
async update (items) {
|
|
190
|
-
try {
|
|
191
|
-
this.#setLoading(true)
|
|
192
|
-
const toUpdate = Array.isArray(items) ? items : [items]
|
|
193
|
-
|
|
194
|
-
const updated = toUpdate.map(item => {
|
|
195
|
-
if (!this.#items.has(item.id)) {
|
|
196
|
-
throw new Error(`Item with id ${item.id} not found`)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const validated = this.validate(item)
|
|
200
|
-
if (!validated) {
|
|
201
|
-
throw new Error(`Invalid item: ${item.id}`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const transformed = this.transform(item)
|
|
205
|
-
this.#items.set(item.id, transformed)
|
|
206
|
-
return transformed
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
this.#notify(COLLECTION_EVENTS.UPDATE, updated)
|
|
210
|
-
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
211
|
-
|
|
212
|
-
return updated
|
|
213
|
-
} catch (error) {
|
|
214
|
-
this.#setError(error)
|
|
215
|
-
throw error
|
|
216
|
-
} finally {
|
|
217
|
-
this.#setLoading(false)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Remove items from collection
|
|
223
|
-
* @param {string|Array<string>} ids - Item IDs to remove
|
|
224
|
-
* @returns {Promise<Array<string>>} Removed item IDs
|
|
225
|
-
*/
|
|
226
|
-
async remove (ids) {
|
|
227
|
-
try {
|
|
228
|
-
this.#setLoading(true)
|
|
229
|
-
const toRemove = Array.isArray(ids) ? ids : [ids]
|
|
230
|
-
|
|
231
|
-
toRemove.forEach(id => {
|
|
232
|
-
if (!this.#items.has(id)) {
|
|
233
|
-
throw new Error(`Item with id ${id} not found`)
|
|
234
|
-
}
|
|
235
|
-
this.#items.delete(id)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
this.#notify(COLLECTION_EVENTS.REMOVE, toRemove)
|
|
239
|
-
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
240
|
-
|
|
241
|
-
return toRemove
|
|
242
|
-
} catch (error) {
|
|
243
|
-
this.#setError(error)
|
|
244
|
-
throw error
|
|
245
|
-
} finally {
|
|
246
|
-
this.#setLoading(false)
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Clear all items from collection
|
|
252
|
-
*/
|
|
253
|
-
clear () {
|
|
254
|
-
this.#items.clear()
|
|
255
|
-
this.#query = null
|
|
256
|
-
this.#sort = null
|
|
257
|
-
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
258
|
-
}
|
|
259
|
-
}
|