├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ ├── ---feature-request.md │ ├── ---say-thank-you.md │ └── -question.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── app.config.ts ├── app.vue ├── components │ └── content │ │ ├── BuyMeACoffee.vue │ │ ├── LoginForm.vue │ │ ├── LoginFormVorms.vue │ │ ├── ModalBottom.vue │ │ ├── ModalBottomPreview.vue │ │ ├── ModalConfirm.vue │ │ ├── ModalConfirmPlainCss.vue │ │ ├── ModalConfirmPlainCssPreview.vue │ │ ├── ModalConfirmPreview.vue │ │ ├── ModalDragResize.vue │ │ ├── ModalDragResizePreview.vue │ │ ├── ModalFullscreen.vue │ │ ├── ModalFullscreenPreview.vue │ │ ├── ModalIdPreview.vue │ │ ├── ModalLoginForm.vue │ │ ├── ModalLoginFormPreview.vue │ │ ├── ModalLongScroll.vue │ │ ├── ModalLongScrollPreview.vue │ │ ├── ModalNestedPreview.vue │ │ ├── Playground.vue │ │ ├── ReadMore.vue │ │ ├── UseModalPreview.vue │ │ ├── VButton.vue │ │ └── VModelPreview.vue ├── content │ ├── 1.index.md │ ├── 2.get-started │ │ ├── .2.advanced │ │ │ ├── 1.Testing.md │ │ │ └── _dir.yml │ │ ├── 1.guide │ │ │ ├── 1.concepts.md │ │ │ ├── 2.setup.md │ │ │ ├── 3.migration-guide.md │ │ │ ├── 4.types.md │ │ │ └── _dir.yml │ │ ├── 3.migration │ │ │ └── _dir.yml │ │ └── _dir.yml │ ├── 3.api │ │ ├── 1.components │ │ │ ├── 1.vue-final-modal.md │ │ │ ├── 2.modals-container.md │ │ │ └── _dir.yml │ │ ├── 2.composables │ │ │ ├── 1.use-modal.md │ │ │ ├── 2.use-vfm.md │ │ │ └── _dir.yml │ │ └── _dir.yml │ └── 4.use-cases │ │ ├── .2.modal-transition.md │ │ ├── 1.playground.md │ │ ├── 3.confirm-modal.md │ │ ├── 4.modal-nested.md │ │ ├── 5.modal-fullscreen.md │ │ ├── 6.modal-bottom.md │ │ ├── 7.modal-login-form.md │ │ ├── 8.modal-drag-resize.md │ │ └── 9.modal-long-scroll.md ├── index.d.ts ├── nuxt.config.ts ├── package.json ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icon.png │ ├── logo-dark.svg │ ├── logo-light.svg │ ├── logo-new.svg │ ├── preview.png │ └── site.webmanifest ├── renovate.json ├── tailwind.config.ts ├── tokens.config.ts └── tsconfig.json ├── examples ├── nuxt3 │ ├── .gitignore │ ├── app.vue │ ├── components │ │ ├── MyModal.vue │ │ ├── MyModalPreview.vue │ │ └── VButton.vue │ ├── nuxt.config.ts │ ├── package.json │ ├── pages │ │ └── index.vue │ ├── tailwind.config.ts │ └── tsconfig.json └── vue3 │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ └── vite.svg │ ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── components │ │ ├── MyModal.vue │ │ ├── MyModalPreview.vue │ │ └── VButton.vue │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts │ ├── tailwind.config.cjs │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── netlify.toml ├── package.json ├── packages ├── nuxt │ ├── .gitignore │ ├── .nuxtrc │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── playground │ │ ├── app.vue │ │ ├── components │ │ │ ├── PlainCssConfirmModal.vue │ │ │ ├── PlainCssConfirmModalPreview.vue │ │ │ └── showConfirmModal.ts │ │ ├── nuxt.config.ts │ │ └── package.json │ ├── src │ │ ├── module.ts │ │ └── runtime │ │ │ └── plugin.ts │ └── tsconfig.json └── vue-final-modal │ ├── .gitignore │ ├── .release-it.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── cypress.config.ts │ ├── cypress │ ├── components │ │ ├── App.vue │ │ ├── Form.vue │ │ ├── ModalCloseByScopedSlot.vue │ │ ├── focusTrap.spec.ts │ │ ├── scopedSlot.spec.ts │ │ ├── useModal.spec.ts │ │ └── useZIndex.spec.ts │ └── support │ │ ├── commands.ts │ │ ├── component-index.html │ │ └── component.ts │ ├── package.json │ ├── src │ ├── Component.ts │ ├── Modal.ts │ ├── components │ │ ├── ModalsContainer.vue │ │ └── VueFinalModal │ │ │ ├── VueFinalModal.vue │ │ │ ├── VueFinalModalProps.ts │ │ │ ├── useBodyScrollLock.ts │ │ │ ├── useFocusTrap.ts │ │ │ ├── useModelValue.ts │ │ │ ├── useToClose.ts │ │ │ ├── useTransition.ts │ │ │ ├── useZIndex.ts │ │ │ └── vVisible.ts │ ├── dom.ts │ ├── global.d.ts │ ├── index.ts │ ├── injectionSymbols.ts │ ├── plugin.ts │ ├── useApi.ts │ ├── useSwipeToClose.ts │ ├── useSwipeable.ts │ └── utils.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── viteplay ├── .gitignore ├── 404.vue ├── App.vue ├── app.ts ├── index.html ├── package.json ├── public └── favicon.ico ├── router.ts ├── src └── components │ ├── DefaultSlot.vue │ └── VueFinalModal │ ├── Basic.example.vue │ ├── BottomSheet.example.vue │ ├── ConfirmModal.vue │ ├── Fullscreen.example.vue │ ├── NestedModal.example.vue │ ├── StopEvent.example.vue │ ├── TestModal.vue │ └── modalsHelpers.ts ├── tsconfig.json ├── vite.config.ts └── viteplay.iframe.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .cache -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu", 3 | "rules": { 4 | "vue/multi-word-component-names": "off", 5 | "@typescript-eslint/consistent-type-definitions": ["off"] 6 | }, 7 | "ignorePatterns": ["**/*.md"] 8 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: "If something isn't working \U0001F915" 4 | title: '' 5 | labels: bug 6 | assignees: hunterliu1003 7 | 8 | --- 9 | 10 | 12 | 13 | ### Version 14 | 15 | vue-final-modal: 16 | vue: 17 | nuxt: 18 | 19 | ### OS 20 | 21 | Mac or Windows 22 | 23 | ### Reproduction Link 24 | 29 | 30 | ### Steps to reproduce 31 | 32 | 33 | ### What is Expected? 34 | 35 | 36 | ### What is actually happening? 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: "Let us know if you have a feature request \U0001F4A1" 4 | title: '' 5 | labels: enhancement 6 | assignees: hunterliu1003 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | 12 | 13 | ### Describe the solution you'd like 14 | 15 | 16 | ### Describe alternatives you've considered 17 | 18 | 19 | ### Additional context 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---say-thank-you.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❤️ Say thank you" 3 | about: Tell us how you use vue-final-modal & support our efforts in maintaining vue-final-modal 4 | title: '' 5 | labels: '' 6 | assignees: hunterliu1003 7 | 8 | --- 9 | 10 | # ❤️ I'm using **`vue-final-modal`** 11 | 12 | If you (or your company) are using **`vue-final-modal`** - please let us know. We'd love to hear from you! 13 | 14 | **`vue-final-modal`** is a community driven project, which means it's not maintained by a company. If you would like to help `vue-final-modal` - any of the following is greatly appreciated. 15 | 16 | - [ ] Give the repository a star ⭐️ 17 | - [ ] Help out with issues 18 | - [ ] Review pull requests 19 | - [ ] Blog about vue-final-modal 20 | - [ ] Make tutorials 21 | - [ ] Give talks 22 | 23 | Thank you! 💐 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓Question" 3 | about: Ask a question about vue-final-modal 4 | title: '' 5 | labels: question 6 | assignees: hunterliu1003 7 | 8 | --- 9 | 10 | 12 | 13 | ### Version 14 | 15 | vue-final-modal: 16 | vue: 17 | nuxt: 18 | 19 | ### OS 20 | 21 | Mac or Windows 22 | 23 | ### Reproduction Link 24 | 29 | 30 | ### Steps to reproduce 31 | 32 | 33 | ### What is Expected? 34 | 35 | 36 | ### What is actually happening? 37 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - [ ] Feature 2 | - [ ] Feature review 3 | - [ ] Unit test 4 | - [ ] Unit test review 5 | - [ ] Docs 6 | - [ ] Docs review 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 4 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vite-ssg-dist 3 | .vite-ssg-temp 4 | *.local 5 | dist 6 | dist-ssr 7 | node_modules 8 | .idea/ 9 | *.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 4 | }, 5 | "[vue]": { 6 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 7 | } 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Chung Hang (Hunter) Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Final Modal 4 2 | 3 | The most powerful yet most light-weight modal library for Vue 3. 4 | 5 |

Vue Final Modal Logo

6 | 7 |

8 | Downloads 9 | License 10 | Netlify Status 11 |

12 |

13 | Version 14 |

15 | 16 |

17 | 18 | Buy Me A Coffee 19 | 20 |

21 | 22 | ## Playground 23 | 24 | - [Stackblitz for Vue 3](https://stackblitz.com/github/vue-final/vue-final-modal/tree/master/examples/vue3) 25 | - [Stackblitz for Nuxt 3](https://stackblitz.com/github/vue-final/vue-final-modal/tree/master/examples/nuxt3) 26 | 27 | ## [Documentation](https://v4.vue-final-modal.org/) 28 | 29 | Checkout [Migration guide from v3](https://v4.vue-final-modal.org/get-started/guide/migration-guide). 30 | 31 | Looking for old version? 32 | 33 | - [vue-final-modal@3.x for Vue 3](https://v3.vue-final-modal.org/) 34 | - [vue-final-modal@2.x for Vue 2](https://v2.vue-final-modal.org/) 35 | 36 | ## Contribution Guide 37 | 38 | ```bash [pnpm] 39 | # Install packages 40 | pnpm install --shamefully-hoist 41 | 42 | # Build vue-final-modal library first 43 | pnpm build:vfm 44 | 45 | # Run both docs and viteplay 46 | pnpm dev 47 | 48 | # Run dev for vue-final-modal 49 | pnpm dev:vfm 50 | 51 | # Run docs: http://localhost:3000/ 52 | pnpm dev:docs 53 | 54 | # Run viteplay: http://localhost:5173/ 55 | pnpm dev:viteplay 56 | ``` 57 | 58 | ## Contributors 59 | 60 | Thank you to all the people who already contributed to `vue-final-modal`! 61 | 62 | 63 | contributors 64 | 65 | 66 | Made with [contributors-img](https://contrib.rocks). 67 | 68 | 🚀 If you have any ideas for optimization of `vue-final-modal`, feel free to open [issues](https://github.com/hunterliu1003/vue-final-modal/issues) or [pull requests](https://github.com/hunterliu1003/vue-final-modal/pulls). 69 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | title: 'Vue Final Modal', 4 | description: 'Vue Final Modal is the most powerful yet most light-weight modal library for Vue 3', 5 | layout: 'default', 6 | image: 'https://vue-final-modal.org/preview.png', 7 | url: 'https://vue-final-modal.org', 8 | socials: { 9 | twitter: '@hunterliu1003', 10 | github: 'vue-final/vue-final-modal', 11 | }, 12 | aside: { 13 | level: 1, 14 | }, 15 | header: { 16 | logo: { 17 | dark: '/logo-new.svg', 18 | light: '/logo-new.svg', 19 | }, 20 | }, 21 | footer: {}, 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/components/content/BuyMeACoffee.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/components/content/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /docs/components/content/LoginFormVorms.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 78 | 79 | 117 | -------------------------------------------------------------------------------- /docs/components/content/ModalBottom.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | -------------------------------------------------------------------------------- /docs/components/content/ModalBottomPreview.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /docs/components/content/ModalConfirm.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /docs/components/content/ModalConfirmPlainCss.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 57 | -------------------------------------------------------------------------------- /docs/components/content/ModalConfirmPlainCssPreview.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /docs/components/content/ModalConfirmPreview.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /docs/components/content/ModalDragResize.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | -------------------------------------------------------------------------------- /docs/components/content/ModalDragResizePreview.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /docs/components/content/ModalFullscreen.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /docs/components/content/ModalFullscreenPreview.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /docs/components/content/ModalIdPreview.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /docs/components/content/ModalLoginForm.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /docs/components/content/ModalLoginFormPreview.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /docs/components/content/ModalLongScroll.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /docs/components/content/ModalLongScrollPreview.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 34 | -------------------------------------------------------------------------------- /docs/components/content/ModalNestedPreview.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 36 | -------------------------------------------------------------------------------- /docs/components/content/Playground.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 123 | 124 | 129 | -------------------------------------------------------------------------------- /docs/components/content/ReadMore.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /docs/components/content/UseModalPreview.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /docs/components/content/VButton.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 37 | 38 | 55 | -------------------------------------------------------------------------------- /docs/components/content/VModelPreview.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /docs/content/1.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Vue Final Modal" 3 | description: "Vue Final Modal is the most powerful yet most light-weight modal library for Vue 3" 4 | navigation: false 5 | layout: page 6 | --- 7 | 8 | ::block-hero 9 | --- 10 | cta: 11 | - Get Started 12 | - /get-started/guide/concepts 13 | secondary: 14 | - Star on GitHub 15 | - https://github.com/vue-final/vue-final-modal 16 | snippet: npm install vue-final-modal@latest 17 | --- 18 | 19 | #title 20 | Modals made easy for Vue Developers 21 | 22 | #description 23 | The most powerful yet most light-weight modal library for Vue 3. 24 | 25 | #right 26 | :: 27 | 28 | ::buy-me-a-coffee 29 | :: 30 | 31 | ::card-grid 32 | #title 33 | Powerful Features 34 | 35 | #default 36 | ::card 37 | --- 38 | icon: ph:file-css-duotone 39 | --- 40 | #title 41 | Custom Styling 42 | #description 43 | Write your own CSS or bring libraries like Tailwind/WindiCSS. 44 | :: 45 | 46 | ::card 47 | --- 48 | icon: ph:file-code 49 | --- 50 | #title 51 | useModal 52 | #description 53 | Open and close modal programmatically with `useModal()`{lang=ts} composable function. 54 | :: 55 | 56 | ::card 57 | --- 58 | icon: tabler:brand-nuxt 59 | --- 60 | #title 61 | Support Nuxt 3 62 | #description 63 | SSR support with ``{lang=ts} by default. 64 | :: 65 | 66 | ::card 67 | --- 68 | icon: ph:file-ts 69 | --- 70 | #title 71 | Rewrite with Typescript 72 | #description 73 | Rewrite with Typescript for better DX. 74 | :: 75 | 76 | ::card 77 | --- 78 | icon: icon-park-outline:christmas-tree-one 79 | --- 80 | #title 81 | Only 7.x kB! 82 | #description 83 | Vue Final Modal focuses on the core functionality of modals, leaves the complex CSS up to the developer. 84 | :: 85 | 86 | #root 87 | :: 88 | 89 | ## Contribution Guide 90 | 91 | ```bash [pnpm] 92 | # Install packages 93 | pnpm install --shamefully-hoist 94 | 95 | # Build vue-final-modal library first 96 | pnpm build:vfm 97 | 98 | # Run both docs and viteplay 99 | pnpm dev 100 | 101 | # Run docs: http://localhost:3000/ 102 | pnpm dev:docs 103 | 104 | # Run viteplay: http://localhost:5173/ 105 | pnpm dev:viteplay 106 | ``` 107 | 108 | 109 | ## Contributors 110 | 111 | Thank you to all the people who already contributed to `vue-final-modal`! 112 | 113 | 114 | contributors 115 | 116 | 117 | Made with [contributors-img](https://contrib.rocks). 118 | 119 | 🚀 If you have any ideas for optimization of `vue-final-modal`, feel free to open [issues](https://github.com/hunterliu1003/vue-final-modal/issues) or [pull requests](https://github.com/hunterliu1003/vue-final-modal/pulls). 120 | -------------------------------------------------------------------------------- /docs/content/2.get-started/.2.advanced/1.Testing.md: -------------------------------------------------------------------------------- 1 | # Testing a modal 2 | 3 | ## TBD -------------------------------------------------------------------------------- /docs/content/2.get-started/.2.advanced/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: heroicons-outline:lightning-bolt 2 | -------------------------------------------------------------------------------- /docs/content/2.get-started/1.guide/1.concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | These are the core concepts you need to know about vue-final-modal and its approach to modals management. 4 | 5 | ::alert{type=danger} 6 | ❗️ If you came from vue-final-modal 3.x, please check the [migration guide](/get-started/guide/migration-guide). 7 | :: 8 | 9 | ## What is VueFinalModal? 10 | 11 | `vue-final-modal` is a modal library built specifically for Vue.js and as such it makes some assumptions and enforces "best-practices" for your modals while being versatile and customizable. 12 | 13 | `vue-final-modal` is a collection of [composables](https://vuejs.org/guide/reusability/composables.html#what-is-a-composable) and Vue components, the main things that will be covered is how to use these components to build your own modal components and use them with [useModal()](/api/composables/use-modal) composable function. 14 | 15 | ### Built-in features 16 | 17 | ::list{type=success} 18 | - SSR (Nuxt 3) support. [Teleport](/api/components/vue-final-modal#teleportto) your modals to `'body'`{lang=ts} by default. 19 | - [`useModal()`{lang=ts}](http://localhost:3000/api/composables/use-modal) composable to manage your modal programmatically. 20 | - Trap keyboard focus within modal element (uses [focus-trap](https://github.com/focus-trap/focus-trap) for best web accessibility). 21 | - Full control to the zIndex of the nested modals (see [zIndexFn prop](/api/components/vue-final-modal#zindexfn)). 22 | - Customizable `` for the modal content and overlay. 23 | - Tiny footprint! Only 4.x kB! Vue Final Modal focuses on the core functionality of modals, leaves the complex CSS up to the developer. 24 | :: 25 | 26 | ### A Styleless component 27 | 28 | [\](/api/components/vue-final-modal) is a styleless component that provides the features that modal required as much as possible but leave the layout as a slot to developer. 29 | 30 | So you have to define the modal style by yourself. 31 | 32 | ::alert{type=info} 33 | Follow the [Setup section](/get-started/guide/setup) to learn how to use VueFinalModal. 34 | :: 35 | 36 | ## You Might Not Need VueFinalModal 37 | 38 | Although I created `vue-final-modal`, I still try not to use it as much as possible. 39 | 40 | Here are the articles that I highly recommended you to read before diving into VueFinalModal: 41 | 42 | - [Popups: 10 Problematic Trends and Alternatives](https://www.nngroup.com/articles/popups/) 43 | - [Can Hated Design Elements Be Made to Work?](https://www.nngroup.com/articles/making-hated-design-elements-work/) 44 | - [The Most Hated Online Advertising Techniques](https://www.nngroup.com/articles/most-hated-advertising-techniques/) 45 | - [Modal & Nonmodal Dialogs: When (& When Not) to Use Them](https://www.nngroup.com/articles/modal-nonmodal-dialog/) 46 | - [Modes in User Interfaces: When They Help and When They Hurt Users](https://www.nngroup.com/articles/modes/) 47 | 48 | -------------------------------------------------------------------------------- /docs/content/2.get-started/1.guide/3.migration-guide.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | This document should help you navigate the tricky path of migrating between different versions of vue-final-modal that introduced breaking changes: 4 | 5 | ## Migrating from 3.x to 4.0 6 | 7 | vue-final-modal 4.0 introduced a lot of breaking changes. 8 | 9 | You should treat 4.x as a different library and read the documentation carefully, however these are some pointers towards more of the significant changes. 10 | 11 | To migrate from `3.x` you need to understand some of the changes to the public API and note that there is not direct path to upgrade and if you have a large app and you don't need some of the newer features, consider staying at the 3.x releases. 12 | 13 | ## Step by step 14 | 15 | ### Removed `vfmPlugin` 16 | 17 | #### In Vue 3 18 | 19 | So this: 20 | 21 | ```ts [main.ts] 22 | import { vfmPlugin } from 'vue-final-modal' 23 | 24 | app.use(vfmPlugin) 25 | ``` 26 | 27 | Will be re-written as this: 28 | 29 | ```ts [main.ts] 30 | import { createVfm } from 'vue-final-modal' 31 | const vfm = createVfm() 32 | app.use(vfm) 33 | ``` 34 | 35 | #### In Nuxt 3 36 | 37 | So this: 38 | 39 | ```ts [./plugins/vue-final-modal.ts] 40 | import { defineNuxtPlugin } from '#imports' 41 | import { vfmPlugin } from 'vue-final-modal' 42 | 43 | export default defineNuxtPlugin(nuxtApp => { 44 | nuxtApp.vueApp.use(vfmPlugin) 45 | }) 46 | ``` 47 | 48 | Will be re-written as this: 49 | 50 | ```ts [./plugins/vue-final-modal.ts] 51 | import { createVfm } from 'vue-final-modal' 52 | 53 | export default defineNuxtPlugin((nuxtApp) => { 54 | const vfm = createVfm() as any 55 | 56 | nuxtApp.vueApp.use(vfm) 57 | }) 58 | ``` 59 | 60 | ### Import Required CSS 61 | 62 | #### In Vue 3 63 | 64 | ```ts [main.ts] 65 | import 'vue-final-modal/style.css' 66 | ``` 67 | 68 | #### In Nuxt 3 69 | 70 | ```ts [./nuxt.config.ts] 71 | export default defineNuxtConfig({ 72 | css: ['vue-final-modal/style.css'], 73 | }) 74 | ``` 75 | 76 | ### Removed Dynamic Modal `$vfm.show` 77 | 78 | If you are using `$vfm.show()`{lang=ts} to create dynamic modals. You have to re-written them with [useModal()](/api/composables/use-modal) composable. 79 | 80 | So this: 81 | 82 | ```ts 83 | this.$vfm.show({ 84 | component: ModalConfirm, 85 | bind: { 86 | name: 'ModalConfirmName' 87 | }, 88 | on: { 89 | confirm() { 90 | this.$vfm.hide('ModalConfirmName') 91 | }, 92 | opened() { 93 | console.log('modal opened') 94 | }, 95 | }, 96 | slots: { 97 | default: '

The content of the modal

' 98 | } 99 | }) 100 | ``` 101 | 102 | Will be re-written as this: 103 | 104 | ```ts 105 | const { open, close } = useModal({ 106 | component: ModalConfirm, 107 | attrs: { 108 | title: 'Hello World!', 109 | onConfirm() { 110 | close() 111 | }, 112 | onOpened() { 113 | console.log('modal opened') 114 | } 115 | }, 116 | slots: { 117 | default: '

The content of the modal

', 118 | }, 119 | }) 120 | open() 121 | ``` 122 | 123 | ### Removed `params` 124 | 125 | `params` is not a good practice and hard to keep type-save in Typescript. 126 | So if you are using `params`, you have to re-written it with [useModal()](/api/composables/use-modal) composable. 127 | 128 | ### Delete Drag & Resize 129 | 130 | To keep vue-final-modal simple and easier to maintain, I decided to remove the Drag & Resize feature. 131 | 132 | If you are using Drag & Resize, please consider staying at the 3.x releases. 133 | 134 | Here is a new drag & resize example with an awesome library `vue3-drag-resize`: 135 | ::ReadMore{link="/use-cases/modal-drag-resize"} 136 | :: 137 | 138 | ### Renamed properties 139 | 140 | | 3.x | 4.0 | Description | 141 | | -------------- | ---------------------------------------------------------------------- | ------------------------- | 142 | | `name` | [modalId](/api/components/vue-final-modal#modalid) | - | 143 | | `ssr` | [display-directive](/api/components/vue-final-modal#displaydirective) | - | 144 | | `attach` | [teleportTo](/api/components/vue-final-modal#teleportto) | Use `` in Vue 3 | 145 | | `preventClick` | [background](/api/components/vue-final-modal#background) | - | 146 | | `transition` | [contentTransition](/api/components/vue-final-modal#contenttransition) | - | 147 | 148 | ### Deleted properties 149 | 150 | | 3.x | 4.0 | 151 | | ------------------ | ------------------------------------------------------------------ | 152 | | `classes` | Just use `class` | 153 | | `styles` | Just use `class` | 154 | | `focusRetain` | Merged into [focusTrap](/api/components/vue-final-modal#focustrap) | 155 | | `zIndexAuto` | Replaced by [zIndexFn](/api/components/vue-final-modal#zindexfn) | 156 | | `zIndexBase` | Replaced by [zIndexFn](/api/components/vue-final-modal#zindexfn) | 157 | | `zIndex` | Replaced by [zIndexFn](/api/components/vue-final-modal#zindexfn) | 158 | | `drag` | Not support `Drag & Resize` anymore | 159 | | `fitParent` | Not support `Drag & Resize` anymore | 160 | | `dragSelector` | Not support `Drag & Resize` anymore | 161 | | `keepChangedStyle` | Not support `Drag & Resize` anymore | 162 | | `resize` | Not support `Drag & Resize` anymore | 163 | | `resizeDirections` | Not support `Drag & Resize` anymore | 164 | | `minWidth` | Not support `Drag & Resize` anymore | 165 | | `minHeight` | Not support `Drag & Resize` anymore | 166 | | `maxWidth` | Not support `Drag & Resize` anymore | 167 | | `maxHeight` | Not support `Drag & Resize` anymore | 168 | 169 | ::ReadMore{link="/use-cases/modal-drag-resize"} 170 | :: 171 | 172 | ### Renamed APIs `$vfm` to `useVfm` 173 | 174 | - You can still use `this.$vfm`{lang=ts} in Options API. 175 | - If you are using script setup syntax: 176 | ```ts 177 | import { $vfm } from 'vue-final-modal' 178 | 179 | $vfm.show(...) 180 | $vfm.hide(...) 181 | $vfm.hideAll(...) 182 | ``` 183 | 184 | Will be re-written as this: 185 | 186 | ```ts 187 | import { useVfm } from 'vue-final-modal' 188 | 189 | const vfm = useVfm() 190 | 191 | vfm.open(...) 192 | vfm.close(...) 193 | vfm.closeAll(...) 194 | ``` 195 | 196 | | 3.x | 4.0 | 197 | | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | 198 | | `$vfm.show()` | [vfm.open()](/api/composables/use-vfm#functions) | 199 | | `$vfm.hide()` | [vfm.close()](/api/composables/use-vfm#functions) | 200 | | `$vfm.hideAll()` | [vfm.closeAll()](/api/composables/use-vfm#functions) | -------------------------------------------------------------------------------- /docs/content/2.get-started/1.guide/4.types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | The exported types in VueFinalModal. 4 | 5 | ## ModalId 6 | 7 | ```ts 8 | export type ModalId = number | string | symbol 9 | ``` 10 | 11 | ## StyleValue 12 | 13 | ```ts 14 | export type StyleValue = string | CSSProperties | (string | CSSProperties)[] 15 | ``` 16 | 17 | ## ModalSlot 18 | 19 | ```ts 20 | export interface ModalSlotOptions { component: Raw; attrs?: Record } 21 | export type ModalSlot = string | ComponentType | ModalSlotOptions 22 | ``` 23 | 24 | ## UseModalOptions 25 | 26 | ```ts 27 | export type UseModalOptions = { 28 | defaultModelValue?: boolean 29 | keepAlive?: boolean 30 | component?: T 31 | attrs?: ComponentProps 32 | slots?: { 33 | [key: string]: ModalSlot 34 | } 35 | } 36 | ``` 37 | 38 | ## UseModalOptionsPrivate 39 | 40 | ```ts 41 | export type UseModalOptionsPrivate = { 42 | id: symbol 43 | modelValue: boolean 44 | resolveOpened: () => void 45 | resolveClosed: () => void 46 | } 47 | ``` 48 | 49 | ## UseModalReturnType 50 | 51 | ```ts 52 | export interface UseModalReturnType { 53 | options: UseModalOptions & UseModalOptionsPrivate 54 | open: () => Promise 55 | close: () => Promise 56 | patchOptions: (options: Partial>) => void 57 | destroy: () => void 58 | } 59 | ``` 60 | 61 | ## Vfm 62 | 63 | ```ts 64 | export type Vfm = { 65 | install(app: App): void 66 | modals: ComputedRef[] 67 | openedModals: ComputedRef[] 68 | openedModalOverlays: ComputedRef[] 69 | dynamicModals: (UseModalOptions & UseModalOptionsPrivate)[] 70 | modalsContainers: Ref 71 | get: (modalId: ModalId) => undefined | ComputedRef 72 | toggle: (modalId: ModalId, show?: boolean) => undefined | Promise 73 | open: (modalId: ModalId) => undefined | Promise 74 | close: (modalId: ModalId) => undefined | Promise 75 | closeAll: () => Promise<[PromiseSettledResult[]>]> 76 | } 77 | ``` 78 | 79 | ## Modal 80 | 81 | ```ts 82 | export type Modal = { 83 | modalId?: ModalId 84 | hideOverlay: Ref | undefined 85 | overlayVisible: Ref 86 | toggle: (show?: boolean) => Promise 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/content/2.get-started/1.guide/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: akar-icons:book -------------------------------------------------------------------------------- /docs/content/2.get-started/3.migration/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: heroicons-outline:code 2 | -------------------------------------------------------------------------------- /docs/content/2.get-started/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: heroicons-outline:book-open 2 | -------------------------------------------------------------------------------- /docs/content/3.api/1.components/2.modals-container.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | A container for the dynamic modals that created by `useModal()`. 4 | 5 | ``{lang=ts} is an [renderless component](https://adamwathan.me/renderless-components-in-vuejs/) that is responsible for hosting the Vue instances of your dynamic modals. You don't need to do add anything else to the ``{lang=ts}, as long as you include it in your Vue tree, you can use Dynamic modals. 6 | 7 | Checkout the [Prerequisite section](/api/composables/use-modal#prerequisite) to learn how to use dynamic modals with `useModal()`{lang=ts}. 8 | 9 | ::ReadMore{link="/api/composables/use-modal"} 10 | :: -------------------------------------------------------------------------------- /docs/content/3.api/1.components/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: heroicons-outline:cube 2 | -------------------------------------------------------------------------------- /docs/content/3.api/2.composables/1.use-modal.md: -------------------------------------------------------------------------------- 1 | # useModal() 2 | 3 | A composable function to define a dynamic modal. 4 | 5 | With `useModal()`{lang=ts}, that means you don't have to add the modal to your Vue template and you don't have to use [v-model](/get-started/guide/setup#v-model) or [modalId](/get-started/guide/setup#modalid) to open or close the modal. You can simply use it to create a dynamic modal everywhere and control it programmatically. 6 | 7 | ## Prerequisite 8 | 9 | Using `useModal()`{lang=ts} to control dynamic modal must add [\](/api/components/modals-container) to your main `App.vue` file like so: 10 | 11 | ```vue [App.vue] 12 | 15 | 16 | 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Passing Props and Events 27 | 28 | ```ts 29 | import { VueFinalModal, useModal } from 'vue-final-modal' 30 | 31 | const { open, close, destroy, options, patchOptions } = useModal({ 32 | // Open the modal or not when the modal was created, the default value is `false`. 33 | defaultModelValue: false, 34 | /** 35 | * If set `keepAlive` to `true`: 36 | * 1. The `displayDirective` will be set to `show` by default. 37 | * 2. The modal component will not be removed after the modal closed until you manually execute `destroy()`. 38 | */ 39 | keepAlive: false, 40 | // `component` is optional and the default value is ``. 41 | component: VueFinalModal, 42 | attrs: { 43 | // Bind props to the modal component (VueFinalModal in this case). 44 | clickToClose: true, 45 | escToClose: true, 46 | // Bind events to the modal component (VueFinalModal in this case). 47 | // Any custom events can be listened for when prefixed with "on", e.g. "onEventName". 48 | onBeforeOpen() { /* on before open */ }, 49 | onOpened() { /* on opened */ }, 50 | onBeforeClose() { /* on before close */ }, 51 | onClosed() { /* on closed */ }, 52 | } 53 | }) 54 | 55 | // Open the modal 56 | open().then(() => { /* Do something after modal opened */ }) 57 | // Close the modal 58 | close().then(() => { /* Do something after modal closed */ }) 59 | // Destroy the modal manually, it only be needed when the `keepAlive` is set to `true` 60 | destroy() 61 | // Checkout the modal options 62 | options // the state of the dynamic modal 63 | 64 | // Overwrite the modal options 65 | patchOptions({ 66 | attrs: { 67 | // Overwrite the modal's props 68 | clickToClose: false, 69 | escToClose: false, 70 | } 71 | }) 72 | ``` 73 | 74 | ### Passing Slots 75 | 76 | #### with `String` 77 | 78 | 79 | ```ts 80 | import { VueFinalModal, useModal } from 'vue-final-modal' 81 | 82 | const modalInstance = useModal({ 83 | component: VueFinalModal, 84 | attrs: { ... }, 85 | slots: { 86 | default: '

