diff --git a/docs/functions/spawnDialog.md b/docs/functions/spawnDialog.md new file mode 100644 index 0000000000..7af6a4a3bc --- /dev/null +++ b/docs/functions/spawnDialog.md @@ -0,0 +1,111 @@ + +```ts static +import { + spawnDialog, +} from '@nextcloud/vue/dist/Functions/dialog.js' +``` + +## Definitions + +```ts static +/** + * Helper to spawn a Vue dialog without having to mount it from a component + * + * @param dialog The dialog component to spawn - the component must emit the 'close' event whenever it is closed + * @param props Properties to pass to the dialog + * @param props.container Optionally pass a query selector for the dialog container element + * @param props.appContext Optionally the app context to use (e.g. for registered components) + * @param onClose Callback when the dialog is closed (parameters of the 'close' event of the dialog) + */ +function spawnDialog( + dialog: Component | AsyncComponent, + props: Record, + onClose: (...rest: unknown[]) => void = () => {}, +): void; +``` + +## Usage + +The main use case is to be able to spawn a dialog from code, without the need of including the dialog component in the template. +So a Vue dialog can be spawned in any context and not just from Vue components but also from callback functions or other API. + +```vue + + + +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3423377dd4..c5afebcb16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.5.1", "babel-loader-exclude-node-modules-except": "^1.2.1", + "core-js": "^3.39.0", "file-loader": "^6.2.0", "gettext-extractor": "^3.8.0", "gettext-parser": "^8.0.0", @@ -3127,6 +3128,17 @@ "npm": "^10.0.0" } }, + "node_modules/@nextcloud/browser-storage/node_modules/core-js": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz", + "integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/@nextcloud/browserslist-config": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-3.0.1.tgz", @@ -8170,10 +8182,11 @@ } }, "node_modules/core-js": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz", - "integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" diff --git a/package.json b/package.json index edc992038f..fa2fccba01 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.5.1", "babel-loader-exclude-node-modules-except": "^1.2.1", + "core-js": "^3.39.0", "file-loader": "^6.2.0", "gettext-extractor": "^3.8.0", "gettext-parser": "^8.0.0", diff --git a/src/functions/dialog/index.ts b/src/functions/dialog/index.ts new file mode 100644 index 0000000000..ace34837f6 --- /dev/null +++ b/src/functions/dialog/index.ts @@ -0,0 +1,51 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { AppContext, Component } from 'vue' +import { createVNode, getCurrentInstance, render, toRaw } from 'vue' + +interface DialogProps { + [index: string]: unknown + container?: string + appContext?: AppContext +} + +/** + * Helper to spawn a Vue dialog without having to mount it from a component + * + * @param dialog The dialog component to spawn + * @param props Properties to pass to the dialog + * @param props.container Optionally pass a query selector for the dialog container element + * @param props.appContext Optionally the app context to use (e.g. for registered components) + * @param onClose Callback when the dialog is closed + */ +export function spawnDialog( + dialog: Component, + props?: DialogProps, + onClose: (...rest: unknown[]) => void = () => {}, +): void { + const el = document.createElement('div') + const container: HTMLElement = typeof props?.container === 'string' + ? (document.querySelector(props.container) || document.body) + : document.body + container.appendChild(el) + + const vueComponent = createVNode(dialog, { + ...props, + onclose: (...rest: unknown[]) => { + onClose(...rest.map(v => toRaw(v))) + // destroy the component + render(null, el) + el.remove() + }, + }) + + // If there is an instance use it to get access to registered components + const appContext = props?.appContext ?? getCurrentInstance()?.appContext + if (appContext) { + vueComponent.appContext = { ...appContext } + } + + render(vueComponent, el) +} diff --git a/src/functions/index.js b/src/functions/index.ts similarity index 90% rename from src/functions/index.js rename to src/functions/index.ts index 807f98ef08..bc0b822256 100644 --- a/src/functions/index.js +++ b/src/functions/index.ts @@ -4,6 +4,7 @@ */ export * from './a11y/index.ts' +export * from './dialog/index.ts' export * from './emoji/index.ts' export * from './reference/index.js' export * from './isDarkTheme/index.ts' diff --git a/src/index.ts b/src/index.ts index 8fdbc79edb..b8ef8f3cec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ export * from './components/index' export * from './composables/index.js' -export * from './functions/index.js' +export * from './functions/index.ts' export * from './directives/index.js' export * from './mixins/index.js' diff --git a/styleguide.config.cjs b/styleguide.config.cjs index b2275b55dd..08a2aa5c3a 100644 --- a/styleguide.config.cjs +++ b/styleguide.config.cjs @@ -48,6 +48,11 @@ module.exports = async () => { }, }, }, + resolve: { + alias: { + vue$: 'vue/dist/vue.esm-browser.js', + }, + }, }), exampleMode: 'collapse', @@ -137,6 +142,10 @@ module.exports = async () => { name: 'registerReference', content: 'docs/functions/registerReference.md', }, + { + name: 'spawnDialog', + content: 'docs/functions/spawnDialog.md', + }, ], }, { diff --git a/styleguide/global.requires.js b/styleguide/global.requires.js index 69ee8cb033..ad950758e9 100644 --- a/styleguide/global.requires.js +++ b/styleguide/global.requires.js @@ -10,7 +10,9 @@ import axios from '@nextcloud/axios' import { isA11yActivation } from '../src/functions/a11y/index.ts' import { EmojiSkinTone, emojiSearch, emojiAddRecent, getCurrentSkinTone, setCurrentSkinTone } from '../src/functions/emoji/index.ts' +import { spawnDialog } from '../src/functions/dialog/index.ts' import usernameToColor from '../src/functions/usernameToColor/index.js' +import NcDialog from '../src/components/NcDialog/index.js' const USER_GROUPS = [ { id: 'admin', displayname: 'The administrators' }, @@ -157,6 +159,9 @@ window.emojiAddRecent = emojiAddRecent window.getCurrentSkinTone = getCurrentSkinTone window.setCurrentSkinTone = setCurrentSkinTone window.usernameToColor = usernameToColor +window.spawnDialog = spawnDialog +// Exported components +window.NcDialog = NcDialog // Exported composables window.useIsDarkTheme = useIsDarkTheme