@signal24/vue-foundation 4.3.7 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@signal24/vue-foundation",
3
3
  "type": "module",
4
- "version": "4.3.7",
4
+ "version": "4.5.0",
5
5
  "description": "Common components, directives, and helpers for Vue 3 apps",
6
6
  "module": "./dist/vue-foundation.es.js",
7
7
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import AlertModal from './alert-modal.vue';
2
- import { createModalInjection, presentModal, removeModalInjection } from './modal-container';
2
+ import { createOverlayInjection, presentOverlay, removeOverlayInjection } from './overlay-container';
3
3
 
4
4
  function resolveAlertParams(titleOrMessage: string | Error, message?: string | Error) {
5
5
  const title = message ? (titleOrMessage as string) : undefined;
@@ -10,13 +10,13 @@ function resolveAlertParams(titleOrMessage: string | Error, message?: string | E
10
10
  export async function showAlert(title: string, message: string | Error): Promise<void>;
11
11
  export async function showAlert(message: string | Error): Promise<void>;
12
12
  export async function showAlert(titleOrMessage: string | Error, message?: string | Error): Promise<void> {
13
- await presentModal(AlertModal, resolveAlertParams(titleOrMessage, message));
13
+ await presentOverlay(AlertModal, resolveAlertParams(titleOrMessage, message));
14
14
  }
15
15
 
16
16
  export async function showConfirm(title: string, message: string): Promise<boolean>;
17
17
  export async function showConfirm(message: string): Promise<boolean>;
18
18
  export async function showConfirm(titleOrMessage: string, message?: string): Promise<boolean> {
19
- const result = await presentModal(AlertModal, {
19
+ const result = await presentOverlay(AlertModal, {
20
20
  ...resolveAlertParams(titleOrMessage, message),
21
21
  shouldConfirm: true
22
22
  });
@@ -26,7 +26,7 @@ export async function showConfirm(titleOrMessage: string, message?: string): Pro
26
26
  export async function showConfirmDestroy(title: string, message: string): Promise<boolean>;
27
27
  export async function showConfirmDestroy(message: string): Promise<boolean>;
28
28
  export async function showConfirmDestroy(titleOrMessage: string, message?: string): Promise<boolean> {
29
- const result = await presentModal(AlertModal, {
29
+ const result = await presentOverlay(AlertModal, {
30
30
  ...resolveAlertParams(titleOrMessage, message),
31
31
  shouldConfirm: true,
32
32
  classes: ['destructive']
@@ -37,11 +37,11 @@ export async function showConfirmDestroy(titleOrMessage: string, message?: strin
37
37
  export function showWait(title: string, message: string): () => void;
38
38
  export function showWait(message: string): () => void;
39
39
  export function showWait(titleOrMessage: string, message?: string): () => void {
40
- const injection = createModalInjection(AlertModal, {
40
+ const injection = createOverlayInjection(AlertModal, {
41
41
  ...resolveAlertParams(titleOrMessage, message),
42
42
  isBare: true,
43
43
  classes: ['wait'],
44
44
  callback: () => {}
45
45
  });
46
- return () => removeModalInjection(injection);
46
+ return () => removeOverlayInjection(injection);
47
47
  }
@@ -5,6 +5,6 @@ import VfModal from './modal.vue';
5
5
  import VfSmartSelect from './smart-select.vue';
6
6
 
7
7
  export * from './alert-helpers';
8
- export * from './modal-container';
8
+ export * from './overlay-container';
9
9
 
10
10
  export { VfAjaxSelect, VfAlertModal, VfEzSmartSelect, VfModal, VfSmartSelect };
@@ -1,27 +1,25 @@
1
1
  <template>
2
- <Teleport to="#vf-modal-target">
3
- <div :id="id" class="vf-overlay vf-modal-wrap" :class="props.class" ref="overlay">
4
- <form action="." class="vf-modal" :class="{ scrolls }" @submit.prevent="$emit('formSubmit')" ref="form">
5
- <div v-if="$slots.header" class="vf-modal-header">
6
- <slot name="header" />
7
- <i v-if="props.closeX" class="close" @click="closeParent"></i>
8
- </div>
9
- <div class="vf-modal-content">
10
- <slot />
11
- </div>
12
- <div v-if="$slots.footer" class="vf-modal-footer">
13
- <slot name="footer" />
14
- </div>
15
- </form>
16
- </div>
17
- </Teleport>
2
+ <div :id="id" class="vf-overlay vf-modal-wrap" :class="props.class" ref="overlay">
3
+ <form action="." class="vf-modal" :class="{ scrolls }" @submit.prevent="$emit('formSubmit')" ref="form">
4
+ <div v-if="$slots.header" class="vf-modal-header">
5
+ <slot name="header" />
6
+ <i v-if="props.closeX" class="close" @click="closeParent"></i>
7
+ </div>
8
+ <div class="vf-modal-content">
9
+ <slot />
10
+ </div>
11
+ <div v-if="$slots.footer" class="vf-modal-footer">
12
+ <slot name="footer" />
13
+ </div>
14
+ </form>
15
+ </div>
18
16
  </template>
19
17
 
20
18
  <script lang="ts" setup>
21
19
  import { getCurrentInstance, onBeforeUnmount, onMounted, ref } from 'vue';
22
20
 
23
21
  import { maskForm, unmaskForm } from '../helpers/mask';
24
- import { removeModalInjectionByInternalInstance } from './modal-container';
22
+ import { dismissOverlayInjectionByInternalInstance } from './overlay-container';
25
23
 
26
24
  const instance = getCurrentInstance();
27
25
 
@@ -72,7 +70,7 @@ function handleEscapeKey(e: KeyboardEvent) {
72
70
  }
73
71
 
74
72
  function closeParent() {
75
- removeModalInjectionByInternalInstance(instance!);
73
+ dismissOverlayInjectionByInternalInstance(instance!);
76
74
  }
77
75
 
78
76
  function mask() {
@@ -0,0 +1,103 @@
1
+ <template>
2
+ <div class="vf-overlay-anchor" :class="anchorClasses" :style="anchorStyles" @click.stop="removeOverlay">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import type { CSSProperties } from 'vue';
9
+ import { getCurrentInstance, onMounted, ref } from 'vue';
10
+
11
+ import { dismissOverlayInjectionById } from '.';
12
+ import type { OverlayAnchorOptions, OverlayAnchorOptionsObject } from './overlay-types';
13
+
14
+ const props = defineProps<{
15
+ overlayId: string;
16
+ anchor: OverlayAnchorOptions;
17
+ }>();
18
+
19
+ const anchorEl = props.anchor instanceof HTMLElement ? props.anchor : props.anchor.el;
20
+
21
+ const anchorStyles = ref<CSSProperties>({ visibility: 'hidden', top: '0', left: '0' });
22
+ const anchorClasses = ref<string[]>([]);
23
+ const instance = getCurrentInstance();
24
+
25
+ onMounted(updateAttributes);
26
+
27
+ function updateAttributes() {
28
+ const overlayEl = instance!.vnode.el as HTMLElement;
29
+ const { styles, classes } = computeAnchoredStyle(overlayEl, anchorEl);
30
+ anchorStyles.value = styles;
31
+ anchorClasses.value = classes;
32
+ }
33
+
34
+ function computeAnchoredStyle(
35
+ overlayEl: HTMLElement,
36
+ anchorEl: HTMLElement
37
+ ): {
38
+ styles: CSSProperties;
39
+ classes: string[];
40
+ } {
41
+ const anchorOpts: Omit<OverlayAnchorOptionsObject, 'el'> = props.anchor instanceof HTMLElement ? {} : props.anchor;
42
+
43
+ const anchorRect = anchorEl.getBoundingClientRect();
44
+ const overlayRect = overlayEl.getBoundingClientRect();
45
+
46
+ if (anchorOpts.matchWidth) {
47
+ overlayRect.width = anchorRect.width;
48
+ }
49
+ if (anchorOpts.matchHeight) {
50
+ overlayRect.height = anchorRect.height;
51
+ }
52
+
53
+ const classes: string[] = anchorOpts.class ? (Array.isArray(anchorOpts.class) ? anchorOpts.class : [anchorOpts.class]) : [];
54
+ let top: number, left: number;
55
+
56
+ if (anchorOpts.y === 'center') {
57
+ top = anchorRect.top + anchorRect.height / 2 - overlayRect.height / 2;
58
+ classes.push('anchored-center-y');
59
+ } else {
60
+ const canDisplayBelow = anchorRect.bottom + overlayRect.height < window.innerHeight || anchorOpts.y === 'below';
61
+ const canDisplayBelowResolved = canDisplayBelow && anchorOpts.y !== 'above';
62
+ top = canDisplayBelowResolved ? anchorRect.bottom : anchorRect.top - overlayRect.height;
63
+ classes.push(canDisplayBelowResolved ? 'anchored-top' : 'anchored-bottom');
64
+ }
65
+
66
+ if (anchorOpts.x === 'center') {
67
+ left = anchorRect.left + anchorRect.width / 2 - overlayRect.width / 2;
68
+ classes.push('anchored-center-x');
69
+ } else {
70
+ const canAlignLefts = anchorRect.left + overlayRect.width < window.innerWidth || anchorOpts.x === 'left';
71
+ const canAlignLeftsResolved = canAlignLefts && anchorOpts.x !== 'right';
72
+ left = canAlignLeftsResolved ? anchorRect.left : anchorRect.right - overlayRect.width;
73
+ classes.push(canAlignLeftsResolved ? 'anchored-left' : 'anchored-right');
74
+ }
75
+
76
+ return {
77
+ styles: {
78
+ top: `${top}px`,
79
+ left: `${left}px`,
80
+ ...(anchorOpts.matchWidth ? { width: `${overlayRect.width}px` } : {}),
81
+ ...(anchorOpts.matchHeight ? { height: `${overlayRect.height}px` } : {})
82
+ },
83
+ classes
84
+ };
85
+ }
86
+
87
+ function removeOverlay() {
88
+ window.removeEventListener('click', removeOverlay);
89
+ dismissOverlayInjectionById(props.overlayId);
90
+ }
91
+
92
+ onMounted(() => {
93
+ setTimeout(() => {
94
+ window.addEventListener('click', removeOverlay);
95
+ }, 10);
96
+ });
97
+ </script>
98
+
99
+ <style lang="scss">
100
+ .vf-overlay-anchor {
101
+ position: absolute;
102
+ }
103
+ </style>
@@ -0,0 +1,176 @@
1
+ import type { Writable } from 'type-fest';
2
+ import {
3
+ type AllowedComponentProps,
4
+ type ComponentInternalInstance,
5
+ type ComponentPublicInstance,
6
+ type ComputedOptions,
7
+ defineComponent,
8
+ h,
9
+ markRaw,
10
+ type MethodOptions,
11
+ type Raw,
12
+ reactive,
13
+ renderList,
14
+ Teleport,
15
+ type VNode,
16
+ type VNodeProps
17
+ } from 'vue';
18
+
19
+ import OverlayAnchor from './overlay-anchor.vue';
20
+ import type { OverlayAnchorOptions } from './overlay-types';
21
+
22
+ interface OverlayOptions {
23
+ anchor?: OverlayAnchorOptions;
24
+ }
25
+
26
+ interface OverlayInjection<C extends OverlayComponent> {
27
+ id: string;
28
+ component: OverlayComponentUnwrapped<C>;
29
+ props: OverlayComponentProps<C>;
30
+ options: OverlayOptions;
31
+ vnode: VNode;
32
+ wrapperVnode?: VNode;
33
+ }
34
+
35
+ let overlayCount = 0;
36
+ const OverlayInjections: OverlayInjection<any>[] = reactive([]);
37
+
38
+ export const OverlayContainer = defineComponent({
39
+ setup() {
40
+ return () =>
41
+ h('div', [
42
+ renderList(OverlayInjections, injection => {
43
+ return h(Teleport, { key: injection.id, to: '#vf-overlay-target' }, [injection.wrapperVnode ?? injection.vnode]);
44
+ })
45
+ ]);
46
+ }
47
+ });
48
+
49
+ // copied in from Vue since it's not exported
50
+ // tood: it may be a lot easier than this. see the docs for props passed to "h"
51
+ export type Vue__ComponentPublicInstanceConstructor<
52
+ T extends ComponentPublicInstance<Props, RawBindings, D, C, M> = ComponentPublicInstance<any>,
53
+ Props = any,
54
+ RawBindings = any,
55
+ D = any,
56
+ C extends ComputedOptions = ComputedOptions,
57
+ M extends MethodOptions = MethodOptions
58
+ > = {
59
+ __isFragment?: never;
60
+ __isTeleport?: never;
61
+ __isSuspense?: never;
62
+ new (...args: any[]): T;
63
+ };
64
+
65
+ export type ObjectComponentConfig<T extends Vue__ComponentPublicInstanceConstructor> = T extends Vue__ComponentPublicInstanceConstructor<infer P>
66
+ ? P
67
+ : never;
68
+ export type ObjectComponentProps<T extends Vue__ComponentPublicInstanceConstructor> = Writable<
69
+ Omit<ObjectComponentConfig<T>['$props'], keyof VNodeProps | keyof AllowedComponentProps>
70
+ >;
71
+
72
+ type ObjectOrDefault<T> = T extends object ? T : PropsWithCallback<{}>;
73
+ export type OverlayComponent = Vue__ComponentPublicInstanceConstructor | ((props: any) => any);
74
+ export type OverlayComponentConfig<T> = T extends Vue__ComponentPublicInstanceConstructor
75
+ ? {
76
+ props: ObjectComponentProps<T>;
77
+ component: Raw<T>;
78
+ }
79
+ : T extends (props: infer P) => any
80
+ ? {
81
+ props: Omit<ObjectOrDefault<P>, keyof VNodeProps | keyof AllowedComponentProps>;
82
+ component: T;
83
+ }
84
+ : never;
85
+ export type OverlayComponentUnwrapped<T extends OverlayComponent> = OverlayComponentConfig<T>['component'];
86
+ export type OverlayComponentProps<T extends OverlayComponent> = OverlayComponentConfig<T>['props'];
87
+
88
+ interface PropsWithCallback<T> {
89
+ callback?: (result: T) => void;
90
+ }
91
+ type ComponentReturn<M extends OverlayComponent> = OverlayComponentProps<M> extends PropsWithCallback<infer R> ? R : never;
92
+
93
+ export type AnyComponentPublicInstance = { $: ComponentInternalInstance };
94
+
95
+ export function createOverlayInjection<C extends OverlayComponent>(
96
+ component: C,
97
+ props: OverlayComponentProps<C>,
98
+ options?: OverlayOptions
99
+ ): OverlayInjection<C> {
100
+ // create or reconfigure the existing overlay target
101
+ // re-injecting every time keeps the overlay container at the very end of the DOM
102
+ const targetEl = document.getElementById('vf-overlay-target') ?? document.createElement('div');
103
+ targetEl.id = 'vf-overlay-target';
104
+ targetEl.removeAttribute('inert');
105
+ document.body.appendChild(targetEl);
106
+
107
+ const overlayId = String(++overlayCount);
108
+ const rawComponent = markRaw(component);
109
+ const vnode = h(rawComponent, props);
110
+ const wrapperVnode = options?.anchor ? h(OverlayAnchor, { overlayId, anchor: options.anchor }, () => [vnode]) : undefined;
111
+
112
+ // todo: dunno what's going on with types here
113
+ const injection: OverlayInjection<C> = {
114
+ id: overlayId,
115
+ component: rawComponent as any,
116
+ props,
117
+ options: options ?? {},
118
+ vnode,
119
+ wrapperVnode
120
+ };
121
+ OverlayInjections.push(injection);
122
+
123
+ return injection;
124
+ }
125
+
126
+ export function dismissOverlayInjectionByInstance(instance: AnyComponentPublicInstance) {
127
+ dismissOverlayInjectionByInternalInstance(instance.$);
128
+ }
129
+
130
+ export function dismissOverlayInjectionByInternalInstance(instance: ComponentInternalInstance) {
131
+ let targetInstance: ComponentInternalInstance | null = instance;
132
+ while (targetInstance && !dismissOverlayInjectionByVnode(targetInstance.vnode)) {
133
+ targetInstance = targetInstance.parent;
134
+ }
135
+ }
136
+
137
+ export function dismissOverlayInjectionByVnode(vnode: VNode) {
138
+ const injectionIdx = OverlayInjections.findIndex(i => i.vnode.component === vnode.component);
139
+ if (injectionIdx >= 0) {
140
+ OverlayInjections[injectionIdx].props.callback();
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ export function dismissOverlayInjectionById(id: string) {
147
+ const injectionIdx = OverlayInjections.findIndex(i => i.id === id);
148
+ if (injectionIdx >= 0) {
149
+ OverlayInjections[injectionIdx].props.callback();
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+
155
+ export function removeOverlayInjection(injection: OverlayInjection<any>) {
156
+ const index = OverlayInjections.indexOf(injection);
157
+ if (index >= 0) {
158
+ OverlayInjections.splice(index, 1);
159
+ }
160
+ }
161
+
162
+ export async function presentOverlay<C extends OverlayComponent, R extends ComponentReturn<C>>(
163
+ component: C,
164
+ props: Omit<OverlayComponentProps<C>, 'callback'>,
165
+ options?: OverlayOptions
166
+ ): Promise<R | undefined> {
167
+ return new Promise<R>(resolve => {
168
+ let overlayInjection: OverlayInjection<C> | null = null;
169
+ const callback = (result: R) => {
170
+ removeOverlayInjection(overlayInjection!);
171
+ resolve(result);
172
+ };
173
+ const resolvedProps = { ...props, callback } as OverlayComponentProps<C>;
174
+ overlayInjection = createOverlayInjection(component, resolvedProps, options);
175
+ });
176
+ }
@@ -0,0 +1,10 @@
1
+ export interface OverlayAnchorOptionsObject {
2
+ el: HTMLElement;
3
+ class?: string | string[];
4
+ x?: 'auto' | 'left' | 'center' | 'right';
5
+ y?: 'auto' | 'above' | 'center' | 'below';
6
+ matchWidth?: boolean;
7
+ matchHeight?: boolean;
8
+ }
9
+
10
+ export type OverlayAnchorOptions = HTMLElement | OverlayAnchorOptionsObject;
@@ -1,4 +1,4 @@
1
- import type { AnyComponentPublicInstance } from '../components/modal-container';
1
+ import type { AnyComponentPublicInstance } from '../components/overlay-container';
2
2
 
3
3
  /*///////////////////////////////////////////////
4
4
  Component Overlay Masking
@@ -53,6 +53,8 @@ export function isApiError(err: any): err is IApiError {
53
53
  export function installApiClientInterceptors({ apiClient, onRequest, onError, CancelablePromise }: IWrappedApiClientOptions) {
54
54
  const originalRequest = apiClient.request.request.bind(apiClient.request);
55
55
  apiClient.request.request = (options: IRequestOptions) => {
56
+ options = rewriteOptionsForFileUpload(options);
57
+
56
58
  if (onRequest) {
57
59
  options = onRequest(options);
58
60
  }
@@ -82,3 +84,39 @@ export function installApiClientInterceptors({ apiClient, onRequest, onError, Ca
82
84
  });
83
85
  };
84
86
  }
87
+
88
+ export class FileUploadRequest {
89
+ constructor(blob: Blob) {
90
+ this.blob = blob;
91
+ }
92
+
93
+ validator = null;
94
+ lastModifiedDate = null;
95
+ size = 0;
96
+ path = '';
97
+ name = '';
98
+ type = '';
99
+ blob!: Blob;
100
+ }
101
+
102
+ function rewriteOptionsForFileUpload(options: IRequestOptions): IRequestOptions {
103
+ const hasFileUpload = typeof options.body === 'object' && Object.values(options.body).some(v => v instanceof FileUploadRequest);
104
+ if (!hasFileUpload) return options;
105
+
106
+ const formData: Record<string, any> = {};
107
+ const jsonBody: Record<string, any> = {};
108
+ for (const [key, value] of Object.entries(options.body)) {
109
+ if (value instanceof FileUploadRequest) {
110
+ formData[key] = value.blob;
111
+ } else {
112
+ jsonBody[key] = value;
113
+ }
114
+ }
115
+ formData._payload = new Blob([JSON.stringify(jsonBody)], { type: 'application/json' });
116
+
117
+ return {
118
+ ...options,
119
+ body: undefined,
120
+ formData
121
+ };
122
+ }
@@ -1,44 +0,0 @@
1
- import type { Writable } from 'type-fest';
2
- import { type AllowedComponentProps, type ComponentInternalInstance, type ComponentPublicInstance, type ComputedOptions, type MethodOptions, type Raw, type VNode, type VNodeProps } from 'vue';
3
- interface ModalInjection<C extends ModalComponent> {
4
- id: string;
5
- component: ModalComponentUnwrapped<C>;
6
- props: ModalComponentProps<C>;
7
- vnode: VNode;
8
- }
9
- export declare const ModalContainer: import("vue").DefineComponent<{}, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
10
- [key: string]: any;
11
- }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, VNodeProps & AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
12
- export type Vue__ComponentPublicInstanceConstructor<T extends ComponentPublicInstance<Props, RawBindings, D, C, M> = ComponentPublicInstance<any>, Props = any, RawBindings = any, D = any, C extends ComputedOptions = ComputedOptions, M extends MethodOptions = MethodOptions> = {
13
- __isFragment?: never;
14
- __isTeleport?: never;
15
- __isSuspense?: never;
16
- new (...args: any[]): T;
17
- };
18
- export type ObjectComponentConfig<T extends Vue__ComponentPublicInstanceConstructor> = T extends Vue__ComponentPublicInstanceConstructor<infer P> ? P : never;
19
- export type ObjectComponentProps<T extends Vue__ComponentPublicInstanceConstructor> = Writable<Omit<ObjectComponentConfig<T>['$props'], keyof VNodeProps | keyof AllowedComponentProps>>;
20
- type ObjectOrDefault<T> = T extends object ? T : PropsWithCallback<{}>;
21
- export type ModalComponent = Vue__ComponentPublicInstanceConstructor | ((props: any) => any);
22
- export type ModalComponentConfig<T> = T extends Vue__ComponentPublicInstanceConstructor ? {
23
- props: ObjectComponentProps<T>;
24
- component: Raw<T>;
25
- } : T extends (props: infer P) => any ? {
26
- props: Omit<ObjectOrDefault<P>, keyof VNodeProps | keyof AllowedComponentProps>;
27
- component: T;
28
- } : never;
29
- export type ModalComponentUnwrapped<T extends ModalComponent> = ModalComponentConfig<T>['component'];
30
- export type ModalComponentProps<T extends ModalComponent> = ModalComponentConfig<T>['props'];
31
- interface PropsWithCallback<T> {
32
- callback?: (result: T) => void;
33
- }
34
- type ComponentReturn<M extends ModalComponent> = ModalComponentProps<M> extends PropsWithCallback<infer R> ? R : never;
35
- export type AnyComponentPublicInstance = {
36
- $: ComponentInternalInstance;
37
- };
38
- export declare function createModalInjection<C extends ModalComponent>(component: C, props: ModalComponentProps<C>): ModalInjection<C>;
39
- export declare function removeModalInjection(injection: ModalInjection<any>): void;
40
- export declare function removeModalInjectionByInstance(instance: AnyComponentPublicInstance): void;
41
- export declare function removeModalInjectionByInternalInstance(instance: ComponentInternalInstance): void;
42
- export declare function removeModalInjectionByVnode(vnode: VNode): boolean;
43
- export declare function presentModal<C extends ModalComponent, R extends ComponentReturn<C>>(component: C, props: Omit<ModalComponentProps<C>, 'callback'>): Promise<R | undefined>;
44
- export {};
@@ -1,147 +0,0 @@
1
- import type { Writable } from 'type-fest';
2
- import {
3
- type AllowedComponentProps,
4
- type ComponentInternalInstance,
5
- type ComponentPublicInstance,
6
- type ComputedOptions,
7
- defineComponent,
8
- h,
9
- markRaw,
10
- type MethodOptions,
11
- type Raw,
12
- reactive,
13
- renderList,
14
- type VNode,
15
- type VNodeProps
16
- } from 'vue';
17
-
18
- interface ModalInjection<C extends ModalComponent> {
19
- id: string;
20
- component: ModalComponentUnwrapped<C>;
21
- props: ModalComponentProps<C>;
22
- vnode: VNode;
23
- }
24
-
25
- let modalCount = 0;
26
- const ModalInjections: ModalInjection<any>[] = reactive([]);
27
-
28
- export const ModalContainer = defineComponent({
29
- setup() {
30
- return () =>
31
- h('div', { id: 'modal-container' }, [
32
- renderList(ModalInjections, injection => {
33
- return injection.vnode;
34
- })
35
- ]);
36
- }
37
- });
38
-
39
- // copied in from Vue since it's not exported
40
- export type Vue__ComponentPublicInstanceConstructor<
41
- T extends ComponentPublicInstance<Props, RawBindings, D, C, M> = ComponentPublicInstance<any>,
42
- Props = any,
43
- RawBindings = any,
44
- D = any,
45
- C extends ComputedOptions = ComputedOptions,
46
- M extends MethodOptions = MethodOptions
47
- > = {
48
- __isFragment?: never;
49
- __isTeleport?: never;
50
- __isSuspense?: never;
51
- new (...args: any[]): T;
52
- };
53
-
54
- export type ObjectComponentConfig<T extends Vue__ComponentPublicInstanceConstructor> = T extends Vue__ComponentPublicInstanceConstructor<infer P>
55
- ? P
56
- : never;
57
- export type ObjectComponentProps<T extends Vue__ComponentPublicInstanceConstructor> = Writable<
58
- Omit<ObjectComponentConfig<T>['$props'], keyof VNodeProps | keyof AllowedComponentProps>
59
- >;
60
-
61
- type ObjectOrDefault<T> = T extends object ? T : PropsWithCallback<{}>;
62
- export type ModalComponent = Vue__ComponentPublicInstanceConstructor | ((props: any) => any);
63
- export type ModalComponentConfig<T> = T extends Vue__ComponentPublicInstanceConstructor
64
- ? {
65
- props: ObjectComponentProps<T>;
66
- component: Raw<T>;
67
- }
68
- : T extends (props: infer P) => any
69
- ? {
70
- props: Omit<ObjectOrDefault<P>, keyof VNodeProps | keyof AllowedComponentProps>;
71
- component: T;
72
- }
73
- : never;
74
- export type ModalComponentUnwrapped<T extends ModalComponent> = ModalComponentConfig<T>['component'];
75
- export type ModalComponentProps<T extends ModalComponent> = ModalComponentConfig<T>['props'];
76
-
77
- interface PropsWithCallback<T> {
78
- callback?: (result: T) => void;
79
- }
80
- type ComponentReturn<M extends ModalComponent> = ModalComponentProps<M> extends PropsWithCallback<infer R> ? R : never;
81
-
82
- export type AnyComponentPublicInstance = { $: ComponentInternalInstance };
83
-
84
- export function createModalInjection<C extends ModalComponent>(component: C, props: ModalComponentProps<C>): ModalInjection<C> {
85
- // create or reconfigure the existing modal target
86
- // re-injecting every time keeps the modal container at the very end of the DOM
87
- const targetEl = document.getElementById('vf-modal-target') ?? document.createElement('div');
88
- targetEl.id = 'vf-modal-target';
89
- targetEl.removeAttribute('inert');
90
- document.body.appendChild(targetEl);
91
-
92
- const rawComponent = markRaw(component);
93
-
94
- // todo: dunno what's going on with types here
95
- const injection: ModalInjection<C> = {
96
- id: String(++modalCount),
97
- component: rawComponent as any,
98
- props,
99
- vnode: h(rawComponent, props)
100
- };
101
- ModalInjections.push(injection);
102
-
103
- return injection;
104
- }
105
-
106
- export function removeModalInjection(injection: ModalInjection<any>) {
107
- const index = ModalInjections.indexOf(injection);
108
- if (index >= 0) {
109
- ModalInjections.splice(index, 1);
110
- }
111
- }
112
-
113
- export function removeModalInjectionByInstance(instance: AnyComponentPublicInstance) {
114
- removeModalInjectionByInternalInstance(instance.$);
115
- }
116
-
117
- export function removeModalInjectionByInternalInstance(instance: ComponentInternalInstance) {
118
- let targetInstance: ComponentInternalInstance | null = instance;
119
- while (targetInstance && !removeModalInjectionByVnode(targetInstance.vnode)) {
120
- targetInstance = targetInstance.parent;
121
- }
122
- }
123
-
124
- export function removeModalInjectionByVnode(vnode: VNode) {
125
- const injectionIdx = ModalInjections.findIndex(i => i.vnode.component === vnode.component);
126
- if (injectionIdx >= 0) {
127
- ModalInjections[injectionIdx].props.callback?.(undefined);
128
- ModalInjections.splice(injectionIdx, 1);
129
- return true;
130
- }
131
- return false;
132
- }
133
-
134
- export async function presentModal<C extends ModalComponent, R extends ComponentReturn<C>>(
135
- component: C,
136
- props: Omit<ModalComponentProps<C>, 'callback'>
137
- ): Promise<R | undefined> {
138
- return new Promise<R>(resolve => {
139
- let modalInjection: ModalInjection<C> | null = null;
140
- const callback = (result: R) => {
141
- removeModalInjection(modalInjection!);
142
- resolve(result);
143
- };
144
- const resolvedProps = { ...props, callback } as ModalComponentProps<C>;
145
- modalInjection = createModalInjection(component, resolvedProps);
146
- });
147
- }