@makigamestudio/ui-core 0.4.2 → 0.5.0
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/fesm2022/makigamestudio-ui-core.mjs +688 -460
- package/fesm2022/makigamestudio-ui-core.mjs.map +1 -1
- package/package.json +1 -1
- package/theme.css +5 -30
- package/types/makigamestudio-ui-core.d.ts +435 -225
|
@@ -1,342 +1,682 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Injectable, signal, inject, input, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { PopoverController, IonList, IonItem, IonIcon, IonLabel, IonSpinner, IonButton } from '@ionic/angular/standalone';
|
|
4
4
|
import { addIcons } from 'ionicons';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* TestClass - Example class implementing TestInterface.
|
|
9
|
-
* Demonstrates the pattern for creating model classes with validation and factory methods.
|
|
10
|
-
*/
|
|
11
|
-
class TestClass {
|
|
12
|
-
id;
|
|
13
|
-
title;
|
|
14
|
-
description;
|
|
15
|
-
createdAt;
|
|
16
|
-
isActive;
|
|
17
|
-
metadata;
|
|
18
|
-
constructor(data) {
|
|
19
|
-
this.id = data.id;
|
|
20
|
-
this.title = data.title;
|
|
21
|
-
this.description = data.description;
|
|
22
|
-
this.createdAt = data.createdAt instanceof Date ? data.createdAt : new Date(data.createdAt);
|
|
23
|
-
this.isActive = data.isActive;
|
|
24
|
-
this.metadata = data.metadata;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Factory method to create a new TestClass instance with a generated ID.
|
|
28
|
-
*/
|
|
29
|
-
static create(title, description) {
|
|
30
|
-
return new TestClass({
|
|
31
|
-
id: crypto.randomUUID(),
|
|
32
|
-
title,
|
|
33
|
-
description,
|
|
34
|
-
createdAt: new Date(),
|
|
35
|
-
isActive: true
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Creates a new instance with updated properties (immutable pattern).
|
|
40
|
-
*/
|
|
41
|
-
update(changes) {
|
|
42
|
-
return new TestClass({
|
|
43
|
-
...this,
|
|
44
|
-
...changes
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Returns a deactivated copy of this instance.
|
|
49
|
-
*/
|
|
50
|
-
deactivate() {
|
|
51
|
-
return this.update({ isActive: false });
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Converts the instance to a plain JSON object.
|
|
55
|
-
*/
|
|
56
|
-
toJSON() {
|
|
57
|
-
return {
|
|
58
|
-
id: this.id,
|
|
59
|
-
title: this.title,
|
|
60
|
-
description: this.description,
|
|
61
|
-
createdAt: this.createdAt,
|
|
62
|
-
isActive: this.isActive,
|
|
63
|
-
metadata: this.metadata
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
5
|
+
import { chevronDownOutline } from 'ionicons/icons';
|
|
67
6
|
|
|
68
7
|
/**
|
|
69
8
|
* @file Action button type enumeration
|
|
70
9
|
* @description Defines the display types for action buttons
|
|
71
10
|
*/
|
|
72
11
|
/**
|
|
73
|
-
* Enumeration of action button
|
|
12
|
+
* Enumeration of action button types.
|
|
13
|
+
*
|
|
14
|
+
* Both types support flexible display formats:
|
|
15
|
+
* - Icon + Label: Provide both `icon` and `label` properties
|
|
16
|
+
* - Icon only: Provide `icon` without `label`
|
|
17
|
+
* - Label only: Provide `label` without `icon`
|
|
74
18
|
*
|
|
75
19
|
* @example
|
|
76
20
|
* ```typescript
|
|
77
|
-
*
|
|
21
|
+
* // Default button with label and icon
|
|
22
|
+
* const saveButton: ActionButton = {
|
|
78
23
|
* id: 'save-btn',
|
|
79
24
|
* label: 'Save',
|
|
25
|
+
* icon: 'save-outline',
|
|
80
26
|
* type: ActionButtonType.Default,
|
|
81
27
|
* handler: () => console.log('Saved!')
|
|
82
28
|
* };
|
|
29
|
+
*
|
|
30
|
+
* // Icon-only button (no label)
|
|
31
|
+
* const iconButton: ActionButton = {
|
|
32
|
+
* id: 'settings-btn',
|
|
33
|
+
* icon: 'settings-outline',
|
|
34
|
+
* type: ActionButtonType.Default,
|
|
35
|
+
* ariaLabel: 'Settings',
|
|
36
|
+
* handler: () => console.log('Settings')
|
|
37
|
+
* };
|
|
38
|
+
*
|
|
39
|
+
* // Label-only button (no icon)
|
|
40
|
+
* const textButton: ActionButton = {
|
|
41
|
+
* id: 'cancel-btn',
|
|
42
|
+
* label: 'Cancel',
|
|
43
|
+
* type: ActionButtonType.Default,
|
|
44
|
+
* handler: () => console.log('Cancelled')
|
|
45
|
+
* };
|
|
83
46
|
* ```
|
|
84
47
|
*/
|
|
85
48
|
var ActionButtonType;
|
|
86
49
|
(function (ActionButtonType) {
|
|
87
50
|
/**
|
|
88
|
-
* Standard button
|
|
51
|
+
* Standard action button.
|
|
52
|
+
* Display format (icon-only, label-only, or icon+label) is determined by which properties are provided.
|
|
89
53
|
*/
|
|
90
54
|
ActionButtonType["Default"] = "default";
|
|
91
|
-
/**
|
|
92
|
-
* Button displaying only an icon, no label.
|
|
93
|
-
*/
|
|
94
|
-
ActionButtonType["IconOnly"] = "icon-only";
|
|
95
55
|
/**
|
|
96
56
|
* Button that opens a dropdown popover with child actions.
|
|
57
|
+
* Like Default, supports icon-only, label-only, or icon+label display.
|
|
97
58
|
*/
|
|
98
59
|
ActionButtonType["Dropdown"] = "dropdown";
|
|
99
60
|
})(ActionButtonType || (ActionButtonType = {}));
|
|
100
61
|
|
|
101
62
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
63
|
+
* @file Action Button List Service
|
|
64
|
+
* @description Provides logic for managing lists of action buttons in dropdowns/popovers.
|
|
65
|
+
*
|
|
66
|
+
* This service encapsulates all list-related logic for action buttons, making it
|
|
67
|
+
* reusable across different UI library implementations (Ionic popover, Material menu,
|
|
68
|
+
* PrimeNG overlay, etc.).
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* // Ionic implementation (ActionButtonListComponent)
|
|
73
|
+
* @Component({
|
|
74
|
+
* providers: [ActionButtonListService],
|
|
75
|
+
* template: `
|
|
76
|
+
* <ion-list>
|
|
77
|
+
* @for (button of buttonList(); track button.id) {
|
|
78
|
+
* <ion-item
|
|
79
|
+
* [disabled]="!canSelectButton(button)"
|
|
80
|
+
* (click)="onSelect(button)"
|
|
81
|
+
* >
|
|
82
|
+
* @if (isButtonLoading(button)) {
|
|
83
|
+
* <ion-spinner slot="start" />
|
|
84
|
+
* }
|
|
85
|
+
* <ion-label>{{ button.label }}</ion-label>
|
|
86
|
+
* </ion-item>
|
|
87
|
+
* }
|
|
88
|
+
* </ion-list>
|
|
89
|
+
* `
|
|
90
|
+
* })
|
|
91
|
+
* export class ActionButtonListComponent {
|
|
92
|
+
* private readonly listService = inject(ActionButtonListService);
|
|
93
|
+
*
|
|
94
|
+
* readonly buttonList = computed(() =>
|
|
95
|
+
* this.listService.resolveButtonList(this._propsButtons(), this.buttons())
|
|
96
|
+
* );
|
|
97
|
+
*
|
|
98
|
+
* protected isButtonLoading(button: ActionButton): boolean {
|
|
99
|
+
* return this.listService.isButtonLoading(button, this.loadingChildIds());
|
|
100
|
+
* }
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* // Material Menu implementation example
|
|
104
|
+
* @Component({
|
|
105
|
+
* selector: 'mat-action-menu',
|
|
106
|
+
* providers: [ActionButtonListService],
|
|
107
|
+
* template: `
|
|
108
|
+
* <mat-menu #menu="matMenu">
|
|
109
|
+
* @for (button of buttonList(); track button.id) {
|
|
110
|
+
* <button mat-menu-item
|
|
111
|
+
* [disabled]="!canSelectButton(button)"
|
|
112
|
+
* (click)="onSelect(button)"
|
|
113
|
+
* >
|
|
114
|
+
* @if (isButtonLoading(button)) {
|
|
115
|
+
* <mat-spinner diameter="16" />
|
|
116
|
+
* } @else if (button.icon) {
|
|
117
|
+
* <mat-icon>{{ button.icon }}</mat-icon>
|
|
118
|
+
* }
|
|
119
|
+
* <span>{{ button.label }}</span>
|
|
120
|
+
* </button>
|
|
121
|
+
* }
|
|
122
|
+
* </mat-menu>
|
|
123
|
+
* `
|
|
124
|
+
* })
|
|
125
|
+
* export class MatActionMenuComponent {
|
|
126
|
+
* private readonly listService = inject(ActionButtonListService);
|
|
127
|
+
* // ... similar implementation
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
104
130
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Service that provides logic for managing lists of action buttons.
|
|
133
|
+
*
|
|
134
|
+
* This service is designed to be provided at the component level to maintain
|
|
135
|
+
* consistency with other button services.
|
|
136
|
+
*
|
|
137
|
+
* @usageNotes
|
|
138
|
+
* ### Button List Resolution
|
|
139
|
+
* The service handles two input sources for buttons:
|
|
140
|
+
* 1. Direct property assignment (via PopoverController.create componentProps)
|
|
141
|
+
* 2. Angular signal input binding
|
|
142
|
+
*
|
|
143
|
+
* Props take precedence over input when both are provided.
|
|
144
|
+
*
|
|
145
|
+
* ### Loading State
|
|
146
|
+
* Loading state can come from:
|
|
147
|
+
* 1. The button's own `loading` property
|
|
148
|
+
* 2. A set of loading child IDs passed from the parent button
|
|
149
|
+
*
|
|
150
|
+
* ### Selection Validation
|
|
151
|
+
* A button can be selected only if it's not loading and not disabled.
|
|
152
|
+
*/
|
|
153
|
+
class ActionButtonListService {
|
|
123
154
|
/**
|
|
124
|
-
*
|
|
155
|
+
* Resolves the button list from either props or input.
|
|
156
|
+
* Props (from PopoverController) take precedence over input binding.
|
|
157
|
+
*
|
|
158
|
+
* @param propsButtons - Buttons passed via component props
|
|
159
|
+
* @param inputButtons - Buttons passed via signal input
|
|
160
|
+
* @returns The resolved button array
|
|
125
161
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this._items.update(items => [...items, newItem]);
|
|
129
|
-
this._error.set(null);
|
|
130
|
-
return newItem;
|
|
162
|
+
resolveButtonList(propsButtons, inputButtons) {
|
|
163
|
+
return propsButtons.length > 0 ? propsButtons : inputButtons;
|
|
131
164
|
}
|
|
132
165
|
/**
|
|
133
|
-
*
|
|
166
|
+
* Checks if a button is currently in a loading state.
|
|
167
|
+
*
|
|
168
|
+
* @param button - The button to check
|
|
169
|
+
* @param loadingChildIds - Set of child IDs currently loading (optional)
|
|
170
|
+
* @returns True if the button is loading
|
|
134
171
|
*/
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
if (filteredItems.length === currentItems.length) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
this._items.set(filteredItems);
|
|
142
|
-
return true;
|
|
172
|
+
isButtonLoading(button, loadingChildIds) {
|
|
173
|
+
const ids = loadingChildIds ?? new Set();
|
|
174
|
+
return (button.loading ?? false) || ids.has(button.id);
|
|
143
175
|
}
|
|
144
176
|
/**
|
|
145
|
-
*
|
|
177
|
+
* Checks if a button is currently in a loading state using a signal.
|
|
178
|
+
* Convenience method when loadingChildIds is provided as a signal.
|
|
179
|
+
*
|
|
180
|
+
* @param button - The button to check
|
|
181
|
+
* @param loadingChildIdsSignal - Signal containing loading child IDs (optional)
|
|
182
|
+
* @returns True if the button is loading
|
|
146
183
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
if (item.id === id) {
|
|
151
|
-
updatedItem = item.update(changes);
|
|
152
|
-
return updatedItem;
|
|
153
|
-
}
|
|
154
|
-
return item;
|
|
155
|
-
}));
|
|
156
|
-
return updatedItem;
|
|
184
|
+
isButtonLoadingFromSignal(button, loadingChildIdsSignal) {
|
|
185
|
+
const loadingChildIds = loadingChildIdsSignal ? loadingChildIdsSignal() : new Set();
|
|
186
|
+
return this.isButtonLoading(button, loadingChildIds);
|
|
157
187
|
}
|
|
158
188
|
/**
|
|
159
|
-
*
|
|
189
|
+
* Determines if a button can be selected (clicked).
|
|
190
|
+
*
|
|
191
|
+
* @param button - The button to check
|
|
192
|
+
* @param loadingChildIds - Set of child IDs currently loading (optional)
|
|
193
|
+
* @returns True if the button can be selected
|
|
160
194
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
canSelectButton(button, loadingChildIds) {
|
|
196
|
+
const isLoading = this.isButtonLoading(button, loadingChildIds);
|
|
197
|
+
const isDisabled = button.config?.disabled ?? false;
|
|
198
|
+
return !isLoading && !isDisabled;
|
|
163
199
|
}
|
|
164
200
|
/**
|
|
165
|
-
*
|
|
201
|
+
* Determines whether to show a loading spinner for a button.
|
|
202
|
+
*
|
|
203
|
+
* @param button - The button to check
|
|
204
|
+
* @param loadingChildIds - Set of child IDs currently loading (optional)
|
|
205
|
+
* @returns True if spinner should be shown
|
|
166
206
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
shouldShowSpinner(button, loadingChildIds) {
|
|
208
|
+
const isLoading = this.isButtonLoading(button, loadingChildIds);
|
|
209
|
+
const showSpinner = button.showLoadingSpinner ?? true;
|
|
210
|
+
return isLoading && showSpinner;
|
|
169
211
|
}
|
|
212
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
213
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListService });
|
|
214
|
+
}
|
|
215
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListService, decorators: [{
|
|
216
|
+
type: Injectable
|
|
217
|
+
}] });
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @file Button Display Service
|
|
221
|
+
* @description Provides display logic for action buttons (icon slots, label visibility, etc.).
|
|
222
|
+
*
|
|
223
|
+
* This service encapsulates all display-related logic for buttons, making it
|
|
224
|
+
* reusable across different UI library implementations.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* // Ionic implementation
|
|
229
|
+
* @Component({
|
|
230
|
+
* providers: [ButtonDisplayService],
|
|
231
|
+
* template: `
|
|
232
|
+
* <ion-button
|
|
233
|
+
[fill]="button().config?.fill"
|
|
234
|
+
[size]="button().config?.size"
|
|
235
|
+
[color]="button().config?.color"
|
|
236
|
+
[shape]="button().config?.shape"
|
|
237
|
+
[expand]="button().config?.expand"
|
|
238
|
+
[strong]="button().config?.strong"
|
|
239
|
+
[disabled]="isDisabled()"
|
|
240
|
+
[attr.aria-label]="button().ariaLabel"
|
|
241
|
+
[title]="button().tooltip ?? ''"
|
|
242
|
+
(click)="onClick($event)"
|
|
243
|
+
>
|
|
244
|
+
@if (showLoadingSpinner()) {
|
|
245
|
+
<ion-spinner [slot]="iconSlot()" name="crescent" />
|
|
246
|
+
} @else if (button().icon) {
|
|
247
|
+
<ion-icon [name]="button().icon" [slot]="iconSlot()" class="button-icon" />
|
|
248
|
+
}
|
|
249
|
+
@if (showLabel()) {
|
|
250
|
+
{{ button().label }}
|
|
251
|
+
}
|
|
252
|
+
@if (showDropdownIcon()) {
|
|
253
|
+
<ion-icon name="chevron-down-outline" slot="end" class="dropdown-icon" />
|
|
254
|
+
}
|
|
255
|
+
</ion-button>
|
|
256
|
+
* `
|
|
257
|
+
* })
|
|
258
|
+
* export class ButtonComponent {
|
|
259
|
+
* private readonly displayService = inject(ButtonDisplayService);
|
|
260
|
+
* readonly button = input.required<ActionButton>();
|
|
261
|
+
*
|
|
262
|
+
* readonly showLabel = computed(() =>
|
|
263
|
+
* this.displayService.shouldShowLabel(this.button())
|
|
264
|
+
* );
|
|
265
|
+
* }
|
|
266
|
+
*
|
|
267
|
+
* // PrimeNG implementation example
|
|
268
|
+
* @Component({
|
|
269
|
+
* selector: 'p-action-button',
|
|
270
|
+
* providers: [ButtonDisplayService],
|
|
271
|
+
* template: `
|
|
272
|
+
* <p-button
|
|
273
|
+
* [icon]="button().icon"
|
|
274
|
+
* [iconPos]="showLabel() ? 'left' : undefined"
|
|
275
|
+
* [label]="showLabel() ? button().label : null"
|
|
276
|
+
* />
|
|
277
|
+
* `
|
|
278
|
+
* })
|
|
279
|
+
* export class PrimeActionButtonComponent {
|
|
280
|
+
* private readonly displayService = inject(ButtonDisplayService);
|
|
281
|
+
* readonly button = input.required<ActionButton>();
|
|
282
|
+
*
|
|
283
|
+
* readonly showLabel = computed(() =>
|
|
284
|
+
* this.displayService.shouldShowLabel(this.button())
|
|
285
|
+
* );
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
/**
|
|
290
|
+
* Service that provides display logic for action buttons.
|
|
291
|
+
*
|
|
292
|
+
* This service is designed to be provided at the component level to maintain
|
|
293
|
+
* consistency with other button services, though it contains only pure functions.
|
|
294
|
+
*
|
|
295
|
+
* @usageNotes
|
|
296
|
+
* ### Pure Functions
|
|
297
|
+
* All methods in this service are pure functions that take a button configuration
|
|
298
|
+
* and return display values. They can be safely used in Angular's `computed()`.
|
|
299
|
+
*
|
|
300
|
+
* ### Dropdown Icon
|
|
301
|
+
* Dropdown buttons show a chevron icon unless `config.hideDropdownIcon` is true.
|
|
302
|
+
*/
|
|
303
|
+
class ButtonDisplayService {
|
|
170
304
|
/**
|
|
171
|
-
*
|
|
305
|
+
* Determines whether to show the label text.
|
|
306
|
+
*
|
|
307
|
+
* @param button - The button configuration
|
|
308
|
+
* @returns True when a label is provided
|
|
172
309
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this._error.set(null);
|
|
310
|
+
shouldShowLabel(button) {
|
|
311
|
+
return !!button.label;
|
|
176
312
|
}
|
|
177
313
|
/**
|
|
178
|
-
*
|
|
314
|
+
* Determines whether to show the dropdown chevron icon.
|
|
315
|
+
*
|
|
316
|
+
* @param button - The button configuration
|
|
317
|
+
* @returns True for dropdown buttons without hideDropdownIcon
|
|
179
318
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
319
|
+
shouldShowDropdownIcon(button) {
|
|
320
|
+
return button.type === ActionButtonType.Dropdown && !button.config?.hideDropdownIcon;
|
|
321
|
+
}
|
|
322
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonDisplayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
323
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonDisplayService });
|
|
324
|
+
}
|
|
325
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonDisplayService, decorators: [{
|
|
326
|
+
type: Injectable
|
|
327
|
+
}] });
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @file Button Handler Service
|
|
331
|
+
* @description Executes button handlers with automatic loading state management.
|
|
332
|
+
*
|
|
333
|
+
* This service encapsulates handler execution logic, providing automatic
|
|
334
|
+
* loading state management for async handlers across different UI implementations.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* // Ionic implementation
|
|
339
|
+
* @Component({
|
|
340
|
+
* providers: [ButtonStateService, ButtonHandlerService]
|
|
341
|
+
* })
|
|
342
|
+
* export class ButtonComponent {
|
|
343
|
+
* private readonly stateService = inject(ButtonStateService);
|
|
344
|
+
* private readonly handlerService = inject(ButtonHandlerService);
|
|
345
|
+
*
|
|
346
|
+
* protected async onClick(): Promise<void> {
|
|
347
|
+
* await this.handlerService.executeHandler(
|
|
348
|
+
* this.button(),
|
|
349
|
+
* loading => this.stateService.setLoading(loading)
|
|
350
|
+
* );
|
|
351
|
+
* this.buttonClick.emit(this.button());
|
|
352
|
+
* }
|
|
353
|
+
* }
|
|
354
|
+
*
|
|
355
|
+
* // Generic web component example
|
|
356
|
+
* class ActionButtonElement extends HTMLElement {
|
|
357
|
+
* private handlerService = new ButtonHandlerService();
|
|
358
|
+
* private loading = false;
|
|
359
|
+
*
|
|
360
|
+
* async handleClick() {
|
|
361
|
+
* await this.handlerService.executeHandler(
|
|
362
|
+
* this.buttonConfig,
|
|
363
|
+
* loading => {
|
|
364
|
+
* this.loading = loading;
|
|
365
|
+
* this.render();
|
|
366
|
+
* }
|
|
367
|
+
* );
|
|
368
|
+
* }
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* Service that executes button handlers with automatic loading state management.
|
|
374
|
+
*
|
|
375
|
+
* This service is designed to be provided at the component level to ensure
|
|
376
|
+
* proper isolation of handler execution context.
|
|
377
|
+
*
|
|
378
|
+
* @usageNotes
|
|
379
|
+
* ### Async Handler Support
|
|
380
|
+
* When a handler returns a Promise, the service automatically:
|
|
381
|
+
* 1. Calls `onLoadingChange(true)` before execution
|
|
382
|
+
* 2. Awaits the Promise
|
|
383
|
+
* 3. Calls `onLoadingChange(false)` after completion (success or failure)
|
|
384
|
+
*
|
|
385
|
+
* ### Sync Handler Support
|
|
386
|
+
* For synchronous handlers, no loading state changes are triggered.
|
|
387
|
+
*
|
|
388
|
+
* ### Error Handling
|
|
389
|
+
* The service uses try/finally to ensure loading state is always reset,
|
|
390
|
+
* even if the handler throws an error. The error is not caught, allowing
|
|
391
|
+
* it to propagate to the calling code.
|
|
392
|
+
*/
|
|
393
|
+
class ButtonHandlerService {
|
|
394
|
+
/**
|
|
395
|
+
* Executes a button's handler with automatic loading state management.
|
|
396
|
+
*
|
|
397
|
+
* @param button - The button whose handler to execute
|
|
398
|
+
* @param onLoadingChange - Callback invoked when loading state changes
|
|
399
|
+
* @returns Promise that resolves when handler completes
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```typescript
|
|
403
|
+
* await handlerService.executeHandler(
|
|
404
|
+
* myButton,
|
|
405
|
+
* loading => this.isLoading.set(loading)
|
|
406
|
+
* );
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
async executeHandler(button, onLoadingChange) {
|
|
410
|
+
const result = button.handler();
|
|
411
|
+
if (result instanceof Promise) {
|
|
412
|
+
onLoadingChange(true);
|
|
413
|
+
try {
|
|
414
|
+
await result;
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
onLoadingChange(false);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
182
420
|
}
|
|
183
421
|
/**
|
|
184
|
-
*
|
|
422
|
+
* Executes a child button's handler with loading state tracking by ID.
|
|
423
|
+
*
|
|
424
|
+
* This method is designed for dropdown children where multiple buttons
|
|
425
|
+
* might be loading simultaneously and need individual tracking.
|
|
426
|
+
*
|
|
427
|
+
* @param child - The child button whose handler to execute
|
|
428
|
+
* @param onChildLoadingChange - Callback with child ID and loading state
|
|
429
|
+
* @returns Promise that resolves when handler completes
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```typescript
|
|
433
|
+
* await handlerService.executeChildHandler(
|
|
434
|
+
* selectedChild,
|
|
435
|
+
* (childId, loading) => {
|
|
436
|
+
* if (loading) {
|
|
437
|
+
* this.stateService.addLoadingChild(childId);
|
|
438
|
+
* } else {
|
|
439
|
+
* this.stateService.removeLoadingChild(childId);
|
|
440
|
+
* }
|
|
441
|
+
* }
|
|
442
|
+
* );
|
|
443
|
+
* ```
|
|
185
444
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
445
|
+
async executeChildHandler(child, onChildLoadingChange) {
|
|
446
|
+
onChildLoadingChange(child.id, true);
|
|
447
|
+
try {
|
|
448
|
+
const result = child.handler();
|
|
449
|
+
if (result instanceof Promise) {
|
|
450
|
+
await result;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
onChildLoadingChange(child.id, false);
|
|
455
|
+
}
|
|
188
456
|
}
|
|
189
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type:
|
|
190
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type:
|
|
457
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
458
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonHandlerService });
|
|
191
459
|
}
|
|
192
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type:
|
|
193
|
-
type: Injectable
|
|
194
|
-
args: [{
|
|
195
|
-
providedIn: 'root'
|
|
196
|
-
}]
|
|
460
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonHandlerService, decorators: [{
|
|
461
|
+
type: Injectable
|
|
197
462
|
}] });
|
|
198
463
|
|
|
199
464
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
465
|
+
* @file Button State Service
|
|
466
|
+
* @description Manages loading states and computed state values for action buttons.
|
|
467
|
+
*
|
|
468
|
+
* This service encapsulates all state management logic for buttons, making it
|
|
469
|
+
* reusable across different UI library implementations (Ionic, Material, PrimeNG, etc.).
|
|
202
470
|
*
|
|
203
471
|
* @example
|
|
204
|
-
* ```
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* [
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
472
|
+
* ```typescript
|
|
473
|
+
* // Ionic implementation (ButtonComponent)
|
|
474
|
+
* @Component({
|
|
475
|
+
* providers: [ButtonStateService]
|
|
476
|
+
* })
|
|
477
|
+
* export class ButtonComponent {
|
|
478
|
+
* private readonly stateService = inject(ButtonStateService);
|
|
479
|
+
* readonly button = input.required<ActionButton>();
|
|
480
|
+
*
|
|
481
|
+
* readonly isLoading = computed(() =>
|
|
482
|
+
* this.stateService.isLoading(this.button())
|
|
483
|
+
* );
|
|
484
|
+
* }
|
|
485
|
+
*
|
|
486
|
+
* // Material implementation example
|
|
487
|
+
* @Component({
|
|
488
|
+
* selector: 'mat-action-button',
|
|
489
|
+
* providers: [ButtonStateService],
|
|
490
|
+
* template: `
|
|
491
|
+
* <button mat-button [disabled]="isDisabled()">
|
|
492
|
+
* @if (showLoadingSpinner()) {
|
|
493
|
+
* <mat-spinner diameter="16" />
|
|
494
|
+
* }
|
|
495
|
+
* {{ button().label }}
|
|
496
|
+
* </button>
|
|
497
|
+
* `
|
|
498
|
+
* })
|
|
499
|
+
* export class MatActionButtonComponent {
|
|
500
|
+
* private readonly stateService = inject(ButtonStateService);
|
|
501
|
+
* readonly button = input.required<ActionButton>();
|
|
502
|
+
*
|
|
503
|
+
* readonly isDisabled = computed(() =>
|
|
504
|
+
* this.stateService.isDisabled(this.button())
|
|
505
|
+
* );
|
|
506
|
+
* readonly showLoadingSpinner = computed(() =>
|
|
507
|
+
* this.stateService.showLoadingSpinner(this.button())
|
|
508
|
+
* );
|
|
509
|
+
* }
|
|
211
510
|
* ```
|
|
212
511
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Service that manages button loading states and computes derived state values.
|
|
514
|
+
*
|
|
515
|
+
* This service is designed to be provided at the component level (not root)
|
|
516
|
+
* to ensure each button instance has isolated state management.
|
|
517
|
+
*
|
|
518
|
+
* @usageNotes
|
|
519
|
+
* ### Providing the Service
|
|
520
|
+
* Always provide this service at the component level to isolate state:
|
|
521
|
+
* ```typescript
|
|
522
|
+
* @Component({
|
|
523
|
+
* providers: [ButtonStateService]
|
|
524
|
+
* })
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* ### State Management
|
|
528
|
+
* The service manages two types of loading state:
|
|
529
|
+
* 1. **Internal loading** - Set when executing async handlers via `setLoading()`
|
|
530
|
+
* 2. **Child loading** - Tracks which dropdown children are loading via `setChildLoading()`
|
|
531
|
+
*
|
|
532
|
+
* ### Computed States
|
|
533
|
+
* All computed methods are pure functions that can be used in Angular's `computed()`:
|
|
534
|
+
* - `isLoading(button)` - Combined loading state
|
|
535
|
+
* - `isDisabled(button)` - Whether button should be disabled
|
|
536
|
+
* - `showLoadingSpinner(button)` - Whether to show spinner
|
|
537
|
+
* - `hasLoadingChild(button)` - Whether any child is loading
|
|
538
|
+
*/
|
|
539
|
+
class ButtonStateService {
|
|
540
|
+
/**
|
|
541
|
+
* Internal loading state for async handler execution.
|
|
542
|
+
*/
|
|
543
|
+
_isLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isLoading" }] : []));
|
|
544
|
+
/**
|
|
545
|
+
* Set of child button IDs that are currently loading (for dropdowns).
|
|
546
|
+
*/
|
|
547
|
+
_loadingChildIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "_loadingChildIds" }] : []));
|
|
548
|
+
/**
|
|
549
|
+
* Read-only access to internal loading state.
|
|
550
|
+
*/
|
|
551
|
+
internalLoading = this._isLoading.asReadonly();
|
|
552
|
+
/**
|
|
553
|
+
* Read-only access to loading child IDs.
|
|
554
|
+
*/
|
|
555
|
+
loadingChildIds = this._loadingChildIds.asReadonly();
|
|
556
|
+
/**
|
|
557
|
+
* Sets the internal loading state.
|
|
558
|
+
*
|
|
559
|
+
* @param loading - Whether the button is loading
|
|
560
|
+
*/
|
|
561
|
+
setLoading(loading) {
|
|
562
|
+
this._isLoading.set(loading);
|
|
237
563
|
}
|
|
238
|
-
|
|
239
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Adds a child ID to the loading set.
|
|
566
|
+
*
|
|
567
|
+
* @param childId - The ID of the child button that started loading
|
|
568
|
+
*/
|
|
569
|
+
addLoadingChild(childId) {
|
|
570
|
+
this._loadingChildIds.update(ids => new Set([...ids, childId]));
|
|
240
571
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Removes a child ID from the loading set.
|
|
574
|
+
*
|
|
575
|
+
* @param childId - The ID of the child button that finished loading
|
|
576
|
+
*/
|
|
577
|
+
removeLoadingChild(childId) {
|
|
578
|
+
this._loadingChildIds.update(ids => {
|
|
579
|
+
const newIds = new Set(ids);
|
|
580
|
+
newIds.delete(childId);
|
|
581
|
+
return newIds;
|
|
582
|
+
});
|
|
244
583
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Checks if any child button is in a loading state with spinner enabled.
|
|
586
|
+
*
|
|
587
|
+
* @param button - The parent button configuration
|
|
588
|
+
* @returns True if any child is loading and should show spinner
|
|
589
|
+
*/
|
|
590
|
+
hasLoadingChild(button) {
|
|
591
|
+
if (button.type !== ActionButtonType.Dropdown || !button.children) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
const loadingChildIds = this._loadingChildIds();
|
|
595
|
+
return button.children.some(child => {
|
|
596
|
+
const showSpinner = child.showLoadingSpinner ?? true;
|
|
597
|
+
const isLoadingFromProp = child.loading && showSpinner;
|
|
598
|
+
const isLoadingFromExecution = loadingChildIds.has(child.id) && showSpinner;
|
|
599
|
+
return isLoadingFromProp || isLoadingFromExecution;
|
|
600
|
+
});
|
|
248
601
|
}
|
|
249
602
|
/**
|
|
250
|
-
*
|
|
603
|
+
* Computes the combined loading state for a button.
|
|
604
|
+
*
|
|
605
|
+
* @param button - The button configuration
|
|
606
|
+
* @returns True if the button is in any loading state
|
|
251
607
|
*/
|
|
252
|
-
|
|
253
|
-
this.
|
|
608
|
+
isLoading(button) {
|
|
609
|
+
const parentLoading = this._isLoading() || (button.loading ?? false);
|
|
610
|
+
const childLoading = this.hasLoadingChild(button) && (button.showLoadingSpinner ?? true);
|
|
611
|
+
return parentLoading || childLoading;
|
|
254
612
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Determines whether to display the loading spinner.
|
|
615
|
+
*
|
|
616
|
+
* @param button - The button configuration
|
|
617
|
+
* @returns True if spinner should be shown
|
|
618
|
+
*/
|
|
619
|
+
showLoadingSpinner(button) {
|
|
620
|
+
return this.isLoading(button) && (button.showLoadingSpinner ?? true);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Computes whether the button should be disabled.
|
|
624
|
+
*
|
|
625
|
+
* For dropdown buttons: Only disabled if config says so or if parent itself is loading
|
|
626
|
+
* (not disabled by inherited child loading - user can still open dropdown).
|
|
627
|
+
*
|
|
628
|
+
* For non-dropdown buttons: Disabled when loading or explicitly disabled in config.
|
|
629
|
+
*
|
|
630
|
+
* @param button - The button configuration
|
|
631
|
+
* @returns True if the button should be disabled
|
|
632
|
+
*/
|
|
633
|
+
isDisabled(button) {
|
|
634
|
+
const configDisabled = button.config?.disabled ?? false;
|
|
635
|
+
if (button.type === ActionButtonType.Dropdown) {
|
|
636
|
+
const parentSelfLoading = this._isLoading() || (button.loading ?? false);
|
|
637
|
+
return configDisabled || parentSelfLoading;
|
|
267
638
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<ion-icon slot="start" name="trash-outline"></ion-icon>
|
|
284
|
-
Delete
|
|
285
|
-
</ion-button>
|
|
286
|
-
</ion-card-content>
|
|
287
|
-
}
|
|
288
|
-
</ion-card>
|
|
289
|
-
`, isInline: true, styles: [":host{display:block}ion-card{--background: var(--maki-card-background, var(--ion-card-background, #fff));--color: var(--maki-card-color, var(--ion-text-color));margin:var(--maki-card-margin, 16px);border-radius:var(--maki-card-border-radius, 12px);transition:opacity .2s ease,transform .2s ease;cursor:pointer}ion-card:hover{transform:translateY(-2px)}ion-card.inactive{opacity:.6}ion-card-title{color:var(--maki-primary, var(--ion-color-primary));font-weight:600}ion-card-subtitle{display:flex;align-items:center;gap:8px}.status-badge{font-size:.75rem;padding:2px 8px;border-radius:4px;font-weight:500}.status-badge.inactive{background-color:var(--ion-color-medium-tint);color:var(--ion-color-medium-contrast)}.card-actions{display:flex;gap:8px;padding-top:0}.card-actions ion-button{--border-color: var(--maki-primary, var(--ion-color-primary));--color: var(--maki-primary, var(--ion-color-primary))}\n"], dependencies: [{ kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
639
|
+
return this.isLoading(button) || configDisabled;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Checks if a specific button is currently loading.
|
|
643
|
+
* Used primarily for checking child button loading state.
|
|
644
|
+
*
|
|
645
|
+
* @param button - The button to check
|
|
646
|
+
* @returns True if the button is loading
|
|
647
|
+
*/
|
|
648
|
+
isButtonLoading(button) {
|
|
649
|
+
const loadingChildIds = this._loadingChildIds();
|
|
650
|
+
return (button.loading ?? false) || loadingChildIds.has(button.id);
|
|
651
|
+
}
|
|
652
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
653
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonStateService });
|
|
290
654
|
}
|
|
291
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type:
|
|
292
|
-
type:
|
|
293
|
-
|
|
294
|
-
IonCard,
|
|
295
|
-
IonCardHeader,
|
|
296
|
-
IonCardTitle,
|
|
297
|
-
IonCardSubtitle,
|
|
298
|
-
IonCardContent,
|
|
299
|
-
IonButton,
|
|
300
|
-
IonIcon
|
|
301
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
302
|
-
<ion-card #cardElement [class.inactive]="!item().isActive" (click)="onCardClick()">
|
|
303
|
-
<ion-card-header>
|
|
304
|
-
<ion-card-title>{{ item().title }}</ion-card-title>
|
|
305
|
-
@if (formattedDate()) {
|
|
306
|
-
<ion-card-subtitle>
|
|
307
|
-
{{ formattedDate() }}
|
|
308
|
-
@if (!item().isActive) {
|
|
309
|
-
<span class="status-badge inactive">Inactive</span>
|
|
310
|
-
}
|
|
311
|
-
</ion-card-subtitle>
|
|
312
|
-
}
|
|
313
|
-
</ion-card-header>
|
|
655
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonStateService, decorators: [{
|
|
656
|
+
type: Injectable
|
|
657
|
+
}] });
|
|
314
658
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
659
|
+
/**
|
|
660
|
+
* @file Button Services Barrel Export
|
|
661
|
+
* @description Exports all button-related services for use across different UI implementations.
|
|
662
|
+
*/
|
|
320
663
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
Edit
|
|
326
|
-
</ion-button>
|
|
327
|
-
<ion-button fill="outline" size="small" color="danger" (click)="onDeleteClick($event)">
|
|
328
|
-
<ion-icon slot="start" name="trash-outline"></ion-icon>
|
|
329
|
-
Delete
|
|
330
|
-
</ion-button>
|
|
331
|
-
</ion-card-content>
|
|
332
|
-
}
|
|
333
|
-
</ion-card>
|
|
334
|
-
`, styles: [":host{display:block}ion-card{--background: var(--maki-card-background, var(--ion-card-background, #fff));--color: var(--maki-card-color, var(--ion-text-color));margin:var(--maki-card-margin, 16px);border-radius:var(--maki-card-border-radius, 12px);transition:opacity .2s ease,transform .2s ease;cursor:pointer}ion-card:hover{transform:translateY(-2px)}ion-card.inactive{opacity:.6}ion-card-title{color:var(--maki-primary, var(--ion-color-primary));font-weight:600}ion-card-subtitle{display:flex;align-items:center;gap:8px}.status-badge{font-size:.75rem;padding:2px 8px;border-radius:4px;font-weight:500}.status-badge.inactive{background-color:var(--ion-color-medium-tint);color:var(--ion-color-medium-contrast)}.card-actions{display:flex;gap:8px;padding-top:0}.card-actions ion-button{--border-color: var(--maki-primary, var(--ion-color-primary));--color: var(--maki-primary, var(--ion-color-primary))}\n"] }]
|
|
335
|
-
}], ctorParameters: () => [], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], showActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActions", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], editClick: [{ type: i0.Output, args: ["editClick"] }], deleteClick: [{ type: i0.Output, args: ["deleteClick"] }], cardElement: [{ type: i0.ViewChild, args: ['cardElement', { isSignal: true }] }] } });
|
|
664
|
+
/**
|
|
665
|
+
* @file Services Barrel Export
|
|
666
|
+
* @description Exports all services from the ui-core library.
|
|
667
|
+
*/
|
|
336
668
|
|
|
337
669
|
/**
|
|
338
670
|
* @file Action Button List Component
|
|
339
|
-
* @description
|
|
671
|
+
* @description Ionic implementation levantando lista de action buttons en un popover.
|
|
672
|
+
*
|
|
673
|
+
* This component is the Ionic-specific implementation that uses the shared
|
|
674
|
+
* ActionButtonListService for all business logic. The component only handles:
|
|
675
|
+
* - Ionic template rendering (ion-list, ion-item, ion-icon, ion-spinner)
|
|
676
|
+
* - Ionic-specific popover dismissal via PopoverController
|
|
677
|
+
*
|
|
678
|
+
* All list logic, loading state checks, and button resolution is delegated
|
|
679
|
+
* to the injected service, making the logic reusable for other UI libraries.
|
|
340
680
|
*/
|
|
341
681
|
/**
|
|
342
682
|
* Component that renders a list of action buttons for use in popovers/dropdowns.
|
|
@@ -370,37 +710,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
370
710
|
* - `buttonSelect`: Emits the selected `ActionButton` when clicked
|
|
371
711
|
*/
|
|
372
712
|
class ActionButtonListComponent {
|
|
373
|
-
/**
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
*/
|
|
377
|
-
dropdownType = ActionButtonType.Dropdown;
|
|
378
|
-
/**
|
|
379
|
-
* Popover controller for dismissing the popover when an item is selected.
|
|
380
|
-
*/
|
|
713
|
+
/** Reference to ActionButtonType.Dropdown for template comparison. */
|
|
714
|
+
ActionButtonType = ActionButtonType;
|
|
715
|
+
/** Ionic PopoverController for dismissing the popover when an item is selected. */
|
|
381
716
|
popoverCtrl = inject(PopoverController, { optional: true });
|
|
382
|
-
/**
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
*/
|
|
717
|
+
/** Service for list logic. */
|
|
718
|
+
listService = inject(ActionButtonListService);
|
|
719
|
+
/** Internal signal to store buttons passed via componentProps. */
|
|
386
720
|
_buttonsFromProps = signal([], ...(ngDevMode ? [{ debugName: "_buttonsFromProps" }] : []));
|
|
387
|
-
/**
|
|
388
|
-
* Internal signal to store the Set of currently loading child button IDs.
|
|
389
|
-
*/
|
|
721
|
+
/** Internal signal to store the Set of currently loading child button IDs. */
|
|
390
722
|
_loadingChildIdsFromProps = signal(null, ...(ngDevMode ? [{ debugName: "_loadingChildIdsFromProps" }] : []));
|
|
391
|
-
/**
|
|
392
|
-
* The list of action buttons to display (when used via template binding).
|
|
393
|
-
*/
|
|
723
|
+
/** The list of action buttons to display (when used via template binding). */
|
|
394
724
|
buttons = input([], ...(ngDevMode ? [{ debugName: "buttons" }] : []));
|
|
395
|
-
/**
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
buttonList = () => {
|
|
400
|
-
const fromProps = this._buttonsFromProps();
|
|
401
|
-
const fromInput = this.buttons();
|
|
402
|
-
return fromProps.length > 0 ? fromProps : fromInput;
|
|
403
|
-
};
|
|
725
|
+
/** Emits when a button is selected from the list. */
|
|
726
|
+
buttonSelect = output();
|
|
727
|
+
/** Computed button list that works with both signal input and componentProps. */
|
|
728
|
+
buttonList = computed(() => this.listService.resolveButtonList(this._buttonsFromProps(), this.buttons()), ...(ngDevMode ? [{ debugName: "buttonList" }] : []));
|
|
404
729
|
/**
|
|
405
730
|
* Setter to capture buttons passed via PopoverController.create({ componentProps }).
|
|
406
731
|
* This is called when Ionic sets the property directly on the component instance.
|
|
@@ -414,21 +739,15 @@ class ActionButtonListComponent {
|
|
|
414
739
|
set loadingChildIds(value) {
|
|
415
740
|
this._loadingChildIdsFromProps.set(value);
|
|
416
741
|
}
|
|
417
|
-
/**
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const loadingChildIdsSignal = this._loadingChildIdsFromProps();
|
|
422
|
-
const loadingChildIds = loadingChildIdsSignal ? loadingChildIdsSignal() : new Set();
|
|
423
|
-
return button.loading || loadingChildIds.has(button.id);
|
|
742
|
+
/** Checks if a button can be selected. */
|
|
743
|
+
canSelectButton(button) {
|
|
744
|
+
const loadingChildIds = this._loadingChildIdsFromProps()?.() ?? new Set();
|
|
745
|
+
return this.listService.canSelectButton(button, loadingChildIds);
|
|
424
746
|
}
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
constructor() {
|
|
430
|
-
// Register any icons that might be used
|
|
431
|
-
addIcons({});
|
|
747
|
+
/** Checks if a button should show its loading spinner. */
|
|
748
|
+
shouldShowSpinner(button) {
|
|
749
|
+
const loadingChildIds = this._loadingChildIdsFromProps()?.() ?? new Set();
|
|
750
|
+
return this.listService.shouldShowSpinner(button, loadingChildIds);
|
|
432
751
|
}
|
|
433
752
|
/**
|
|
434
753
|
* Handles click on a button item.
|
|
@@ -437,23 +756,24 @@ class ActionButtonListComponent {
|
|
|
437
756
|
* @param button - The clicked action button
|
|
438
757
|
*/
|
|
439
758
|
onButtonClick(button) {
|
|
440
|
-
|
|
759
|
+
const loadingChildIds = this._loadingChildIdsFromProps()?.() ?? new Set();
|
|
760
|
+
if (!this.listService.canSelectButton(button, loadingChildIds)) {
|
|
441
761
|
return;
|
|
442
762
|
}
|
|
443
763
|
this.buttonSelect.emit(button);
|
|
444
764
|
this.popoverCtrl?.dismiss(button, 'select');
|
|
445
765
|
}
|
|
446
766
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
447
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ActionButtonListComponent, isStandalone: true, selector: "maki-action-button-list", inputs: { buttons: { classPropertyName: "buttons", publicName: "buttons", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonSelect: "buttonSelect" }, ngImport: i0, template: `
|
|
767
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ActionButtonListComponent, isStandalone: true, selector: "maki-action-button-list", inputs: { buttons: { classPropertyName: "buttons", publicName: "buttons", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonSelect: "buttonSelect" }, providers: [ActionButtonListService], ngImport: i0, template: `
|
|
448
768
|
<ion-list lines="none">
|
|
449
769
|
@for (button of buttonList(); track button.id) {
|
|
450
770
|
<ion-item
|
|
451
771
|
[button]="true"
|
|
452
|
-
[disabled]="
|
|
453
|
-
[detail]="button.type ===
|
|
772
|
+
[disabled]="!canSelectButton(button)"
|
|
773
|
+
[detail]="button.type === ActionButtonType.Dropdown"
|
|
454
774
|
(click)="onButtonClick(button)"
|
|
455
775
|
>
|
|
456
|
-
@if (
|
|
776
|
+
@if (shouldShowSpinner(button)) {
|
|
457
777
|
<ion-spinner slot="start" name="crescent" />
|
|
458
778
|
} @else if (button.icon) {
|
|
459
779
|
<ion-icon slot="start" [color]="button.config?.color" [name]="button.icon" />
|
|
@@ -472,16 +792,16 @@ class ActionButtonListComponent {
|
|
|
472
792
|
}
|
|
473
793
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListComponent, decorators: [{
|
|
474
794
|
type: Component,
|
|
475
|
-
args: [{ selector: 'maki-action-button-list', standalone: true, imports: [IonList, IonItem, IonIcon, IonLabel, IonSpinner], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
795
|
+
args: [{ selector: 'maki-action-button-list', standalone: true, imports: [IonList, IonItem, IonIcon, IonLabel, IonSpinner], providers: [ActionButtonListService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
476
796
|
<ion-list lines="none">
|
|
477
797
|
@for (button of buttonList(); track button.id) {
|
|
478
798
|
<ion-item
|
|
479
799
|
[button]="true"
|
|
480
|
-
[disabled]="
|
|
481
|
-
[detail]="button.type ===
|
|
800
|
+
[disabled]="!canSelectButton(button)"
|
|
801
|
+
[detail]="button.type === ActionButtonType.Dropdown"
|
|
482
802
|
(click)="onButtonClick(button)"
|
|
483
803
|
>
|
|
484
|
-
@if (
|
|
804
|
+
@if (shouldShowSpinner(button)) {
|
|
485
805
|
<ion-spinner slot="start" name="crescent" />
|
|
486
806
|
} @else if (button.icon) {
|
|
487
807
|
<ion-icon slot="start" [color]="button.config?.color" [name]="button.icon" />
|
|
@@ -497,18 +817,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
497
817
|
}
|
|
498
818
|
</ion-list>
|
|
499
819
|
`, styles: [":host{display:block}::ng-deep ion-popover::part(content){width:fit-content}ion-list{padding:0}ion-item{--padding-start: var(--maki-action-button-list-padding, 16px);--padding-end: var(--maki-action-button-list-padding, 16px);--min-height: var(--maki-action-button-list-item-height, 44px);cursor:pointer;width:100%}ion-item[disabled]{cursor:not-allowed}ion-label{white-space:nowrap}ion-icon{font-size:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}ion-spinner{width:var(--maki-action-button-list-icon-size, 20px);height:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}\n"] }]
|
|
500
|
-
}],
|
|
820
|
+
}], propDecorators: { buttons: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttons", required: false }] }], buttonSelect: [{ type: i0.Output, args: ["buttonSelect"] }] } });
|
|
501
821
|
|
|
502
822
|
/**
|
|
503
823
|
* @file Button Component
|
|
504
|
-
* @description
|
|
824
|
+
* @description Ionic implementation of a configurable button component.
|
|
825
|
+
*
|
|
826
|
+
* This component is the Ionic-specific implementation that uses the shared
|
|
827
|
+
* button services for all business logic. The component only handles:
|
|
828
|
+
* - Ionic template rendering (ion-button, ion-icon, ion-spinner)
|
|
829
|
+
* - Ionic-specific popover creation via PopoverController
|
|
830
|
+
*
|
|
831
|
+
* All state management, display logic, and handler execution is delegated
|
|
832
|
+
* to the injected services, making the logic reusable for other UI libraries.
|
|
505
833
|
*/
|
|
506
834
|
/**
|
|
507
835
|
* A configurable button component that renders an `ion-button` based on
|
|
508
836
|
* an `ActionButton` configuration object.
|
|
509
837
|
*
|
|
510
838
|
* Features:
|
|
511
|
-
* -
|
|
839
|
+
* - Two button types: Default and Dropdown
|
|
840
|
+
* - Flexible display: icon-only, label-only, or icon+label (determined by properties)
|
|
512
841
|
* - Automatic loading state management for async handlers
|
|
513
842
|
* - Dropdown support via PopoverController with child actions
|
|
514
843
|
* - Full Ionic button styling configuration (fill, size, color, shape, expand)
|
|
@@ -516,12 +845,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
516
845
|
*
|
|
517
846
|
* @example
|
|
518
847
|
* ```html
|
|
519
|
-
* <!--
|
|
848
|
+
* <!-- Button with label and icon -->
|
|
520
849
|
* <maki-button [button]="saveButton" />
|
|
521
850
|
*
|
|
522
|
-
* <!-- Icon-only button -->
|
|
851
|
+
* <!-- Icon-only button (no label) -->
|
|
523
852
|
* <maki-button [button]="iconButton" />
|
|
524
853
|
*
|
|
854
|
+
* <!-- Label-only button (no icon) -->
|
|
855
|
+
* <maki-button [button]="textButton" />
|
|
856
|
+
*
|
|
525
857
|
* <!-- Dropdown button -->
|
|
526
858
|
* <maki-button [button]="menuButton" />
|
|
527
859
|
* ```
|
|
@@ -532,7 +864,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
532
864
|
* saveButton: ActionButton = {
|
|
533
865
|
* id: 'save',
|
|
534
866
|
* label: 'Save',
|
|
535
|
-
* icon: '
|
|
867
|
+
* icon: 'save-outline',
|
|
536
868
|
* type: ActionButtonType.Default,
|
|
537
869
|
* config: { fill: 'solid', color: 'primary' },
|
|
538
870
|
* handler: async () => {
|
|
@@ -546,8 +878,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
546
878
|
* type: ActionButtonType.Dropdown,
|
|
547
879
|
* handler: () => {},
|
|
548
880
|
* children: [
|
|
549
|
-
* { id: 'edit', label: 'Edit', icon: '
|
|
550
|
-
* { id: 'delete', label: 'Delete', icon: '
|
|
881
|
+
* { id: 'edit', label: 'Edit', icon: 'create-outline', type: ActionButtonType.Default, handler: () => this.edit() },
|
|
882
|
+
* { id: 'delete', label: 'Delete', icon: 'trash-outline', type: ActionButtonType.Default, handler: () => this.delete() }
|
|
551
883
|
* ]
|
|
552
884
|
* };
|
|
553
885
|
* ```
|
|
@@ -571,110 +903,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
571
903
|
* When a child is selected, its handler is executed and `childSelect` is emitted.
|
|
572
904
|
*/
|
|
573
905
|
class ButtonComponent {
|
|
574
|
-
/**
|
|
575
|
-
* Popover controller for creating dropdown popovers.
|
|
576
|
-
*/
|
|
906
|
+
/** Ionic PopoverController for dropdown popovers. */
|
|
577
907
|
popoverCtrl = inject(PopoverController);
|
|
578
|
-
/**
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
*/
|
|
586
|
-
_loadingChildIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "_loadingChildIds" }] : []));
|
|
587
|
-
/**
|
|
588
|
-
* The action button configuration.
|
|
589
|
-
*/
|
|
908
|
+
/** Service for button state management. */
|
|
909
|
+
stateService = inject(ButtonStateService);
|
|
910
|
+
/** Service for button display logic. */
|
|
911
|
+
displayService = inject(ButtonDisplayService);
|
|
912
|
+
/** Service for handler execution. */
|
|
913
|
+
handlerService = inject(ButtonHandlerService);
|
|
914
|
+
/** The action button configuration. */
|
|
590
915
|
button = input.required(...(ngDevMode ? [{ debugName: "button" }] : []));
|
|
591
|
-
/**
|
|
592
|
-
* Emits when the button is clicked (for non-dropdown buttons).
|
|
593
|
-
* Emits the button configuration that was clicked.
|
|
594
|
-
*/
|
|
916
|
+
/** Emits when the button is clicked (for non-dropdown buttons). */
|
|
595
917
|
buttonClick = output();
|
|
596
|
-
/**
|
|
597
|
-
* Emits when a child button is selected from a dropdown.
|
|
598
|
-
* Only emits for `ActionButtonType.Dropdown` buttons.
|
|
599
|
-
*/
|
|
918
|
+
/** Emits when a child button is selected from a dropdown. */
|
|
600
919
|
childSelect = output();
|
|
601
|
-
/**
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return isLoadingFromProp || isLoadingFromExecution;
|
|
614
|
-
});
|
|
615
|
-
}, ...(ngDevMode ? [{ debugName: "hasLoadingChild" }] : []));
|
|
616
|
-
/**
|
|
617
|
-
* Combined loading state from external prop, internal async state, and child loading.
|
|
618
|
-
* Returns true if:
|
|
619
|
-
* - Internal loading state is active, OR
|
|
620
|
-
* - External loading prop is true, OR
|
|
621
|
-
* - Any child is loading AND parent has showLoadingSpinner !== false
|
|
622
|
-
*/
|
|
623
|
-
isLoading = computed(() => {
|
|
624
|
-
const btn = this.button();
|
|
625
|
-
const parentLoading = this._isLoading() || (btn.loading ?? false);
|
|
626
|
-
const childLoading = this.hasLoadingChild() && (btn.showLoadingSpinner ?? true);
|
|
627
|
-
return parentLoading || childLoading;
|
|
628
|
-
}, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
629
|
-
/**
|
|
630
|
-
* Whether to display the loading spinner.
|
|
631
|
-
* Shows spinner only if button is loading AND showLoadingSpinner is not false.
|
|
632
|
-
*/
|
|
633
|
-
showLoadingSpinner = computed(() => {
|
|
634
|
-
return this.isLoading() && (this.button().showLoadingSpinner ?? true);
|
|
635
|
-
}, ...(ngDevMode ? [{ debugName: "showLoadingSpinner" }] : []));
|
|
636
|
-
/**
|
|
637
|
-
* Whether the button is disabled.
|
|
638
|
-
* - Non-dropdown buttons: Disabled when loading or explicitly disabled in config
|
|
639
|
-
* - Dropdown buttons: Only disabled when explicitly disabled in config or when parent itself is loading
|
|
640
|
-
* (not disabled by inherited child loading - user can still open dropdown to see loading children)
|
|
641
|
-
*/
|
|
642
|
-
isDisabled = computed(() => {
|
|
643
|
-
const btn = this.button();
|
|
644
|
-
const configDisabled = btn.config?.disabled ?? false;
|
|
645
|
-
// For dropdown buttons, only disable if config says so or if parent itself is loading
|
|
646
|
-
// Don't disable for inherited child loading (user should be able to open dropdown)
|
|
647
|
-
if (btn.type === ActionButtonType.Dropdown) {
|
|
648
|
-
const parentSelfLoading = this._isLoading() || (btn.loading ?? false);
|
|
649
|
-
return configDisabled || parentSelfLoading;
|
|
650
|
-
}
|
|
651
|
-
// For non-dropdown buttons, disable when loading
|
|
652
|
-
return this.isLoading() || configDisabled;
|
|
653
|
-
}, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
654
|
-
/**
|
|
655
|
-
* Determines the slot for the icon based on button type and label presence.
|
|
656
|
-
*/
|
|
657
|
-
iconSlot = computed(() => {
|
|
658
|
-
const btn = this.button();
|
|
659
|
-
if (btn.type === ActionButtonType.IconOnly) {
|
|
660
|
-
return 'icon-only';
|
|
661
|
-
}
|
|
662
|
-
return btn.label ? 'start' : 'icon-only';
|
|
663
|
-
}, ...(ngDevMode ? [{ debugName: "iconSlot" }] : []));
|
|
664
|
-
/**
|
|
665
|
-
* Whether to show the label text.
|
|
666
|
-
*/
|
|
667
|
-
showLabel = computed(() => {
|
|
668
|
-
const btn = this.button();
|
|
669
|
-
return btn.type !== ActionButtonType.IconOnly && btn.label;
|
|
670
|
-
}, ...(ngDevMode ? [{ debugName: "showLabel" }] : []));
|
|
671
|
-
/**
|
|
672
|
-
* Whether to show the dropdown chevron icon.
|
|
673
|
-
*/
|
|
674
|
-
showDropdownIcon = computed(() => {
|
|
675
|
-
const btn = this.button();
|
|
676
|
-
return btn.type === ActionButtonType.Dropdown && !btn.config?.hideDropdownIcon;
|
|
677
|
-
}, ...(ngDevMode ? [{ debugName: "showDropdownIcon" }] : []));
|
|
920
|
+
/** Whether the button is in a loading state. */
|
|
921
|
+
isLoading = computed(() => this.stateService.isLoading(this.button()), ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
922
|
+
/** Whether to display the loading spinner. */
|
|
923
|
+
showLoadingSpinner = computed(() => this.stateService.showLoadingSpinner(this.button()), ...(ngDevMode ? [{ debugName: "showLoadingSpinner" }] : []));
|
|
924
|
+
/** Whether the button is disabled. */
|
|
925
|
+
isDisabled = computed(() => this.stateService.isDisabled(this.button()), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
926
|
+
/** The slot for the icon based on label presence (Ionic-specific). */
|
|
927
|
+
iconSlot = computed(() => (this.button().label ? 'start' : 'icon-only'), ...(ngDevMode ? [{ debugName: "iconSlot" }] : []));
|
|
928
|
+
/** Whether to show the label text. */
|
|
929
|
+
showLabel = computed(() => this.displayService.shouldShowLabel(this.button()), ...(ngDevMode ? [{ debugName: "showLabel" }] : []));
|
|
930
|
+
/** Whether to show the dropdown chevron icon. */
|
|
931
|
+
showDropdownIcon = computed(() => this.displayService.shouldShowDropdownIcon(this.button()), ...(ngDevMode ? [{ debugName: "showDropdownIcon" }] : []));
|
|
678
932
|
constructor() {
|
|
679
933
|
addIcons({ chevronDownOutline });
|
|
680
934
|
}
|
|
@@ -695,7 +949,7 @@ class ButtonComponent {
|
|
|
695
949
|
await this.openDropdown(event);
|
|
696
950
|
}
|
|
697
951
|
else {
|
|
698
|
-
await this.executeHandler(btn);
|
|
952
|
+
await this.handlerService.executeHandler(btn, loading => this.stateService.setLoading(loading));
|
|
699
953
|
this.buttonClick.emit(btn);
|
|
700
954
|
}
|
|
701
955
|
}
|
|
@@ -715,7 +969,7 @@ class ButtonComponent {
|
|
|
715
969
|
component: ActionButtonListComponent,
|
|
716
970
|
componentProps: {
|
|
717
971
|
buttonsFromPopover: children,
|
|
718
|
-
loadingChildIds: this.
|
|
972
|
+
loadingChildIds: this.stateService.loadingChildIds
|
|
719
973
|
},
|
|
720
974
|
event,
|
|
721
975
|
translucent: true,
|
|
@@ -727,44 +981,19 @@ class ButtonComponent {
|
|
|
727
981
|
await popover.present();
|
|
728
982
|
const { data, role } = await popover.onDidDismiss();
|
|
729
983
|
if (role === 'select' && data) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const result = data.handler();
|
|
734
|
-
if (result instanceof Promise) {
|
|
735
|
-
await result;
|
|
984
|
+
await this.handlerService.executeChildHandler(data, (childId, loading) => {
|
|
985
|
+
if (loading) {
|
|
986
|
+
this.stateService.addLoadingChild(childId);
|
|
736
987
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const newIds = new Set(ids);
|
|
742
|
-
newIds.delete(data.id);
|
|
743
|
-
return newIds;
|
|
744
|
-
});
|
|
745
|
-
}
|
|
988
|
+
else {
|
|
989
|
+
this.stateService.removeLoadingChild(childId);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
746
992
|
this.childSelect.emit(data);
|
|
747
993
|
}
|
|
748
994
|
}
|
|
749
|
-
/**
|
|
750
|
-
* Executes a button's handler with automatic loading state management.
|
|
751
|
-
*
|
|
752
|
-
* @param btn - The button whose handler to execute
|
|
753
|
-
*/
|
|
754
|
-
async executeHandler(btn) {
|
|
755
|
-
const result = btn.handler();
|
|
756
|
-
if (result instanceof Promise) {
|
|
757
|
-
this._isLoading.set(true);
|
|
758
|
-
try {
|
|
759
|
-
await result;
|
|
760
|
-
}
|
|
761
|
-
finally {
|
|
762
|
-
this._isLoading.set(false);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
995
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
767
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ButtonComponent, isStandalone: true, selector: "maki-button", inputs: { button: { classPropertyName: "button", publicName: "button", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { buttonClick: "buttonClick", childSelect: "childSelect" }, ngImport: i0, template: `
|
|
996
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ButtonComponent, isStandalone: true, selector: "maki-button", inputs: { button: { classPropertyName: "button", publicName: "button", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { buttonClick: "buttonClick", childSelect: "childSelect" }, providers: [ButtonStateService, ButtonDisplayService, ButtonHandlerService], ngImport: i0, template: `
|
|
768
997
|
<ion-button
|
|
769
998
|
[fill]="button().config?.fill"
|
|
770
999
|
[size]="button().config?.size"
|
|
@@ -789,11 +1018,11 @@ class ButtonComponent {
|
|
|
789
1018
|
<ion-icon name="chevron-down-outline" slot="end" class="dropdown-icon" />
|
|
790
1019
|
}
|
|
791
1020
|
</ion-button>
|
|
792
|
-
`, isInline: true, styles: ["
|
|
1021
|
+
`, isInline: true, styles: ["ion-button{--padding-start: var(--maki-button-padding-start);--padding-end: var(--maki-button-padding-end);text-transform:none}ion-button.button-has-icon-only::part(native){padding:0}ion-spinner{width:var(--maki-button-spinner-size, 16px);height:var(--maki-button-spinner-size, 16px);margin:0 4px 0 0}.button-icon{font-size:var(--maki-button-icon-size, 16px);margin:0 4px 0 0}.button-icon[slot=icon-only]{margin:0}.dropdown-icon{font-size:var(--maki-button-dropdown-icon-size, 16px);margin-inline-start:var(--maki-button-dropdown-icon-gap, 4px)}\n"], dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
793
1022
|
}
|
|
794
1023
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonComponent, decorators: [{
|
|
795
1024
|
type: Component,
|
|
796
|
-
args: [{ selector: 'maki-button', standalone: true, imports: [IonButton, IonIcon, IonSpinner], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1025
|
+
args: [{ selector: 'maki-button', standalone: true, imports: [IonButton, IonIcon, IonSpinner], providers: [ButtonStateService, ButtonDisplayService, ButtonHandlerService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
797
1026
|
<ion-button
|
|
798
1027
|
[fill]="button().config?.fill"
|
|
799
1028
|
[size]="button().config?.size"
|
|
@@ -818,17 +1047,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
818
1047
|
<ion-icon name="chevron-down-outline" slot="end" class="dropdown-icon" />
|
|
819
1048
|
}
|
|
820
1049
|
</ion-button>
|
|
821
|
-
`, styles: ["
|
|
1050
|
+
`, styles: ["ion-button{--padding-start: var(--maki-button-padding-start);--padding-end: var(--maki-button-padding-end);text-transform:none}ion-button.button-has-icon-only::part(native){padding:0}ion-spinner{width:var(--maki-button-spinner-size, 16px);height:var(--maki-button-spinner-size, 16px);margin:0 4px 0 0}.button-icon{font-size:var(--maki-button-icon-size, 16px);margin:0 4px 0 0}.button-icon[slot=icon-only]{margin:0}.dropdown-icon{font-size:var(--maki-button-dropdown-icon-size, 16px);margin-inline-start:var(--maki-button-dropdown-icon-gap, 4px)}\n"] }]
|
|
822
1051
|
}], ctorParameters: () => [], propDecorators: { button: [{ type: i0.Input, args: [{ isSignal: true, alias: "button", required: true }] }], buttonClick: [{ type: i0.Output, args: ["buttonClick"] }], childSelect: [{ type: i0.Output, args: ["childSelect"] }] } });
|
|
823
1052
|
|
|
824
1053
|
/*
|
|
825
1054
|
* Public API Surface of @makigamestudio/ui-core
|
|
826
1055
|
*/
|
|
827
|
-
// Models
|
|
828
1056
|
|
|
829
1057
|
/**
|
|
830
1058
|
* Generated bundle index. Do not edit.
|
|
831
1059
|
*/
|
|
832
1060
|
|
|
833
|
-
export { ActionButtonListComponent, ActionButtonType, ButtonComponent,
|
|
1061
|
+
export { ActionButtonListComponent, ActionButtonListService, ActionButtonType, ButtonComponent, ButtonDisplayService, ButtonHandlerService, ButtonStateService };
|
|
834
1062
|
//# sourceMappingURL=makigamestudio-ui-core.mjs.map
|