@aerogel/core 0.0.0-next.b85327579d32f21c6a9fa21142f0165cdd320d7e → 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 +2310 -664
- package/dist/aerogel-core.js +3542 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +38 -34
- package/src/bootstrap/bootstrap.test.ts +7 -10
- package/src/bootstrap/index.ts +43 -14
- package/src/bootstrap/options.ts +4 -1
- package/src/components/AppLayout.vue +14 -0
- package/src/components/AppModals.vue +14 -0
- package/src/components/AppOverlays.vue +9 -0
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +19 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +48 -0
- package/src/components/contracts/DropdownMenu.ts +25 -0
- package/src/components/contracts/ErrorReportModal.ts +33 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +26 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +34 -0
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +15 -0
- package/src/components/contracts/index.ts +11 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/HeadlessInputInput.vue +75 -0
- package/src/components/headless/HeadlessInputLabel.vue +18 -0
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/HeadlessModal.vue +57 -0
- package/src/components/headless/HeadlessModalContent.vue +30 -0
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +120 -0
- package/src/components/headless/HeadlessSelectError.vue +25 -0
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +42 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +18 -0
- package/src/components/headless/HeadlessSwitch.vue +96 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +20 -2
- package/src/components/index.ts +6 -7
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +17 -0
- package/src/components/ui/Button.vue +100 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +50 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +22 -0
- package/src/components/ui/DropdownMenuOptions.vue +44 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +73 -0
- package/src/components/ui/ErrorReportModalButtons.vue +118 -0
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +34 -0
- package/src/components/ui/Markdown.vue +97 -0
- package/src/components/ui/Modal.vue +123 -0
- package/src/components/ui/ModalContext.vue +31 -0
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +38 -0
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +21 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +35 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/ui/StartupCrash.vue +31 -0
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/Toast.vue +46 -0
- package/src/components/ui/index.ts +33 -0
- package/src/directives/index.ts +27 -6
- package/src/directives/measure.ts +46 -0
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +70 -15
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +38 -6
- package/src/errors/settings/Debug.vue +39 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/errors/utils.ts +35 -0
- package/src/forms/FormController.test.ts +110 -0
- package/src/forms/FormController.ts +246 -0
- package/src/forms/index.ts +3 -2
- package/src/forms/utils.ts +51 -20
- package/src/forms/validation.ts +19 -0
- package/src/index.css +73 -0
- package/src/{main.ts → index.ts} +3 -2
- package/src/jobs/Job.ts +147 -0
- package/src/jobs/index.ts +10 -0
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/lang/index.ts +12 -6
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/plugins/Plugin.ts +2 -1
- package/src/plugins/index.ts +22 -0
- package/src/services/App.state.ts +42 -5
- package/src/services/App.ts +52 -6
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +110 -36
- package/src/services/Service.ts +160 -50
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +25 -10
- package/src/services/store.ts +8 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +20 -9
- package/src/ui/UI.ts +322 -74
- package/src/ui/index.ts +32 -23
- package/src/ui/utils.ts +16 -0
- package/src/utils/classes.ts +41 -0
- package/src/utils/composition/events.ts +4 -5
- package/src/utils/composition/forms.ts +20 -4
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +33 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +60 -4
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +38 -121
- package/.eslintrc.js +0 -3
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/noeldemartin.config.js +0 -2
- package/src/components/AGAppLayout.vue +0 -11
- package/src/components/AGAppOverlays.vue +0 -39
- package/src/components/basic/AGMarkdown.vue +0 -35
- package/src/components/basic/index.ts +0 -3
- package/src/components/forms/AGButton.vue +0 -34
- package/src/components/forms/AGCheckbox.vue +0 -35
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -36
- package/src/components/forms/index.ts +0 -6
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -50
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
- package/src/components/headless/forms/index.ts +0 -6
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -6
- package/src/components/modals/AGAlertModal.vue +0 -26
- package/src/components/modals/AGConfirmModal.vue +0 -30
- package/src/components/modals/AGLoadingModal.vue +0 -19
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -36
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/index.ts +0 -8
- package/src/directives/initial-focus.ts +0 -11
- package/src/forms/Form.test.ts +0 -58
- package/src/forms/Form.ts +0 -176
- package/src/forms/composition.ts +0 -6
- package/src/globals.ts +0 -6
- package/tsconfig.json +0 -10
- package/vite.config.ts +0 -13
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { objectOnly } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
export type Replace<
|
|
4
|
+
TOriginal extends Record<string, unknown>,
|
|
5
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
6
|
+
> = {
|
|
7
|
+
[K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function replaceExisting<
|
|
11
|
+
TOriginal extends Record<string, unknown>,
|
|
12
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
13
|
+
>(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
...objectOnly(replacements, Object.keys(original)),
|
|
17
|
+
} as Replace<TOriginal, TReplacements>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isTesting } from '@noeldemartin/utils';
|
|
2
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
import Events from '@aerogel/core/services/Events';
|
|
5
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
6
|
+
|
|
7
|
+
export interface AerogelTestingRuntime {
|
|
8
|
+
on: (typeof Events)['on'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default definePlugin({
|
|
12
|
+
async install() {
|
|
13
|
+
if (!isTesting()) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
globalThis.testingRuntime = {
|
|
18
|
+
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
// eslint-disable-next-line no-var
|
|
25
|
+
var testingRuntime: AerogelTestingRuntime | undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
|
+
import { beforeEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
vi.mock('dompurify', async () => {
|
|
5
|
+
return { default: { sanitize: (html: string) => html } };
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
FakeLocalStorage.reset();
|
|
10
|
+
FakeLocalStorage.patchGlobal();
|
|
11
|
+
});
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { defineServiceState } from '
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
6
|
+
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
6
8
|
id: string;
|
|
7
9
|
properties: Record<string, unknown>;
|
|
8
10
|
component: Component;
|
|
11
|
+
closing: boolean;
|
|
9
12
|
beforeClose: Promise<T | undefined>;
|
|
10
13
|
afterClose: Promise<T | undefined>;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
export interface
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
> {}
|
|
16
|
+
export interface UIToast {
|
|
17
|
+
id: string;
|
|
18
|
+
component: Component;
|
|
19
|
+
properties: Record<string, unknown>;
|
|
20
|
+
}
|
|
19
21
|
|
|
20
22
|
export default defineServiceState({
|
|
21
23
|
name: 'ui',
|
|
22
|
-
initialState: {
|
|
24
|
+
initialState: {
|
|
25
|
+
modals: [] as UIModal[],
|
|
26
|
+
toasts: [] as UIToast[],
|
|
27
|
+
layout: getCurrentLayout(),
|
|
28
|
+
},
|
|
29
|
+
computed: {
|
|
30
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
32
|
+
openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
|
|
33
|
+
},
|
|
23
34
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,89 +1,283 @@
|
|
|
1
|
-
import { facade, fail, uuid } from '@noeldemartin/utils';
|
|
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
|
-
|
|
6
|
-
import
|
|
5
|
+
import type { ClosureArgs } from '@noeldemartin/utils';
|
|
6
|
+
|
|
7
|
+
import App from '@aerogel/core/services/App';
|
|
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';
|
|
18
|
+
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
19
|
+
import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
20
|
+
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
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';
|
|
7
24
|
|
|
8
25
|
import Service from './UI.state';
|
|
9
|
-
import
|
|
26
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
27
|
+
import type { UIModal, UIToast } from './UI.state';
|
|
10
28
|
|
|
11
29
|
interface ModalCallbacks<T = unknown> {
|
|
12
30
|
willClose(result: T | undefined): void;
|
|
13
|
-
|
|
31
|
+
hasClosed(result: T | undefined): void;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
type
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|
|
20
48
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} as const;
|
|
49
|
+
export interface UIModalContext {
|
|
50
|
+
modal: UIModal;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}
|
|
26
53
|
|
|
27
|
-
export type
|
|
54
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
55
|
+
acceptText?: string;
|
|
56
|
+
acceptVariant?: ButtonVariant;
|
|
57
|
+
cancelText?: string;
|
|
58
|
+
cancelVariant?: ButtonVariant;
|
|
59
|
+
actions?: Record<string, () => unknown>;
|
|
60
|
+
required?: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
export type LoadingOptions = AcceptRefs<{
|
|
64
|
+
title?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
progress?: number;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
70
|
+
extends ConfirmOptions {
|
|
71
|
+
checkboxes?: T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PromptOptions = AcceptRefs<{
|
|
75
|
+
label?: string;
|
|
76
|
+
defaultValue?: string;
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
acceptText?: string;
|
|
79
|
+
acceptVariant?: ButtonVariant;
|
|
80
|
+
cancelText?: string;
|
|
81
|
+
cancelVariant?: ButtonVariant;
|
|
82
|
+
trim?: boolean;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
export interface ToastOptions {
|
|
86
|
+
component?: Component;
|
|
87
|
+
variant?: ToastVariant;
|
|
88
|
+
actions?: ToastAction[];
|
|
89
|
+
}
|
|
28
90
|
|
|
29
91
|
export class UIService extends Service {
|
|
30
92
|
|
|
31
93
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
32
|
-
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
|
+
}
|
|
99
|
+
|
|
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!`);
|
|
106
|
+
}
|
|
33
107
|
|
|
34
108
|
public alert(message: string): void;
|
|
35
109
|
public alert(title: string, message: string): void;
|
|
36
110
|
public alert(messageOrTitle: string, message?: string): void {
|
|
37
|
-
const
|
|
111
|
+
const getProperties = (): AlertModalProps => {
|
|
112
|
+
if (typeof message !== 'string') {
|
|
113
|
+
return { message: messageOrTitle };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
title: messageOrTitle,
|
|
118
|
+
message,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
38
121
|
|
|
39
|
-
this.
|
|
122
|
+
this.modal(this.requireComponent('alert-modal'), getProperties());
|
|
40
123
|
}
|
|
41
124
|
|
|
42
|
-
|
|
43
|
-
public async confirm(
|
|
44
|
-
public async confirm(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
125
|
+
/* eslint-disable max-len */
|
|
126
|
+
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
127
|
+
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
128
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
129
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
130
|
+
/* eslint-enable max-len */
|
|
131
|
+
|
|
132
|
+
public async confirm(
|
|
133
|
+
messageOrTitle: string,
|
|
134
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
135
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
136
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
137
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
138
|
+
if (typeof messageOrOptions !== 'string') {
|
|
139
|
+
return {
|
|
140
|
+
...(messageOrOptions ?? {}),
|
|
141
|
+
message: messageOrTitle,
|
|
142
|
+
required: !!messageOrOptions?.required,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
...(options ?? {}),
|
|
148
|
+
title: messageOrTitle,
|
|
149
|
+
message: messageOrOptions,
|
|
150
|
+
required: !!options?.required,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const properties = getProperties();
|
|
155
|
+
const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
|
|
156
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
157
|
+
const checkboxes =
|
|
158
|
+
typeof result === 'object'
|
|
159
|
+
? result[1]
|
|
160
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
161
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
162
|
+
[checkbox]: defaultValue ?? false,
|
|
163
|
+
...values,
|
|
164
|
+
}),
|
|
165
|
+
{} as Record<string, boolean>,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
169
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (confirmed && isDevelopment()) {
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [false, checkboxes];
|
|
179
|
+
}
|
|
51
180
|
|
|
52
|
-
return
|
|
181
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
53
182
|
}
|
|
54
183
|
|
|
55
|
-
public async
|
|
56
|
-
public async
|
|
57
|
-
public async
|
|
58
|
-
|
|
184
|
+
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
185
|
+
public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
|
|
186
|
+
public async prompt(
|
|
187
|
+
messageOrTitle: string,
|
|
188
|
+
messageOrOptions?: string | PromptOptions,
|
|
189
|
+
options?: PromptOptions,
|
|
190
|
+
): Promise<string | null> {
|
|
191
|
+
const trim = options?.trim ?? true;
|
|
192
|
+
const getProperties = (): PromptModalProps => {
|
|
193
|
+
if (typeof messageOrOptions !== 'string') {
|
|
194
|
+
return {
|
|
195
|
+
message: messageOrTitle,
|
|
196
|
+
...(messageOrOptions ?? {}),
|
|
197
|
+
} as PromptModalProps;
|
|
198
|
+
}
|
|
59
199
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
200
|
+
return {
|
|
201
|
+
title: messageOrTitle,
|
|
202
|
+
message: messageOrOptions,
|
|
203
|
+
...(options ?? {}),
|
|
204
|
+
} as PromptModalProps;
|
|
205
|
+
};
|
|
63
206
|
|
|
64
|
-
await this.
|
|
207
|
+
const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
|
|
208
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
65
209
|
|
|
66
|
-
return result;
|
|
210
|
+
return result ?? null;
|
|
67
211
|
}
|
|
68
212
|
|
|
69
|
-
public
|
|
70
|
-
|
|
213
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
214
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
215
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
216
|
+
public async loading<T>(
|
|
217
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
218
|
+
operation?: Promise<T> | (() => T),
|
|
219
|
+
): Promise<T> {
|
|
220
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
221
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
222
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
223
|
+
return {
|
|
224
|
+
props: { message: operationOrMessageOrOptions },
|
|
225
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
230
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
props: operationOrMessageOrOptions,
|
|
235
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const { operationPromise, props } = processArgs();
|
|
240
|
+
const modal = await this.modal(this.requireComponent('loading-modal'), props);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const result = await operationPromise;
|
|
244
|
+
|
|
245
|
+
await after({ ms: 500 });
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
} finally {
|
|
249
|
+
await this.closeModal(modal.id);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
254
|
+
const { component, ...otherOptions } = options;
|
|
255
|
+
const toast: UIToast = {
|
|
256
|
+
id: uuid(),
|
|
257
|
+
properties: { message, ...otherOptions },
|
|
258
|
+
component: markRaw(component ?? this.requireComponent('toast')),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
71
262
|
}
|
|
72
263
|
|
|
73
|
-
public
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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>>>;
|
|
269
|
+
|
|
270
|
+
public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
|
|
77
271
|
const id = uuid();
|
|
78
|
-
const callbacks: Partial<ModalCallbacks<ModalResult<
|
|
79
|
-
const modal:
|
|
272
|
+
const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
|
|
273
|
+
const modal: UIModal<ModalResult<T>> = {
|
|
80
274
|
id,
|
|
81
|
-
|
|
275
|
+
closing: false,
|
|
276
|
+
properties: props ?? {},
|
|
82
277
|
component: markRaw(component),
|
|
83
278
|
beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
|
|
84
|
-
afterClose: new Promise((resolve) => (callbacks.
|
|
279
|
+
afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
|
|
85
280
|
};
|
|
86
|
-
const activeModal = this.modals.at(-1);
|
|
87
281
|
const modals = this.modals.concat(modal);
|
|
88
282
|
|
|
89
283
|
this.modalCallbacks[modal.id] = callbacks;
|
|
@@ -91,61 +285,115 @@ export class UIService extends Service {
|
|
|
91
285
|
this.setState({ modals });
|
|
92
286
|
|
|
93
287
|
await nextTick();
|
|
94
|
-
await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
|
|
95
|
-
await Promise.all([
|
|
96
|
-
activeModal || Events.emit('show-overlays-backdrop'),
|
|
97
|
-
Events.emit('show-modal', { id: modal.id }),
|
|
98
|
-
]);
|
|
99
288
|
|
|
100
289
|
return modal;
|
|
101
290
|
}
|
|
102
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
|
+
|
|
103
308
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
309
|
+
if (!App.isMounted()) {
|
|
310
|
+
await this.removeModal(id, result);
|
|
311
|
+
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
104
315
|
await Events.emit('close-modal', { id, result });
|
|
105
316
|
}
|
|
106
317
|
|
|
107
|
-
|
|
108
|
-
|
|
318
|
+
public async closeAllModals(): Promise<void> {
|
|
319
|
+
while (this.modals.length > 0) {
|
|
320
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
109
323
|
|
|
324
|
+
protected override async boot(): Promise<void> {
|
|
110
325
|
this.watchModalEvents();
|
|
326
|
+
this.watchMountedEvent();
|
|
327
|
+
this.watchViewportBreakpoints();
|
|
111
328
|
}
|
|
112
329
|
|
|
113
|
-
private
|
|
114
|
-
|
|
330
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
331
|
+
this.setState(
|
|
332
|
+
'modals',
|
|
333
|
+
this.modals.filter((m) => m.id !== id),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
this.modalCallbacks[id]?.hasClosed?.(result);
|
|
337
|
+
|
|
338
|
+
delete this.modalCallbacks[id];
|
|
115
339
|
}
|
|
116
340
|
|
|
117
341
|
private watchModalEvents(): void {
|
|
118
|
-
Events.on('modal-will-close', ({ modal, result }) => {
|
|
119
|
-
this.
|
|
342
|
+
Events.on('modal-will-close', ({ modal: { id }, result }) => {
|
|
343
|
+
const modal = this.modals.find((_modal) => id === _modal.id);
|
|
120
344
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
345
|
+
if (modal) {
|
|
346
|
+
modal.closing = true;
|
|
123
347
|
}
|
|
348
|
+
|
|
349
|
+
this.modalCallbacks[id]?.willClose?.(result);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
|
|
353
|
+
await this.removeModal(id, result);
|
|
124
354
|
});
|
|
355
|
+
}
|
|
125
356
|
|
|
126
|
-
|
|
127
|
-
|
|
357
|
+
private watchMountedEvent(): void {
|
|
358
|
+
Events.once('application-mounted', async () => {
|
|
359
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const splash = globalThis.document.getElementById('splash');
|
|
128
364
|
|
|
129
|
-
|
|
365
|
+
if (!splash) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
130
368
|
|
|
131
|
-
|
|
369
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
370
|
+
splash.style.opacity = '0';
|
|
132
371
|
|
|
133
|
-
|
|
372
|
+
await after({ ms: 600 });
|
|
373
|
+
}
|
|
134
374
|
|
|
135
|
-
|
|
375
|
+
splash.remove();
|
|
136
376
|
});
|
|
137
377
|
}
|
|
138
378
|
|
|
379
|
+
private watchViewportBreakpoints(): void {
|
|
380
|
+
if (!globalThis.matchMedia) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
385
|
+
|
|
386
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
387
|
+
}
|
|
388
|
+
|
|
139
389
|
}
|
|
140
390
|
|
|
141
|
-
export default facade(
|
|
391
|
+
export default facade(UIService);
|
|
142
392
|
|
|
143
|
-
declare module '
|
|
393
|
+
declare module '@aerogel/core/services/Events' {
|
|
144
394
|
export interface EventsPayload {
|
|
145
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
146
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
147
395
|
'close-modal': { id: string; result?: unknown };
|
|
148
|
-
'
|
|
149
|
-
'
|
|
396
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
397
|
+
'modal-has-closed': { modal: UIModal; result?: unknown };
|
|
150
398
|
}
|
|
151
399
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,43 +1,52 @@
|
|
|
1
|
+
import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
|
|
2
|
+
import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
|
|
3
|
+
import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
|
|
4
|
+
import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
|
|
5
|
+
import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
|
|
6
|
+
import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
|
|
7
|
+
import Toast from '@aerogel/core/components/ui/Toast.vue';
|
|
8
|
+
import { bootServices } from '@aerogel/core/services';
|
|
9
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
10
|
+
|
|
11
|
+
import UI from './UI';
|
|
12
|
+
import type { UIComponents } from './UI';
|
|
1
13
|
import type { Component } from 'vue';
|
|
2
14
|
|
|
3
|
-
import { bootServices } from '@/services';
|
|
4
|
-
import { definePlugin } from '@/plugins';
|
|
5
|
-
|
|
6
|
-
import UI, { UIComponents } from './UI';
|
|
7
|
-
import AGAlertModal from '../components/modals/AGAlertModal.vue';
|
|
8
|
-
import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
|
|
9
|
-
import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
|
|
10
|
-
import type { UIComponent } from './UI';
|
|
11
|
-
|
|
12
|
-
export { UI, UIComponents, UIComponent };
|
|
13
|
-
|
|
14
15
|
const services = { $ui: UI };
|
|
15
16
|
|
|
17
|
+
export * from './UI';
|
|
18
|
+
export * from './utils';
|
|
19
|
+
export { default as UI } from './UI';
|
|
20
|
+
|
|
16
21
|
export type UIServices = typeof services;
|
|
17
22
|
|
|
18
23
|
export default definePlugin({
|
|
19
24
|
async install(app, options) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
24
34
|
};
|
|
25
35
|
|
|
26
|
-
Object.entries({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}).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
|
+
}
|
|
30
39
|
|
|
31
40
|
await bootServices(app, services);
|
|
32
41
|
},
|
|
33
42
|
});
|
|
34
43
|
|
|
35
|
-
declare module '
|
|
36
|
-
interface AerogelOptions {
|
|
37
|
-
components?: Partial<
|
|
44
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
45
|
+
export interface AerogelOptions {
|
|
46
|
+
components?: Partial<Partial<UIComponents>>;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
declare module '
|
|
50
|
+
declare module '@aerogel/core/services' {
|
|
42
51
|
export interface Services extends UIServices {}
|
|
43
52
|
}
|
package/src/ui/utils.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const MOBILE_BREAKPOINT = 768;
|
|
2
|
+
|
|
3
|
+
export const Layouts = {
|
|
4
|
+
Mobile: 'mobile',
|
|
5
|
+
Desktop: 'desktop',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type Layout = (typeof Layouts)[keyof typeof Layouts];
|
|
9
|
+
|
|
10
|
+
export function getCurrentLayout(): Layout {
|
|
11
|
+
if (globalThis.innerWidth > MOBILE_BREAKPOINT) {
|
|
12
|
+
return Layouts.Desktop;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Layouts.Mobile;
|
|
16
|
+
}
|