The content of the modal

' 87 | } 88 | }) 89 | ``` 90 | 91 | ::alert{type=warning} 92 | Security Note: https://vuejs.org/api/built-in-directives.html#v-html 93 | Dynamically rendering arbitrary HTML on your website can be very dangerousbecause it can easily lead to XSS attacks. Only use v-html on trusted content and never on user-provided content. 94 | :: 95 | 96 | #### with `Component` 97 | 98 | 99 | ```ts 100 | import { VueFinalModal, useModal } from 'vue-final-modal' 101 | // ModalContent is the component you want to put into the modal content 102 | import ModalContent from './ModalContent.vue' 103 | 104 | 105 | const modalInstance = useModal({ 106 | component: VueFinalModal, 107 | attrs: { ... }, 108 | slots: { 109 | // You can import your own component as a slot and put it to `slots.default` without binding props and events. 110 | default: ModalContent 111 | } 112 | }) 113 | ``` 114 | 115 | #### with `Component`, `Props` and `Events` 116 | 117 | ```ts 118 | import { VueFinalModal, useModal, useModalSlot } from 'vue-final-modal' 119 | // ModalContent is the component you want to put into the modal content 120 | import ModalContent from './ModalContent.vue' 121 | 122 | const modalInstance = useModal({ 123 | component: VueFinalModal, 124 | attrs: { ... }, 125 | slots: { 126 | default: useModalSlot({ 127 | component: ModalContent, 128 | attrs: { 129 | // Bind ModalContent props 130 | title: 'Hello world!' 131 | // Bind ModalContent events 132 | onConfirm() { } 133 | } 134 | }) 135 | } 136 | }) 137 | ``` 138 | 139 | ::alert{type=info} 140 | `useModalSlot()` is a function that provides better DX for type checking. It just returns the same object you passed in. 141 | :: 142 | 143 | ## Type Declarations 144 | 145 | ::alert{type=info} 146 | Checkout [Types section](/get-started/guide/types). 147 | :: 148 | -------------------------------------------------------------------------------- /docs/content/3.api/2.composables/2.use-vfm.md: -------------------------------------------------------------------------------- 1 | # useVfm() 2 | 3 | `useVfm()`{lang=ts} provides global state for the modal components and also provides functions that can be used to control the modal 4 | components. 5 | 6 | ## Usage 7 | 8 | ```vue 9 | 24 | ``` 25 | 26 | ## Global state 27 | 28 | - `vfm.modals`{lang=ts}: All modals including opened and closed modals. 29 | - `vfm.openedModals`{lang=ts}: All opened modals including opened `vfm.modals` and opened `vfm.dynamicModals`. 30 | - `vfm.dynamicModals`{lang=ts}: All opened dynamic modals that create by [useModal()](/api/composables/use-modal). 31 | 32 | ## Functions 33 | 34 | `vfm` instance provides some utility functions that allows you to operate a modal (see [modalId](/api/components/vue-final-modal#modalid-optional)) or modals. 35 | 36 | - `vfm.get(modalId)`{lang=ts}: Get a modal instance by given a modalId 37 | - `vfm.toggle(modalId)`{lang=ts}: Toggle a modal by given a modalId 38 | ```ts 39 | vfm.toggle(modalId).then(() => { 40 | // Do something after the modal opened or closed 41 | }) 42 | ``` 43 | - `vfm.open(modalId)`{lang=ts}: Open a modal by given a modalId 44 | ```ts 45 | vfm.open(modalId).then(() => { 46 | // Do something after the modal opened 47 | }) 48 | ``` 49 | - `vfm.close(modalId)`{lang=ts}: Close a modal by given a modalId 50 | ```ts 51 | vfm.close(modalId).then(() => { 52 | // Do something after the modal closed 53 | }) 54 | ``` 55 | - `vfm.closeAll()`{lang=ts}: Close all modals 56 | ```ts 57 | vfm.closeAll().then(() => { 58 | // Do something after all modals closed 59 | }) 60 | ``` 61 | 62 | 63 | ## Type Declarations 64 | 65 | ::ReadMore{link="/get-started/guide/types"} 66 | :: -------------------------------------------------------------------------------- /docs/content/3.api/2.composables/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: heroicons-outline:switch-horizontal 2 | -------------------------------------------------------------------------------- /docs/content/3.api/_dir.yml: -------------------------------------------------------------------------------- 1 | title: 'API' 2 | icon: heroicons-outline:bookmark-alt 3 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/.2.modal-transition.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Transition | Examples' 3 | --- 4 | 5 | # Modal Transition 6 | 7 | TBD -------------------------------------------------------------------------------- /docs/content/4.use-cases/1.playground.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Playground | Examples' 3 | --- 4 | 5 | # Playground 6 | 7 | ::code-group 8 | ::code-block{label="Preview" preview} 9 | :playground 10 | :: 11 | 12 | ```vue [Preview.vue] 13 | 37 | 38 | 135 | 136 | 141 | ``` 142 | :: 143 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/3.confirm-modal.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Confirm | Examples' 3 | --- 4 | 5 | # Modal Confirm 6 | 7 | Create a `` component with `` and TailwindCSS. 8 | 9 | ## Preview 10 | 11 | ::code-group 12 | ::code-block{label="Preview" preview} 13 | ::modal-confirm-preview 14 | :: 15 | 16 | ```vue [Preview.vue] 17 | 24 | 25 | 38 | ``` 39 | :: 40 | 41 | ## `` component 42 | 43 | ::code-group 44 | ```vue [ModalConfirm.vue] 45 | 57 | 58 | 73 | ``` 74 | :: 75 | 76 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/4.modal-nested.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Nested | Examples' 3 | --- 4 | 5 | # Modal Nested 6 | 7 | Use `` to demo how to use nested modal. 8 | 9 | ## Preview 10 | 11 | ::code-group 12 | ::code-block{label="Preview" preview} 13 | :modal-nested-preview 14 | :: 15 | 16 | ```vue [Preview.vue] 17 | 48 | 49 | 56 | ``` 57 | :: 58 | 59 | ## `` component 60 | 61 | ::code-group 62 | ```vue [ModalConfirm.vue] 63 | 75 | 76 | 91 | ``` 92 | :: 93 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/5.modal-fullscreen.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Fullscreen | Examples' 3 | --- 4 | 5 | # Modal Fullscreen 6 | 7 | Create a `` component with `` and TailwindCSS. 8 | 9 | ## Preview 10 | 11 | ::code-group 12 | ::code-block{label="Preview" preview} 13 | ::modal-fullscreen-preview 14 | :: 15 | 16 | ```vue [Preview.vue] 17 | 33 | 34 | 41 | ``` 42 | :: 43 | 44 | ## `` component 45 | 46 | ::code-group 47 | ```vue [ModalFullscreen.vue] 48 | 59 | 60 | 75 | ``` 76 | :: 77 | 78 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/6.modal-bottom.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Bottom | Examples' 3 | --- 4 | 5 | # Modal Bottom 6 | 7 | Create a `` component with `` and TailwindCSS. 8 | 9 | ## Preview 10 | 11 | ::code-group 12 | ::code-block{label="Preview" preview} 13 | ::modal-bottom-preview 14 | :: 15 | 16 | ```vue [Preview.vue] 17 | 33 | 34 | 41 | ``` 42 | :: 43 | 44 | ## `` component 45 | 46 | ::code-group 47 | ```vue [ModalBottom.vue] 48 | 55 | 56 | 70 | ``` 71 | :: 72 | 73 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/7.modal-login-form.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Login Form | Examples' 3 | --- 4 | 5 | # Modal Login Form 6 | 7 | Create a `` component with `` and TailwindCSS. 8 | 9 | A example that use form component in vue-final-modal. 10 | 11 | This example use [Vorms](https://vorms.mini-ghost.dev/) to handle the form validate. 12 | 13 | ## Preview 14 | 15 | ::code-group 16 | ::code-block{label="Preview" preview} 17 | :modal-login-form-preview 18 | :: 19 | 20 | ```vue [Preview.vue] 21 | 35 | 36 | 43 | ``` 44 | :: 45 | 46 | ## `` component 47 | 48 | ::code-group 49 | ```vue [ModalLoginForm.vue] 50 | 59 | 60 | 70 | ``` 71 | :: 72 | 73 | ## `` component 74 | 75 | ::code-group 76 | ```vue [LoginFormVorms.vue] 77 | 110 | 111 | 152 | 153 | 191 | ``` 192 | :: 193 | 194 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/8.modal-drag-resize.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Drag Resize | Examples' 3 | --- 4 | 5 | # Modal Drag Resize 6 | 7 | A drag and resize modal example. 8 | 9 | Here is a basic drag and resize modal example that using [vue3-drag-resize](https://github.com/kirillmurashov/vue-drag-resize/tree/vue3). 10 | 11 | ## Preview 12 | 13 | ::code-group 14 | ::code-block{label="Preview" preview} 15 | ::modal-drag-resize-preview 16 | :: 17 | :: 18 | 19 | ```vue [Preview.vue] 20 | 32 | 33 | 40 | ``` 41 | :: 42 | 43 | ## `` component 44 | 45 | ::code-group 46 | ```vue [DragResizeModal.vue] 47 | 67 | 68 | 97 | ``` 98 | :: 99 | -------------------------------------------------------------------------------- /docs/content/4.use-cases/9.modal-long-scroll.md: -------------------------------------------------------------------------------- 1 | --- 2 | head.title: 'Modal Long Scroll | Examples' 3 | --- 4 | 5 | # Modal Long Scroll 6 | 7 | A long scrollable modal example like [Bootstrap Modal scrolling-long-content](https://getbootstrap.com/docs/5.3/components/modal/#scrolling-long-content) 8 | 9 | ## Preview 10 | 11 | ::code-group 12 | ::code-block{label="Preview" preview} 13 | ::modal-long-scroll-preview 14 | :: 15 | :: 16 | 17 | ```vue [Preview.vue] 18 | 43 | 44 | 51 | 52 | ``` 53 | :: 54 | 55 | ## `` component 56 | 57 | ::code-group 58 | ```vue [ModalLongScroll.vue] 59 | 71 | 72 | 95 | ``` 96 | :: 97 | -------------------------------------------------------------------------------- /docs/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Vfm } from "vue-final-modal" 2 | 3 | declare module '#app' { 4 | interface NuxtApp { 5 | $vfm: Vfm 6 | } 7 | } 8 | 9 | export { } -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: '@nuxt-themes/docus', 3 | modules: ['@nuxtjs/tailwindcss', '@vue-final-modal/nuxt'], 4 | }) 5 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxi preview" 10 | }, 11 | "devDependencies": { 12 | "@nuxt-themes/docus": "^1.15.0", 13 | "@nuxtjs/tailwindcss": "^6.10.1", 14 | "nuxt": "^3.8.2" 15 | }, 16 | "dependencies": { 17 | "@vorms/core": "^1.1.0", 18 | "@vue-final-modal/nuxt": "workspace:1.0.3", 19 | "vue-final-modal": "workspace:4.5.4", 20 | "vue3-drag-resize": "^2.0.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/icon.png -------------------------------------------------------------------------------- /docs/public/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 未命名-2 -------------------------------------------------------------------------------- /docs/public/logo-light.svg: -------------------------------------------------------------------------------- 1 | 未命名-2 -------------------------------------------------------------------------------- /docs/public/logo-new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/docs/public/preview.png -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ], 5 | "lockFileMaintenance": { 6 | "enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | primary: { 6 | 50: '#ecfdf5', 7 | 100: '#d1fae5', 8 | 200: '#a7f3d0', 9 | 300: '#6ee7b7', 10 | 400: '#34d399', 11 | 500: '#10b981', 12 | 600: '#059669', 13 | 700: '#047857', 14 | 800: '#065f46', 15 | 900: '#064e3b', 16 | }, 17 | }, 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from 'pinceau' 2 | 3 | export default defineTheme({ 4 | color: { 5 | primary: { 6 | 50: { value: '#ecfdf5' }, 7 | 100: { value: '#d1fae5' }, 8 | 200: { value: '#a7f3d0' }, 9 | 300: { value: '#6ee7b7' }, 10 | 400: { value: '#34d399' }, 11 | 500: { value: '#10b981' }, 12 | 600: { value: '#059669' }, 13 | 700: { value: '#047857' }, 14 | 800: { value: '#065f46' }, 15 | 900: { value: '#064e3b' }, 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } -------------------------------------------------------------------------------- /examples/nuxt3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /examples/nuxt3/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /examples/nuxt3/components/MyModal.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /examples/nuxt3/components/MyModalPreview.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /examples/nuxt3/components/VButton.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 37 | 38 | 55 | -------------------------------------------------------------------------------- /examples/nuxt3/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: '@nuxt-themes/docus', 3 | modules: ['@nuxtjs/tailwindcss', '@vue-final-modal/nuxt'], 4 | }) 5 | -------------------------------------------------------------------------------- /examples/nuxt3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "nuxt build", 6 | "dev": "nuxt dev", 7 | "generate": "nuxt generate", 8 | "preview": "nuxt preview", 9 | "postinstall": "nuxt prepare" 10 | }, 11 | "devDependencies": { 12 | "@nuxt-themes/docus": "^1.15.0", 13 | "@nuxtjs/tailwindcss": "^6.10.1", 14 | "nuxt": "3.8.2" 15 | }, 16 | "dependencies": { 17 | "@vue-final-modal/nuxt": "^1.0.3", 18 | "vue-final-modal": "^4.5.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/nuxt3/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /examples/nuxt3/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | primary: { 6 | 50: '#ecfdf5', 7 | 100: '#d1fae5', 8 | 200: '#a7f3d0', 9 | 300: '#6ee7b7', 10 | 400: '#34d399', 11 | 500: '#10b981', 12 | 600: '#059669', 13 | 700: '#047857', 14 | 800: '#065f46', 15 | 900: '#064e3b', 16 | }, 17 | }, 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /examples/nuxt3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /examples/vue3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/vue3/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.3.7", 13 | "vue-final-modal": "^4.5.4" 14 | }, 15 | "devDependencies": { 16 | "@iconify/vue": "^4.1.1", 17 | "@vitejs/plugin-vue": "^4.5.0", 18 | "autoprefixer": "^10.4.16", 19 | "postcss": "^8.4.31", 20 | "tailwindcss": "^3.3.5", 21 | "typescript": "^5.3.2", 22 | "vite": "^5.0.5" 23 | } 24 | } -------------------------------------------------------------------------------- /examples/vue3/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/vue3/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /examples/vue3/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vue3/src/components/MyModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | -------------------------------------------------------------------------------- /examples/vue3/src/components/MyModalPreview.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /examples/vue3/src/components/VButton.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 39 | 40 | 54 | -------------------------------------------------------------------------------- /examples/vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createVfm } from 'vue-final-modal' 3 | import 'vue-final-modal/style.css' 4 | import './style.css' 5 | import App from './App.vue' 6 | 7 | createApp(App).use(createVfm()).mount('#app') 8 | -------------------------------------------------------------------------------- /examples/vue3/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | } 10 | 11 | @media (prefers-color-scheme: light) { 12 | :root { 13 | color: #213547; 14 | background-color: #ffffff; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/vue3/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /examples/vue3/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | content: [], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } 10 | -------------------------------------------------------------------------------- /examples/vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /examples/vue3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/vue3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "16" 3 | 4 | [build] 5 | publish = "docs/dist" 6 | 7 | command = "pnpm install --shamefully-hoist && cd packages/vue-final-modal && pnpm build && cd .. && cd .. && cd docs && pnpm generate" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vfm", 3 | "private": true, 4 | "scripts": { 5 | "dev": "concurrently \"pnpm:dev:docs\" \"pnpm:dev:viteplay\"", 6 | "dev:docs": "pnpm --filter docs dev", 7 | "build:docs": "pnpm --filter docs build", 8 | "generate:docs": "pnpm --filter docs generate", 9 | "dev:viteplay": "pnpm --filter viteplay dev", 10 | "build:viteplay": "pnpm --filter viteplay build", 11 | "dev:vfm": "pnpm --filter vue-final-modal dev", 12 | "build:vfm": "pnpm --filter vue-final-modal build", 13 | "test:vfm": "pnpm --filter vue-final-modal cypress:run", 14 | "release:vfm": "pnpm --filter vue-final-modal release", 15 | "lint": "eslint . --ext=.ts,.vue --fix", 16 | "typecheck": "pnpm --parallel typecheck", 17 | "prepare:module": "pnpm --filter @vue-final-modal/nuxt dev:prepare && pnpm --filter @vue-final-modal/nuxt prepack", 18 | "postinstall": "pnpm build:vfm && pnpm prepare:module" 19 | }, 20 | "dependencies": { 21 | "vue": "^3.3.7" 22 | }, 23 | "devDependencies": { 24 | "@antfu/eslint-config": "^0.37.0", 25 | "@types/node": "^20.10.0", 26 | "@vitejs/plugin-vue": "^4.5.0", 27 | "concurrently": "^8.2.2", 28 | "eslint": "^8.36.0", 29 | "pnpm": "^8.11.0", 30 | "typescript": "^5.3.2", 31 | "vite": "^5.0.12", 32 | "vue-tsc": "1.8.20" 33 | } 34 | } -------------------------------------------------------------------------------- /packages/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /packages/nuxt/.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | -------------------------------------------------------------------------------- /packages/nuxt/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Chung Hang (Hunter) Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Module 2 | 3 | ## Development 4 | 5 | - Run `npm run dev:prepare` to generate type stubs. 6 | - Use `npm run dev` to start [playground](./playground) in development mode. 7 | -------------------------------------------------------------------------------- /packages/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-final-modal/nuxt", 3 | "private": false, 4 | "version": "1.0.3", 5 | "license": "MIT", 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/module.mjs", 10 | "require": "./dist/module.cjs" 11 | } 12 | }, 13 | "main": "./dist/module.cjs", 14 | "types": "./dist/types.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "prepack": "nuxt-module-build", 20 | "dev": "nuxi dev playground", 21 | "dev:build": "nuxi build playground", 22 | "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", 23 | "release": "pnpm prepack && release-it" 24 | }, 25 | "dependencies": { 26 | "@nuxt/kit": "^3.8.2", 27 | "vue-final-modal": "^4.5.4" 28 | }, 29 | "devDependencies": { 30 | "@nuxt/module-builder": "^0.5.4", 31 | "@nuxt/schema": "^3.8.2", 32 | "@nuxtjs/eslint-config-typescript": "^12.1.0", 33 | "nuxt": "^3.8.2", 34 | "release-it": "^16.1.3" 35 | }, 36 | "peerDependencies": { 37 | "vue-final-modal": ">=4.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/nuxt/playground/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /packages/nuxt/playground/components/PlainCssConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | 28 | 57 | -------------------------------------------------------------------------------- /packages/nuxt/playground/components/PlainCssConfirmModalPreview.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | -------------------------------------------------------------------------------- /packages/nuxt/playground/components/showConfirmModal.ts: -------------------------------------------------------------------------------- 1 | import { useModal } from 'vue-final-modal' 2 | import PlainCssConfirmModal from './PlainCssConfirmModal.vue' 3 | 4 | export function showConfirmModal() { 5 | const { open, close } = useModal({ 6 | component: PlainCssConfirmModal, 7 | attrs: { 8 | title: 'Hello World!', 9 | onConfirm() { 10 | close() 11 | }, 12 | }, 13 | slots: { 14 | default: '

