@aerogel/core 0.0.0-next.b656a964404fbde17d9cce7668722596098e47fd → 0.0.0-next.b9379d15fd4f40346d655134b49c9015ead9c536
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aerogel-core.css +1 -0
- package/dist/aerogel-core.d.ts +649 -560
- package/dist/aerogel-core.js +1878 -1455
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +6 -4
- package/src/components/AppLayout.vue +1 -3
- package/src/components/AppOverlays.vue +0 -27
- package/src/components/contracts/AlertModal.ts +15 -0
- package/src/components/contracts/ConfirmModal.ts +12 -5
- package/src/components/contracts/DropdownMenu.ts +17 -3
- package/src/components/contracts/ErrorReportModal.ts +8 -4
- package/src/components/contracts/Input.ts +7 -7
- package/src/components/contracts/LoadingModal.ts +12 -4
- package/src/components/contracts/Modal.ts +12 -4
- package/src/components/contracts/PromptModal.ts +8 -2
- package/src/components/contracts/Select.ts +21 -12
- package/src/components/contracts/Toast.ts +4 -2
- package/src/components/contracts/index.ts +3 -1
- package/src/components/headless/HeadlessButton.vue +2 -1
- package/src/components/headless/HeadlessInput.vue +3 -3
- package/src/components/headless/HeadlessInputInput.vue +5 -5
- package/src/components/headless/HeadlessInputTextArea.vue +3 -3
- package/src/components/headless/HeadlessModal.vue +22 -51
- package/src/components/headless/HeadlessModalContent.vue +11 -5
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +34 -18
- package/src/components/headless/HeadlessSelectOption.vue +1 -1
- package/src/components/headless/HeadlessSelectOptions.vue +16 -4
- package/src/components/headless/HeadlessSelectValue.vue +4 -1
- package/src/components/headless/HeadlessSwitch.vue +96 -0
- package/src/components/headless/index.ts +2 -0
- package/src/components/ui/AdvancedOptions.vue +1 -1
- package/src/components/ui/AlertModal.vue +7 -3
- package/src/components/ui/Button.vue +17 -15
- package/src/components/ui/Checkbox.vue +4 -4
- package/src/components/ui/ConfirmModal.vue +12 -4
- package/src/components/ui/DropdownMenu.vue +18 -19
- package/src/components/ui/DropdownMenuOption.vue +22 -0
- package/src/components/ui/DropdownMenuOptions.vue +44 -0
- package/src/components/ui/EditableContent.vue +3 -3
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/ui/ErrorReportModal.vue +18 -7
- package/src/components/ui/Input.vue +3 -3
- package/src/components/ui/LoadingModal.vue +6 -4
- package/src/components/ui/Markdown.vue +29 -1
- package/src/components/ui/Modal.vue +74 -21
- package/src/components/ui/ModalContext.vue +2 -1
- package/src/components/ui/ProgressBar.vue +8 -7
- package/src/components/ui/PromptModal.vue +8 -5
- package/src/components/ui/Select.vue +10 -4
- package/src/components/ui/SelectLabel.vue +13 -2
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +24 -20
- package/src/components/ui/SelectTrigger.vue +2 -2
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/Toast.vue +19 -15
- package/src/components/ui/index.ts +6 -0
- package/src/directives/measure.ts +11 -5
- package/src/errors/Errors.ts +18 -15
- package/src/errors/index.ts +6 -2
- package/src/errors/settings/Debug.vue +39 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/forms/FormController.test.ts +32 -9
- package/src/forms/FormController.ts +23 -22
- package/src/forms/index.ts +0 -1
- package/src/forms/utils.ts +34 -34
- package/src/index.css +34 -7
- package/src/lang/index.ts +3 -2
- package/src/lang/settings/Language.vue +2 -2
- package/src/services/App.ts +6 -1
- package/src/services/Events.test.ts +8 -8
- package/src/services/Events.ts +2 -8
- package/src/services/index.ts +3 -3
- package/src/ui/UI.state.ts +3 -13
- package/src/ui/UI.ts +87 -79
- package/src/ui/index.ts +16 -17
- package/src/utils/classes.ts +9 -17
- package/src/utils/composition/events.ts +2 -4
- package/src/utils/composition/forms.ts +7 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown.ts +35 -1
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +6 -1
- package/src/components/contracts/shared.ts +0 -9
- package/src/forms/composition.ts +0 -6
|
@@ -10,12 +10,12 @@ describe('Events', () => {
|
|
|
10
10
|
// Arrange
|
|
11
11
|
let counter = 0;
|
|
12
12
|
|
|
13
|
-
Events.on('
|
|
13
|
+
Events.on('application-mounted', () => counter++);
|
|
14
14
|
|
|
15
15
|
// Act
|
|
16
|
-
await Events.emit('
|
|
17
|
-
await Events.emit('
|
|
18
|
-
await Events.emit('
|
|
16
|
+
await Events.emit('application-mounted');
|
|
17
|
+
await Events.emit('application-mounted');
|
|
18
|
+
await Events.emit('application-mounted');
|
|
19
19
|
|
|
20
20
|
// Assert
|
|
21
21
|
expect(counter).toEqual(3);
|
|
@@ -25,12 +25,12 @@ describe('Events', () => {
|
|
|
25
25
|
// Arrange
|
|
26
26
|
const storage: string[] = [];
|
|
27
27
|
|
|
28
|
-
Events.on('
|
|
29
|
-
Events.on('
|
|
30
|
-
Events.on('
|
|
28
|
+
Events.on('application-mounted', () => storage.push('second'));
|
|
29
|
+
Events.on('application-mounted', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
|
|
30
|
+
Events.on('application-mounted', { priority: EventListenerPriorities.High }, () => storage.push('first'));
|
|
31
31
|
|
|
32
32
|
// Act
|
|
33
|
-
await Events.emit('
|
|
33
|
+
await Events.emit('application-mounted');
|
|
34
34
|
|
|
35
35
|
// Assert
|
|
36
36
|
expect(storage).toEqual(['first', 'second', 'third']);
|
package/src/services/Events.ts
CHANGED
|
@@ -10,7 +10,6 @@ export type AerogelGlobalEvents = Partial<{ [Event in EventWithoutPayload]: () =
|
|
|
10
10
|
Partial<{ [Event in EventWithPayload]: EventListener<EventsPayload[Event]> }>;
|
|
11
11
|
|
|
12
12
|
export type EventListener<T = unknown> = (payload: T) => unknown;
|
|
13
|
-
export type UnknownEvent<T> = T extends keyof EventsPayload ? never : T;
|
|
14
13
|
|
|
15
14
|
export type EventWithoutPayload = {
|
|
16
15
|
[K in keyof EventsPayload]: EventsPayload[K] extends void ? K : never;
|
|
@@ -34,12 +33,12 @@ export class EventsService extends Service {
|
|
|
34
33
|
|
|
35
34
|
protected override async boot(): Promise<void> {
|
|
36
35
|
Object.entries(globalThis.__aerogelEvents__ ?? {}).forEach(([event, listener]) =>
|
|
37
|
-
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
this.on(event as any, listener as EventListener));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
public emit<Event extends EventWithoutPayload>(event: Event): Promise<void>;
|
|
41
41
|
public emit<Event extends EventWithPayload>(event: Event, payload: EventsPayload[Event]): Promise<void>;
|
|
42
|
-
public emit<Event extends string>(event: UnknownEvent<Event>, payload?: unknown): Promise<void>;
|
|
43
42
|
public async emit(event: string, payload?: unknown): Promise<void> {
|
|
44
43
|
const listeners = this.listeners[event] ?? { priorities: [], handlers: {} };
|
|
45
44
|
|
|
@@ -55,9 +54,6 @@ export class EventsService extends Service {
|
|
|
55
54
|
public on<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
56
55
|
public on<Event extends EventWithPayload>(event: Event, priority: EventListenerPriority, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
57
56
|
public on<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
58
|
-
public on<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
|
|
59
|
-
public on<Event extends string>(event: UnknownEvent<Event>, priority: EventListenerPriority, listener: EventListener): () => void; // prettier-ignore
|
|
60
|
-
public on<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
|
|
61
57
|
/* eslint-enable max-len */
|
|
62
58
|
|
|
63
59
|
public on(
|
|
@@ -83,8 +79,6 @@ export class EventsService extends Service {
|
|
|
83
79
|
public once<Event extends EventWithoutPayload>(event: Event, options: Partial<EventListenerOptions>, listener: () => unknown): () => void; // prettier-ignore
|
|
84
80
|
public once<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
85
81
|
public once<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
86
|
-
public once<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
|
|
87
|
-
public once<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
|
|
88
82
|
/* eslint-enable max-len */
|
|
89
83
|
|
|
90
84
|
public once(
|
package/src/services/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { App as
|
|
1
|
+
import type { App as AppInstance } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
4
|
import { isDevelopment, isTesting } from '@noeldemartin/utils';
|
|
@@ -30,7 +30,7 @@ export type DefaultServices = typeof defaultServices;
|
|
|
30
30
|
|
|
31
31
|
export interface Services extends DefaultServices {}
|
|
32
32
|
|
|
33
|
-
export async function bootServices(app:
|
|
33
|
+
export async function bootServices(app: AppInstance, services: Record<string, Service>): Promise<void> {
|
|
34
34
|
await Promise.all(
|
|
35
35
|
Object.entries(services).map(async ([name, service]) => {
|
|
36
36
|
await service
|
|
@@ -54,7 +54,7 @@ export default definePlugin({
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
app.use(getPiniaStore());
|
|
57
|
-
|
|
57
|
+
options.settings?.forEach((setting) => App.addSetting(setting));
|
|
58
58
|
|
|
59
59
|
await bootServices(app, services);
|
|
60
60
|
},
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -8,22 +8,11 @@ export interface UIModal<T = unknown> {
|
|
|
8
8
|
id: string;
|
|
9
9
|
properties: Record<string, unknown>;
|
|
10
10
|
component: Component;
|
|
11
|
+
closing: boolean;
|
|
11
12
|
beforeClose: Promise<T | undefined>;
|
|
12
13
|
afterClose: Promise<T | undefined>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export interface UIModalContext {
|
|
16
|
-
modal: UIModal;
|
|
17
|
-
childIndex?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ModalComponent<
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
22
|
-
Properties extends object = object,
|
|
23
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
24
|
-
Result = unknown,
|
|
25
|
-
> {}
|
|
26
|
-
|
|
27
16
|
export interface UIToast {
|
|
28
17
|
id: string;
|
|
29
18
|
component: Component;
|
|
@@ -38,7 +27,8 @@ export default defineServiceState({
|
|
|
38
27
|
layout: getCurrentLayout(),
|
|
39
28
|
},
|
|
40
29
|
computed: {
|
|
41
|
-
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
42
30
|
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
32
|
+
openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
|
|
43
33
|
},
|
|
44
34
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,42 +1,55 @@
|
|
|
1
1
|
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
|
+
import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
|
|
3
4
|
import type { Component } from 'vue';
|
|
4
|
-
import type {
|
|
5
|
+
import type { ClosureArgs } from '@noeldemartin/utils';
|
|
5
6
|
|
|
6
7
|
import App from '@aerogel/core/services/App';
|
|
7
8
|
import Events from '@aerogel/core/services/Events';
|
|
9
|
+
import type {
|
|
10
|
+
ConfirmModalCheckboxes,
|
|
11
|
+
ConfirmModalExpose,
|
|
12
|
+
ConfirmModalProps,
|
|
13
|
+
} from '@aerogel/core/components/contracts/ConfirmModal';
|
|
14
|
+
import type {
|
|
15
|
+
ErrorReportModalExpose,
|
|
16
|
+
ErrorReportModalProps,
|
|
17
|
+
} from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
8
18
|
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
9
|
-
import type { AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
19
|
+
import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
10
20
|
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
11
|
-
import type {
|
|
12
|
-
import type {
|
|
13
|
-
import type {
|
|
14
|
-
import type { ToastAction, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
21
|
+
import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
|
|
22
|
+
import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
23
|
+
import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
15
24
|
|
|
16
25
|
import Service from './UI.state';
|
|
17
26
|
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
-
import type {
|
|
27
|
+
import type { UIModal, UIToast } from './UI.state';
|
|
19
28
|
|
|
20
29
|
interface ModalCallbacks<T = unknown> {
|
|
21
30
|
willClose(result: T | undefined): void;
|
|
22
|
-
|
|
31
|
+
hasClosed(result: T | undefined): void;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
export type ModalResult<T> = ModalExposeResult<ComponentExposed<T>>;
|
|
35
|
+
export type ModalExposeResult<T> = T extends { close(result?: infer Result): Promise<void> } ? Result : unknown;
|
|
36
|
+
export type UIComponent<Props = {}, Exposed = {}> = { new (...args: ClosureArgs): Exposed & { $props: Props } };
|
|
37
|
+
|
|
38
|
+
export interface UIComponents {
|
|
39
|
+
'alert-modal': UIComponent<AlertModalProps, AlertModalExpose>;
|
|
40
|
+
'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose>;
|
|
41
|
+
'error-report-modal': UIComponent<ErrorReportModalProps, ErrorReportModalExpose>;
|
|
42
|
+
'loading-modal': UIComponent<LoadingModalProps, LoadingModalExpose>;
|
|
43
|
+
'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose>;
|
|
44
|
+
'router-link': UIComponent;
|
|
45
|
+
'startup-crash': UIComponent;
|
|
46
|
+
toast: UIComponent<ToastProps, ToastExpose>;
|
|
47
|
+
}
|
|
38
48
|
|
|
39
|
-
export
|
|
49
|
+
export interface UIModalContext {
|
|
50
|
+
modal: UIModal;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
export type ConfirmOptions = AcceptRefs<{
|
|
42
55
|
acceptText?: string;
|
|
@@ -78,10 +91,18 @@ export interface ToastOptions {
|
|
|
78
91
|
export class UIService extends Service {
|
|
79
92
|
|
|
80
93
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
81
|
-
private components: Partial<
|
|
94
|
+
private components: Partial<UIComponents> = {};
|
|
95
|
+
|
|
96
|
+
public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
|
|
97
|
+
this.components[name] = component;
|
|
98
|
+
}
|
|
82
99
|
|
|
83
|
-
public
|
|
84
|
-
return this.components[name] ??
|
|
100
|
+
public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
|
|
101
|
+
return this.components[name] ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
|
|
105
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
public alert(message: string): void;
|
|
@@ -98,10 +119,7 @@ export class UIService extends Service {
|
|
|
98
119
|
};
|
|
99
120
|
};
|
|
100
121
|
|
|
101
|
-
this.
|
|
102
|
-
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
-
getProperties(),
|
|
104
|
-
);
|
|
122
|
+
this.modal(this.requireComponent('alert-modal'), getProperties());
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
/* eslint-disable max-len */
|
|
@@ -133,18 +151,8 @@ export class UIService extends Service {
|
|
|
133
151
|
};
|
|
134
152
|
};
|
|
135
153
|
|
|
136
|
-
type ConfirmModalComponent = ModalComponent<
|
|
137
|
-
AcceptRefs<ConfirmModalProps>,
|
|
138
|
-
boolean | [boolean, Record<string, boolean>]
|
|
139
|
-
>;
|
|
140
|
-
|
|
141
154
|
const properties = getProperties();
|
|
142
|
-
const
|
|
143
|
-
this.requireComponent(UIComponents.ConfirmModal),
|
|
144
|
-
properties,
|
|
145
|
-
);
|
|
146
|
-
const result = await modal.beforeClose;
|
|
147
|
-
|
|
155
|
+
const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
|
|
148
156
|
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
149
157
|
const checkboxes =
|
|
150
158
|
typeof result === 'object'
|
|
@@ -196,11 +204,7 @@ export class UIService extends Service {
|
|
|
196
204
|
} as PromptModalProps;
|
|
197
205
|
};
|
|
198
206
|
|
|
199
|
-
const
|
|
200
|
-
this.requireComponent(UIComponents.PromptModal),
|
|
201
|
-
getProperties(),
|
|
202
|
-
);
|
|
203
|
-
const rawResult = await modal.beforeClose;
|
|
207
|
+
const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
|
|
204
208
|
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
205
209
|
|
|
206
210
|
return result ?? null;
|
|
@@ -233,7 +237,7 @@ export class UIService extends Service {
|
|
|
233
237
|
};
|
|
234
238
|
|
|
235
239
|
const { operationPromise, props } = processArgs();
|
|
236
|
-
const modal = await this.
|
|
240
|
+
const modal = await this.modal(this.requireComponent('loading-modal'), props);
|
|
237
241
|
|
|
238
242
|
try {
|
|
239
243
|
const result = await operationPromise;
|
|
@@ -251,30 +255,29 @@ export class UIService extends Service {
|
|
|
251
255
|
const toast: UIToast = {
|
|
252
256
|
id: uuid(),
|
|
253
257
|
properties: { message, ...otherOptions },
|
|
254
|
-
component: markRaw(component ?? this.requireComponent(
|
|
258
|
+
component: markRaw(component ?? this.requireComponent('toast')),
|
|
255
259
|
};
|
|
256
260
|
|
|
257
261
|
this.setState('toasts', this.toasts.concat(toast));
|
|
258
262
|
}
|
|
259
263
|
|
|
260
|
-
public
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
public modal<T extends Component>(
|
|
265
|
+
...args: {} extends ComponentProps<T>
|
|
266
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
267
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
268
|
+
): Promise<UIModal<ModalResult<T>>>;
|
|
263
269
|
|
|
264
|
-
public async
|
|
265
|
-
component: TModalComponent,
|
|
266
|
-
properties?: ModalProperties<TModalComponent>,
|
|
267
|
-
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
270
|
+
public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
|
|
268
271
|
const id = uuid();
|
|
269
|
-
const callbacks: Partial<ModalCallbacks<ModalResult<
|
|
270
|
-
const modal: UIModal<ModalResult<
|
|
272
|
+
const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
|
|
273
|
+
const modal: UIModal<ModalResult<T>> = {
|
|
271
274
|
id,
|
|
272
|
-
|
|
275
|
+
closing: false,
|
|
276
|
+
properties: props ?? {},
|
|
273
277
|
component: markRaw(component),
|
|
274
278
|
beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
|
|
275
|
-
afterClose: new Promise((resolve) => (callbacks.
|
|
279
|
+
afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
|
|
276
280
|
};
|
|
277
|
-
const activeModal = this.modals.at(-1);
|
|
278
281
|
const modals = this.modals.concat(modal);
|
|
279
282
|
|
|
280
283
|
this.modalCallbacks[modal.id] = callbacks;
|
|
@@ -282,15 +285,26 @@ export class UIService extends Service {
|
|
|
282
285
|
this.setState({ modals });
|
|
283
286
|
|
|
284
287
|
await nextTick();
|
|
285
|
-
await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
|
|
286
|
-
await Promise.all([
|
|
287
|
-
activeModal || Events.emit('show-overlays-backdrop'),
|
|
288
|
-
Events.emit('show-modal', { id: modal.id }),
|
|
289
|
-
]);
|
|
290
288
|
|
|
291
289
|
return modal;
|
|
292
290
|
}
|
|
293
291
|
|
|
292
|
+
public modalForm<T extends Component>(
|
|
293
|
+
...args: {} extends ComponentProps<T>
|
|
294
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
295
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
296
|
+
): Promise<ModalResult<T> | undefined>;
|
|
297
|
+
|
|
298
|
+
public async modalForm<T extends Component>(
|
|
299
|
+
component: T,
|
|
300
|
+
props?: ComponentProps<T>,
|
|
301
|
+
): Promise<ModalResult<T> | undefined> {
|
|
302
|
+
const modal = await this.modal<T>(component, props as ComponentProps<T>);
|
|
303
|
+
const result = await modal.beforeClose;
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
294
308
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
295
309
|
if (!App.isMounted()) {
|
|
296
310
|
await this.removeModal(id, result);
|
|
@@ -319,25 +333,23 @@ export class UIService extends Service {
|
|
|
319
333
|
this.modals.filter((m) => m.id !== id),
|
|
320
334
|
);
|
|
321
335
|
|
|
322
|
-
this.modalCallbacks[id]?.
|
|
336
|
+
this.modalCallbacks[id]?.hasClosed?.(result);
|
|
323
337
|
|
|
324
338
|
delete this.modalCallbacks[id];
|
|
325
|
-
|
|
326
|
-
const activeModal = this.modals.at(-1);
|
|
327
|
-
|
|
328
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
private watchModalEvents(): void {
|
|
332
|
-
Events.on('modal-will-close', ({ modal, result }) => {
|
|
333
|
-
this.
|
|
342
|
+
Events.on('modal-will-close', ({ modal: { id }, result }) => {
|
|
343
|
+
const modal = this.modals.find((_modal) => id === _modal.id);
|
|
334
344
|
|
|
335
|
-
if (
|
|
336
|
-
|
|
345
|
+
if (modal) {
|
|
346
|
+
modal.closing = true;
|
|
337
347
|
}
|
|
348
|
+
|
|
349
|
+
this.modalCallbacks[id]?.willClose?.(result);
|
|
338
350
|
});
|
|
339
351
|
|
|
340
|
-
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
352
|
+
Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
|
|
341
353
|
await this.removeModal(id, result);
|
|
342
354
|
});
|
|
343
355
|
}
|
|
@@ -381,11 +393,7 @@ export default facade(UIService);
|
|
|
381
393
|
declare module '@aerogel/core/services/Events' {
|
|
382
394
|
export interface EventsPayload {
|
|
383
395
|
'close-modal': { id: string; result?: unknown };
|
|
384
|
-
'hide-modal': { id: string };
|
|
385
|
-
'hide-overlays-backdrop': void;
|
|
386
|
-
'modal-closed': { modal: UIModal; result?: unknown };
|
|
387
396
|
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
388
|
-
'
|
|
389
|
-
'show-overlays-backdrop': void;
|
|
397
|
+
'modal-has-closed': { modal: UIModal; result?: unknown };
|
|
390
398
|
}
|
|
391
399
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { Component } from 'vue';
|
|
2
|
-
|
|
3
1
|
import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
|
|
4
2
|
import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
|
|
5
3
|
import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
|
|
@@ -10,8 +8,9 @@ import Toast from '@aerogel/core/components/ui/Toast.vue';
|
|
|
10
8
|
import { bootServices } from '@aerogel/core/services';
|
|
11
9
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
12
10
|
|
|
13
|
-
import UI
|
|
14
|
-
import type {
|
|
11
|
+
import UI from './UI';
|
|
12
|
+
import type { UIComponents } from './UI';
|
|
13
|
+
import type { Component } from 'vue';
|
|
15
14
|
|
|
16
15
|
const services = { $ui: UI };
|
|
17
16
|
|
|
@@ -23,20 +22,20 @@ export type UIServices = typeof services;
|
|
|
23
22
|
|
|
24
23
|
export default definePlugin({
|
|
25
24
|
async install(app, options) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const components: Partial<Record<keyof UIComponents, Component>> = {
|
|
26
|
+
'alert-modal': AlertModal,
|
|
27
|
+
'confirm-modal': ConfirmModal,
|
|
28
|
+
'error-report-modal': ErrorReportModal,
|
|
29
|
+
'loading-modal': LoadingModal,
|
|
30
|
+
'prompt-modal': PromptModal,
|
|
31
|
+
'startup-crash': StartupCrash,
|
|
32
|
+
'toast': Toast,
|
|
33
|
+
...options.components,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
Object.entries({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}).forEach(([name, component]) => UI.registerComponent(name as UIComponent, component));
|
|
36
|
+
for (const [name, component] of Object.entries(components)) {
|
|
37
|
+
UI.registerComponent(name as keyof UIComponents, component as UIComponents[keyof UIComponents]);
|
|
38
|
+
}
|
|
40
39
|
|
|
41
40
|
await bootServices(app, services);
|
|
42
41
|
},
|
|
@@ -44,7 +43,7 @@ export default definePlugin({
|
|
|
44
43
|
|
|
45
44
|
declare module '@aerogel/core/bootstrap/options' {
|
|
46
45
|
export interface AerogelOptions {
|
|
47
|
-
components?: Partial<
|
|
46
|
+
components?: Partial<Partial<UIComponents>>;
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
|
package/src/utils/classes.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import {
|
|
2
|
+
import { unref } from 'vue';
|
|
3
3
|
import { cva } from 'class-variance-authority';
|
|
4
4
|
import { twMerge } from 'tailwind-merge';
|
|
5
5
|
import type { ClassValue } from 'clsx';
|
|
6
|
-
import type {
|
|
6
|
+
import type { PropType } from 'vue';
|
|
7
7
|
import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
|
|
8
8
|
|
|
9
9
|
export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
|
|
10
10
|
export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
|
|
11
|
-
export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
|
|
12
11
|
export type Variants<T extends Record<string, string | boolean>> = Required<{
|
|
13
12
|
[K in keyof T]: Exclude<T[K], undefined> extends string
|
|
14
13
|
? { [key in Exclude<T[K], undefined>]: string | null }
|
|
@@ -26,22 +25,15 @@ export type PickComponentProps<TValues, TDefinitions> = {
|
|
|
26
25
|
[K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
|
|
27
26
|
};
|
|
28
27
|
|
|
29
|
-
export function
|
|
30
|
-
value:
|
|
28
|
+
export function variantClasses<T>(
|
|
29
|
+
value: { baseClasses?: string } & CVAProps<T>,
|
|
31
30
|
config: { baseClasses?: string } & CVAConfig<T>,
|
|
32
|
-
):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const variants = cva(configBaseClasses, configs as CVAConfig<T>);
|
|
37
|
-
const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
|
|
38
|
-
extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
|
|
31
|
+
): string {
|
|
32
|
+
const { baseClasses: valueBaseClasses, ...values } = value;
|
|
33
|
+
const { baseClasses: configBaseClasses, ...configs } = config;
|
|
34
|
+
const variants = cva(configBaseClasses, configs as CVAConfig<T>);
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
}, {} as CVAProps<T>);
|
|
42
|
-
|
|
43
|
-
return classes(variants(values), unref(valueBaseClasses));
|
|
44
|
-
});
|
|
36
|
+
return classes(variants(values as CVAProps<T>), unref(valueBaseClasses));
|
|
45
37
|
}
|
|
46
38
|
|
|
47
39
|
export function classes(...inputs: ClassValue[]): string {
|
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
EventWithPayload,
|
|
7
7
|
EventWithoutPayload,
|
|
8
8
|
EventsPayload,
|
|
9
|
-
UnknownEvent,
|
|
10
9
|
} from '@aerogel/core/services/Events';
|
|
11
10
|
|
|
12
11
|
export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
|
|
@@ -14,11 +13,10 @@ export function useEvent<Event extends EventWithPayload>(
|
|
|
14
13
|
event: Event,
|
|
15
14
|
listener: EventListener<EventsPayload[Event]>
|
|
16
15
|
): void;
|
|
17
|
-
export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
|
|
18
|
-
export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
|
|
19
16
|
|
|
20
17
|
export function useEvent(event: string, listener: EventListener): void {
|
|
21
|
-
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const unsubscribe = Events.on(event as any, listener);
|
|
22
20
|
|
|
23
21
|
onUnmounted(() => unsubscribe());
|
|
24
22
|
}
|
|
@@ -2,9 +2,11 @@ import { objectWithout } from '@noeldemartin/utils';
|
|
|
2
2
|
import { computed, inject, onUnmounted, useAttrs } from 'vue';
|
|
3
3
|
import type { ClassValue } from 'clsx';
|
|
4
4
|
import type { ComputedRef } from 'vue';
|
|
5
|
-
import type { FormController } from '@aerogel/core/forms';
|
|
6
5
|
import type { Nullable } from '@noeldemartin/utils';
|
|
7
6
|
|
|
7
|
+
import FormController from '@aerogel/core/forms/FormController';
|
|
8
|
+
import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
|
|
9
|
+
|
|
8
10
|
export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
|
|
9
11
|
const form = inject<FormController | null>('form', null);
|
|
10
12
|
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
@@ -12,6 +14,10 @@ export function onFormFocus(input: { name: Nullable<string> }, listener: () => u
|
|
|
12
14
|
onUnmounted(() => stop?.());
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
|
|
18
|
+
return new FormController(fields) as FormController<T> & FormData<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
|
|
16
22
|
const attrs = useAttrs();
|
|
17
23
|
const classes = computed(() => attrs.class);
|
package/src/utils/index.ts
CHANGED
package/src/utils/markdown.ts
CHANGED
|
@@ -2,10 +2,18 @@ import DOMPurify from 'dompurify';
|
|
|
2
2
|
import { stringMatchAll, tap } from '@noeldemartin/utils';
|
|
3
3
|
import { Renderer, marked } from 'marked';
|
|
4
4
|
|
|
5
|
+
let router: MarkdownRouter | null = null;
|
|
6
|
+
|
|
5
7
|
function makeRenderer(): Renderer {
|
|
6
8
|
return tap(new Renderer(), (renderer) => {
|
|
7
9
|
renderer.link = function(link) {
|
|
8
|
-
|
|
10
|
+
const defaultLink = Renderer.prototype.link.apply(this, [link]);
|
|
11
|
+
|
|
12
|
+
if (!link.href.startsWith('#')) {
|
|
13
|
+
return defaultLink.replace('<a', '<a target="_blank"');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return defaultLink;
|
|
9
17
|
};
|
|
10
18
|
});
|
|
11
19
|
}
|
|
@@ -20,11 +28,37 @@ function renderActionLinks(html: string): string {
|
|
|
20
28
|
return html;
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
function renderRouteLinks(html: string): string {
|
|
32
|
+
const matches = stringMatchAll<3>(html, /<a[^>]*href="#route:([^"]+)"[^>]*>([^<]+)<\/a>/g);
|
|
33
|
+
|
|
34
|
+
for (const [link, route, text] of matches) {
|
|
35
|
+
const url = router?.resolve(route) ?? route;
|
|
36
|
+
|
|
37
|
+
html = html.replace(link, `<a data-markdown-route="${route}" href="${url}">${text}</a>`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return html;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MarkdownRouter {
|
|
44
|
+
resolve(route: string): string;
|
|
45
|
+
visit(route: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getMarkdownRouter(): MarkdownRouter | null {
|
|
49
|
+
return router;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setMarkdownRouter(markdownRouter: MarkdownRouter): void {
|
|
53
|
+
router = markdownRouter;
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
export function renderMarkdown(markdown: string): string {
|
|
24
57
|
let html = marked(markdown, { renderer: makeRenderer(), async: false });
|
|
25
58
|
|
|
26
59
|
html = safeHtml(html);
|
|
27
60
|
html = renderActionLinks(html);
|
|
61
|
+
html = renderRouteLinks(html);
|
|
28
62
|
|
|
29
63
|
return html;
|
|
30
64
|
}
|
package/src/utils/vue.ts
CHANGED
|
@@ -12,7 +12,12 @@ function renderVNodeAttrs(node: VNode): string {
|
|
|
12
12
|
}, '');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
export function defineDirective<TValue = any, TModifiers extends string = string>(
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
directive: Directive<any, TValue, TModifiers>,
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
): Directive<any, TValue, TModifiers> {
|
|
16
21
|
return directive;
|
|
17
22
|
}
|
|
18
23
|
|