@aerogel/core 0.0.0-next.669133ce9c5f0eeaff1b63c4e2962e90129cfc1a → 0.0.0-next.71f28064caa2ea968f0e99396b672de218176260
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.cjs.js +1 -1
- package/dist/aerogel-core.cjs.js.map +1 -1
- package/dist/aerogel-core.d.ts +108 -19
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/forms/AGInput.vue +2 -0
- package/src/components/headless/forms/AGHeadlessInput.ts +4 -1
- package/src/components/headless/forms/AGHeadlessInput.vue +2 -2
- package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
- package/src/components/headless/forms/AGHeadlessInputInput.vue +9 -1
- package/src/components/headless/forms/AGHeadlessInputTextArea.vue +40 -0
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +2 -0
- package/src/components/lib/AGErrorMessage.vue +2 -2
- package/src/components/lib/AGMarkdown.vue +7 -2
- package/src/directives/index.ts +2 -0
- package/src/directives/measure.ts +11 -2
- package/src/errors/Errors.ts +9 -16
- package/src/errors/index.ts +1 -0
- package/src/errors/utils.ts +19 -0
- package/src/forms/Form.ts +22 -2
- package/src/lang/Lang.ts +10 -22
- package/src/main.ts +1 -0
- package/src/services/Events.ts +12 -0
- package/src/testing/index.ts +11 -5
- package/src/ui/UI.ts +5 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aerogel/core",
|
|
3
3
|
"description": "The Lightest Solid",
|
|
4
|
-
"version": "0.0.0-next.
|
|
4
|
+
"version": "0.0.0-next.71f28064caa2ea968f0e99396b672de218176260",
|
|
5
5
|
"main": "dist/aerogel-core.cjs.js",
|
|
6
6
|
"module": "dist/aerogel-core.esm.js",
|
|
7
7
|
"types": "dist/aerogel-core.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
'ring-1 ring-red-500': $input?.errors,
|
|
14
14
|
}"
|
|
15
15
|
/>
|
|
16
|
+
<AGHeadlessInputDescription />
|
|
16
17
|
<div class="absolute bottom-0 left-0 translate-y-full">
|
|
17
18
|
<AGHeadlessInputError class="mt-1 text-sm text-red-500" />
|
|
18
19
|
</div>
|
|
@@ -26,6 +27,7 @@ import { useInputProps } from '@/components/headless/forms/AGHeadlessInput';
|
|
|
26
27
|
import type { IAGHeadlessInput } from '@/components/headless/forms/AGHeadlessInput';
|
|
27
28
|
|
|
28
29
|
import AGHeadlessInput from '../headless/forms/AGHeadlessInput.vue';
|
|
30
|
+
import AGHeadlessInputDescription from '../headless/forms/AGHeadlessInputDescription.vue';
|
|
29
31
|
import AGHeadlessInputError from '../headless/forms/AGHeadlessInputError.vue';
|
|
30
32
|
import AGHeadlessInputInput from '../headless/forms/AGHeadlessInputInput.vue';
|
|
31
33
|
import AGHeadlessInputLabel from '../headless/forms/AGHeadlessInputLabel.vue';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ComputedRef, DeepReadonly, ExtractPropTypes, Ref } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { stringProp } from '@/utils';
|
|
3
|
+
import { mixedProp, stringProp } from '@/utils';
|
|
4
4
|
import { extractComponentProps } from '@/components/utils';
|
|
5
5
|
|
|
6
6
|
export interface IAGHeadlessInput {
|
|
7
7
|
id: string;
|
|
8
8
|
name: ComputedRef<string | null>;
|
|
9
9
|
label: ComputedRef<string | null>;
|
|
10
|
+
description: ComputedRef<string | boolean | null>;
|
|
10
11
|
value: ComputedRef<string | number | boolean | null>;
|
|
11
12
|
errors: DeepReadonly<Ref<string[] | null>>;
|
|
12
13
|
update(value: string | number | boolean | null): void;
|
|
@@ -15,6 +16,8 @@ export interface IAGHeadlessInput {
|
|
|
15
16
|
export const inputProps = {
|
|
16
17
|
name: stringProp(),
|
|
17
18
|
label: stringProp(),
|
|
19
|
+
description: stringProp(),
|
|
20
|
+
modelValue: mixedProp<string | number | boolean>([String, Number, Boolean]),
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
export function useInputProps(): typeof inputProps {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { computed, inject, provide, readonly } from 'vue';
|
|
10
10
|
import { uuid } from '@noeldemartin/utils';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { stringProp } from '@/utils/vue';
|
|
13
13
|
import type Form from '@/forms/Form';
|
|
14
14
|
|
|
15
15
|
import { useInputProps } from './AGHeadlessInput';
|
|
@@ -18,7 +18,6 @@ import type { IAGHeadlessInput } from './AGHeadlessInput';
|
|
|
18
18
|
const emit = defineEmits(['update:modelValue']);
|
|
19
19
|
const props = defineProps({
|
|
20
20
|
as: stringProp('div'),
|
|
21
|
-
modelValue: mixedProp<string | number | boolean>([String, Number, Boolean]),
|
|
22
21
|
...useInputProps(),
|
|
23
22
|
});
|
|
24
23
|
const errors = computed(() => {
|
|
@@ -33,6 +32,7 @@ const api: IAGHeadlessInput = {
|
|
|
33
32
|
id: `input-${uuid()}`,
|
|
34
33
|
name: computed(() => props.name),
|
|
35
34
|
label: computed(() => props.label),
|
|
35
|
+
description: computed(() => props.description),
|
|
36
36
|
value: computed(() => {
|
|
37
37
|
if (form && props.name) {
|
|
38
38
|
return form.getFieldValue(props.name) as string | number | boolean | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot :id="`${input.id}-description`">
|
|
3
|
+
<AGMarkdown
|
|
4
|
+
v-if="show"
|
|
5
|
+
v-bind="$attrs"
|
|
6
|
+
:id="`${input.id}-description`"
|
|
7
|
+
:text="text"
|
|
8
|
+
/>
|
|
9
|
+
</slot>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed } from 'vue';
|
|
14
|
+
|
|
15
|
+
import { injectReactiveOrFail } from '@/utils/vue';
|
|
16
|
+
|
|
17
|
+
import AGMarkdown from '../../lib/AGMarkdown.vue';
|
|
18
|
+
import type { IAGHeadlessInput } from './AGHeadlessInput';
|
|
19
|
+
|
|
20
|
+
defineOptions({ inheritAttrs: false });
|
|
21
|
+
|
|
22
|
+
const input = injectReactiveOrFail<IAGHeadlessInput>(
|
|
23
|
+
'input',
|
|
24
|
+
'<AGHeadlessInputDescription> must be a child of a <AGHeadlessInput>',
|
|
25
|
+
);
|
|
26
|
+
const text = computed(() => (typeof input.description === 'string' ? input.description : ''));
|
|
27
|
+
const show = computed(() => !!input.description);
|
|
28
|
+
</script>
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
<input
|
|
3
3
|
:id="input.id"
|
|
4
4
|
ref="$input"
|
|
5
|
+
:name="name"
|
|
5
6
|
:type="type"
|
|
6
7
|
:value="value"
|
|
7
8
|
:aria-invalid="input.errors ? 'true' : 'false'"
|
|
8
|
-
:aria-describedby="
|
|
9
|
+
:aria-describedby="
|
|
10
|
+
input.errors ? `${input.id}-error` : input.description ? `${input.id}-description` : undefined
|
|
11
|
+
"
|
|
9
12
|
:checked="checked"
|
|
10
13
|
@input="update"
|
|
11
14
|
>
|
|
@@ -17,6 +20,8 @@ import { computed, ref } from 'vue';
|
|
|
17
20
|
import { injectReactiveOrFail, stringProp } from '@/utils';
|
|
18
21
|
import type { IAGHeadlessInput } from '@/components/headless/forms/AGHeadlessInput';
|
|
19
22
|
|
|
23
|
+
import { onFormFocus } from './composition';
|
|
24
|
+
|
|
20
25
|
const props = defineProps({
|
|
21
26
|
type: stringProp('text'),
|
|
22
27
|
});
|
|
@@ -26,6 +31,7 @@ const input = injectReactiveOrFail<IAGHeadlessInput>(
|
|
|
26
31
|
'input',
|
|
27
32
|
'<AGHeadlessInputInput> must be a child of a <AGHeadlessInput>',
|
|
28
33
|
);
|
|
34
|
+
const name = computed(() => input.name ?? undefined);
|
|
29
35
|
const value = computed(() => input.value);
|
|
30
36
|
const checked = computed(() => {
|
|
31
37
|
if (props.type !== 'checkbox') {
|
|
@@ -42,4 +48,6 @@ function update() {
|
|
|
42
48
|
|
|
43
49
|
input.update(props.type === 'checkbox' ? $input.value.checked : $input.value.value);
|
|
44
50
|
}
|
|
51
|
+
|
|
52
|
+
onFormFocus(input, () => $input.value?.focus());
|
|
45
53
|
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<textarea
|
|
3
|
+
:id="input.id"
|
|
4
|
+
ref="$textArea"
|
|
5
|
+
:name="name"
|
|
6
|
+
:value="value"
|
|
7
|
+
:aria-invalid="input.errors ? 'true' : 'false'"
|
|
8
|
+
:aria-describedby="
|
|
9
|
+
input.errors ? `${input.id}-error` : input.description ? `${input.id}-description` : undefined
|
|
10
|
+
"
|
|
11
|
+
@input="update"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { computed, ref } from 'vue';
|
|
17
|
+
|
|
18
|
+
import { injectReactiveOrFail } from '@/utils';
|
|
19
|
+
import type { IAGHeadlessInput } from '@/components/headless/forms/AGHeadlessInput';
|
|
20
|
+
|
|
21
|
+
import { onFormFocus } from './composition';
|
|
22
|
+
|
|
23
|
+
const $textArea = ref<HTMLTextAreaElement>();
|
|
24
|
+
const input = injectReactiveOrFail<IAGHeadlessInput>(
|
|
25
|
+
'input',
|
|
26
|
+
'<AGHeadlessInputTextArea> must be a child of a <AGHeadlessInput>',
|
|
27
|
+
);
|
|
28
|
+
const name = computed(() => input.name ?? undefined);
|
|
29
|
+
const value = computed(() => input.value as string);
|
|
30
|
+
|
|
31
|
+
function update() {
|
|
32
|
+
if (!$textArea.value) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
input.update($textArea.value.value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onFormFocus(input, () => $textArea.value?.focus());
|
|
40
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { inject, onUnmounted } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type Form from '@/forms/Form';
|
|
4
|
+
|
|
5
|
+
export function onFormFocus(input: { name: string | null }, listener: () => unknown): void {
|
|
6
|
+
const form = inject<Form | null>('form', null);
|
|
7
|
+
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
8
|
+
|
|
9
|
+
onUnmounted(() => stop?.());
|
|
10
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
export * from './composition';
|
|
1
2
|
export * from './AGHeadlessInput';
|
|
2
3
|
export * from './AGHeadlessSelect';
|
|
3
4
|
export * from './AGHeadlessSelectOption';
|
|
4
5
|
export { default as AGHeadlessButton } from './AGHeadlessButton.vue';
|
|
5
6
|
export { default as AGHeadlessInput } from './AGHeadlessInput.vue';
|
|
7
|
+
export { default as AGHeadlessInputDescription } from './AGHeadlessInputDescription.vue';
|
|
6
8
|
export { default as AGHeadlessInputError } from './AGHeadlessInputError.vue';
|
|
7
9
|
export { default as AGHeadlessInputInput } from './AGHeadlessInputInput.vue';
|
|
8
10
|
export { default as AGHeadlessInputLabel } from './AGHeadlessInputLabel.vue';
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
6
|
import { computed } from 'vue';
|
|
7
7
|
|
|
8
|
-
import Errors from '@/errors/Errors';
|
|
9
8
|
import { requiredObjectProp } from '@/utils/vue';
|
|
9
|
+
import { getErrorMessage } from '@/errors/utils';
|
|
10
10
|
import type { ErrorSource } from '@/errors/Errors.state';
|
|
11
11
|
|
|
12
12
|
import AGMarkdown from './AGMarkdown.vue';
|
|
13
13
|
|
|
14
14
|
const props = defineProps({ error: requiredObjectProp<ErrorSource>() });
|
|
15
|
-
const message = computed(() =>
|
|
15
|
+
const message = computed(() => getErrorMessage(props.error));
|
|
16
16
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
|
-
import { computed, h } from 'vue';
|
|
6
|
+
import { computed, h, useAttrs } from 'vue';
|
|
7
7
|
|
|
8
8
|
import { renderMarkdown } from '@/utils/markdown';
|
|
9
9
|
import { booleanProp, objectProp, stringProp } from '@/utils/vue';
|
|
@@ -17,6 +17,7 @@ const props = defineProps({
|
|
|
17
17
|
text: stringProp(),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
const attrs = useAttrs();
|
|
20
21
|
const markdown = computed(() => props.text ?? (props.langKey && translate(props.langKey, props.langParams ?? {})));
|
|
21
22
|
const html = computed(() => {
|
|
22
23
|
if (!markdown.value) {
|
|
@@ -32,5 +33,9 @@ const html = computed(() => {
|
|
|
32
33
|
return renderedHtml;
|
|
33
34
|
});
|
|
34
35
|
const root = () =>
|
|
35
|
-
h(props.as ?? (props.inline ? 'span' : 'div'), {
|
|
36
|
+
h(props.as ?? (props.inline ? 'span' : 'div'), {
|
|
37
|
+
class: props.inline ? '' : 'prose',
|
|
38
|
+
innerHTML: html.value,
|
|
39
|
+
...attrs,
|
|
40
|
+
});
|
|
36
41
|
</script>
|
package/src/directives/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { defineDirective } from '@/utils/vue';
|
|
2
2
|
|
|
3
|
+
export interface ElementSize {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type MeasureDirectiveListener = (size: ElementSize) => unknown;
|
|
9
|
+
|
|
3
10
|
export default defineDirective({
|
|
4
|
-
mounted(element: HTMLElement, { value }
|
|
11
|
+
mounted(element: HTMLElement, { value }) {
|
|
12
|
+
const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
|
|
5
13
|
const sizes = element.getBoundingClientRect();
|
|
6
14
|
|
|
15
|
+
// TODO guard with modifiers.css once typed properly
|
|
7
16
|
element.style.setProperty('--width', `${sizes.width}px`);
|
|
8
17
|
element.style.setProperty('--height', `${sizes.height}px`);
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
listener?.({ width: sizes.width, height: sizes.height });
|
|
11
20
|
},
|
|
12
21
|
});
|
package/src/errors/Errors.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { translateWithDefault } from '@/lang/utils';
|
|
|
7
7
|
|
|
8
8
|
import Service from './Errors.state';
|
|
9
9
|
import { Colors } from '@/components/constants';
|
|
10
|
+
import { Events } from '@/services';
|
|
10
11
|
import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
|
|
11
12
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
12
13
|
import type { ModalComponent } from '@/ui/UI.state';
|
|
@@ -39,6 +40,8 @@ export class ErrorsService extends Service {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
43
|
+
await Events.emit('error', { error, message });
|
|
44
|
+
|
|
42
45
|
if (App.testing) {
|
|
43
46
|
throw error;
|
|
44
47
|
}
|
|
@@ -114,22 +117,6 @@ export class ErrorsService extends Service {
|
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
public getErrorMessage(error: ErrorSource): string {
|
|
118
|
-
if (typeof error === 'string') {
|
|
119
|
-
return error;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (error instanceof Error || error instanceof JSError) {
|
|
123
|
-
return error.message;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (isObject(error)) {
|
|
127
|
-
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
120
|
private logError(error: unknown): void {
|
|
134
121
|
// eslint-disable-next-line no-console
|
|
135
122
|
console.error(error);
|
|
@@ -190,3 +177,9 @@ export class ErrorsService extends Service {
|
|
|
190
177
|
}
|
|
191
178
|
|
|
192
179
|
export default facade(ErrorsService);
|
|
180
|
+
|
|
181
|
+
declare module '@/services/Events' {
|
|
182
|
+
export interface EventsPayload {
|
|
183
|
+
error: { error: ErrorSource; message?: string };
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { definePlugin } from '@/plugins';
|
|
|
6
6
|
import Errors from './Errors';
|
|
7
7
|
import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
8
|
|
|
9
|
+
export * from './utils';
|
|
9
10
|
export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
|
|
10
11
|
|
|
11
12
|
const services = { $errors: Errors };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { translateWithDefault } from '@/lang/utils';
|
|
3
|
+
import type { ErrorSource } from './Errors.state';
|
|
4
|
+
|
|
5
|
+
export function getErrorMessage(error: ErrorSource): string {
|
|
6
|
+
if (typeof error === 'string') {
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
11
|
+
return error.message;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isObject(error)) {
|
|
15
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
19
|
+
}
|
package/src/forms/Form.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MagicObject } from '@noeldemartin/utils';
|
|
2
|
-
import { computed, reactive, readonly, ref } from 'vue';
|
|
1
|
+
import { MagicObject, arrayRemove } from '@noeldemartin/utils';
|
|
2
|
+
import { computed, nextTick, reactive, readonly, ref } from 'vue';
|
|
3
3
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
4
4
|
import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
|
|
5
5
|
|
|
@@ -44,6 +44,8 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
|
|
|
44
44
|
|
|
45
45
|
const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
|
|
46
46
|
|
|
47
|
+
export type FormListener = (input: string) => unknown;
|
|
48
|
+
|
|
47
49
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
48
50
|
|
|
49
51
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
@@ -52,6 +54,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
52
54
|
private _data: FormData<Fields>;
|
|
53
55
|
private _submitted: Ref<boolean>;
|
|
54
56
|
private _errors: FormErrors<Fields>;
|
|
57
|
+
private _listeners: Record<string, FormListener[]> = {};
|
|
55
58
|
|
|
56
59
|
constructor(fields: Fields) {
|
|
57
60
|
super();
|
|
@@ -114,6 +117,23 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
114
117
|
return this.validate();
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
public on(event: string, listener: FormListener): () => void {
|
|
121
|
+
this._listeners[event] ??= [];
|
|
122
|
+
this._listeners[event]?.push(listener);
|
|
123
|
+
|
|
124
|
+
return () => this.off(event, listener);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public off(event: string, listener: FormListener): void {
|
|
128
|
+
arrayRemove(this._listeners[event] ?? [], listener);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async focus(input: string): Promise<void> {
|
|
132
|
+
await nextTick();
|
|
133
|
+
|
|
134
|
+
this._listeners['focus']?.forEach((listener) => listener(input));
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
protected __get(property: string): unknown {
|
|
118
138
|
if (!(property in this._fields)) {
|
|
119
139
|
return super.__get(property);
|
package/src/lang/Lang.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { facade
|
|
1
|
+
import { facade } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
import App from '@/services/App';
|
|
4
4
|
import Service from '@/services/Service';
|
|
5
5
|
|
|
6
6
|
export interface LangProvider {
|
|
7
7
|
translate(key: string, parameters?: Record<string, unknown>): string;
|
|
8
|
+
translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown>): string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export class LangService extends Service {
|
|
@@ -21,6 +22,12 @@ export class LangService extends Service {
|
|
|
21
22
|
|
|
22
23
|
return key;
|
|
23
24
|
},
|
|
25
|
+
translateWithDefault: (_, defaultMessage) => {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
App.development && console.warn('Lang provider is missing');
|
|
28
|
+
|
|
29
|
+
return defaultMessage;
|
|
30
|
+
},
|
|
24
31
|
};
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -32,27 +39,8 @@ export class LangService extends Service {
|
|
|
32
39
|
return this.provider.translate(key, parameters) ?? key;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
public translateWithDefault(key: string, defaultMessage: string): string
|
|
36
|
-
|
|
37
|
-
public translateWithDefault(
|
|
38
|
-
key: string,
|
|
39
|
-
defaultMessageOrParameters?: string | Record<string, unknown>,
|
|
40
|
-
defaultMessage?: string,
|
|
41
|
-
): string {
|
|
42
|
-
defaultMessage ??= defaultMessageOrParameters as string;
|
|
43
|
-
|
|
44
|
-
const parameters = typeof defaultMessageOrParameters === 'string' ? {} : defaultMessageOrParameters ?? {};
|
|
45
|
-
const message = this.provider.translate(key, parameters) ?? key;
|
|
46
|
-
|
|
47
|
-
if (message === key) {
|
|
48
|
-
return Object.entries(parameters).reduce(
|
|
49
|
-
(renderedMessage, [name, value]) =>
|
|
50
|
-
renderedMessage.replace(new RegExp(`\\{\\s*${name}\\s*\\}`, 'g'), toString(value)),
|
|
51
|
-
defaultMessage,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return message;
|
|
42
|
+
public translateWithDefault(key: string, defaultMessage: string, parameters: Record<string, unknown> = {}): string {
|
|
43
|
+
return this.provider.translateWithDefault(key, defaultMessage, parameters);
|
|
56
44
|
}
|
|
57
45
|
|
|
58
46
|
}
|
package/src/main.ts
CHANGED
package/src/services/Events.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface EventsPayload {}
|
|
|
6
6
|
export interface EventListenerOptions {
|
|
7
7
|
priority: number;
|
|
8
8
|
}
|
|
9
|
+
export type AerogelGlobalEvents = Partial<{ [Event in EventWithoutPayload]: () => unknown }> &
|
|
10
|
+
Partial<{ [Event in EventWithPayload]: EventListener<EventsPayload[Event]> }>;
|
|
9
11
|
|
|
10
12
|
export type EventListener<T = unknown> = (payload: T) => unknown;
|
|
11
13
|
export type UnknownEvent<T> = T extends keyof EventsPayload ? never : T;
|
|
@@ -28,6 +30,11 @@ export class EventsService extends Service {
|
|
|
28
30
|
|
|
29
31
|
private listeners: Record<string, { priorities: number[]; handlers: Record<number, EventListener[]> }> = {};
|
|
30
32
|
|
|
33
|
+
protected async boot(): Promise<void> {
|
|
34
|
+
Object.entries(globalThis.__aerogelEvents__ ?? {}).forEach(([event, listener]) =>
|
|
35
|
+
this.on(event as string, listener as EventListener));
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
public emit<Event extends EventWithoutPayload>(event: Event): Promise<void>;
|
|
32
39
|
public emit<Event extends EventWithPayload>(event: Event, payload: EventsPayload[Event]): Promise<void>;
|
|
33
40
|
public emit<Event extends string>(event: UnknownEvent<Event>, payload?: unknown): Promise<void>;
|
|
@@ -141,3 +148,8 @@ export class EventsService extends Service {
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
export default facade(EventsService);
|
|
151
|
+
|
|
152
|
+
declare global {
|
|
153
|
+
// eslint-disable-next-line no-var
|
|
154
|
+
var __aerogelEvents__: AerogelGlobalEvents | undefined;
|
|
155
|
+
}
|
package/src/testing/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import Events from '@/services/Events';
|
|
1
4
|
import { definePlugin } from '@/plugins';
|
|
2
5
|
|
|
3
|
-
export interface AerogelTestingRuntime {
|
|
6
|
+
export interface AerogelTestingRuntime {
|
|
7
|
+
on: (typeof Events)['on'];
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
export default definePlugin({
|
|
6
11
|
async install() {
|
|
@@ -8,12 +13,13 @@ export default definePlugin({
|
|
|
8
13
|
return;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
globalThis.testingRuntime = {
|
|
17
|
+
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
18
|
+
};
|
|
12
19
|
},
|
|
13
20
|
});
|
|
14
21
|
|
|
15
22
|
declare global {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
23
|
+
// eslint-disable-next-line no-var
|
|
24
|
+
var testingRuntime: AerogelTestingRuntime | undefined;
|
|
19
25
|
}
|
package/src/ui/UI.ts
CHANGED
|
@@ -167,7 +167,7 @@ export class UIService extends Service {
|
|
|
167
167
|
const snackbar: Snackbar = {
|
|
168
168
|
id: uuid(),
|
|
169
169
|
properties: { message, ...options },
|
|
170
|
-
component: options.component ??
|
|
170
|
+
component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
|
|
171
171
|
};
|
|
172
172
|
|
|
173
173
|
this.setState('snackbars', this.snackbars.concat(snackbar));
|
|
@@ -274,10 +274,12 @@ export default facade(UIService);
|
|
|
274
274
|
|
|
275
275
|
declare module '@/services/Events' {
|
|
276
276
|
export interface EventsPayload {
|
|
277
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
278
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
279
277
|
'close-modal': { id: string; result?: unknown };
|
|
280
278
|
'hide-modal': { id: string };
|
|
279
|
+
'hide-overlays-backdrop': void;
|
|
280
|
+
'modal-closed': { modal: Modal; result?: unknown };
|
|
281
|
+
'modal-will-close': { modal: Modal; result?: unknown };
|
|
281
282
|
'show-modal': { id: string };
|
|
283
|
+
'show-overlays-backdrop': void;
|
|
282
284
|
}
|
|
283
285
|
}
|