The content of the modal

', 15 | }, 16 | }) 17 | 18 | open() 19 | } 20 | -------------------------------------------------------------------------------- /packages/nuxt/playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import vfmModule from '..' 3 | 4 | export default defineNuxtConfig({ 5 | modules: [ 6 | vfmModule, 7 | ], 8 | }) 9 | -------------------------------------------------------------------------------- /packages/nuxt/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "vue-final-modal-module-playground" 4 | } 5 | -------------------------------------------------------------------------------- /packages/nuxt/src/module.ts: -------------------------------------------------------------------------------- 1 | import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit' 2 | 3 | export default defineNuxtModule({ 4 | meta: { 5 | name: '@vue-final-modal/nuxt', 6 | configKey: 'vue-final-modal', 7 | }, 8 | setup(options, nuxt) { 9 | const { resolve } = createResolver(import.meta.url) 10 | 11 | // Transpile runtime 12 | nuxt.options.build.transpile.push(resolve('./runtime')) 13 | 14 | nuxt.hook('prepare:types', ({ references }) => { 15 | references.push({ types: '@vue-final-modal/nuxt' }) 16 | }) 17 | 18 | // Add runtime plugin before the router plugin 19 | // https://github.com/nuxt/framework/issues/9130 20 | nuxt.hook('modules:done', () => { 21 | addPlugin(resolve('./runtime/plugin')) 22 | }) 23 | 24 | nuxt.options.css.push('vue-final-modal/style.css') 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createVfm } from 'vue-final-modal' 2 | import { defineNuxtPlugin } from '#imports' 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | const vfm = createVfm() 6 | 7 | nuxtApp.vueApp.use(vfm) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/vue-final-modal/.gitignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /packages/vue-final-modal/.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "git": { 6 | "requireCleanWorkingDir": false 7 | }, 8 | "plugins": { 9 | "@release-it/conventional-changelog": { 10 | "preset": "angular", 11 | "infile": "CHANGELOG.md", 12 | "ignoreRecommendedBump": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue-final-modal/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Chung Hang (Hunter) Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/vue-final-modal/README.md: -------------------------------------------------------------------------------- 1 | # Vue Final Modal 4 2 | 3 | The most powerful yet most light-weight modal library for Vue 3. 4 | 5 |

Vue Final Modal Logo

6 | 7 |

8 | Downloads 9 | License 10 | Netlify Status 11 |

12 |

13 | Version 14 |

15 | 16 |

17 | 18 | Buy Me A Coffee 19 | 20 |

21 | 22 | ## Playground 23 | 24 | - [Stackblitz for Vue 3](https://stackblitz.com/github/vue-final/vue-final-modal/tree/master/examples/vue3) 25 | - [Stackblitz for Nuxt 3](https://stackblitz.com/github/vue-final/vue-final-modal/tree/master/examples/nuxt3) 26 | 27 | ## [Documentation](https://v4.vue-final-modal.org/) 28 | 29 | Checkout [Migration guide from v3](https://v4.vue-final-modal.org/get-started/migration/from-v3). 30 | 31 | Looking for old version? 32 | 33 | - [vue-final-modal@3.x for Vue 3](https://v3.vue-final-modal.org/) 34 | - [vue-final-modal@2.x for Vue 2](https://v2.vue-final-modal.org/) 35 | 36 | ## Contribution Guide 37 | 38 | ```bash [pnpm] 39 | # Install packages 40 | pnpm install --shamefully-hoist 41 | 42 | # Build vue-final-modal library first 43 | pnpm build:vfm 44 | 45 | # Run both docs and viteplay 46 | pnpm dev 47 | 48 | # Run docs: http://localhost:3000/ 49 | pnpm dev:docs 50 | 51 | # Run viteplay: http://localhost:5173/ 52 | pnpm dev:viteplay 53 | ``` 54 | 55 | ## Contributors 56 | 57 | Thank you to all the people who already contributed to `vue-final-modal`! 58 | 59 | 60 | contributors 61 | 62 | 63 | Made with [contributors-img](https://contrib.rocks). 64 | 65 | 🚀 If you have any ideas for optimization of `vue-final-modal`, feel free to open [issues](https://github.com/hunterliu1003/vue-final-modal/issues) or [pull requests](https://github.com/hunterliu1003/vue-final-modal/pulls). 66 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | component: { 5 | video: false, 6 | screenshotOnRunFailure: false, 7 | specPattern: 'cypress/components/**/*.spec.ts', 8 | devServer: { 9 | framework: 'vue', 10 | bundler: 'vite', 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/Form.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/ModalCloseByScopedSlot.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/focusTrap.spec.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import Form from './Form.vue' 3 | import { VueFinalModal, createVfm, useModal } from '~/index' 4 | 5 | describe('Test focusTrap', () => { 6 | it('Props: focusTrap', () => { 7 | const vfm = createVfm() 8 | 9 | const firstModal = useModal({ 10 | component: VueFinalModal, 11 | attrs: { contentClass: 'first-modal-content' }, 12 | slots: { 13 | default: Form, 14 | }, 15 | }) 16 | 17 | const secondModal = useModal({ 18 | component: VueFinalModal, 19 | attrs: { contentClass: 'second-modal-content' }, 20 | slots: { 21 | default: '

Hello world!

', 22 | }, 23 | }) 24 | 25 | cy.mount(App, { 26 | global: { 27 | plugins: [vfm], 28 | stubs: { transition: false }, 29 | }, 30 | }) 31 | .then(async () => { 32 | await firstModal.open() 33 | cy.focused().as('firstModalFocus') 34 | cy.get('@firstModalFocus').should('have.class', 'first-modal-content') 35 | }) 36 | .then(async () => { 37 | cy.get('.form-submit').focus() 38 | cy.focused().as('formSubmitFocus') 39 | cy.get('@formSubmitFocus').should('have.class', 'form-submit') 40 | }) 41 | .then(async () => { 42 | await secondModal.open() 43 | cy.focused().as('secondModalFocus') 44 | cy.get('@secondModalFocus').should('have.class', 'second-modal-content') 45 | }) 46 | .then(async () => { 47 | await secondModal.close() 48 | cy.focused().as('formSubmitFocus') 49 | cy.get('@formSubmitFocus').should('have.class', 'form-submit') 50 | }) 51 | .then(async () => { 52 | await firstModal.close() 53 | await firstModal.open() 54 | cy.focused().as('firstModalFocus') 55 | cy.get('@firstModalFocus').should('have.class', 'first-modal-content') 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/scopedSlot.spec.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import ModalCloseByScopedSlot from './ModalCloseByScopedSlot.vue' 3 | import { createVfm, useModal } from '~/index' 4 | 5 | describe('Test scopedSlot', () => { 6 | it('close() scoped slot ', () => { 7 | const vfm = createVfm() 8 | const modalName = 'modal-close-by-scoped-slot' 9 | useModal({ 10 | defaultModelValue: true, 11 | component: ModalCloseByScopedSlot, 12 | attrs: { class: modalName }, 13 | }) 14 | 15 | cy.mount(App, { 16 | global: { 17 | plugins: [vfm], 18 | stubs: { transition: false }, 19 | }, 20 | }).as('app') 21 | 22 | cy.get(`.${modalName}`).should('exist') 23 | cy.get(`.${modalName}`).find('button').click() 24 | cy.get(`.${modalName}`).should('not.exist') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/useModal.spec.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import Form from './Form.vue' 3 | import { createVfm, useModal } from '~/index' 4 | 5 | describe('Test useModal()', () => { 6 | it('Should be closed by default', () => { 7 | const vfm = createVfm() 8 | const modal = useModal({ 9 | slots: { default: 'Hello World!' }, 10 | }) 11 | 12 | cy.mount(App, { 13 | global: { 14 | plugins: [vfm], 15 | stubs: { transition: false }, 16 | }, 17 | }).as('app') 18 | 19 | cy.contains('Hello World!').should('not.exist') 20 | cy.get('@app').then(() => modal.open()) 21 | cy.contains('Hello World!').should('exist') 22 | }) 23 | 24 | it('Should be opened by given defaultModelValue: true', () => { 25 | const vfm = createVfm() 26 | useModal({ 27 | defaultModelValue: true, 28 | slots: { 29 | default: 'Hello World!', 30 | }, 31 | }) 32 | 33 | cy.mount(App, { 34 | global: { 35 | plugins: [vfm], 36 | stubs: { transition: false }, 37 | }, 38 | }) 39 | 40 | cy.contains('Hello World!') 41 | }) 42 | 43 | it('Events', () => { 44 | const vfm = createVfm() 45 | 46 | const onBeforeOpen = cy.spy().as('onBeforeOpen') 47 | const onOpened = cy.spy().as('onOpened') 48 | const onBeforeClose = cy.spy().as('onBeforeClose') 49 | const onClosed = cy.spy().as('onClosed') 50 | 51 | const modal = useModal({ 52 | attrs: { 53 | onBeforeOpen, 54 | onOpened, 55 | onBeforeClose, 56 | onClosed, 57 | }, 58 | slots: { default: Form }, 59 | }) 60 | 61 | cy.mount(App, { 62 | global: { 63 | plugins: [vfm], 64 | stubs: { transition: false }, 65 | }, 66 | }).as('app') 67 | 68 | cy.get('@onBeforeOpen').should('have.callCount', 0) 69 | cy.get('@onOpened').should('have.callCount', 0) 70 | cy.get('@app').then(() => modal.open()) 71 | cy.get('@onBeforeOpen').should('have.callCount', 1) 72 | cy.get('@onOpened').should('have.callCount', 1) 73 | 74 | cy.get('@onBeforeClose').should('have.callCount', 0) 75 | cy.get('@onClosed').should('have.callCount', 0) 76 | cy.get('@app').then(() => modal.close()) 77 | cy.get('@onBeforeClose').should('have.callCount', 1) 78 | cy.get('@onClosed').should('have.callCount', 1) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/components/useZIndex.spec.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import { VueFinalModal, createVfm, useModal } from '~/index' 3 | 4 | describe('Test useZIndex()', () => { 5 | it('Props: zIndexFn()', () => { 6 | const vfm = createVfm() 7 | const firstModal = useModal({ 8 | component: VueFinalModal, 9 | attrs: { class: 'first-modal' }, 10 | }) 11 | 12 | const secondModal = useModal({ 13 | component: VueFinalModal, 14 | attrs: { class: 'second-modal' }, 15 | }) 16 | 17 | const thirdModal = useModal({ 18 | component: VueFinalModal, 19 | attrs: { class: 'third-modal' }, 20 | }) 21 | 22 | cy.mount(App, { 23 | global: { 24 | plugins: [vfm], 25 | stubs: { transition: false }, 26 | }, 27 | }).as('app') 28 | 29 | cy.get('@app').then(() => firstModal.open()) 30 | cy.get('.first-modal').should(($el) => { 31 | expect($el).to.have.css('zIndex', '1000') 32 | }) 33 | 34 | cy.get('@app').then(() => secondModal.open()) 35 | cy.get('.second-modal').should(($el) => { 36 | expect($el).to.have.css('zIndex', '1002') 37 | }) 38 | 39 | cy.get('@app').then(() => thirdModal.open()) 40 | cy.get('.third-modal').should(($el) => { 41 | expect($el).to.have.css('zIndex', '1004') 42 | }) 43 | 44 | cy.get('@app').then(() => { 45 | thirdModal.patchOptions({ 46 | attrs: { 47 | zIndexFn: ({ index }) => 1234 + 2 * index, 48 | }, 49 | }) 50 | }) 51 | cy.get('.third-modal').should(($el) => { 52 | expect($el).to.have.css('zIndex', '1238') 53 | }) 54 | 55 | cy.get('@app').then(() => firstModal.close()) 56 | cy.get('.second-modal').should(($el) => { 57 | expect($el).to.have.css('zIndex', '1000') 58 | }) 59 | cy.get('.third-modal').should(($el) => { 60 | expect($el).to.have.css('zIndex', '1236') 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /packages/vue-final-modal/cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/vue' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount(MyComponent) 40 | -------------------------------------------------------------------------------- /packages/vue-final-modal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-final-modal", 3 | "private": false, 4 | "version": "4.5.5", 5 | "type": "module", 6 | "source": "src/index.ts", 7 | "main": "./dist/index.umd.js", 8 | "module": "./dist/index.es.mjs", 9 | "types": "./dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.es.mjs", 13 | "require": "./dist/index.umd.js", 14 | "types": "./dist/index.d.ts" 15 | }, 16 | "./style.css": { 17 | "import": "./dist/style.css", 18 | "require": "./dist/style.css" 19 | } 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "scripts": { 25 | "dev": "vite build -w", 26 | "build": "vite build", 27 | "cypress:open": "cypress open --browser chrome --component", 28 | "cypress:run": "cypress run --browser chrome --component", 29 | "typecheck": "vue-tsc --noEmit", 30 | "release": "pnpm build && pnpm cypress:run && release-it" 31 | }, 32 | "dependencies": { 33 | "@vueuse/core": "^10.5.0", 34 | "@vueuse/integrations": "^10.5.0", 35 | "focus-trap": "^7.5.4" 36 | }, 37 | "devDependencies": { 38 | "@cypress/vue": "^5.0.5", 39 | "@release-it/conventional-changelog": "^5.1.1", 40 | "@vue-macros/volar": "^0.8.4", 41 | "cypress": "^13.6.4", 42 | "release-it": "^16.1.3", 43 | "unplugin-vue-define-options": "^1.3.8", 44 | "unplugin-vue-macros": "^2.3.0", 45 | "vite-plugin-dts": "^3.6.3", 46 | "vue": "^3.3.7" 47 | }, 48 | "peerDependencies": { 49 | "@vueuse/core": ">=10.0.0", 50 | "@vueuse/integrations": ">=10.0.0", 51 | "focus-trap": ">=7.2.0", 52 | "vue": ">=3.2.0" 53 | }, 54 | "homepage": "https://vue-final-modal.org/", 55 | "bugs": { 56 | "url": "https://github.com/vue-final/vue-final-modal/issues", 57 | "email": "hunterliu1003@gmail.com" 58 | }, 59 | "repository": { 60 | "type": "git", 61 | "url": "https://github.com/vue-final/vue-final-modal.git", 62 | "directory": "packages/vue-final-modal" 63 | }, 64 | "author": "Hunter Liu", 65 | "license": "MIT", 66 | "keywords": [ 67 | "vue", 68 | "modal", 69 | "dialog", 70 | "popup" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/Component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-component-type-helpers 3 | * Copy from https://github.com/vuejs/language-tools/tree/master/packages/component-type-helpers 4 | */ 5 | 6 | // export type ComponentType = 7 | // T extends new () => {} ? 1 : 8 | // T extends (...args: any) => any ? 2 : 9 | // 0 10 | 11 | export type ComponentProps = 12 | T extends new () => { $props: infer P } ? NonNullable

: 13 | T extends (props: infer P, ...args: any) => any ? P : 14 | {} 15 | 16 | export type ComponentSlots = 17 | T extends new () => { $slots: infer S } ? NonNullable : 18 | T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any }, ...args: any) => any ? NonNullable : 19 | {} 20 | 21 | export type ComponentEmit = 22 | T extends new () => { $emit: infer E } ? NonNullable : 23 | T extends (props: any, ctx: { slots: any; attrs: any; emit: infer E }, ...args: any) => any ? NonNullable : 24 | {} 25 | 26 | // export type ComponentExposed = 27 | // T extends new () => infer E ? E : 28 | // T extends (props: any, ctx: any, expose: (exposed: infer E) => any, ...args: any) => any ? NonNullable : 29 | // {} 30 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/Modal.ts: -------------------------------------------------------------------------------- 1 | import type { App, CSSProperties, Component, ComponentInternalInstance, FunctionalComponent, Raw, Ref } from 'vue' 2 | import type { ComponentProps, ComponentSlots } from './Component' 3 | 4 | export type ModalId = number | string | symbol 5 | export type StyleValue = string | CSSProperties | (string | CSSProperties)[] 6 | 7 | export interface ModalSlotOptions { component: Raw; attrs?: Record } 8 | export type ModalSlot = string | Component | ModalSlotOptions 9 | 10 | type ComponentConstructor = (abstract new (...args: any) => any) 11 | /** Including both generic and non-generic vue components */ 12 | export type ComponentType = ComponentConstructor | FunctionalComponent 13 | 14 | export type UseModalOptions = { 15 | defaultModelValue?: boolean 16 | keepAlive?: boolean 17 | component?: T 18 | attrs?: ComponentProps 19 | slots?: { 20 | [K in keyof ComponentSlots]?: ModalSlot 21 | } 22 | } 23 | 24 | export type UseModalOptionsPrivate = { 25 | id: symbol 26 | modelValue: boolean 27 | resolveOpened: () => void 28 | resolveClosed: () => void 29 | } 30 | 31 | export interface UseModalReturnType { 32 | options: UseModalOptions & UseModalOptionsPrivate 33 | open: () => Promise 34 | close: () => Promise 35 | patchOptions: (options: Partial>) => void 36 | destroy: () => void 37 | } 38 | 39 | export type Vfm = { 40 | install(app: App): void 41 | modals: ComponentInternalInstance[] 42 | openedModals: ComponentInternalInstance[] 43 | openedModalOverlays: ComponentInternalInstance[] 44 | dynamicModals: (UseModalOptions & UseModalOptionsPrivate)[] 45 | modalsContainers: Ref 46 | get: (modalId: ModalId) => undefined | ComponentInternalInstance 47 | toggle: (modalId: ModalId, show?: boolean) => undefined | Promise 48 | open: (modalId: ModalId) => undefined | Promise 49 | close: (modalId: ModalId) => undefined | Promise 50 | closeAll: () => Promise[]> 51 | } 52 | 53 | export type ModalExposed = { 54 | modalId?: Ref 55 | hideOverlay?: Ref 56 | overlayBehavior: Ref<'auto' | 'persist'> 57 | overlayVisible: Ref 58 | toggle: (show?: boolean) => Promise 59 | } 60 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/ModalsContainer.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/VueFinalModal/useFocusTrap.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { useFocusTrap as _useFocusTrap } from '@vueuse/integrations/useFocusTrap' 3 | import type VueFinalModal from './VueFinalModal.vue' 4 | 5 | export function useFocusTrap( 6 | props: InstanceType['$props'], 7 | options: { 8 | focusEl: Ref 9 | }, 10 | ) { 11 | if (props.focusTrap === false) { 12 | return { 13 | focus() {}, 14 | blur() {}, 15 | } 16 | } 17 | 18 | const { focusEl } = options 19 | const { hasFocus, activate, deactivate } = _useFocusTrap(focusEl, props.focusTrap) 20 | 21 | function focus() { 22 | requestAnimationFrame(() => { 23 | activate() 24 | }) 25 | } 26 | 27 | function blur() { 28 | if (hasFocus.value) 29 | deactivate() 30 | } 31 | 32 | return { focus, blur } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/VueFinalModal/useModelValue.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, ref, watch } from 'vue' 2 | import type { Ref } from 'vue' 3 | import type VueFinalModal from './VueFinalModal.vue' 4 | 5 | export function useModelValue( 6 | props: InstanceType['$props'], 7 | emit: InstanceType['$emit'], 8 | options: { 9 | open: () => boolean 10 | close: () => boolean 11 | }, 12 | ) { 13 | let skip = false 14 | const { open, close } = options 15 | 16 | /** The truth of modal open or close */ 17 | const _modelValueLocal = ref(false) 18 | 19 | /** 20 | * The proxy of `_modelValueLocal` 21 | */ 22 | const modelValueLocal = { 23 | get value() { 24 | return _modelValueLocal.value 25 | }, 26 | set value(val: boolean) { 27 | setModelValueLocal(val) 28 | }, 29 | } as Ref 30 | 31 | /** 32 | * Because of the open/close can be stopped in `@beforeOpen`, `@beforeClose` events. 33 | * So the function is to make sure `_modelValueLocal`, `props.modelValue` are always the same value 34 | */ 35 | function setModelValueLocal(val: boolean) { 36 | const success = val ? open() : close() 37 | if (success) { 38 | _modelValueLocal.value = val 39 | if (val !== props.modelValue) 40 | emit('update:modelValue', val) 41 | } 42 | else { 43 | skip = true 44 | emit('update:modelValue', !val) 45 | nextTick(() => { 46 | skip = false 47 | }) 48 | } 49 | } 50 | 51 | watch(() => props.modelValue, (val) => { 52 | if (skip) 53 | return 54 | modelValueLocal.value = !!val 55 | }) 56 | 57 | return { 58 | modelValueLocal, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/VueFinalModal/useToClose.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { ref } from 'vue' 3 | import type VueFinalModal from './VueFinalModal.vue' 4 | 5 | export function useToClose( 6 | props: InstanceType['$props'], 7 | emit: InstanceType['$emit'], 8 | options: { 9 | vfmRootEl: Ref 10 | vfmContentEl: Ref 11 | visible: Ref 12 | modelValueLocal: Ref 13 | }) { 14 | const { vfmRootEl, vfmContentEl, visible, modelValueLocal } = options 15 | const lastMousedownEl = ref() 16 | 17 | function onEsc() { 18 | if (visible.value && props.escToClose) 19 | modelValueLocal.value = false 20 | } 21 | 22 | function onMousedown(e?: MouseEvent) { 23 | lastMousedownEl.value = e?.target 24 | } 25 | 26 | function onMouseupRoot(): void { 27 | // skip when the lastMousedownEl didn't equal vfmRootEl 28 | if (lastMousedownEl.value !== vfmRootEl.value) 29 | return 30 | 31 | if (props.clickToClose) { 32 | modelValueLocal.value = false 33 | } 34 | else { 35 | vfmContentEl.value?.focus() 36 | emit('clickOutside') 37 | } 38 | } 39 | 40 | return { 41 | onEsc, 42 | onMouseupRoot, 43 | onMousedown, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/VueFinalModal/useTransition.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref, TransitionProps } from 'vue' 2 | import { computed, nextTick, ref, watch } from 'vue' 3 | import type VueFinalModal from './VueFinalModal.vue' 4 | 5 | export enum TransitionState { 6 | Enter, 7 | Entering, 8 | Leave, 9 | Leaving, 10 | } 11 | 12 | type TransitionListeners = { 13 | beforeEnter: () => void 14 | afterEnter: () => void 15 | beforeLeave: () => void 16 | afterLeave: () => void 17 | } 18 | 19 | function useTransitionState(_visible = false): [Ref, Ref, TransitionListeners ] { 20 | const visible = ref(_visible) 21 | const state = ref(visible.value ? TransitionState.Enter : undefined) 22 | 23 | const listeners: TransitionListeners = { 24 | beforeEnter() { state.value = TransitionState.Entering }, 25 | afterEnter() { state.value = TransitionState.Enter }, 26 | beforeLeave() { state.value = TransitionState.Leaving }, 27 | afterLeave() { state.value = TransitionState.Leave }, 28 | } 29 | 30 | return [visible, state, listeners] 31 | } 32 | 33 | export function useTransition( 34 | props: InstanceType['$props'], 35 | options: { 36 | modelValueLocal: Ref 37 | onEntering?: () => void 38 | onEnter?: () => void 39 | onLeaving?: () => void 40 | onLeave?: () => void 41 | }, 42 | ): { 43 | visible: Ref 44 | contentVisible: Ref 45 | contentListeners: TransitionListeners 46 | contentTransition: ComputedRef 47 | overlayVisible: Ref 48 | overlayListeners: TransitionListeners 49 | overlayTransition: ComputedRef 50 | enterTransition: () => void 51 | leaveTransition: () => void 52 | } { 53 | const { modelValueLocal, onEntering, onEnter, onLeaving, onLeave } = options 54 | const visible = ref(modelValueLocal.value) 55 | 56 | const [contentVisible, contentState, contentListeners] = useTransitionState(visible.value) 57 | const [overlayVisible, overlayState, overlayListeners] = useTransitionState(visible.value) 58 | 59 | const contentTransition = computed(() => { 60 | if (typeof props.contentTransition === 'string') 61 | return { name: props.contentTransition, appear: true } 62 | return { appear: true, ...props.contentTransition } 63 | }) 64 | 65 | const overlayTransition = computed(() => { 66 | if (typeof props.overlayTransition === 'string') 67 | return { name: props.overlayTransition, appear: true } 68 | return { appear: true, ...props.overlayTransition } 69 | }) 70 | 71 | const isReadyToBeDestroyed = computed(() => 72 | (props.hideOverlay || overlayState.value === TransitionState.Leave) 73 | && contentState.value === TransitionState.Leave) 74 | 75 | watch( 76 | isReadyToBeDestroyed, 77 | (value) => { 78 | if (value) 79 | visible.value = false 80 | }, 81 | ) 82 | 83 | watch(contentState, (state) => { 84 | if (state === TransitionState.Entering) { 85 | if (!visible.value) 86 | return 87 | onEntering?.() 88 | } 89 | else if (state === TransitionState.Enter) { 90 | if (!visible.value) 91 | return 92 | onEnter?.() 93 | } 94 | else if (state === TransitionState.Leaving) { 95 | onLeaving?.() 96 | } 97 | else if (state === TransitionState.Leave) { 98 | onLeave?.() 99 | } 100 | }) 101 | 102 | async function enterTransition() { 103 | visible.value = true 104 | await nextTick() 105 | contentVisible.value = true 106 | overlayVisible.value = true 107 | } 108 | 109 | function leaveTransition() { 110 | contentVisible.value = false 111 | overlayVisible.value = false 112 | } 113 | 114 | return { 115 | visible, 116 | 117 | contentVisible, 118 | contentListeners, 119 | contentTransition, 120 | 121 | overlayVisible, 122 | overlayListeners, 123 | overlayTransition, 124 | 125 | enterTransition, 126 | leaveTransition, 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/VueFinalModal/useZIndex.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type VueFinalModal from './VueFinalModal.vue' 3 | 4 | export function useZIndex( 5 | props: InstanceType['$props'], 6 | ) { 7 | const zIndex = ref() 8 | 9 | function refreshZIndex(index: number) { 10 | zIndex.value = props.zIndexFn?.({ index: index <= -1 ? 0 : index }) 11 | } 12 | 13 | function resetZIndex() { 14 | zIndex.value = undefined 15 | } 16 | 17 | return { 18 | zIndex, 19 | refreshZIndex, 20 | resetZIndex, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/components/VueFinalModal/vVisible.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | 3 | interface VVisibleElement extends HTMLElement { 4 | // _vov = vue original visibility 5 | _vov: string 6 | } 7 | 8 | export const vVisible: Directive = { 9 | beforeMount(el, { value }, { transition }) { 10 | el._vov = el.style.visibility === 'hidden' ? '' : el.style.visibility 11 | if (transition && value) 12 | transition.beforeEnter(el) 13 | else 14 | setVisibility(el, value) 15 | }, 16 | mounted(el, { value }, { transition }) { 17 | if (transition && value) 18 | transition.enter(el) 19 | }, 20 | updated(el, { value, oldValue }, { transition }) { 21 | if (!value === !oldValue) 22 | return 23 | if (transition) { 24 | if (value) { 25 | transition.beforeEnter(el) 26 | setVisibility(el, true) 27 | transition.enter(el) 28 | } 29 | else { 30 | transition.leave(el, () => { 31 | setVisibility(el, false) 32 | }) 33 | } 34 | } 35 | else { 36 | setVisibility(el, value) 37 | } 38 | }, 39 | beforeUnmount(el, { value }) { 40 | setVisibility(el, value) 41 | }, 42 | } 43 | 44 | function setVisibility(el: VVisibleElement, value: unknown): void { 45 | el.style.visibility = value ? el._vov : 'hidden' 46 | } 47 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/dom.ts: -------------------------------------------------------------------------------- 1 | import { noop } from '~/utils' 2 | 3 | export const getPosition = (e: TouchEvent | MouseEvent) => { 4 | if (e instanceof MouseEvent) { 5 | const { clientX: x, clientY: y } = e 6 | return { x, y } 7 | } 8 | else { 9 | const { clientX: x, clientY: y } = e.targetTouches[0] 10 | return { x, y } 11 | } 12 | } 13 | 14 | export function checkPassiveEventSupport(document: Document) { 15 | if (!document) 16 | return false 17 | let supportsPassive = false 18 | const optionsBlock = { 19 | get passive() { 20 | supportsPassive = true 21 | return false 22 | }, 23 | } 24 | document.addEventListener('x', noop, optionsBlock) 25 | document.removeEventListener('x', noop) 26 | return supportsPassive 27 | } 28 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/global.d.ts: -------------------------------------------------------------------------------- 1 | // Global compile-time constants 2 | declare var __DEV__: boolean 3 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/index.ts: -------------------------------------------------------------------------------- 1 | import ModalsContainer from './components/ModalsContainer.vue' 2 | import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue' 3 | 4 | import type { Vfm } from './Modal' 5 | 6 | /** Types */ 7 | export * from './Modal' 8 | export * from './Component' 9 | 10 | /** Plugin */ 11 | export { createVfm, getModalExposed } from './plugin' 12 | 13 | /** Components */ 14 | export { 15 | ModalsContainer, 16 | VueFinalModal, 17 | } 18 | 19 | export { vueFinalModalProps } from './components/VueFinalModal/VueFinalModalProps' 20 | 21 | export type { VueFinalModalEmits } from './components/VueFinalModal/VueFinalModal.vue' 22 | 23 | /** Composables */ 24 | export { useVfm, useModal, useVfmAttrs, useModalSlot } from './useApi' 25 | 26 | declare module 'vue' { 27 | export interface ComponentCustomProperties { 28 | /** 29 | * Vue Final Modal global state for the modal components and also provides 30 | * functions that can be used to control the modal components. {@link Vfm} 31 | */ 32 | $vfm: Vfm 33 | } 34 | } 35 | 36 | export { } 37 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/injectionSymbols.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | import type { Vfm } from './Modal' 3 | 4 | export const vfmSymbol = Symbol(__DEV__ ? 'vfm' : '') as InjectionKey 5 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { App, ComponentInternalInstance, ComputedRef } from 'vue' 2 | import { getCurrentInstance, inject, markRaw, ref, shallowReactive } from 'vue' 3 | import { vfmSymbol } from './injectionSymbols' 4 | import type { ModalExposed, ModalId, UseModalOptions, UseModalOptionsPrivate, Vfm } from './Modal' 5 | import { noop } from './utils' 6 | 7 | // eslint-disable-next-line import/no-mutable-exports 8 | export let activeVfm: Vfm | undefined 9 | 10 | export const setActiveVfm = (vfm: Vfm | undefined) => 11 | (activeVfm = vfm) 12 | 13 | export const defaultVfm: Vfm = { 14 | install: noop, 15 | modals: [], 16 | openedModals: [], 17 | openedModalOverlays: [], 18 | dynamicModals: [], 19 | modalsContainers: ref([]), 20 | get: () => undefined, 21 | toggle: () => undefined, 22 | open: () => undefined, 23 | close: () => undefined, 24 | closeAll: () => Promise.allSettled([]), 25 | } 26 | 27 | export const getActiveVfm = () => 28 | (getCurrentInstance() && inject(vfmSymbol, defaultVfm)) || activeVfm 29 | 30 | export function createVfm() { 31 | const modals: ComponentInternalInstance[] = shallowReactive([]) 32 | const openedModals: ComponentInternalInstance[] = shallowReactive([]) 33 | const openedModalOverlays: ComponentInternalInstance[] = shallowReactive([]) 34 | const dynamicModals: (UseModalOptions & UseModalOptionsPrivate)[] = shallowReactive([]) 35 | const modalsContainers = ref([]) 36 | 37 | const vfm: Vfm = markRaw({ 38 | install(app: App) { 39 | app.provide(vfmSymbol, vfm) 40 | app.config.globalProperties.$vfm = vfm 41 | }, 42 | modals, 43 | openedModals, 44 | openedModalOverlays, 45 | dynamicModals, 46 | modalsContainers, 47 | get(modalId: ModalId) { 48 | return modals.find(modal => getModalExposed(modal)?.value.modalId?.value === modalId) 49 | }, 50 | toggle(modalId: ModalId, show?: boolean) { 51 | const modal = vfm.get(modalId) 52 | return getModalExposed(modal)?.value.toggle(show) 53 | }, 54 | open(modalId: ModalId) { 55 | return vfm.toggle(modalId, true) 56 | }, 57 | close(modalId: ModalId) { 58 | return vfm.toggle(modalId, false) 59 | }, 60 | closeAll() { 61 | return Promise.allSettled(openedModals 62 | .reduce[]>((acc, cur) => { 63 | const modalExposed = getModalExposed(cur) 64 | const promise = modalExposed?.value.toggle(false) 65 | if (promise) 66 | acc.push(promise) 67 | return acc 68 | }, []), 69 | ) 70 | }, 71 | }) 72 | 73 | setActiveVfm(vfm) 74 | 75 | return vfm 76 | } 77 | 78 | export function getModalExposed(componentInternalInstance: undefined | null | ComponentInternalInstance): undefined | null | ComputedRef { 79 | return componentInternalInstance?.exposed?.modalExposed 80 | } 81 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/useApi.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'vue' 2 | import { computed, markRaw, nextTick, reactive, useAttrs } from 'vue' 3 | import { tryOnUnmounted } from '@vueuse/core' 4 | import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue' 5 | import type { ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal' 6 | import { activeVfm, getActiveVfm } from './plugin' 7 | import type { ComponentEmit, ComponentProps } from './Component' 8 | import { isString, objectEntries } from '~/utils' 9 | 10 | /** 11 | * Returns the vfm instance. Equivalent to using `$vfm` inside 12 | * templates. 13 | */ 14 | export function useVfm(): Vfm { 15 | const vfm = getActiveVfm() 16 | if (__DEV__ && !vfm) { 17 | throw new Error( 18 | '[Vue Final Modal]: getActiveVfm was called with no active Vfm. Did you forget to install vfm?\n' 19 | + '\tconst vfm = createVfm()\n' 20 | + '\tapp.use(vfm)\n' 21 | + 'This will fail in production.', 22 | ) 23 | } 24 | 25 | return vfm! 26 | } 27 | 28 | function withMarkRaw(options: Partial>, DefaultComponent: Component = VueFinalModal) { 29 | const { component, slots: innerSlots, ...rest } = options 30 | 31 | const slots: UseModalOptions['slots'] = typeof innerSlots === 'undefined' 32 | ? {} 33 | : Object.fromEntries(objectEntries(innerSlots).map(([name, maybeComponent]) => { 34 | if (isString(maybeComponent)) 35 | return [name, maybeComponent] as const 36 | 37 | if (isModalSlotOptions(maybeComponent)) { 38 | return [name, { 39 | ...maybeComponent, 40 | component: markRaw(maybeComponent.component), 41 | }] 42 | } 43 | 44 | return [name, markRaw(maybeComponent as Component)] 45 | })) as UseModalOptions['slots'] 46 | 47 | return { 48 | ...rest, 49 | component: markRaw(component || DefaultComponent), 50 | slots, 51 | } 52 | } 53 | 54 | /** 55 | * Create a dynamic modal. 56 | */ 57 | export function useModal(_options: UseModalOptions): UseModalReturnType { 58 | const options = reactive({ 59 | id: Symbol(__DEV__ ? 'useModal' : ''), 60 | modelValue: !!_options?.defaultModelValue, 61 | resolveOpened: () => { }, 62 | resolveClosed: () => { }, 63 | attrs: {}, 64 | ...withMarkRaw(_options), 65 | }) as UseModalOptions & UseModalOptionsPrivate 66 | tryOnUnmounted(() => { 67 | if (!options?.keepAlive) 68 | destroy() 69 | }) 70 | 71 | if (options.modelValue === true) { 72 | // nextTick will break the SSR, so use `activeVfm` first and then `useVfm()` 73 | if (activeVfm) { 74 | activeVfm?.dynamicModals.push(options) 75 | } 76 | else { 77 | nextTick(() => { 78 | const vfm = useVfm() 79 | vfm?.dynamicModals.push(options) 80 | }) 81 | } 82 | } 83 | 84 | async function open(): Promise { 85 | // nextTick will break the SSR, so use `activeVfm` first and then `useVfm()` 86 | let vfm: Vfm 87 | if (activeVfm) { 88 | vfm = activeVfm 89 | } 90 | else { 91 | await nextTick() 92 | vfm = useVfm() 93 | } 94 | if (options.modelValue) 95 | return Promise.resolve('[Vue Final Modal] modal is already opened.') 96 | 97 | destroy() 98 | options.modelValue = true 99 | vfm.dynamicModals.push(options) 100 | 101 | return new Promise((resolve) => { 102 | options.resolveOpened = () => resolve('opened') 103 | }) 104 | } 105 | 106 | function close(): Promise { 107 | if (!options.modelValue) 108 | return Promise.resolve('[Vue Final Modal] modal is already closed.') 109 | 110 | options.modelValue = false 111 | return new Promise((resolve) => { 112 | options.resolveClosed = () => resolve('closed') 113 | }) 114 | } 115 | 116 | function patchOptions(_options: Partial>) { 117 | const { slots, ...rest } = withMarkRaw(_options, options.component) 118 | 119 | if (_options.defaultModelValue !== undefined) 120 | options.defaultModelValue = _options.defaultModelValue 121 | if (_options?.keepAlive !== undefined) 122 | options.keepAlive = _options?.keepAlive 123 | 124 | // patch options.component and options.attrs 125 | patchComponentOptions(options, rest) 126 | 127 | // patch options.slots 128 | if (slots) { 129 | objectEntries(slots).forEach(([name, slot]) => { 130 | const originSlot = options.slots![name] 131 | if (isString(originSlot)) 132 | options.slots![name] = slot 133 | else if (isModalSlotOptions(originSlot) && isModalSlotOptions(slot)) 134 | patchComponentOptions(originSlot, slot) 135 | else 136 | options.slots![name] = slot 137 | }) 138 | } 139 | } 140 | 141 | function patchComponentOptions( 142 | options: UseModalOptions | ModalSlotOptions, 143 | newOptions: Partial> | ModalSlotOptions, 144 | ) { 145 | if (newOptions.component) 146 | options.component = newOptions.component 147 | 148 | if (newOptions.attrs) 149 | patchAttrs(options.attrs!, newOptions.attrs) 150 | } 151 | 152 | function patchAttrs>(attrs: T, newAttrs: Partial): T { 153 | Object.entries(newAttrs).forEach(([key, value]) => { 154 | attrs[key as keyof T] = value as any 155 | }) 156 | 157 | return attrs 158 | } 159 | 160 | function destroy(): void { 161 | const vfm = useVfm() 162 | const index = vfm.dynamicModals.indexOf(options) 163 | if (index !== -1) 164 | vfm.dynamicModals.splice(index, 1) 165 | } 166 | 167 | return { 168 | options, 169 | open, 170 | close, 171 | patchOptions, 172 | destroy, 173 | } 174 | } 175 | 176 | export function useModalSlot(options: { 177 | component: T 178 | attrs?: ComponentProps 179 | }) { 180 | return options 181 | } 182 | 183 | export function isModalSlotOptions(value: unknown): value is ModalSlotOptions { 184 | if (typeof value === 'object' && value !== null) 185 | return 'component' in value 186 | else 187 | return false 188 | } 189 | 190 | export function pickModalProps(props: Record, modalProps: Record) { 191 | return Object.keys(modalProps).reduce>((acc, propName) => { 192 | acc[propName] = props?.[propName] 193 | return acc 194 | }, {}) 195 | } 196 | 197 | export function byPassAllModalEvents(emit?: ComponentEmit): ComponentProps { 198 | return { 199 | 'onUpdate:modelValue': (val: boolean) => emit?.('update:modelValue', val), 200 | 201 | 'onBeforeClose': (payload: { stop: () => void }) => emit?.('beforeClose', payload), 202 | 'onClosed': () => emit?.('closed'), 203 | 'onBeforeOpen': (payload: { stop: () => void }) => emit?.('beforeOpen', payload), 204 | 'onOpened': () => emit?.('opened'), 205 | 206 | /** onClickOutside will only be emitted when clickToClose equal to `false` */ 207 | 'onClickOutside': () => emit?.('clickOutside'), 208 | } 209 | } 210 | 211 | export function useVfmAttrs(options: { 212 | props: ComponentProps 213 | modalProps: ComponentProps 214 | emit?: any 215 | }) { 216 | const { props, modalProps, emit } = options 217 | const bindProps = computed(() => pickModalProps(props, modalProps)) 218 | const bindEmits = byPassAllModalEvents(emit) 219 | const attrs = useAttrs() 220 | const vfmAttrs = computed(() => ({ 221 | ...bindProps.value, 222 | ...bindEmits, 223 | ...attrs, 224 | })) 225 | 226 | return vfmAttrs 227 | } 228 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/useSwipeToClose.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from '@vueuse/core' 2 | import type { Ref } from 'vue' 3 | import { computed, ref, watch } from 'vue' 4 | import type VueFinalModal from './components/VueFinalModal/VueFinalModal.vue' 5 | import { useSwipeable } from './useSwipeable' 6 | import { clamp, noop } from './utils' 7 | 8 | export function useSwipeToClose( 9 | props: InstanceType['$props'], 10 | options: { 11 | vfmContentEl: Ref 12 | modelValueLocal: Ref 13 | }, 14 | ) { 15 | const { vfmContentEl, modelValueLocal } = options 16 | const LIMIT_DISTANCE = 0.1 17 | const LIMIT_SPEED = 300 18 | 19 | const swipeBannerEl = ref() 20 | const swipeEl = computed(() => { 21 | if (props.swipeToClose === undefined || props.swipeToClose === 'none') 22 | return undefined 23 | else 24 | return (props.showSwipeBanner ? swipeBannerEl.value : vfmContentEl.value) 25 | }) 26 | 27 | const offset = ref(0) 28 | const isCollapsed = ref(true) 29 | 30 | let stopSelectionChange = noop 31 | let shouldCloseModal = true 32 | let swipeStart: number 33 | let allowSwipe = false 34 | 35 | const { lengthX, lengthY, direction: _direction, isSwiping } = useSwipeable(swipeEl, { 36 | threshold: props.threshold, 37 | onSwipeStart(e) { 38 | stopSelectionChange = useEventListener(document, 'selectionchange', () => { 39 | isCollapsed.value = window.getSelection()?.isCollapsed 40 | }) 41 | swipeStart = new Date().getTime() 42 | allowSwipe = canSwipe(e?.target) 43 | }, 44 | onSwipe() { 45 | if (!allowSwipe) 46 | return 47 | if (!isCollapsed.value) 48 | return 49 | if (_direction.value !== props.swipeToClose) 50 | return 51 | if (_direction.value === 'up') { 52 | const offsetY = clamp(Math.abs(lengthY.value || 0), 0, swipeEl.value?.offsetHeight || 0) - (props.threshold || 0) 53 | offset.value = offsetY 54 | } 55 | else if (_direction.value === 'down') { 56 | const offsetY = clamp(Math.abs(lengthY.value || 0), 0, swipeEl.value?.offsetHeight || 0) - (props.threshold || 0) 57 | offset.value = -offsetY 58 | } 59 | else if (_direction.value === 'right') { 60 | const offsetX = clamp(Math.abs(lengthX.value || 0), 0, swipeEl.value?.offsetWidth || 0) - (props.threshold || 0) 61 | offset.value = -offsetX 62 | } 63 | else if (_direction.value === 'left') { 64 | const offsetX = clamp(Math.abs(lengthX.value || 0), 0, swipeEl.value?.offsetWidth || 0) - (props.threshold || 0) 65 | offset.value = offsetX 66 | } 67 | }, 68 | onSwipeEnd(e, _direction) { 69 | stopSelectionChange() 70 | if (!isCollapsed.value) { 71 | isCollapsed.value = true 72 | return 73 | } 74 | 75 | const swipeEnd = new Date().getTime() 76 | 77 | const validDirection = _direction === props.swipeToClose 78 | const validDistance = (() => { 79 | if (_direction === 'up' || _direction === 'down') 80 | return Math.abs(lengthY?.value || 0) > LIMIT_DISTANCE * (swipeEl.value?.offsetHeight || 0) 81 | else if (_direction === 'left' || _direction === 'right') 82 | return Math.abs(lengthX?.value || 0) > LIMIT_DISTANCE * (swipeEl.value?.offsetWidth || 0) 83 | })() 84 | const validSpeed = swipeEnd - swipeStart <= LIMIT_SPEED 85 | 86 | if (shouldCloseModal && allowSwipe && validDirection && (validDistance || validSpeed)) { 87 | modelValueLocal.value = false 88 | return 89 | } 90 | 91 | offset.value = 0 92 | }, 93 | }) 94 | 95 | const bindSwipe = computed(() => { 96 | if (props.swipeToClose === 'none') 97 | return 98 | const translateDirection = (() => { 99 | switch (props.swipeToClose) { 100 | case 'up': 101 | case 'down': 102 | return 'translateY' 103 | case 'left': 104 | case 'right': 105 | return 'translateX' 106 | } 107 | })() 108 | return { 109 | class: { 'vfm-bounce-back': !isSwiping.value }, 110 | style: { transform: `${translateDirection}(${-offset.value}px)` }, 111 | } 112 | }) 113 | 114 | watch( 115 | () => isCollapsed.value, 116 | (val) => { 117 | if (!val) 118 | offset.value = 0 119 | }, 120 | ) 121 | 122 | watch( 123 | () => modelValueLocal.value, 124 | (val) => { 125 | if (val) 126 | offset.value = 0 127 | }, 128 | ) 129 | 130 | watch( 131 | () => offset.value, 132 | (newValue, oldValue) => { 133 | switch (props.swipeToClose) { 134 | case 'down': 135 | case 'right': 136 | shouldCloseModal = newValue < oldValue 137 | break 138 | case 'up': 139 | case 'left': 140 | shouldCloseModal = newValue > oldValue 141 | break 142 | } 143 | }, 144 | ) 145 | 146 | function onTouchStartSwipeBanner(e: TouchEvent) { 147 | if (props.preventNavigationGestures) 148 | e.preventDefault() 149 | } 150 | 151 | function canSwipe(target?: null | EventTarget): boolean { 152 | const tagName = (target as HTMLElement)?.tagName 153 | if (!tagName || ['INPUT', 'TEXTAREA'].includes(tagName)) 154 | return false 155 | 156 | const allow = (() => { 157 | switch (props.swipeToClose) { 158 | case 'up': 159 | return (target as HTMLElement)?.scrollTop + (target as HTMLElement)?.clientHeight === (target as HTMLElement)?.scrollHeight 160 | case 'left': 161 | return (target as HTMLElement)?.scrollLeft + (target as HTMLElement)?.clientWidth === (target as HTMLElement)?.scrollWidth 162 | case 'down': 163 | return (target as HTMLElement)?.scrollTop === 0 164 | case 'right': 165 | return (target as HTMLElement)?.scrollLeft === 0 166 | default: 167 | return false 168 | } 169 | })() 170 | 171 | if (target === swipeEl.value) 172 | return allow 173 | else 174 | return allow && canSwipe((target as HTMLElement)?.parentElement) 175 | } 176 | 177 | return { 178 | vfmContentEl, 179 | swipeBannerEl, 180 | bindSwipe, 181 | onTouchStartSwipeBanner, 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/useSwipeable.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { computed, onMounted, reactive, ref } from 'vue' 3 | import { useEventListener } from '@vueuse/core' 4 | import { checkPassiveEventSupport, getPosition } from './dom' 5 | 6 | export type SwiperDirection = 'up' | 'right' | 'down' | 'left' | 'none' 7 | 8 | export function useSwipeable( 9 | el: Ref, 10 | { 11 | threshold = 0, 12 | onSwipeStart, 13 | onSwipe, 14 | onSwipeEnd, 15 | passive = true, 16 | }: { 17 | threshold?: number 18 | onSwipeStart?: (e?: MouseEvent | TouchEvent) => void 19 | onSwipe?: (e?: MouseEvent | TouchEvent) => void 20 | onSwipeEnd?: (e?: MouseEvent | TouchEvent, direction?: SwiperDirection) => void 21 | passive?: boolean 22 | }, 23 | ) { 24 | const coordsStart = reactive({ x: 0, y: 0 }) 25 | const coordsEnd = reactive({ x: 0, y: 0 }) 26 | 27 | const diffX = computed(() => coordsStart.x - coordsEnd.x) 28 | const diffY = computed(() => coordsStart.y - coordsEnd.y) 29 | 30 | const { max, abs } = Math 31 | const isThresholdExceeded = computed( 32 | () => max(abs(diffX.value), abs(diffY.value)) >= threshold, 33 | ) 34 | const isSwiping = ref(false) 35 | 36 | const direction = computed(() => { 37 | if (!isThresholdExceeded.value) 38 | return 'none' 39 | 40 | if (abs(diffX.value) > abs(diffY.value)) 41 | return diffX.value > 0 ? 'left' : 'right' 42 | 43 | else 44 | return diffY.value > 0 ? 'up' : 'down' 45 | }) 46 | 47 | const updateCoordsStart = (x: number, y: number) => { 48 | coordsStart.x = x 49 | coordsStart.y = y 50 | } 51 | 52 | const updateCoordsEnd = (x: number, y: number) => { 53 | coordsEnd.x = x 54 | coordsEnd.y = y 55 | } 56 | 57 | let listenerOptions: { passive?: boolean; capture?: boolean } 58 | let events: (() => void)[] 59 | function pointerStart(e: MouseEvent | TouchEvent) { 60 | if (listenerOptions.capture && !listenerOptions.passive) 61 | e.preventDefault() 62 | 63 | const { x, y } = getPosition(e) 64 | updateCoordsStart(x, y) 65 | updateCoordsEnd(x, y) 66 | onSwipeStart?.(e) 67 | 68 | events = [ 69 | useEventListener(el, 'mousemove', pointerMove, listenerOptions), 70 | useEventListener(el, 'touchmove', pointerMove, listenerOptions), 71 | useEventListener(el, 'mouseup', pointerEnd, listenerOptions), 72 | useEventListener(el, 'touchend', pointerEnd, listenerOptions), 73 | useEventListener(el, 'touchcancel', pointerEnd, listenerOptions), 74 | ] 75 | } 76 | 77 | function pointerMove(e: MouseEvent | TouchEvent) { 78 | const { x, y } = getPosition(e) 79 | updateCoordsEnd(x, y) 80 | if (!isSwiping.value && isThresholdExceeded.value) 81 | isSwiping.value = true 82 | 83 | if (isSwiping.value) 84 | onSwipe?.(e) 85 | } 86 | 87 | function pointerEnd(e: MouseEvent | TouchEvent) { 88 | if (isSwiping.value) 89 | onSwipeEnd?.(e, direction.value) 90 | 91 | isSwiping.value = false 92 | 93 | events.forEach(s => s()) 94 | } 95 | 96 | let stops: (() => void)[] = [] 97 | onMounted(() => { 98 | const isPassiveEventSupported = checkPassiveEventSupport(window?.document) 99 | 100 | if (!passive) { 101 | listenerOptions = isPassiveEventSupported 102 | ? { passive: false, capture: true } 103 | : { capture: true } 104 | } 105 | else { 106 | listenerOptions = isPassiveEventSupported 107 | ? { passive: true } 108 | : { capture: false } 109 | } 110 | 111 | stops = [ 112 | useEventListener(el, 'mousedown', pointerStart, listenerOptions), 113 | useEventListener(el, 'touchstart', pointerStart, listenerOptions), 114 | ] 115 | }) 116 | 117 | const stop = () => { 118 | stops.forEach(s => s()) 119 | events.forEach(s => s()) 120 | } 121 | 122 | return { 123 | isSwiping, 124 | direction, 125 | coordsStart, 126 | coordsEnd, 127 | lengthX: diffX, 128 | lengthY: diffY, 129 | stop, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/vue-final-modal/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const once 2 | = (fn: null | ((...args: any[]) => void)) => 3 | (...args: any[]) => { 4 | if (!fn) 5 | return 6 | fn?.(...args) 7 | fn = null 8 | } 9 | 10 | export const noop = () => {} 11 | 12 | export function clamp(val: number, min: number, max: number) { 13 | return val > max ? max : val < min ? min : val 14 | } 15 | 16 | export const isString = (value: unknown): value is string => typeof value === 'string' 17 | 18 | /** 19 | * @example 20 | * const arr = [1, 2, 6, 3, 4, 5] 21 | * arrayMoveItemToLast(arr, 6) 22 | * console.log(arr) // [1, 2, 3, 4, 5, 6] 23 | */ 24 | export function arrayMoveItemToLast(arr: T[], item: T) { 25 | const removedItem = arrayRemoveItem(arr, item)?.[0] || item 26 | arr.push(removedItem) 27 | } 28 | 29 | export function arrayRemoveItem(arr: T[], item: T) { 30 | const index = arr.indexOf(item) 31 | if (index !== -1) 32 | return arr.splice(index, 1) 33 | } 34 | 35 | type Entries = { [K in keyof T]: [K, T[K]] }[keyof T][] 36 | /** 37 | * Type safe variant of `Object.entries()` 38 | */ 39 | export function objectEntries>(object: T): Entries { 40 | return Object.entries(object) as any 41 | } 42 | -------------------------------------------------------------------------------- /packages/vue-final-modal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | "types": [ 7 | "vite/client", 8 | "unplugin-vue-define-options/macros-global", 9 | "cypress" 10 | ], 11 | "paths": { 12 | "~/*": [ 13 | "src/*" 14 | ], 15 | "~": [ 16 | "src" 17 | ] 18 | } 19 | }, 20 | "vueCompilerOptions": { 21 | "plugins": [ 22 | "@vue-macros/volar/define-model", 23 | "@vue-macros/volar/short-vmodel", 24 | "@vue-macros/volar/define-slots" 25 | ], 26 | "shortVmodel": { 27 | "prefix": "$" 28 | } 29 | }, 30 | "exclude": [ 31 | "dist", 32 | "coverage", 33 | "node_modules" 34 | ] 35 | } -------------------------------------------------------------------------------- /packages/vue-final-modal/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | // @ts-expect-error 5 | import VueMacros from 'unplugin-vue-macros/vite' 6 | // @ts-expect-error 7 | import DefineOptions from 'unplugin-vue-define-options/vite' 8 | import dts from 'vite-plugin-dts' 9 | 10 | const name = 'index' 11 | 12 | export default defineConfig({ 13 | resolve: { 14 | alias: { 15 | '~': `${path.resolve(__dirname, 'src')}`, 16 | }, 17 | }, 18 | plugins: [ 19 | VueMacros({ 20 | plugins: { 21 | vue: Vue(), 22 | }, 23 | }), 24 | DefineOptions(), 25 | dts({ 26 | include: 'src', 27 | }), 28 | ], 29 | publicDir: false, 30 | build: { 31 | lib: { 32 | entry: path.resolve(__dirname, 'src/index.ts'), 33 | name, 34 | fileName: format => `${name}.${format}.${format === 'es' ? 'm' : ''}js`, 35 | }, 36 | rollupOptions: { 37 | external: [ 38 | 'vue', 39 | '@vueuse/core', 40 | '@vueuse/integrations/useFocusTrap', 41 | 'focus-trap', 42 | ], 43 | output: { 44 | globals: { 45 | 'vue': 'Vue', 46 | '@vueuse/core': 'VueUse', 47 | '@vueuse/integrations/useFocusTrap': 'VueUseFocusTrap', 48 | 'focus-trap': 'FocusTrap', 49 | }, 50 | }, 51 | }, 52 | }, 53 | define: { 54 | __DEV__: JSON.stringify(!process.env.prod), 55 | }, 56 | }) 57 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # the root package.json 3 | - "." 4 | # all packages in subdirs of packages/ and components/ 5 | - "packages/**" 6 | - "docs" 7 | - "viteplay" 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "lib": [ 6 | "DOM", 7 | "ESNext" 8 | ], 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "jsx": "preserve", 12 | "incremental": false, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "noUnusedLocals": true, 17 | "strictNullChecks": true, 18 | "forceConsistentCasingInFileNames": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "examples" 23 | ] 24 | } -------------------------------------------------------------------------------- /viteplay/.gitignore: -------------------------------------------------------------------------------- 1 | playground 2 | .cache -------------------------------------------------------------------------------- /viteplay/404.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /viteplay/App.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /viteplay/app.ts: -------------------------------------------------------------------------------- 1 | import '@viteplay/vue/dist/style.css' 2 | import { createApp } from 'vue' 3 | import App from './App.vue' 4 | import { router } from './router' 5 | 6 | const app = createApp(App) 7 | 8 | app.use(router).mount('#app') 9 | -------------------------------------------------------------------------------- /viteplay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue Final Modal 4 | Viteplay 7 | 8 | 9 |

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /viteplay/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viteplay", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --host", 7 | "build": "vite build" 8 | }, 9 | "dependencies": { 10 | "vue": "^3.3.7", 11 | "vue-final-modal": "workspace:4.5.4", 12 | "vue-router": "^4.2.5" 13 | }, 14 | "devDependencies": { 15 | "@viteplay/plugin": "^0.2.9", 16 | "@viteplay/vue": "^0.2.9", 17 | "@vue-macros/volar": "^0.8.4", 18 | "unplugin-vue-define-options": "^1.4.0", 19 | "unplugin-vue-macros": "^2.7.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /viteplay/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-final/vue-final-modal/be7430cf843964e6ba3307201be6882482d9b595/viteplay/public/favicon.ico -------------------------------------------------------------------------------- /viteplay/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import { pages } from '@viteplay/vue/client' 3 | 4 | export const router = createRouter({ 5 | history: createWebHistory(), 6 | routes: [ 7 | { 8 | path: '/', 9 | component: () => import('./App.vue'), 10 | }, 11 | ...pages, 12 | { 13 | path: '/:all(.*)*', 14 | component: () => import('./404.vue'), 15 | }, 16 | ], 17 | }) 18 | 19 | let initialPageRender = true 20 | 21 | router.beforeEach((to, from, next) => { 22 | if (initialPageRender && to.path === '/') { 23 | initialPageRender = false 24 | next('/viteplay') 25 | return 26 | } 27 | return next() 28 | }) 29 | -------------------------------------------------------------------------------- /viteplay/src/components/DefaultSlot.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/Basic.example.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 125 | 126 | 127 | ### Markdown docs for Basic example 128 | 129 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/BottomSheet.example.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/ConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | 46 | 59 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/Fullscreen.example.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/NestedModal.example.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/StopEvent.example.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 54 | 55 | 56 | ### Markdown docs for Basic example 57 | 58 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/TestModal.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /viteplay/src/components/VueFinalModal/modalsHelpers.ts: -------------------------------------------------------------------------------- 1 | import { VueFinalModal, useModal, useModalSlot } from 'vue-final-modal' 2 | import DefaultSlot from '../DefaultSlot.vue' 3 | 4 | console.log('helper') 5 | export const modal = useModal({ 6 | component: VueFinalModal, 7 | // defaultModelValue: true, 8 | slots: { 9 | default: useModalSlot({ 10 | component: DefaultSlot, 11 | attrs: { 12 | text: '123', 13 | onCreate() { 14 | // console.log('onCreated') 15 | }, 16 | }, 17 | }), 18 | }, 19 | }) 20 | // modal.open() 21 | -------------------------------------------------------------------------------- /viteplay/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "vite/client", 6 | "unplugin-vue-define-options/macros-global" 7 | ] 8 | }, 9 | "vueCompilerOptions": { 10 | "plugins": [ 11 | "@vue-macros/volar/define-model", 12 | "@vue-macros/volar/short-vmodel", 13 | "@vue-macros/volar/define-slots" 14 | ], 15 | "shortVmodel": { 16 | "prefix": "$" 17 | } 18 | }, 19 | "exclude": [ 20 | ".cache", 21 | "playground", 22 | "node_modules" 23 | ] 24 | } -------------------------------------------------------------------------------- /viteplay/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | // @ts-expect-error 5 | import VueMacros from 'unplugin-vue-macros/vite' 6 | // @ts-expect-error 7 | import DefineOptions from 'unplugin-vue-define-options/vite' 8 | import viteplay from '@viteplay/plugin' 9 | 10 | const componentPath = '../packages/vue-final-modal/src/components' 11 | const examplesPath = '../../../../../viteplay/src/components' 12 | 13 | export default defineConfig({ 14 | resolve: { 15 | alias: { 16 | '~': `${path.resolve(__dirname, 'src')}`, 17 | }, 18 | }, 19 | plugins: [ 20 | VueMacros({ 21 | plugins: { 22 | vue: Vue(), 23 | }, 24 | }), 25 | DefineOptions(), 26 | viteplay({ 27 | pages: [ 28 | { 29 | title: 'VueFinalModal', 30 | component: `${componentPath}/VueFinalModal/VueFinalModal.vue`, 31 | examples: `${examplesPath}/VueFinalModal/*.example.vue`, 32 | }, 33 | ], 34 | // base route for the development pages 35 | base: '/viteplay', 36 | }), 37 | ], 38 | build: { 39 | outDir: 'playground', 40 | }, 41 | define: { 42 | __DEV__: JSON.stringify(!process.env.prod), 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /viteplay/viteplay.iframe.ts: -------------------------------------------------------------------------------- 1 | import { createVfm } from 'vue-final-modal' 2 | import 'vue-final-modal/style.css' 3 | 4 | export default { 5 | extend({ app }: any) { 6 | app.use(createVfm()) 7 | }, 8 | } 9 | --------------------------------------------------------------------------------