├── dev
├── components
│ └── .gitkeep
├── .eslintrc.js
├── tsconfig.json
├── main.ts
├── App.vue
├── Playground.template.vue
├── g-dialog.ts
├── components.d.ts
├── index.html
├── windi.config.ts
├── vite.config.ts
└── auto-imports.d.ts
├── .eslintignore
├── .npmignore
├── .stylelintignore
├── src
├── vite-env.d.ts
├── scss
│ └── main.scss
├── util
│ ├── globals.ts
│ ├── index.ts
│ ├── helpers.ts
│ └── scroll.ts
├── assets
│ ├── example-video.gif
│ └── gitart-dialog-logo.svg
├── shims-vue.d.ts
├── index.ts
├── composables
│ ├── window.ts
│ ├── overlay.ts
│ ├── stack.ts
│ ├── sizeStyle.ts
│ ├── lazyActivation.ts
│ └── scroll.ts
├── components
│ ├── GDialogRoot.vue
│ ├── GDialogOverlay.vue
│ ├── GDialogContent.vue
│ ├── GDialogFrame.vue
│ └── GDialog.vue
├── types
│ └── Plugin.ts
└── plugin.ts
├── .gitignore
├── .eslintrc.js
├── .vscode
├── extensions.json
└── settings.json
├── vitest.config.ts
├── .stylelintrc.js
├── tests
├── unit
│ ├── GDialogRoot.spec.ts
│ └── composables
│ │ └── lazyActivation.spec.ts
└── auto-imports.d.ts
├── tsconfig.json
├── LICENSE
├── README.md
└── package.json
/dev/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | public
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/**/*
3 | !LINCENSE
4 | README.md
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | public
4 | *.md
5 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/scss/main.scss:
--------------------------------------------------------------------------------
1 | html.overflow-y-hidden {
2 | overflow-y: hidden;
3 | }
4 |
--------------------------------------------------------------------------------
/src/util/globals.ts:
--------------------------------------------------------------------------------
1 | export const IN_BROWSER = typeof window !== 'undefined'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dev/Playground.vue
2 | dev/components/*
3 | node_modules
4 | dist
5 | .DS_Store
6 | *.local
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | '@gitart/eslint-config-vue',
4 | ],
5 | }
6 |
--------------------------------------------------------------------------------
/src/util/index.ts:
--------------------------------------------------------------------------------
1 | export * from './globals'
2 | export * from './helpers'
3 | export * from './scroll'
4 |
--------------------------------------------------------------------------------
/src/assets/example-video.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitart-group/vue-dialog/HEAD/src/assets/example-video.gif
--------------------------------------------------------------------------------
/dev/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | extends: [
4 | '../.eslintrc.js',
5 | ],
6 |
7 | rules: {
8 | 'no-undef': 'off',
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "johnsoncodehk.volar",
4 | "dbaeumer.vscode-eslint",
5 | "stylelint.vscode-stylelint",
6 | "streetsidesoftware.code-spell-checker"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/dev/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "gitart-vue-dialog": ["../src/index.ts"]
7 | }
8 | },
9 | "exclude": []
10 | }
11 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports
3 | import { DefineComponent } from 'vue'
4 |
5 | const component: DefineComponent<{}, {}, any>
6 | export default component
7 | }
8 |
--------------------------------------------------------------------------------
/dev/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 |
3 | import 'virtual:windi.css'
4 |
5 | import App from './App.vue'
6 | import dialogPlugin from './g-dialog'
7 |
8 | export const app = createApp(App)
9 |
10 | app.use(dialogPlugin())
11 | app.mount('#app')
12 |
--------------------------------------------------------------------------------
/dev/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/dev/Playground.template.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | Content
7 |
8 |
9 |
15 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.enable": false,
3 | "typescript.tsdk": "node_modules/typescript/lib",
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true,
6 | },
7 | "cSpell.words": [
8 | "gitart",
9 | "stackable",
10 | "vite",
11 | "stylelint",
12 | "unplugin",
13 | "windicss",
14 | "vitejs"
15 | ],
16 | }
17 |
--------------------------------------------------------------------------------
/dev/g-dialog.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from 'vue'
2 | import type { IDialog } from 'gitart-vue-dialog'
3 | import { plugin as dialogPlugin } from 'gitart-vue-dialog'
4 | // import 'gitart-vue-dialog/dist/style.css'
5 |
6 | declare module '@vue/runtime-core' {
7 | export interface GDialogProperties {
8 | $dialog: IDialog
9 | }
10 | }
11 |
12 | export default (): Plugin => dialogPlugin
13 |
--------------------------------------------------------------------------------
/dev/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/vue-next/pull/3399
4 |
5 | declare module 'vue' {
6 | export interface GlobalComponents {
7 | GDialog: typeof import('gitart-vue-dialog')['GDialog']
8 | GDialogRoot: typeof import('gitart-vue-dialog')['GDialogRoot']
9 | }
10 | }
11 |
12 | export { }
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import './scss/main.scss'
2 |
3 | // standalone component
4 | export { default as GDialog } from './components/GDialog.vue'
5 |
6 | // using plugin
7 | export type {
8 | IDialog,
9 | DialogOnCloseEvent,
10 | IDialogItem,
11 | } from './types/Plugin'
12 |
13 | export { default as GDialogRoot } from './components/GDialogRoot.vue'
14 |
15 | export {
16 | plugin, dialogInjectionKey,
17 | } from './plugin'
18 |
--------------------------------------------------------------------------------
/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Welcome To GDialog Playground
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 | import vue from '@vitejs/plugin-vue'
3 | import AutoImport from 'unplugin-auto-import/vite'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | vue({}) as any,
8 | AutoImport({
9 | imports: ['vitest'],
10 | dts: 'tests/auto-imports.d.ts',
11 | }),
12 | ],
13 | test: {
14 | global: true,
15 | environment: 'happy-dom',
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/src/composables/window.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, onUnmounted } from 'vue'
2 |
3 | export function useWindowEventListener (
4 | event: E, listener: (this: Window, ev: WindowEventMap[E]) => any, options?: AddEventListenerOptions,
5 | ) {
6 | onMounted(() => {
7 | window.addEventListener(event, listener, options)
8 | })
9 |
10 | onUnmounted(() => {
11 | window.removeEventListener(event, listener)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['stylelint-config-standard'],
3 | rules: {
4 | indentation: 2,
5 | 'at-rule-no-unknown': [
6 | true,
7 | {
8 | ignoreAtRules: [
9 | 'tailwind',
10 | 'apply',
11 | 'variants',
12 | 'responsive',
13 | 'screen',
14 | ],
15 | },
16 | ],
17 | 'declaration-block-trailing-semicolon': null,
18 | 'no-descending-specificity': null,
19 | 'value-keyword-case': null,
20 | },
21 | }
--------------------------------------------------------------------------------
/src/util/helpers.ts:
--------------------------------------------------------------------------------
1 | export function convertToUnit (str: number, unit?: string): string
2 | export function convertToUnit (str: string | number | null | undefined, unit?: string): string | undefined
3 | export function convertToUnit(str: string | number | null | undefined, unit = 'px'): string | undefined {
4 | if (str == null || str === '')
5 | return undefined
6 |
7 | else if (isNaN(+str!))
8 | return String(str)
9 |
10 | else if (!isFinite(+str!))
11 | return undefined
12 |
13 | else
14 | return `${Number(str)}${unit}`
15 | }
16 |
17 | export default {
18 | convertToUnit,
19 | }
20 |
--------------------------------------------------------------------------------
/dev/windi.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'windicss/helpers'
2 | import plugin from 'windicss/plugin'
3 |
4 | export default defineConfig({
5 | attributify: true,
6 | extract: {
7 | include: ['./**/*.{vue,html,jsx,tsx}'],
8 | },
9 | plugins: [
10 | plugin(({ addComponents }) => {
11 | addComponents({
12 | '.btn': {
13 | 'backgroundColor': '#3490dc',
14 | 'padding': '.5rem 1rem',
15 | 'borderRadius': '.25rem',
16 | 'color': '#fff',
17 | 'fontWeight': '600',
18 | '&:hover': {
19 | backgroundColor: '#2779bd',
20 | },
21 | },
22 | })
23 | }),
24 | ],
25 | })
26 |
--------------------------------------------------------------------------------
/src/composables/overlay.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 |
3 | import { getCurrentInstance, ref, watch } from 'vue'
4 |
5 | const overlays: Ref = ref([])
6 |
7 | const MIN_Z_INDEX = 1000
8 |
9 | export function useOverlay(isActive: Ref) {
10 | const id = getCurrentInstance()!.uid
11 | const zIndex = ref(0)
12 |
13 | watch(isActive, (value) => {
14 | if (value) {
15 | overlays.value.push(id)
16 | zIndex.value = MIN_Z_INDEX + ((overlays.value.indexOf(id) + 1) * 2)
17 | }
18 | else {
19 | overlays.value = overlays.value.filter(x => x !== id)
20 | }
21 | }, {
22 | immediate: true,
23 | })
24 |
25 | return { zIndex }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/unit/GDialogRoot.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import GDialogRoot from '../../src/components/GDialogRoot.vue'
3 | import { dialogInjectionFallback, dialogInjectionKey } from '../../src/plugin'
4 |
5 | describe('Test Provide/Inject for GDialogRoot', () => {
6 | test('Use fallback data if plugin is not installed', () => {
7 | const wrapper = shallowMount(GDialogRoot)
8 | expect(wrapper.vm.dialogs).toBe(dialogInjectionFallback.dialogs)
9 | })
10 |
11 | test('Use plugin data with global provide', () => {
12 | const wrapper = shallowMount(GDialogRoot, {
13 | global: {
14 | provide: {
15 | [dialogInjectionKey as symbol]: () => ({}),
16 | },
17 | },
18 | })
19 |
20 | expect(wrapper.vm.dialogs).not.toBe(dialogInjectionFallback.dialogs)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/tests/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | // We suggest you to commit this file into source control
3 | declare global {
4 | const afterAll: typeof import('vitest')['afterAll']
5 | const afterEach: typeof import('vitest')['afterEach']
6 | const assert: typeof import('vitest')['assert']
7 | const beforeAll: typeof import('vitest')['beforeAll']
8 | const beforeEach: typeof import('vitest')['beforeEach']
9 | const chai: typeof import('vitest')['chai']
10 | const describe: typeof import('vitest')['describe']
11 | const expect: typeof import('vitest')['expect']
12 | const it: typeof import('vitest')['it']
13 | const suite: typeof import('vitest')['suite']
14 | const test: typeof import('vitest')['test']
15 | const vi: typeof import('vitest')['vi']
16 | const vitest: typeof import('vitest')['vitest']
17 | }
18 | export {}
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | "lib": [
7 | "es2015",
8 | "es2016",
9 | "es2017",
10 | "es2018",
11 | "es2019",
12 | "es2020",
13 | "dom",
14 | "dom.iterable"
15 | ],
16 | "allowJs": false,
17 | "declaration": true,
18 | "declarationMap": true,
19 | "sourceMap": true,
20 | "outDir": "./dist",
21 | "downlevelIteration": true,
22 | "strict": true,
23 | "noImplicitReturns": true,
24 | "noUnusedLocals": true,
25 | "moduleResolution": "node",
26 | "esModuleInterop": true,
27 | "experimentalDecorators": true,
28 | "skipLibCheck": true,
29 | "jsx": "preserve",
30 | "emitDecoratorMetadata": true,
31 | "paths": {
32 | "gitart-vue-dialog": ["./src/index.ts"]
33 | }
34 | },
35 | "exclude": [
36 | "node_modules",
37 | "dev"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/src/composables/stack.ts:
--------------------------------------------------------------------------------
1 | import {
2 | computed, effectScope, getCurrentInstance, onScopeDispose, ref, toRaw, watch,
3 | } from 'vue'
4 |
5 | // Types
6 | import type { ComponentInternalInstance, EffectScope, Ref } from 'vue'
7 |
8 | const stack = ref([])
9 |
10 | export function useStack(isActive: Ref) {
11 | const vm = getCurrentInstance()!
12 | let scope: EffectScope | undefined
13 |
14 | watch(isActive, (val) => {
15 | if (val) {
16 | scope = effectScope()
17 | scope.run(() => {
18 | stack.value.push(vm)
19 |
20 | onScopeDispose(() => {
21 | const idx = stack.value.indexOf(vm)
22 | stack.value.splice(idx, 1)
23 | })
24 | })
25 | }
26 | else {
27 | scope?.stop()
28 | }
29 | }, { immediate: true })
30 |
31 | const isTop = computed(() => {
32 | return toRaw(stack.value[stack.value.length - 1]) === vm
33 | })
34 |
35 | return {
36 | isTop,
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/GDialogRoot.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
41 |
--------------------------------------------------------------------------------
/src/composables/sizeStyle.ts:
--------------------------------------------------------------------------------
1 | import type { ComputedRef } from 'vue'
2 |
3 | import { computed } from 'vue'
4 |
5 | import { convertToUnit } from '../util'
6 |
7 | export interface UseSizeStyleProps {
8 | maxWidth?: number | string
9 | width?: number | string
10 | height?: number | string
11 | }
12 |
13 | type UseSizeStyleReturnType = {
14 | sizeStyles: ComputedRef<{
15 | maxWidth: string | undefined
16 | width: string | undefined
17 | height: string | undefined
18 | }>
19 | }
20 |
21 | export const useSizeStyle = (props: UseSizeStyleProps): UseSizeStyleReturnType => {
22 | const sizeStyles = computed(() => ({
23 | maxWidth:
24 | props.maxWidth === 'none'
25 | ? undefined
26 | : convertToUnit(props.maxWidth),
27 |
28 | width:
29 | props.width === 'auto'
30 | ? undefined
31 | : convertToUnit(props.width),
32 |
33 | height:
34 | props.height === 'auto'
35 | ? undefined
36 | : convertToUnit(props.height),
37 | }))
38 |
39 | return {
40 | sizeStyles,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Misha Kryvoruchko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
11 | The MIT License (MIT)
12 |
--------------------------------------------------------------------------------
/src/types/Plugin.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Component, InjectionKey, ShallowUnwrapRef,
3 | } from 'vue'
4 |
5 | export type IDialogItemId = number | string
6 |
7 | export type DialogOnCloseEvent = {
8 | id: IDialogItemId
9 | item: IDialogItem
10 | cancel: () => void
11 | }
12 |
13 | export interface IDialogItem {
14 | component: ShallowUnwrapRef
15 | id: IDialogItemId
16 | props: {
17 | modelValue: boolean
18 | }
19 | onClose?: ((event: DialogOnCloseEvent) => void) | undefined
20 | }
21 |
22 | type DialogAddMethod = (params: {
23 | component: Component
24 | props?: Omit | undefined
25 | id?: IDialogItemId | undefined
26 | }, hooks?: {
27 | onClose?: ((event: DialogOnCloseEvent) => void) | undefined
28 | }) => IDialogItemId
29 |
30 | type DialogRemoveMethod = (
31 | id: IDialogItemId,
32 | closeDelay?: number
33 | ) => void
34 |
35 | interface IDialogMethods {
36 | addDialog: DialogAddMethod
37 | removeDialog: DialogRemoveMethod
38 | }
39 |
40 | export interface IDialog extends IDialogMethods {
41 | dialogs: IDialogItem[]
42 | }
43 |
44 | export type DialogInjectionKey = InjectionKey
45 |
--------------------------------------------------------------------------------
/src/composables/lazyActivation.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 |
3 | import { nextTick, ref, watch } from 'vue'
4 |
5 | type UseLazyActivationFunc = (baseState: Ref) => {
6 | /**
7 | * determine if `baseState' was `true` at least once
8 | */
9 | activatedOnce: Ref
10 |
11 | /**
12 | * proxy value of baseState.
13 | */
14 | active: Ref
15 | }
16 |
17 | /**
18 | * make first activation lazy: `activatedOnce` changes immediately,
19 | * `active` changes on `nextTick`
20 | */
21 | export const useLazyActivation: UseLazyActivationFunc = (baseState) => {
22 | const activatedOnce = ref(false)
23 | const active = ref(false)
24 |
25 | if (baseState.value) {
26 | activatedOnce.value = true
27 | nextTick(() => {
28 | active.value = true
29 | })
30 | }
31 |
32 | watch(baseState, (value) => {
33 | // lazy first activation
34 | if (!activatedOnce.value) {
35 | activatedOnce.value = true
36 | nextTick(() => {
37 | active.value = value
38 | })
39 |
40 | return
41 | }
42 |
43 | active.value = value
44 | })
45 |
46 | return {
47 | activatedOnce,
48 | active,
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/dev/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { defineConfig } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import AutoImport from 'unplugin-auto-import/vite'
5 | import Components from 'unplugin-vue-components/vite'
6 | import WindiCSS from 'vite-plugin-windicss'
7 |
8 | const resolve = (str: string) => path.resolve(__dirname, str)
9 |
10 | export default defineConfig({
11 | root: resolve('.'),
12 |
13 | plugins: [
14 | vue(),
15 | AutoImport({
16 | imports: [
17 | 'vue',
18 | {
19 | 'gitart-vue-dialog': [
20 | 'dialogInjectionKey',
21 | ['plugin', 'dialogPlugin'],
22 | ],
23 | },
24 | ],
25 | dts: resolve('auto-imports.d.ts'),
26 | }),
27 | Components({
28 | resolvers: [
29 | (name: string) => {
30 | if ([
31 | 'GDialog',
32 | 'GDialogRoot',
33 | ].includes(name))
34 | return { importName: name, path: 'gitart-vue-dialog' }
35 |
36 | return null
37 | },
38 | ],
39 | dts: resolve('components.d.ts'),
40 | }),
41 | WindiCSS({
42 | config: resolve('windi.config.ts'),
43 | }),
44 | ],
45 |
46 | server: {
47 | fs: {
48 | strict: false,
49 | allow: [resolve('..')],
50 | },
51 | },
52 |
53 | resolve: {
54 | alias: {
55 | 'gitart-vue-dialog': resolve('../src/index.ts'),
56 | },
57 | },
58 |
59 | build: {
60 | sourcemap: true,
61 | },
62 | })
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://savelife.in.ua/en/)
2 |
3 | 
4 |
5 | Gitart Vue Dialog
6 |
7 | Vue 3 Dialog/Modal Component + Plugin (optional)
8 |
9 | ---
10 |
11 | 📘 [Documentation](https://gitart-vue-dialog.gitart.org/)
12 |
13 | 🤯 [Examples](https://examples.gitart-vue-dialog.gitart.org/)
14 |
15 | ⭐ [You can star it here, thanks :)](https://github.com/gitart-group/vue-dialog)
16 |
17 | Typescript support, customizable, beautifully animated, SSR
18 |
19 | ~13 KiB - index.cjs `gitart-vue-dialog`
20 | ~1.8 KiB - style.css `gitart-vue-dialog/dist/style.css`
21 |
22 |
23 | ## Installation
24 |
25 | ### Standalone Component
26 |
27 | ```js
28 | // main.js or YourComponent.vue
29 | import 'gitart-vue-dialog/dist/style.css'
30 | ```
31 |
32 | ```js
33 | // YourComponent.vue
34 | import { GDialog } from 'gitart-vue-dialog'
35 |
36 | export default {
37 | components: {
38 | GDialog,
39 | },
40 | }
41 | ```
42 |
43 | ### Plugin
44 |
45 | Be sure to read the [documentation](http://gitart-vue-dialog.gitart.org/) for using the plugin
46 |
47 | ```js
48 | import App from './App.vue'
49 | import { plugin as dialogPlugin } from 'gitart-vue-dialog'
50 |
51 | createApp(App)
52 | .use(dialogPlugin)
53 | .mount('#app')
54 | ```
55 |
56 |
57 |
58 | ## Usage
59 |
60 | Read [Documentation](http://gitart-vue-dialog.gitart.org/)
61 |
62 |
63 |
64 |
65 | Made in Ukraine 🇺🇦 by [Mykhailo Kryvorucho](https://twitter.com/MichaelGitart)
--------------------------------------------------------------------------------
/tests/unit/composables/lazyActivation.spec.ts:
--------------------------------------------------------------------------------
1 | import { nextTick, ref } from 'vue'
2 | import { useLazyActivation } from '../../../src/composables/lazyActivation'
3 |
4 | describe('Test lazyActivation composable', () => {
5 | it('Test `true` by default. The activation should be delayed', async() => {
6 | const modelValue = ref(true)
7 |
8 | const {
9 | activatedOnce,
10 | active,
11 | } = useLazyActivation(modelValue)
12 |
13 | expect(activatedOnce.value).toBe(true)
14 | expect(active.value).toBe(false)
15 |
16 | await nextTick()
17 |
18 | expect(active.value).toBe(true)
19 | })
20 |
21 | it('Test `false` by default. All should be false', async() => {
22 | const modelValue = ref(false)
23 |
24 | const {
25 | activatedOnce,
26 | active,
27 | } = useLazyActivation(modelValue)
28 |
29 | expect(activatedOnce.value).toBe(false)
30 | expect(active.value).toBe(false)
31 | })
32 |
33 | it('The first activation must be delayed', async() => {
34 | const modelValue = ref(false)
35 |
36 | const {
37 | activatedOnce,
38 | active,
39 | } = useLazyActivation(modelValue)
40 |
41 | modelValue.value = true
42 | await nextTick()
43 |
44 | expect(activatedOnce.value).toBe(true)
45 | expect(active.value).toBe(false)
46 |
47 | await nextTick()
48 |
49 | expect(active.value).toBe(true)
50 | })
51 |
52 | it('Subsequent activations must be immediate', async() => {
53 | const modelValue = ref(false)
54 |
55 | const {
56 | activatedOnce,
57 | active,
58 | } = useLazyActivation(modelValue)
59 |
60 | // first activation
61 |
62 | modelValue.value = true
63 |
64 | await nextTick()
65 | await nextTick()
66 |
67 | expect(activatedOnce.value).toBe(true)
68 | expect(active.value).toBe(true)
69 |
70 | // deactivation without delay
71 |
72 | modelValue.value = false
73 | await nextTick()
74 |
75 | expect(active.value).toBe(false)
76 |
77 | // activation without delay
78 |
79 | modelValue.value = true
80 | await nextTick()
81 |
82 | expect(active.value).toBe(true)
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/src/util/scroll.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A copy of the vuetify implemantation
3 | * https://github.com/vuetifyjs/vuetify/blob/v2.5.8/packages/vuetify/src/mixins/overlayable/index.ts
4 | */
5 |
6 | /**
7 | * Polyfill for Event.prototype.composedPath
8 | */
9 | // const composedPath = (e: WheelEvent): EventTarget[] => {
10 | // if (e.composedPath) return e.composedPath()
11 |
12 | // const path = []
13 | // let el = e.target as Element | null
14 |
15 | // while (el) {
16 | // path.push(el)
17 |
18 | // if (el.tagName === 'HTML') {
19 | // path.push(document)
20 | // path.push(window)
21 |
22 | // return path
23 | // }
24 |
25 | // el = el.parentElement
26 | // }
27 | // return path
28 | // }
29 |
30 | const hasScrollbar = (el?: Element) => {
31 | if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
32 |
33 | const style = window.getComputedStyle(el)
34 | return ['auto', 'scroll'].includes(style.overflowY) && el.scrollHeight > el.clientHeight
35 | }
36 |
37 | const shouldScroll = (el: Element, delta: number) => {
38 | if (el.scrollTop === 0 && delta < 0) return true
39 | return el.scrollTop + el.clientHeight === el.scrollHeight && delta > 0
40 | }
41 |
42 | export const noScrollableParent = (event: WheelEvent, content: Element | undefined): boolean => {
43 | const path = event.composedPath()
44 | const delta = event.deltaY
45 |
46 | for (let index = 0; index < path.length; index++) {
47 | const el = path[index]
48 |
49 | if (el === document) return true
50 | if (el === document.documentElement) return true
51 | if (el === content) return true
52 |
53 | if (hasScrollbar(el as Element)) return shouldScroll(el as Element, delta)
54 | }
55 |
56 | return true
57 | }
58 |
59 | export const getScrollbarWidth = (): number => {
60 | const container = document.createElement('div')
61 | container.style.visibility = 'hidden'
62 | container.style.overflow = 'scroll'
63 | const inner = document.createElement('div')
64 |
65 | container.appendChild(inner)
66 | document.body.appendChild(container)
67 | const scrollbarWidth = container.offsetWidth - inner.offsetWidth
68 | document.body.removeChild(container)
69 |
70 | return scrollbarWidth
71 | }
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitart-vue-dialog",
3 | "version": "3.1.0",
4 | "scripts": {
5 | "dev": "vite --config dev/vite.config.ts",
6 | "build": "node build/build.js",
7 | "build:types": "vue-tsc --declaration --emitDeclarationOnly --p ./build/tsconfig.json",
8 | "lint": "yarn stylelint && yarn eslint",
9 | "stylelint": "stylelint src/**/*.vue",
10 | "eslint": "eslint . --ext .js,.ts,.vue",
11 | "test": "vitest"
12 | },
13 | "main": "./dist/index.cjs",
14 | "module": "./dist/index.mjs",
15 | "exports": {
16 | ".": {
17 | "types": "./dist/index.d.ts",
18 | "import": "./dist/index.mjs",
19 | "require": "./dist/index.cjs"
20 | },
21 | "./dist/*": "./dist/*",
22 | "./package.json": "./package.json"
23 | },
24 | "types": "./dist/index.d.ts",
25 | "keywords": [
26 | "gitart",
27 | "gitart dialog",
28 | "gitart-vue-dialog",
29 | "vue",
30 | "vue 3",
31 | "vue3",
32 | "dialog",
33 | "modal",
34 | "dialog component",
35 | "modal component",
36 | "vue modal",
37 | "vue3 modal",
38 | "vue dialog",
39 | "vue3 dialog"
40 | ],
41 | "repository": {
42 | "type": "git",
43 | "url": "https://github.com/gitart-group/vue-dialog.git",
44 | "directory": "."
45 | },
46 | "author": {
47 | "name": "Michael Gitart",
48 | "email": "michaelgitart@gmail.com"
49 | },
50 | "license": "MIT",
51 | "devDependencies": {
52 | "@gitart/eslint-config-vue": "0.3.0",
53 | "@types/node": "^18.16.16",
54 | "@vitejs/plugin-vue": "^4.2.3",
55 | "@vue/compiler-sfc": "^3.3.4",
56 | "@vue/runtime-core": "^3.3.4",
57 | "@vue/test-utils": "^2.3.2",
58 | "@vueuse/core": "^10.1.2",
59 | "eslint": "^8.42.0",
60 | "happy-dom": "^9.20.3",
61 | "sass": "^1.62.1",
62 | "shelljs": "^0.8.5",
63 | "stylelint": "^15.7.0",
64 | "stylelint-config-standard": "^33.0.0",
65 | "typescript": "^5.1.3",
66 | "unplugin-auto-import": "^0.16.4",
67 | "unplugin-vue-components": "^0.25.1",
68 | "vite": "^3.2.7",
69 | "vite-plugin-windicss": "^1.9.0",
70 | "vitest": "^0.32.0",
71 | "vue": "^3.3.4",
72 | "vue-tsc": "^1.6.5",
73 | "windicss": "^3.5.6"
74 | },
75 | "peerDependencies": {
76 | "vue": "^3.2.6"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/composables/scroll.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 |
3 | import { IN_BROWSER, getScrollbarWidth, noScrollableParent } from '../util'
4 |
5 | type UseScrollParams = {
6 | overlay: Ref
7 | content: Ref
8 | contentActiveClass: string
9 | fullscreen: boolean
10 | contentFullscreenClass: string
11 | }
12 | interface UseScrollReturnType {
13 | disableScroll: () => void
14 | enableScroll: () => void
15 | }
16 |
17 | type UseScroll = (params: UseScrollParams) => UseScrollReturnType
18 |
19 | export const useScroll: UseScroll = ({
20 | overlay,
21 | content,
22 | contentActiveClass,
23 | fullscreen,
24 | contentFullscreenClass,
25 | }) => {
26 | if (!IN_BROWSER) {
27 | return {
28 | disableScroll: () => {},
29 | enableScroll: () => {},
30 | }
31 | }
32 |
33 | let disabled = false
34 | let disableType: 'byEvents' | 'byOverflow'
35 |
36 | const eventListener = (event: WheelEvent) => {
37 | if (event.target === overlay.value
38 | || event.target === document.body
39 | || noScrollableParent(event, content.value)
40 | )
41 | event.preventDefault()
42 | }
43 |
44 | const scrollbarWidth = getScrollbarWidth()
45 | const zeroScrollBar = scrollbarWidth === 0
46 |
47 | const disableScroll = () => {
48 | if (disabled)
49 | return
50 |
51 | // The mobile has the scroll bar width of 0
52 | // hide the scroll bar for fullscreen mode
53 | if (zeroScrollBar || fullscreen) {
54 | disableType = 'byOverflow'
55 | document.documentElement.classList.add('overflow-y-hidden')
56 | }
57 | else {
58 | disableType = 'byEvents'
59 | window.addEventListener('wheel', eventListener, {
60 | passive: false,
61 | })
62 | }
63 |
64 | disabled = true
65 | }
66 |
67 | const enableScroll = () => {
68 | if (!disabled)
69 | return
70 |
71 | if (disableType === 'byEvents') {
72 | window.removeEventListener('wheel', eventListener)
73 | }
74 | else if (disableType === 'byOverflow') {
75 | const activeContentElements = document.getElementsByClassName(contentActiveClass)
76 | const activeFullscreenContentElements = document.getElementsByClassName(contentFullscreenClass)
77 |
78 | if ((!zeroScrollBar && fullscreen && activeFullscreenContentElements.length === 1) || activeContentElements.length === 1)
79 | document.documentElement.classList.remove('overflow-y-hidden')
80 | }
81 |
82 | disabled = false
83 | }
84 |
85 | return {
86 | disableScroll,
87 | enableScroll,
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/dev/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | // We suggest you to commit this file into source control
3 | declare global {
4 | const computed: typeof import('vue')['computed']
5 | const createApp: typeof import('vue')['createApp']
6 | const customRef: typeof import('vue')['customRef']
7 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
8 | const defineComponent: typeof import('vue')['defineComponent']
9 | const dialogInjectionKey: typeof import('gitart-vue-dialog')['dialogInjectionKey']
10 | const dialogPlugin: typeof import('gitart-vue-dialog')['plugin']
11 | const effectScope: typeof import('vue')['effectScope']
12 | const EffectScope: typeof import('vue')['EffectScope']
13 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
14 | const getCurrentScope: typeof import('vue')['getCurrentScope']
15 | const h: typeof import('vue')['h']
16 | const inject: typeof import('vue')['inject']
17 | const isReadonly: typeof import('vue')['isReadonly']
18 | const isRef: typeof import('vue')['isRef']
19 | const markRaw: typeof import('vue')['markRaw']
20 | const nextTick: typeof import('vue')['nextTick']
21 | const onActivated: typeof import('vue')['onActivated']
22 | const onBeforeMount: typeof import('vue')['onBeforeMount']
23 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
24 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
25 | const onDeactivated: typeof import('vue')['onDeactivated']
26 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
27 | const onMounted: typeof import('vue')['onMounted']
28 | const onRenderTracked: typeof import('vue')['onRenderTracked']
29 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
30 | const onScopeDispose: typeof import('vue')['onScopeDispose']
31 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
32 | const onUnmounted: typeof import('vue')['onUnmounted']
33 | const onUpdated: typeof import('vue')['onUpdated']
34 | const provide: typeof import('vue')['provide']
35 | const reactive: typeof import('vue')['reactive']
36 | const readonly: typeof import('vue')['readonly']
37 | const ref: typeof import('vue')['ref']
38 | const resolveComponent: typeof import('vue')['resolveComponent']
39 | const shallowReactive: typeof import('vue')['shallowReactive']
40 | const shallowReadonly: typeof import('vue')['shallowReadonly']
41 | const shallowRef: typeof import('vue')['shallowRef']
42 | const toRaw: typeof import('vue')['toRaw']
43 | const toRef: typeof import('vue')['toRef']
44 | const toRefs: typeof import('vue')['toRefs']
45 | const triggerRef: typeof import('vue')['triggerRef']
46 | const unref: typeof import('vue')['unref']
47 | const useAttrs: typeof import('vue')['useAttrs']
48 | const useCssModule: typeof import('vue')['useCssModule']
49 | const useSlots: typeof import('vue')['useSlots']
50 | const watch: typeof import('vue')['watch']
51 | const watchEffect: typeof import('vue')['watchEffect']
52 | }
53 | export {}
54 |
--------------------------------------------------------------------------------
/src/components/GDialogOverlay.vue:
--------------------------------------------------------------------------------
1 |
86 |
87 |
88 |
89 |
90 |
95 |
96 |
97 |
98 |
99 |
139 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from 'vue'
2 | import { reactive, shallowReactive } from 'vue'
3 |
4 | import type {
5 | DialogInjectionKey, DialogOnCloseEvent, IDialog, IDialogItem,
6 | } from './types/Plugin'
7 |
8 | const dialogs = shallowReactive([])
9 |
10 | export const errorLogger = {
11 | pluginIsNotInitialized(): void {
12 | console.error('The gitart-vue-dialog plugin is not initialized. Read how to solve: https://gitart-vue-dialog.gitart.org/guide/usage/plugin-usage.html#installation')
13 | },
14 | }
15 |
16 | /**
17 | * Injection key
18 | *
19 | * Provides access to plugin methods and properties using the vue inject method
20 | *
21 | * https://gitart-vue-dialog.gitart.org/guide/usage/plugin-usage.html#usage
22 | *
23 | * @example Usage
24 | * const {
25 | * dialogs,
26 | * removeDialog,
27 | * } = inject(dialogInjectionKey)!
28 | */
29 | export const dialogInjectionKey: DialogInjectionKey = Symbol('GDialog')
30 |
31 | export const dialogInjectionFallback: IDialog = {
32 | dialogs: [],
33 | addDialog: () => {
34 | errorLogger.pluginIsNotInitialized()
35 | return null as any
36 | },
37 | removeDialog: () => {
38 | errorLogger.pluginIsNotInitialized()
39 | },
40 | }
41 |
42 | /**
43 | * Plugin to install
44 | *
45 | * https://gitart-vue-dialog.gitart.org/guide/usage/plugin-usage.html - Documentation
46 | *
47 | * @example installation
48 | * import { plugin as dialogPlugin } from 'gitart-vue-dialog'
49 | * createApp(App)
50 | * .use(dialogPlugin)
51 | * .mount('#app')
52 | */
53 | export const plugin: Plugin = {
54 | install: (app, options) => {
55 | const defaultCloseDelay = options?.closeDelay ?? 500
56 | const defaultProps = options?.props ?? {}
57 |
58 | const $dialog: IDialog = {
59 | dialogs,
60 |
61 | addDialog: ({ component, props, id }, hooks) => {
62 | const dialogId = id ?? Date.now() + Math.random()
63 |
64 | dialogs.push({
65 | component,
66 | id: dialogId,
67 |
68 | props: reactive({
69 | modelValue: true,
70 | ...defaultProps,
71 | ...props,
72 | }),
73 |
74 | onClose: hooks?.onClose,
75 | })
76 |
77 | return dialogId
78 | },
79 |
80 | removeDialog: (id, closeDelay) => {
81 | const dialog = dialogs.find(d => d.id === id)
82 |
83 | if (!dialog || !dialog.props.modelValue)
84 | return
85 |
86 | let canceled = false
87 | const event: DialogOnCloseEvent = {
88 | id,
89 | cancel: () => {
90 | console.warn('Dialog closing canceled')
91 | canceled = true
92 | },
93 | item: dialog,
94 | }
95 |
96 | if (dialog.onClose) {
97 | dialog.onClose(event)
98 |
99 | if (canceled)
100 | return
101 | }
102 |
103 | dialog.props.modelValue = false
104 | setTimeout(() => {
105 | dialogs.splice(dialogs.indexOf(dialog), 1)
106 | }, closeDelay ?? defaultCloseDelay)
107 | },
108 | }
109 |
110 | app.provide(dialogInjectionKey, $dialog)
111 | app.config.globalProperties.$dialog = $dialog
112 | },
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/GDialogContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
100 |
101 |
141 |
--------------------------------------------------------------------------------
/src/components/GDialogFrame.vue:
--------------------------------------------------------------------------------
1 |
116 |
117 |
118 |
119 |
125 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
181 |
--------------------------------------------------------------------------------
/src/assets/gitart-dialog-logo.svg:
--------------------------------------------------------------------------------
1 |
59 |
--------------------------------------------------------------------------------
/src/components/GDialog.vue:
--------------------------------------------------------------------------------
1 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
292 |
293 |
294 |
295 |
312 |
313 |
314 |
315 |
316 |
317 |
--------------------------------------------------------------------------------