@inertia-vuetify/modal 0.0.0-stage → 3.0.0-rc.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/LICENSE +50 -0
- package/README.md +371 -2
- package/dist/CloseButton.vue.d.ts +3 -0
- package/dist/Deferred.vue.d.ts +16 -0
- package/dist/ModalBtn.vue.d.ts +17 -0
- package/dist/ModalContent.vue.d.ts +33 -0
- package/dist/ModalLink.vue.d.ts +201 -0
- package/dist/ModalRenderer.vue.d.ts +6 -0
- package/dist/ModalRoot.vue.d.ts +13 -0
- package/dist/WhenVisible.vue.d.ts +24 -0
- package/dist/app.d.ts +5 -0
- package/dist/cache.d.ts +12 -0
- package/dist/config.d.ts +16 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/index.d.ts +14 -0
- package/dist/inertia-modal-vuetify.js +1140 -0
- package/dist/keys.d.ts +2 -0
- package/dist/modalStack.d.ts +64 -0
- package/dist/transitions.d.ts +10 -0
- package/dist/types.d.ts +109 -0
- package/dist/useModal.d.ts +3 -0
- package/package.json +71 -4
- package/src/CloseButton.vue +13 -0
- package/src/Deferred.vue +21 -0
- package/src/HeadlessModal.vue +174 -0
- package/src/Modal.vue +120 -0
- package/src/ModalBtn.vue +28 -0
- package/src/ModalContent.vue +238 -0
- package/src/ModalLink.vue +189 -0
- package/src/ModalRenderer.vue +21 -0
- package/src/ModalRoot.vue +152 -0
- package/src/WhenVisible.vue +90 -0
- package/src/app.ts +17 -0
- package/src/cache.ts +67 -0
- package/src/config.ts +116 -0
- package/src/helpers.ts +18 -0
- package/src/index.ts +15 -0
- package/src/keys.ts +3 -0
- package/src/modalStack.ts +525 -0
- package/src/transitions.ts +30 -0
- package/src/types.ts +121 -0
- package/src/useModal.ts +7 -0
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type HttpResponse, type RequestPayload } from '@inertiajs/core';
|
|
2
|
+
import { type Component, type ComputedRef, type Ref } from 'vue';
|
|
3
|
+
import type { HttpMethod, ModalConfig, ModalContext, ModalResponseData, PrefetchOptions, ReloadOptions, VisitOptions } from './types';
|
|
4
|
+
type EventCallback = (...args: unknown[]) => void;
|
|
5
|
+
type ComponentResolver = (name: string) => Promise<Component>;
|
|
6
|
+
export declare function prefetch(href: string, options?: PrefetchOptions): Promise<void>;
|
|
7
|
+
/** @deprecated A no-op kept for setup code written for Inertia 2. */
|
|
8
|
+
export declare const initFromPageProps: (_pageProps: Record<string, unknown>) => void;
|
|
9
|
+
export declare class ModalInstance {
|
|
10
|
+
id: string;
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
shouldRender: boolean;
|
|
13
|
+
listeners: Record<string, EventCallback[]>;
|
|
14
|
+
component: Component | null;
|
|
15
|
+
props: Ref<Record<string, unknown>>;
|
|
16
|
+
response: ModalResponseData;
|
|
17
|
+
config: ModalConfig;
|
|
18
|
+
onCloseCallback: (() => void) | null;
|
|
19
|
+
afterLeaveCallback: (() => void) | null;
|
|
20
|
+
index: ComputedRef<number>;
|
|
21
|
+
onTopOfStack: ComputedRef<boolean>;
|
|
22
|
+
name?: string;
|
|
23
|
+
constructor(component: Component | null, response: ModalResponseData, config?: ModalConfig | null, onClose?: (() => void) | null, afterLeave?: (() => void) | null);
|
|
24
|
+
getComponentPropKeys: () => string[];
|
|
25
|
+
getParentModal: () => ModalContext | null;
|
|
26
|
+
getChildModal: () => ModalContext | null;
|
|
27
|
+
show: () => void;
|
|
28
|
+
close: () => void;
|
|
29
|
+
setOpen: (open: boolean) => void;
|
|
30
|
+
afterLeave: () => void;
|
|
31
|
+
on: (event: string, callback: EventCallback) => void;
|
|
32
|
+
off: (event: string, callback?: EventCallback) => void;
|
|
33
|
+
emit: (event: string, ...args: unknown[]) => void;
|
|
34
|
+
registerEventListenersFromAttrs: (attrs: Record<string, unknown>) => (() => void);
|
|
35
|
+
reload: (options?: ReloadOptions) => Promise<void>;
|
|
36
|
+
updateProps: (props: Record<string, unknown>) => void;
|
|
37
|
+
}
|
|
38
|
+
declare function registerLocalModal(name: string, callback: (modal: ModalInstance) => void): void;
|
|
39
|
+
/** Removes a modal without waiting for its leave transition. */
|
|
40
|
+
export declare function discardModal(modal: ModalContext): void;
|
|
41
|
+
declare function pushFromResponseData(responseData: ModalResponseData, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null): Promise<ModalInstance>;
|
|
42
|
+
declare function visit(href: string, method: HttpMethod, payload?: RequestPayload, headers?: Record<string, string>, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null, queryStringArrayFormat?: 'brackets' | 'indices', useBrowserHistory?: boolean, onStart?: (() => void) | null, onSuccess?: ((response?: HttpResponse) => void) | null, onError?: ((...args: unknown[]) => void) | null, props?: Record<string, unknown> | null): Promise<ModalInstance>;
|
|
43
|
+
/** Opens a modal for a URL, or a local modal for `#name`. */
|
|
44
|
+
export declare function visitModal(url: string, options?: VisitOptions): Promise<ModalInstance>;
|
|
45
|
+
declare function push(component: Component | null, response: ModalResponseData, config?: ModalConfig | null, onClose?: (() => void) | null, afterLeave?: (() => void) | null): ModalInstance;
|
|
46
|
+
export declare const modalPropNames: string[];
|
|
47
|
+
export interface ModalStack {
|
|
48
|
+
/** @deprecated A no-op kept for setup code written for Inertia 2. */
|
|
49
|
+
setComponentResolver: (resolver: ComponentResolver) => void;
|
|
50
|
+
getBaseUrl: () => string | null;
|
|
51
|
+
setBaseUrl: (url: string | null) => void;
|
|
52
|
+
isClosingToBaseUrl: (pageUrl: string) => boolean;
|
|
53
|
+
clearClosingToBaseUrl: () => void;
|
|
54
|
+
stack: Readonly<Ref<readonly ModalContext[]>>;
|
|
55
|
+
push: typeof push;
|
|
56
|
+
pushFromResponseData: typeof pushFromResponseData;
|
|
57
|
+
closeAll: (force?: boolean) => void;
|
|
58
|
+
reset: () => void;
|
|
59
|
+
visit: typeof visit;
|
|
60
|
+
registerLocalModal: typeof registerLocalModal;
|
|
61
|
+
removeLocalModal: (name: string) => boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function useModalStack(): ModalStack;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `v-dialog` transition that moves the panel in from off-screen, e.g. `translateX(100%)`.
|
|
3
|
+
* Vuetify only ships short slide transitions, so this uses the Web Animations API.
|
|
4
|
+
*/
|
|
5
|
+
export declare function slideTransition(hiddenTransform: string): {
|
|
6
|
+
component: import("vue").FunctionalComponent<import("vue").TransitionProps, {}, any, {}>;
|
|
7
|
+
css: boolean;
|
|
8
|
+
onEnter: (el: Element, done: () => void) => void;
|
|
9
|
+
onLeave: (el: Element, done: () => void) => void;
|
|
10
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { HttpResponse, Method, RequestPayload } from '@inertiajs/core';
|
|
2
|
+
import type { UnwrapNestedRefs } from 'vue';
|
|
3
|
+
import type { ModalInstance } from './modalStack';
|
|
4
|
+
export type MaxWidth = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl';
|
|
5
|
+
export type ModalPosition = 'top' | 'center' | 'bottom';
|
|
6
|
+
export type SlideoverPosition = 'left' | 'right' | 'bottom';
|
|
7
|
+
export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
8
|
+
export type HttpMethod = Method;
|
|
9
|
+
/**
|
|
10
|
+
* `true`/`false`, `'mobile'` for Vuetify's `display.mobile`, or fullscreen
|
|
11
|
+
* below a Vuetify breakpoint or pixel width.
|
|
12
|
+
*/
|
|
13
|
+
export type Fullscreen = boolean | 'mobile' | Breakpoint | number;
|
|
14
|
+
/** Runs before a user-initiated close; return `false` to keep the modal open. */
|
|
15
|
+
export type BeforeClose = () => boolean | void | Promise<boolean | void>;
|
|
16
|
+
export interface ModalTypeConfig {
|
|
17
|
+
closeButton: boolean;
|
|
18
|
+
closeExplicitly: boolean;
|
|
19
|
+
closeOnClickOutside: boolean;
|
|
20
|
+
maxWidth: MaxWidth | (string & {});
|
|
21
|
+
paddingClasses: string | false;
|
|
22
|
+
panelClasses: string | false;
|
|
23
|
+
position: ModalPosition | SlideoverPosition;
|
|
24
|
+
fullscreen: Fullscreen;
|
|
25
|
+
}
|
|
26
|
+
/** The configuration of a single modal, as resolved from its props, the visit, and the defaults. */
|
|
27
|
+
export type ModalConfig = Partial<ModalTypeConfig & {
|
|
28
|
+
slideover: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
/** What `putConfig()` accepts; omitted keys get their defaults. */
|
|
31
|
+
export interface GlobalModalConfig {
|
|
32
|
+
type?: 'modal' | 'slideover';
|
|
33
|
+
navigate?: boolean;
|
|
34
|
+
appElement?: string | null;
|
|
35
|
+
modal?: Partial<ModalTypeConfig>;
|
|
36
|
+
slideover?: Partial<ModalTypeConfig>;
|
|
37
|
+
}
|
|
38
|
+
/** The `_inertiaui_modal` payload the `inertiaui/modal` Laravel package sends. */
|
|
39
|
+
export interface ModalResponseData {
|
|
40
|
+
id?: string;
|
|
41
|
+
component: string;
|
|
42
|
+
props: Record<string, unknown>;
|
|
43
|
+
url?: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
meta?: {
|
|
46
|
+
deferredProps?: Record<string, string[]>;
|
|
47
|
+
};
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ReloadOptions {
|
|
51
|
+
only?: string[];
|
|
52
|
+
except?: string[];
|
|
53
|
+
method?: HttpMethod;
|
|
54
|
+
data?: Record<string, unknown>;
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
onStart?: () => void;
|
|
57
|
+
onSuccess?: (response: HttpResponse) => void;
|
|
58
|
+
onError?: (error: unknown) => void;
|
|
59
|
+
onFinish?: () => void;
|
|
60
|
+
}
|
|
61
|
+
export interface VisitOptions {
|
|
62
|
+
method?: HttpMethod;
|
|
63
|
+
data?: RequestPayload;
|
|
64
|
+
headers?: Record<string, string>;
|
|
65
|
+
config?: ModalConfig;
|
|
66
|
+
onClose?: () => void;
|
|
67
|
+
onAfterLeave?: () => void;
|
|
68
|
+
queryStringArrayFormat?: 'brackets' | 'indices';
|
|
69
|
+
navigate?: boolean;
|
|
70
|
+
onStart?: () => void;
|
|
71
|
+
onSuccess?: (response?: HttpResponse) => void;
|
|
72
|
+
onError?: (...args: unknown[]) => void;
|
|
73
|
+
listeners?: Record<string, (...args: unknown[]) => void>;
|
|
74
|
+
/** Props for local modals. */
|
|
75
|
+
props?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
export type PrefetchOption = boolean | 'hover' | 'click' | 'mount' | Array<'hover' | 'click' | 'mount'>;
|
|
78
|
+
export interface PrefetchOptions {
|
|
79
|
+
method?: HttpMethod;
|
|
80
|
+
data?: RequestPayload;
|
|
81
|
+
headers?: Record<string, string>;
|
|
82
|
+
queryStringArrayFormat?: 'brackets' | 'indices';
|
|
83
|
+
cacheFor?: number;
|
|
84
|
+
onPrefetching?: () => void;
|
|
85
|
+
onPrefetched?: () => void;
|
|
86
|
+
}
|
|
87
|
+
/** A modal instance as components see it, with its computed refs unwrapped. */
|
|
88
|
+
export type ModalContext = UnwrapNestedRefs<ModalInstance>;
|
|
89
|
+
export interface HeadlessModalSlotProps extends Record<string, unknown> {
|
|
90
|
+
id: string;
|
|
91
|
+
index: number;
|
|
92
|
+
isOpen: boolean;
|
|
93
|
+
onTopOfStack: boolean;
|
|
94
|
+
shouldRender: boolean;
|
|
95
|
+
config: ModalConfig;
|
|
96
|
+
modalContext: ModalContext;
|
|
97
|
+
afterLeave: () => void;
|
|
98
|
+
/** Closes immediately, skipping `before-close`. */
|
|
99
|
+
close: () => void;
|
|
100
|
+
emit: (event: string, ...args: unknown[]) => void;
|
|
101
|
+
getChildModal: () => ModalContext | null;
|
|
102
|
+
getParentModal: () => ModalContext | null;
|
|
103
|
+
reload: (options?: ReloadOptions) => Promise<void>;
|
|
104
|
+
setOpen: (open: boolean) => void;
|
|
105
|
+
}
|
|
106
|
+
export interface ModalSlotProps extends HeadlessModalSlotProps {
|
|
107
|
+
/** Closes like the close button does, so `before-close` can veto it. */
|
|
108
|
+
requestClose: () => Promise<void>;
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inertia-vuetify/modal",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "3.0.0-rc.0",
|
|
4
|
+
"description": "Vuetify modals and slideovers for Inertia.js, compatible with the inertiaui/modal Laravel package",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/inertia-vuetify/modal.git"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/inertia-modal-vuetify.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/inertia-modal-vuetify.js"
|
|
20
|
+
},
|
|
21
|
+
"./src": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./src/index.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "vite serve example",
|
|
28
|
+
"build": "vite build && vue-tsc -p tsconfig.build.json",
|
|
29
|
+
"build:example": "vite build example",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"typecheck": "vue-tsc --noEmit",
|
|
32
|
+
"lint": "oxlint",
|
|
33
|
+
"format": "oxfmt",
|
|
34
|
+
"format:check": "oxfmt --check",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"vue": "^3.5.0",
|
|
39
|
+
"vuetify": "^4.2.0",
|
|
40
|
+
"@inertiajs/core": "^3.0.0",
|
|
41
|
+
"@inertiajs/vue3": "^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@inertiaui/vanilla": "^1.5.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@inertiajs/core": "^3.9.1",
|
|
48
|
+
"@inertiajs/vue3": "^3.9.1",
|
|
49
|
+
"@mdi/font": "^7.4.47",
|
|
50
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
51
|
+
"@vitest/browser-playwright": "^5.0.5",
|
|
52
|
+
"@vue/test-utils": "^2.5.1",
|
|
53
|
+
"oxfmt": "^0.74.0",
|
|
54
|
+
"oxlint": "^1.89.0",
|
|
55
|
+
"playwright": "^1.64.1",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vite": "^8.3.5",
|
|
58
|
+
"vite-plugin-vuetify": "^2.1.3",
|
|
59
|
+
"vitest": "^5.0.5",
|
|
60
|
+
"vue": "^3.5.44",
|
|
61
|
+
"vue-tsc": "^3.3.13",
|
|
62
|
+
"vuetify": "^4.3.0"
|
|
63
|
+
},
|
|
64
|
+
"keywords": [
|
|
65
|
+
"inertia",
|
|
66
|
+
"inertiajs",
|
|
67
|
+
"modal",
|
|
68
|
+
"vuetify",
|
|
69
|
+
"vue"
|
|
70
|
+
],
|
|
71
|
+
"author": "",
|
|
72
|
+
"license": "MIT"
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject } from 'vue'
|
|
3
|
+
import { useLocale } from 'vuetify'
|
|
4
|
+
import { VBtn } from 'vuetify/components/VBtn'
|
|
5
|
+
import { requestCloseKey } from './keys'
|
|
6
|
+
|
|
7
|
+
const requestClose = inject(requestCloseKey)
|
|
8
|
+
const { t } = useLocale()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<VBtn class="im-close-button" icon="$close" variant="text" size="small" :aria-label="t('$vuetify.close')" @click="requestClose?.()" />
|
|
13
|
+
</template>
|
package/src/Deferred.vue
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, type Ref } from 'vue'
|
|
3
|
+
import type { ModalContext } from './types'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ data: string | string[] }>()
|
|
6
|
+
|
|
7
|
+
defineSlots<{ default?(): unknown; fallback?(): unknown }>()
|
|
8
|
+
|
|
9
|
+
const modalContext = inject<Ref<ModalContext | null>>('modalContext')
|
|
10
|
+
|
|
11
|
+
if (!modalContext) {
|
|
12
|
+
throw new Error('Deferred component must be used inside a Modal component')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const allKeysAreAvailable = computed(() => [props.data].flat().every((key) => modalContext.value?.props[key] !== undefined))
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<slot v-if="allKeysAreAvailable" />
|
|
20
|
+
<slot v-else name="fallback" />
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, onBeforeUnmount, onMounted, reactive, ref, useAttrs, watch, type PropType, type Ref } from 'vue'
|
|
3
|
+
import { getConfig, getConfigByType } from './config'
|
|
4
|
+
import ModalRenderer from './ModalRenderer.vue'
|
|
5
|
+
import { discardModal, useModalStack } from './modalStack'
|
|
6
|
+
import type { Fullscreen, HeadlessModalSlotProps, MaxWidth, ModalConfig, ModalContext, ModalPosition, SlideoverPosition } from './types'
|
|
7
|
+
|
|
8
|
+
defineOptions({ inheritAttrs: false })
|
|
9
|
+
|
|
10
|
+
// Props default to null so unset ones fall back to the global config.
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
name: String,
|
|
13
|
+
slideover: { type: Boolean, default: null },
|
|
14
|
+
closeButton: { type: Boolean, default: null },
|
|
15
|
+
closeExplicitly: { type: Boolean, default: null },
|
|
16
|
+
closeOnClickOutside: { type: Boolean, default: null },
|
|
17
|
+
maxWidth: { type: String as PropType<MaxWidth>, default: null },
|
|
18
|
+
paddingClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
|
|
19
|
+
panelClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
|
|
20
|
+
position: { type: String as PropType<ModalPosition | SlideoverPosition>, default: null },
|
|
21
|
+
fullscreen: { type: [Boolean, String, Number] as PropType<Fullscreen>, default: null },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const emits = defineEmits<{
|
|
25
|
+
'modal-event': [event: string, ...args: unknown[]]
|
|
26
|
+
focus: []
|
|
27
|
+
blur: []
|
|
28
|
+
close: []
|
|
29
|
+
success: []
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
defineSlots<{ default?(props: HeadlessModalSlotProps): unknown }>()
|
|
33
|
+
|
|
34
|
+
const modalStack = useModalStack()
|
|
35
|
+
const modalContext: Ref<ModalContext | null> = props.name ? ref(null) : inject('modalContext', ref(null))
|
|
36
|
+
|
|
37
|
+
const config = computed<ModalConfig>(() => {
|
|
38
|
+
const slideover = modalContext.value?.config?.slideover ?? props.slideover ?? getConfig('type') === 'slideover'
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
slideover,
|
|
42
|
+
closeButton: props.closeButton ?? getConfigByType(slideover, 'closeButton'),
|
|
43
|
+
closeExplicitly: props.closeExplicitly ?? getConfigByType(slideover, 'closeExplicitly'),
|
|
44
|
+
closeOnClickOutside: props.closeOnClickOutside ?? getConfigByType(slideover, 'closeOnClickOutside'),
|
|
45
|
+
maxWidth: props.maxWidth ?? getConfigByType(slideover, 'maxWidth'),
|
|
46
|
+
paddingClasses: props.paddingClasses ?? getConfigByType(slideover, 'paddingClasses'),
|
|
47
|
+
panelClasses: props.panelClasses ?? getConfigByType(slideover, 'panelClasses'),
|
|
48
|
+
position: props.position ?? getConfigByType(slideover, 'position'),
|
|
49
|
+
fullscreen: props.fullscreen ?? getConfigByType(slideover, 'fullscreen'),
|
|
50
|
+
...modalContext.value?.config,
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const attrs = useAttrs()
|
|
55
|
+
let unsubscribeEventListeners: (() => void) | undefined
|
|
56
|
+
|
|
57
|
+
function registerEventListeners() {
|
|
58
|
+
unsubscribeEventListeners = modalContext.value?.registerEventListenersFromAttrs(attrs)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (props.name) {
|
|
62
|
+
modalStack.registerLocalModal(props.name, (context) => {
|
|
63
|
+
const previous = modalContext.value
|
|
64
|
+
|
|
65
|
+
// Reopened before its leave transition finished: that instance would never get afterLeave().
|
|
66
|
+
if (previous?.shouldRender && previous.id !== context.id) {
|
|
67
|
+
discardModal(previous)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
modalContext.value = reactive(context)
|
|
71
|
+
registerEventListeners()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
onBeforeUnmount(() => modalStack.removeLocalModal(props.name!))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onMounted(() => {
|
|
78
|
+
if (!props.name) {
|
|
79
|
+
registerEventListeners()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
onBeforeUnmount(() => unsubscribeEventListeners?.())
|
|
84
|
+
|
|
85
|
+
function emit(event: string, ...args: unknown[]) {
|
|
86
|
+
emits('modal-event', event, ...args)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
defineExpose({
|
|
90
|
+
emit,
|
|
91
|
+
afterLeave: () => modalContext.value?.afterLeave(),
|
|
92
|
+
close: () => modalContext.value?.close(),
|
|
93
|
+
reload: (...args: Parameters<ModalContext['reload']>) => modalContext.value?.reload(...args),
|
|
94
|
+
setOpen: (open: boolean) => modalContext.value?.setOpen(open),
|
|
95
|
+
getChildModal: () => modalContext.value?.getChildModal(),
|
|
96
|
+
getParentModal: () => modalContext.value?.getParentModal(),
|
|
97
|
+
|
|
98
|
+
get config() {
|
|
99
|
+
return modalContext.value?.config
|
|
100
|
+
},
|
|
101
|
+
get id() {
|
|
102
|
+
return modalContext.value?.id
|
|
103
|
+
},
|
|
104
|
+
get index() {
|
|
105
|
+
return modalContext.value?.index
|
|
106
|
+
},
|
|
107
|
+
get isOpen() {
|
|
108
|
+
return modalContext.value?.isOpen
|
|
109
|
+
},
|
|
110
|
+
get modalContext() {
|
|
111
|
+
return modalContext.value
|
|
112
|
+
},
|
|
113
|
+
get onTopOfStack() {
|
|
114
|
+
return modalContext.value?.onTopOfStack
|
|
115
|
+
},
|
|
116
|
+
get shouldRender() {
|
|
117
|
+
return modalContext.value?.shouldRender
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
watch(
|
|
122
|
+
() => modalContext.value?.onTopOfStack,
|
|
123
|
+
(onTopOfStack, previousOnTopOfStack) => {
|
|
124
|
+
if (onTopOfStack && !previousOnTopOfStack) {
|
|
125
|
+
emits('focus')
|
|
126
|
+
} else if (!onTopOfStack && previousOnTopOfStack) {
|
|
127
|
+
emits('blur')
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
watch(
|
|
133
|
+
() => modalContext.value?.isOpen,
|
|
134
|
+
(isOpen, previousIsOpen) => {
|
|
135
|
+
if (isOpen) {
|
|
136
|
+
emits('success')
|
|
137
|
+
} else if (previousIsOpen === true) {
|
|
138
|
+
// Not when first mounting closed.
|
|
139
|
+
emits('close')
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{ immediate: true },
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// The next modal in the stack renders inside this one.
|
|
146
|
+
const nextIndex = computed(() => {
|
|
147
|
+
const context = modalContext.value
|
|
148
|
+
|
|
149
|
+
return context ? modalStack.stack.value.find((modal) => modal.shouldRender && modal.index > context.index)?.index : undefined
|
|
150
|
+
})
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<slot
|
|
155
|
+
v-if="modalContext?.shouldRender"
|
|
156
|
+
v-bind="modalContext.props"
|
|
157
|
+
:id="modalContext.id"
|
|
158
|
+
:after-leave="modalContext.afterLeave"
|
|
159
|
+
:close="modalContext.close"
|
|
160
|
+
:config
|
|
161
|
+
:emit
|
|
162
|
+
:get-child-modal="modalContext.getChildModal"
|
|
163
|
+
:get-parent-modal="modalContext.getParentModal"
|
|
164
|
+
:index="modalContext.index"
|
|
165
|
+
:is-open="modalContext.isOpen"
|
|
166
|
+
:modal-context
|
|
167
|
+
:on-top-of-stack="modalContext.onTopOfStack"
|
|
168
|
+
:reload="modalContext.reload"
|
|
169
|
+
:set-open="modalContext.setOpen"
|
|
170
|
+
:should-render="modalContext.shouldRender"
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<ModalRenderer v-if="nextIndex" :index="nextIndex" />
|
|
174
|
+
</template>
|
package/src/Modal.vue
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useTemplateRef, type PropType } from 'vue'
|
|
3
|
+
import HeadlessModal from './HeadlessModal.vue'
|
|
4
|
+
import ModalContent from './ModalContent.vue'
|
|
5
|
+
import type { BeforeClose, Fullscreen, MaxWidth, ModalPosition, ModalSlotProps, ReloadOptions, SlideoverPosition } from './types'
|
|
6
|
+
|
|
7
|
+
// Upstream props default to null so unset ones fall back to the global config.
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
name: String,
|
|
10
|
+
slideover: { type: Boolean, default: null },
|
|
11
|
+
closeButton: { type: Boolean, default: null },
|
|
12
|
+
closeExplicitly: { type: Boolean, default: null },
|
|
13
|
+
closeOnClickOutside: { type: Boolean, default: null },
|
|
14
|
+
maxWidth: { type: String as PropType<MaxWidth>, default: null },
|
|
15
|
+
paddingClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
|
|
16
|
+
panelClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
|
|
17
|
+
position: { type: String as PropType<ModalPosition | SlideoverPosition>, default: null },
|
|
18
|
+
title: String,
|
|
19
|
+
fullscreen: { type: [Boolean, String, Number] as PropType<Fullscreen>, default: null },
|
|
20
|
+
beforeClose: Function as PropType<BeforeClose>,
|
|
21
|
+
dialogProps: Object as PropType<Record<string, unknown>>,
|
|
22
|
+
cardProps: Object as PropType<Record<string, unknown>>,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
'after-leave': []
|
|
27
|
+
blur: []
|
|
28
|
+
close: []
|
|
29
|
+
focus: []
|
|
30
|
+
success: []
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
defineSlots<{
|
|
34
|
+
default?(props: ModalSlotProps): unknown
|
|
35
|
+
title?(props: ModalSlotProps): unknown
|
|
36
|
+
actions?(props: ModalSlotProps): unknown
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const headlessProps = computed(() => ({
|
|
40
|
+
name: props.name,
|
|
41
|
+
slideover: props.slideover,
|
|
42
|
+
closeButton: props.closeButton,
|
|
43
|
+
closeExplicitly: props.closeExplicitly,
|
|
44
|
+
closeOnClickOutside: props.closeOnClickOutside,
|
|
45
|
+
maxWidth: props.maxWidth,
|
|
46
|
+
paddingClasses: props.paddingClasses,
|
|
47
|
+
panelClasses: props.panelClasses,
|
|
48
|
+
position: props.position,
|
|
49
|
+
fullscreen: props.fullscreen,
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
const modal = useTemplateRef<InstanceType<typeof HeadlessModal>>('modal')
|
|
53
|
+
|
|
54
|
+
defineExpose({
|
|
55
|
+
afterLeave: () => modal.value?.afterLeave(),
|
|
56
|
+
close: () => modal.value?.close(),
|
|
57
|
+
emit: (event: string, ...args: unknown[]) => modal.value?.emit(event, ...args),
|
|
58
|
+
getChildModal: () => modal.value?.getChildModal(),
|
|
59
|
+
getParentModal: () => modal.value?.getParentModal(),
|
|
60
|
+
reload: (options?: ReloadOptions) => modal.value?.reload(options),
|
|
61
|
+
setOpen: (open: boolean) => modal.value?.setOpen(open),
|
|
62
|
+
|
|
63
|
+
get config() {
|
|
64
|
+
return modal.value?.config
|
|
65
|
+
},
|
|
66
|
+
get id() {
|
|
67
|
+
return modal.value?.id
|
|
68
|
+
},
|
|
69
|
+
get index() {
|
|
70
|
+
return modal.value?.index
|
|
71
|
+
},
|
|
72
|
+
get isOpen() {
|
|
73
|
+
return modal.value?.isOpen
|
|
74
|
+
},
|
|
75
|
+
get modalContext() {
|
|
76
|
+
return modal.value?.modalContext
|
|
77
|
+
},
|
|
78
|
+
get onTopOfStack() {
|
|
79
|
+
return modal.value?.onTopOfStack
|
|
80
|
+
},
|
|
81
|
+
get shouldRender() {
|
|
82
|
+
return modal.value?.shouldRender
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<HeadlessModal
|
|
89
|
+
ref="modal"
|
|
90
|
+
v-bind="headlessProps"
|
|
91
|
+
v-slot="slotProps"
|
|
92
|
+
@success="emit('success')"
|
|
93
|
+
@close="emit('close')"
|
|
94
|
+
@focus="emit('focus')"
|
|
95
|
+
@blur="emit('blur')"
|
|
96
|
+
>
|
|
97
|
+
<ModalContent
|
|
98
|
+
:key="slotProps.id"
|
|
99
|
+
:modal-context="slotProps.modalContext"
|
|
100
|
+
:config="slotProps.config"
|
|
101
|
+
:title
|
|
102
|
+
:before-close
|
|
103
|
+
:dialog-props
|
|
104
|
+
:card-props
|
|
105
|
+
@after-leave="emit('after-leave')"
|
|
106
|
+
>
|
|
107
|
+
<template #default="{ requestClose }">
|
|
108
|
+
<slot v-bind="{ ...slotProps, requestClose }" />
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<template v-if="$slots.title" #title="{ requestClose }">
|
|
112
|
+
<slot name="title" v-bind="{ ...slotProps, requestClose }" />
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<template v-if="$slots.actions" #actions="{ requestClose }">
|
|
116
|
+
<slot name="actions" v-bind="{ ...slotProps, requestClose }" />
|
|
117
|
+
</template>
|
|
118
|
+
</ModalContent>
|
|
119
|
+
</HeadlessModal>
|
|
120
|
+
</template>
|
package/src/ModalBtn.vue
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { VBtn } from 'vuetify/components/VBtn'
|
|
4
|
+
import ModalLink from './ModalLink.vue'
|
|
5
|
+
|
|
6
|
+
defineOptions({ inheritAttrs: false })
|
|
7
|
+
|
|
8
|
+
defineProps<{ href: string }>()
|
|
9
|
+
|
|
10
|
+
defineSlots<{ default?(props: { loading: boolean }): unknown }>()
|
|
11
|
+
|
|
12
|
+
const loading = ref(false)
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<ModalLink
|
|
17
|
+
v-slot="slotProps"
|
|
18
|
+
v-bind="$attrs"
|
|
19
|
+
:as="VBtn"
|
|
20
|
+
:href
|
|
21
|
+
:loading
|
|
22
|
+
@start="loading = true"
|
|
23
|
+
@success="loading = false"
|
|
24
|
+
@error="loading = false"
|
|
25
|
+
>
|
|
26
|
+
<slot v-bind="slotProps" />
|
|
27
|
+
</ModalLink>
|
|
28
|
+
</template>
|