@aerogel/core 0.0.0-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832 → 0.0.0-next.9f9564ab9f8da05f60d7868db361edbc5601ee39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aerogel/core",
3
3
  "description": "The Lightest Solid",
4
- "version": "0.0.0-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832",
4
+ "version": "0.0.0-next.9f9564ab9f8da05f60d7868db361edbc5601ee39",
5
5
  "main": "dist/aerogel-core.cjs.js",
6
6
  "module": "dist/aerogel-core.esm.js",
7
7
  "types": "dist/aerogel-core.d.ts",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div aria-live="assertive" class="z-60 pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:p-6">
2
+ <div aria-live="assertive" class="pointer-events-none fixed inset-0 z-50 flex items-end px-4 py-6 sm:p-6">
3
3
  <div class="flex w-full flex-col items-end space-y-4">
4
4
  <component
5
5
  :is="snackbar.component"
@@ -1,26 +1,25 @@
1
1
  <template>
2
- <form @submit.prevent="submit">
2
+ <form @submit.prevent="form?.submit()">
3
3
  <slot />
4
4
  </form>
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
- import { provide } from 'vue';
8
+ import { provide, watchEffect } from 'vue';
9
9
 
10
10
  import { objectProp } from '@/utils/vue';
11
11
  import type Form from '@/forms/Form';
12
12
 
13
+ let offSubmit: (() => void) | undefined;
13
14
  const props = defineProps({ form: objectProp<Form>() });
14
-
15
15
  const emit = defineEmits<{ submit: [] }>();
16
16
 
17
- provide('form', props.form);
17
+ watchEffect((onCleanup) => {
18
+ offSubmit?.();
19
+ offSubmit = props.form?.on('submit', () => emit('submit'));
18
20
 
19
- function submit() {
20
- if (props.form && !props.form.submit()) {
21
- return;
22
- }
21
+ onCleanup(() => offSubmit?.());
22
+ });
23
23
 
24
- emit('submit');
25
- }
24
+ provide('form', props.form);
26
25
  </script>
@@ -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,5 +1,5 @@
1
1
  <template>
2
- <component :is="component.tag" v-bind="component.props">
2
+ <component :is="component.as" v-bind="component.props">
3
3
  <slot />
4
4
  </component>
5
5
  </template>
@@ -11,6 +11,7 @@ import { objectWithoutEmpty } from '@noeldemartin/utils';
11
11
  import { booleanProp, objectProp, stringProp } from '@/utils/vue';
12
12
 
13
13
  const props = defineProps({
14
+ as: objectProp(),
14
15
  href: stringProp(),
15
16
  url: stringProp(),
16
17
  route: stringProp(),
@@ -20,9 +21,13 @@ const props = defineProps({
20
21
  });
21
22
 
22
23
  const component = computed(() => {
24
+ if (props.as) {
25
+ return { as: props.as, props: {} };
26
+ }
27
+
23
28
  if (props.route) {
24
29
  return {
25
- tag: 'router-link',
30
+ as: 'router-link',
26
31
  props: {
27
32
  to: objectWithoutEmpty({
28
33
  name: props.route,
@@ -35,7 +40,7 @@ const component = computed(() => {
35
40
 
36
41
  if (props.href || props.url) {
37
42
  return {
38
- tag: 'a',
43
+ as: 'a',
39
44
  props: {
40
45
  target: '_blank',
41
46
  href: props.href || props.url,
@@ -44,7 +49,7 @@ const component = computed(() => {
44
49
  }
45
50
 
46
51
  return {
47
- tag: 'button',
52
+ as: 'button',
48
53
  props: { type: props.submit ? 'submit' : 'button' },
49
54
  };
50
55
  });
@@ -1,20 +1,24 @@
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
+ import type { FormFieldValue } from '@/forms/Form';
5
6
 
6
7
  export interface IAGHeadlessInput {
7
8
  id: string;
8
9
  name: ComputedRef<string | null>;
9
10
  label: ComputedRef<string | null>;
10
- value: ComputedRef<string | number | boolean | null>;
11
+ description: ComputedRef<string | boolean | null>;
12
+ value: ComputedRef<FormFieldValue | null>;
11
13
  errors: DeepReadonly<Ref<string[] | null>>;
12
- update(value: string | number | boolean | null): void;
14
+ update(value: FormFieldValue | null): void;
13
15
  }
14
16
 
15
17
  export const inputProps = {
16
18
  name: stringProp(),
17
19
  label: stringProp(),
20
+ description: stringProp(),
21
+ modelValue: mixedProp<FormFieldValue>([String, Number, Boolean]),
18
22
  };
19
23
 
20
24
  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 { mixedProp, stringProp } from '@/utils/vue';
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,9 +32,10 @@ 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
- return form.getFieldValue(props.name) as string | number | boolean | null;
38
+ return form.getFieldValue(props.name);
39
39
  }
40
40
 
41
41
  return props.modelValue;
@@ -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>
@@ -4,19 +4,23 @@
4
4
  ref="$input"
5
5
  :name="name"
6
6
  :type="type"
7
- :value="value"
8
7
  :aria-invalid="input.errors ? 'true' : 'false'"
9
- :aria-describedby="input.errors ? `${input.id}-error` : undefined"
8
+ :aria-describedby="
9
+ input.errors ? `${input.id}-error` : input.description ? `${input.id}-description` : undefined
10
+ "
10
11
  :checked="checked"
11
12
  @input="update"
12
13
  >
13
14
  </template>
14
15
 
15
16
  <script setup lang="ts">
16
- import { computed, ref } from 'vue';
17
+ import { computed, ref, watchEffect } from 'vue';
17
18
 
18
19
  import { injectReactiveOrFail, stringProp } from '@/utils';
19
20
  import type { IAGHeadlessInput } from '@/components/headless/forms/AGHeadlessInput';
21
+ import type { FormFieldValue } from '@/forms/Form';
22
+
23
+ import { onFormFocus } from './composition';
20
24
 
21
25
  const props = defineProps({
22
26
  type: stringProp('text'),
@@ -42,6 +46,36 @@ function update() {
42
46
  return;
43
47
  }
44
48
 
45
- input.update(props.type === 'checkbox' ? $input.value.checked : $input.value.value);
49
+ input.update(getValue());
46
50
  }
51
+
52
+ function getValue(): FormFieldValue | null {
53
+ if (!$input.value) {
54
+ return null;
55
+ }
56
+
57
+ switch (props.type) {
58
+ case 'checkbox':
59
+ return $input.value.checked;
60
+ case 'date':
61
+ return $input.value.valueAsDate;
62
+ default:
63
+ return $input.value.value;
64
+ }
65
+ }
66
+
67
+ onFormFocus(input, () => $input.value?.focus());
68
+ watchEffect(() => {
69
+ if (!$input.value) {
70
+ return;
71
+ }
72
+
73
+ if (props.type === 'date') {
74
+ $input.value.valueAsDate = value.value as Date;
75
+
76
+ return;
77
+ }
78
+
79
+ $input.value.value = value.value as string;
80
+ });
47
81
  </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,11 +1,14 @@
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';
11
+ export { default as AGHeadlessInputTextArea } from './AGHeadlessInputTextArea.vue';
9
12
  export { default as AGHeadlessSelect } from './AGHeadlessSelect.vue';
10
13
  export { default as AGHeadlessSelectButton } from './AGHeadlessSelectButton.vue';
11
14
  export { default as AGHeadlessSelectError } from './AGHeadlessSelectError.vue';
@@ -3,20 +3,21 @@
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
- import { booleanProp, objectProp, stringProp } from '@/utils/vue';
9
+ import { booleanProp, mixedProp, stringProp } from '@/utils/vue';
10
10
  import { translate } from '@/lang';
11
11
 
12
12
  const props = defineProps({
13
13
  as: stringProp(),
14
14
  inline: booleanProp(),
15
15
  langKey: stringProp(),
16
- langParams: objectProp<Record<string, unknown>>(),
16
+ langParams: mixedProp<number | Record<string, unknown>>(),
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'), { class: props.inline ? '' : 'prose', innerHTML: html.value });
36
+ h(props.as ?? (props.inline ? 'span' : 'div'), {
37
+ innerHTML: html.value,
38
+ ...attrs,
39
+ class: `${attrs.class ?? ''} ${props.inline ? '' : 'prose'}`,
40
+ });
36
41
  </script>
@@ -10,6 +10,8 @@ const builtInDirectives: Record<string, Directive> = {
10
10
  'measure': measure,
11
11
  };
12
12
 
13
+ export * from './measure';
14
+
13
15
  export default definePlugin({
14
16
  install(app, options) {
15
17
  const directives = {
@@ -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 }: { value?: () => unknown }) {
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
- value?.();
19
+ listener?.({ width: sizes.width, height: sizes.height });
11
20
  },
12
21
  });
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
 
@@ -8,6 +8,7 @@ export const FormFieldTypes = {
8
8
  Number: 'number',
9
9
  Boolean: 'boolean',
10
10
  Object: 'object',
11
+ Date: 'date',
11
12
  } as const;
12
13
 
13
14
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -40,10 +41,15 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
40
41
  ? boolean
41
42
  : TType extends typeof FormFieldTypes.Object
42
43
  ? object
44
+ : TType extends typeof FormFieldTypes.Date
45
+ ? Date
43
46
  : never;
44
47
 
45
48
  const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
46
49
 
50
+ export type SubmitFormListener = () => unknown;
51
+ export type FocusFormListener = (input: string) => unknown;
52
+
47
53
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
48
54
 
49
55
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
@@ -52,6 +58,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
52
58
  private _data: FormData<Fields>;
53
59
  private _submitted: Ref<boolean>;
54
60
  private _errors: FormErrors<Fields>;
61
+ private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
55
62
 
56
63
  constructor(fields: Fields) {
57
64
  super();
@@ -89,6 +96,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
89
96
  return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
90
97
  }
91
98
 
99
+ public data(): FormData<Fields> {
100
+ return { ...this._data };
101
+ }
102
+
92
103
  public validate(): boolean {
93
104
  const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
94
105
  formErrors[name] = this.getFieldErrors(name, definition);
@@ -111,7 +122,35 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
111
122
  public submit(): boolean {
112
123
  this._submitted.value = true;
113
124
 
114
- return this.validate();
125
+ const valid = this.validate();
126
+
127
+ valid && this._listeners['submit']?.forEach((listener) => listener());
128
+
129
+ return valid;
130
+ }
131
+
132
+ public on(event: 'focus', listener: FocusFormListener): () => void;
133
+ public on(event: 'submit', listener: SubmitFormListener): () => void;
134
+ public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
135
+ this._listeners[event] ??= [];
136
+
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ this._listeners[event]?.push(listener as any);
139
+
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ return () => this.off(event as any, listener);
142
+ }
143
+
144
+ public off(event: 'focus', listener: FocusFormListener): void;
145
+ public off(event: 'submit', listener: SubmitFormListener): void;
146
+ public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
147
+ arrayRemove(this._listeners[event] ?? [], listener);
148
+ }
149
+
150
+ public async focus(input: string): Promise<void> {
151
+ await nextTick();
152
+
153
+ this._listeners['focus']?.forEach((listener) => listener(input));
115
154
  }
116
155
 
117
156
  protected __get(property: string): unknown {
@@ -1,3 +1,4 @@
1
1
  export * from './Form';
2
2
  export * from './composition';
3
3
  export * from './utils';
4
+ export { default as Form } from './Form';
@@ -8,6 +8,13 @@ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof
8
8
  };
9
9
  }
10
10
 
11
+ export function dateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
12
+ return {
13
+ default: defaultValue,
14
+ type: FormFieldTypes.Date,
15
+ };
16
+ }
17
+
11
18
  export function requiredBooleanInput(
12
19
  defaultValue?: boolean,
13
20
  ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
@@ -18,6 +25,14 @@ export function requiredBooleanInput(
18
25
  };
19
26
  }
20
27
 
28
+ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
29
+ return {
30
+ default: defaultValue,
31
+ type: FormFieldTypes.Date,
32
+ rules: 'required',
33
+ };
34
+ }
35
+
21
36
  export function requiredNumberInput(
22
37
  defaultValue?: number,
23
38
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
package/src/lang/Lang.ts CHANGED
@@ -4,8 +4,8 @@ import App from '@/services/App';
4
4
  import Service from '@/services/Service';
5
5
 
6
6
  export interface LangProvider {
7
- translate(key: string, parameters?: Record<string, unknown>): string;
8
- translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown>): string;
7
+ translate(key: string, parameters?: Record<string, unknown> | number): string;
8
+ translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown> | number): string;
9
9
  }
10
10
 
11
11
  export class LangService extends Service {
@@ -35,11 +35,15 @@ export class LangService extends Service {
35
35
  this.provider = provider;
36
36
  }
37
37
 
38
- public translate(key: string, parameters?: Record<string, unknown>): string {
38
+ public translate(key: string, parameters?: Record<string, unknown> | number): string {
39
39
  return this.provider.translate(key, parameters) ?? key;
40
40
  }
41
41
 
42
- public translateWithDefault(key: string, defaultMessage: string, parameters: Record<string, unknown> = {}): string {
42
+ public translateWithDefault(
43
+ key: string,
44
+ defaultMessage: string,
45
+ parameters: Record<string, unknown> | number = {},
46
+ ): string {
43
47
  return this.provider.translateWithDefault(key, defaultMessage, parameters);
44
48
  }
45
49
 
package/src/main.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './bootstrap';
2
2
  export * from './components';
3
+ export * from './directives';
3
4
  export * from './errors';
4
5
  export * from './forms';
5
6
  export * from './jobs';
@@ -0,0 +1,43 @@
1
+ import { PromisedValue, facade, tap } from '@noeldemartin/utils';
2
+
3
+ import Service from '@/services/Service';
4
+
5
+ export class CacheService extends Service {
6
+
7
+ private cache?: PromisedValue<Cache> = undefined;
8
+
9
+ public async get(url: string): Promise<Response | null> {
10
+ const cache = await this.open();
11
+ const response = await cache.match(url);
12
+
13
+ return response ?? null;
14
+ }
15
+
16
+ public async store(url: string, response: Response): Promise<void> {
17
+ const cache = await this.open();
18
+
19
+ await cache.put(url, response);
20
+ }
21
+
22
+ public async replace(url: string, response: Response): Promise<void> {
23
+ const cache = await this.open();
24
+ const keys = await cache.keys(url);
25
+
26
+ if (keys.length === 0) {
27
+ return;
28
+ }
29
+
30
+ await cache.put(url, response);
31
+ }
32
+
33
+ protected async open(): Promise<Cache> {
34
+ return (this.cache =
35
+ this.cache ??
36
+ tap(new PromisedValue<Cache>(), (cache) => {
37
+ caches.open('app').then((instance) => cache.resolve(instance));
38
+ }));
39
+ }
40
+
41
+ }
42
+
43
+ export default facade(CacheService);
@@ -3,16 +3,18 @@ import type { App as VueApp } from 'vue';
3
3
  import { definePlugin } from '@/plugins';
4
4
 
5
5
  import App from './App';
6
+ import Cache from './Cache';
6
7
  import Events from './Events';
7
8
  import Service from './Service';
8
9
  import { getPiniaStore } from './store';
9
10
 
10
11
  export * from './App';
12
+ export * from './Cache';
11
13
  export * from './Events';
12
14
  export * from './Service';
13
15
  export * from './store';
14
16
 
15
- export { App, Events, Service };
17
+ export { App, Cache, Events, Service };
16
18
 
17
19
  const defaultServices = {
18
20
  $app: App,
package/src/ui/UI.ts CHANGED
@@ -43,6 +43,7 @@ export interface PromptOptions {
43
43
  placeholder?: string;
44
44
  acceptText?: string;
45
45
  cancelText?: string;
46
+ trim?: boolean;
46
47
  }
47
48
 
48
49
  export interface ShowSnackbarOptions {
@@ -115,6 +116,7 @@ export class UIService extends Service {
115
116
  messageOrOptions?: string | PromptOptions,
116
117
  options?: PromptOptions,
117
118
  ): Promise<string | null> {
119
+ const trim = options?.trim ?? true;
118
120
  const getProperties = (): AGPromptModalProps => {
119
121
  if (typeof messageOrOptions !== 'string') {
120
122
  return {
@@ -134,7 +136,8 @@ export class UIService extends Service {
134
136
  this.requireComponent(UIComponents.PromptModal),
135
137
  getProperties(),
136
138
  );
137
- const result = await modal.beforeClose;
139
+ const rawResult = await modal.beforeClose;
140
+ const result = trim ? rawResult?.trim() : rawResult;
138
141
 
139
142
  return result ?? null;
140
143
  }