├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.cjs ├── demo-gif.gif ├── docs ├── .vitepress │ ├── config.ts │ ├── en.ts │ ├── ru.ts │ └── shared.ts ├── examples │ ├── animation.md │ ├── demo.md │ ├── list.md │ ├── multi-modals.md │ ├── prompt-modal.md │ ├── sidebar.md │ ├── simple-modal.md │ └── vue-router-with-modals.md ├── guide │ ├── close-after-route-changed.md │ ├── config.md │ ├── details-animation.md │ ├── details-styles.md │ ├── event-close.md │ ├── getting-started.md │ ├── guide-methods.md │ ├── guide-navigation-guards.md │ ├── guide-returned-value.md │ ├── integration-installation.md │ ├── integration-introduction.md │ ├── integration-practical.md │ ├── modal-object.md │ ├── namespace-notification.md │ ├── namespace.md │ └── store.md ├── images │ ├── logo.png │ └── logo.svg ├── index.md ├── package.json ├── public │ └── why-jenesius-vue-modal.html └── ru │ ├── examples │ ├── animation.md │ ├── demo.md │ ├── list.md │ ├── multi-modals.md │ ├── prompt-modal.md │ ├── sidebar.md │ ├── simple-modal.md │ └── vue-router-with-modals.md │ ├── guide │ ├── close-after-route-changed.md │ ├── config.md │ ├── details-animation.md │ ├── details-styles.md │ ├── event-close.md │ ├── getting-started.md │ ├── guide-methods.md │ ├── guide-navigation-guards.md │ ├── guide-returned-value.md │ ├── integration-installation.md │ ├── integration-introduction.md │ ├── integration-practical.md │ ├── modal-object.md │ ├── namespace-notification.md │ ├── namespace.md │ └── store.md │ └── index.md ├── examples ├── README.md ├── draggable │ ├── App.vue │ ├── README.md │ ├── main.ts │ ├── modal-info.vue │ └── modal-with-header.vue ├── handle-closing │ ├── App.vue │ ├── README.md │ ├── main.ts │ └── modal.vue ├── multi-modal │ ├── App.vue │ ├── README.md │ ├── main.ts │ ├── modal-multi-duplicate.vue │ └── modal-multi.vue ├── open-modal │ ├── App.vue │ ├── README.md │ ├── main.ts │ └── modal.vue ├── pretty-modal │ ├── App.vue │ ├── README.md │ ├── main.ts │ └── modals │ │ ├── modal-alert.vue │ │ ├── modal-auto-height.vue │ │ ├── modal-confirm.vue │ │ ├── modal-info.vue │ │ ├── modal-profile.vue │ │ └── modal-question.vue ├── prompt-modal │ ├── App.vue │ ├── README.md │ ├── main.ts │ └── modal-code.vue ├── side-modal │ ├── App.vue │ ├── README.md │ ├── components │ │ ├── modal-bottom.vue │ │ ├── modal-sidebar.vue │ │ └── other-modal.vue │ └── main.ts ├── simple-animation │ ├── App.vue │ └── main.ts ├── use-namespace │ ├── App.vue │ ├── README.md │ ├── main.ts │ ├── modal.vue │ └── notification-container.vue └── vue-router-with-modal │ ├── App.vue │ ├── README.md │ ├── components │ ├── FlowersList.vue │ ├── SomeInterface.vue │ ├── UserList.vue │ └── modals │ │ ├── FlowersModal.vue │ │ └── Modal.vue │ ├── main.ts │ └── router.ts ├── jest.config.cjs ├── jsconfig.json ├── package-lock.json ├── package.json ├── project ├── .babelrc ├── default-style.css ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.vue │ ├── main.ts │ ├── test │ ├── App.vue │ └── main.ts │ └── vue-shim.ts ├── src ├── .babelrc.json ├── components │ ├── WidgetModalContainer.vue │ └── WidgetModalContainerItem.vue ├── hooks │ └── onBeforeModalClose.ts ├── index.ts ├── methods │ ├── addModal.ts │ ├── closeById.ts │ ├── closeModal.ts │ ├── get-component-from-store.ts │ ├── getCurrentModal.ts │ ├── openModal.ts │ ├── popModal.ts │ ├── prompt-modal.ts │ └── pushModal.ts ├── routerIntegration │ └── index.ts ├── shims-vue.d.ts ├── tsconfig.json └── utils │ ├── Modal.ts │ ├── ModalError.ts │ ├── NamespaceStore.ts │ ├── config.ts │ ├── create-debug.ts │ ├── dto.ts │ ├── guards.ts │ ├── initialize.ts │ ├── state.ts │ └── types.ts ├── tests ├── .babelrc.json ├── IntegrationRouter │ ├── App.vue │ ├── ContainerUsers.vue │ ├── ModalGuard.vue │ ├── ModalRoute.vue │ ├── ModalUser.vue │ ├── router-async.spec.ts │ ├── router.spec.ts │ └── router.ts ├── assets │ └── trigger-click-close.ts ├── before-each.spec.ts ├── before-modal-close.spec.ts ├── components │ ├── modal-button.vue │ ├── modal-prompt-value-with-handler.vue │ ├── modal-prompt-value.vue │ └── modal-title.vue ├── configuration-store-element.spec.ts ├── configuration.spec.ts ├── esc-closing.spec.ts ├── event-close.spec.ts ├── init.spec.ts ├── initialization.spec.ts ├── modal-class.spec.ts ├── modal-namespace.spec.ts ├── modal-object.spec.ts ├── modal-on.spec.ts ├── modal-onclose.spec.ts ├── modal-options.spec.ts ├── modal-props.spec.ts ├── on-before-modal-close.spec.ts ├── prompt-modal.spec.ts ├── unit-test │ ├── add-modal-by-name.spec.ts │ └── get-component-from-store.spec.ts └── wait.ts ├── tsconfig.json ├── vite.config.mjs └── vue.config.cjs /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "plugin:vue/essential", 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "parser": "@typescript-eslint/parser", 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "vue", 14 | "@typescript-eslint" 15 | ], 16 | "rules": { 17 | "vue/no-multiple-template-root": "off" 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Please complete the following information:** 14 | - Browser [e.g. chrome, safari] 15 | - Version [e.g. 22] 16 | 17 | **Additional context** 18 | Add any other context about the problem here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm i vue@latest --force 31 | - run: npm run build --if-present 32 | - run: npm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | ./src/dist 4 | dist 5 | .vite_opt_cache 6 | 7 | docs/.vitepress/cache 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | /web/dist/ 28 | dist 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thnx for any help and bug's report! 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Jenesius 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ["@babel/preset-env", {targets: {node: 'current'}}], 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-transform-runtime", 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /demo-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jenesius/vue-modal/9177d8fe34af0f5584df120d49c36f1b336ac6f6/demo-gif.gif -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import {en} from "./en"; 2 | import {shared} from "./shared"; 3 | import {ru} from "./ru"; 4 | 5 | export default { 6 | ...shared, 7 | locales: { 8 | root: { label: "English", ...en }, 9 | ru: { label: "Русский", ...ru }, 10 | 11 | }, 12 | } -------------------------------------------------------------------------------- /docs/.vitepress/en.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vitepress"; 2 | 3 | export const en = defineConfig({ 4 | lang: 'en-US', 5 | description: "Documentation for vue-modal. Flexible and simple library for creating modal windows in Vue3", 6 | themeConfig: { 7 | nav: nav(), 8 | sidebar: { 9 | '/guide/': {base: '/guide/', items: sidebarGuide()}, 10 | }, 11 | footer: { 12 | message: "Released under the MIT License.", 13 | copyright: "Copyright © 2022-present Jenesius", 14 | }, 15 | }, 16 | }) 17 | 18 | function nav() { 19 | return [ 20 | {text: 'Guide', link: '/guide/getting-started', activeMatch: '/guide/'}, 21 | {text: 'Examples', link: '/examples/list', activeMatch: '/examples/'}, 22 | { 23 | text: 'Found mistake?', 24 | link: 'https://github.com/Jenesius/vue-modal/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=[BUG]' 25 | }, 26 | ] 27 | } 28 | 29 | function sidebarGuide() { 30 | return [ 31 | { 32 | text: 'Guide', 33 | items: [ 34 | {text: 'Get started!', link: 'getting-started'}, 35 | {text: 'Methods', link: 'guide-methods'}, 36 | {text: 'Navigation guards', link: 'guide-navigation-guards'}, 37 | {text: 'Returned value', link: 'guide-returned-value'}, 38 | {text: 'Store', link: "store"} 39 | ] 40 | }, 41 | { 42 | text: 'Details', 43 | items: [ 44 | {text: 'ModalObject', link: 'modal-object'}, 45 | {text: 'Styles', link: 'details-styles'}, 46 | {text: 'Config', link: 'config'}, 47 | {text: 'Event close', link: 'event-close'}, 48 | {text: 'Animation', link: 'details-animation'}, 49 | {text: 'Namespace', link: 'namespace'}, 50 | {text: 'Sidebar modal', link: 'sidebar'}, 51 | ] 52 | }, 53 | { 54 | text: 'Integration With VueRouter', 55 | items: [ 56 | {text: 'Introduction', link: 'integration-introduction'}, 57 | {text: 'Installation', link: 'integration-installation'}, 58 | {text: 'Passing parameters', link: 'integration-practical'}, 59 | {text: 'Closing Modal with Router', link: 'close-after-route-changed'} 60 | ] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /docs/.vitepress/ru.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vitepress"; 2 | export const ru = defineConfig({ 3 | lang: 'ru-RU', 4 | description: "Документация для vue-modal. Гибкая и простая библиотека для создания модальных окон в Vue3.", 5 | 6 | themeConfig: { 7 | nav: nav(), 8 | sidebar: { 9 | '/ru/guide/': { base: '/ru/guide/', items: sidebarGuide()}, 10 | }, 11 | outline: { 12 | label: "На этой странице", 13 | }, 14 | docFooter: { 15 | prev: 'Предыдущая страница', 16 | next: 'Следующая страница', 17 | }, 18 | sidebarMenuLabel: 'Меню', 19 | returnToTopLabel: 'Вернуться наверх', 20 | langMenuLabel: 'Изменить язык', 21 | darkModeSwitchLabel: 'Изменить тему', 22 | lastUpdated: { 23 | text: "Последнее обновление" 24 | } 25 | }, 26 | }) 27 | 28 | function nav() { 29 | return [ 30 | { text: 'Руководство', link: '/guide/getting-started', activeMatch: '/guide/' }, 31 | { text: 'Примеры', link: '/ru/examples/list', activeMatch: '/examples/' }, 32 | { text: 'Нашли ошибку?', link: 'https://github.com/Jenesius/vue-modal/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=[BUG]'}, 33 | ] 34 | } 35 | 36 | function sidebarGuide() { 37 | return [ 38 | { 39 | text: 'Руководство', 40 | items: [ 41 | { text: 'Приступим!', link: 'getting-started' }, 42 | { text: 'Методы', link: 'guide-methods' }, 43 | { text: 'Навигационные хуки', link: 'guide-navigation-guards' }, 44 | { text: 'Возвращаемое значение', link: 'guide-returned-value' }, 45 | { text: 'Хранилище', link: "store" } 46 | ] 47 | }, 48 | { 49 | text: 'Детали', 50 | items: [ 51 | { text: 'ModalObject', link: 'modal-object'}, 52 | { text: 'Стили', link: 'details-styles'}, 53 | { text: 'Конфигурация', link: 'config'}, 54 | { text: 'Событие закрытия', link: 'event-close' }, 55 | { text: 'Анимация', link: 'details-animation'}, 56 | { text: 'Namespace', link: 'namespace' }, 57 | { text: 'Боковое окно', link: 'sidebar' }, 58 | ] 59 | }, 60 | { 61 | text: 'Интеграция с VueRouter', 62 | items: [ 63 | { text: 'Введение', link: 'integration-introduction'}, 64 | { text: 'Установка', link: 'integration-installation'}, 65 | { text: 'Передача параметров', link: 'integration-practical'}, 66 | { text: 'Закрытие модального окна через маршрут', link: 'close-after-route-changed' } 67 | ] 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /docs/.vitepress/shared.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vitepress"; 2 | 3 | export const shared = defineConfig({ 4 | title: 'Jenesius vue-modal', 5 | lastUpdated: true, 6 | 7 | head: [ 8 | ['link', { rel: 'icon', href: `/images/logo.png` }], 9 | ['meta', { name: 'keywords', content: 'jenesius-vue-modal, vue-modal, vue3 modal, modal vue,Vue 3, modal windows, popup, modal vue-router, open modal, close modal, vue modal documentation' }] 10 | ], 11 | sitemap: { 12 | hostname: "https://modal.jenesius.com" 13 | }, 14 | themeConfig: { 15 | logo: '/images/logo.svg', 16 | socialLinks: [ 17 | { icon: 'github', link: 'https://github.com/Jenesius/vue-modal' }, 18 | ], 19 | }, 20 | }) -------------------------------------------------------------------------------- /docs/examples/animation.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Custom Animation 6 | 7 | -------------------------------------------------------------------------------- /docs/examples/demo.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Demo 6 | -------------------------------------------------------------------------------- /docs/examples/list.md: -------------------------------------------------------------------------------- 1 | - [Simple modal](simple-modal.md) 2 | - [Prompt modal](prompt-modal.md) 3 | - [Multi modal](multi-modals.md) 4 | - [VueRouter with Modals](vue-router-with-modals.md) 5 | - [Demo](demo.md) 6 | - [Animation](animation.md) 7 | - [Sidebar modal](sidebar.md) 8 | -------------------------------------------------------------------------------- /docs/examples/multi-modals.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Multi Modal 6 | 7 | -------------------------------------------------------------------------------- /docs/examples/prompt-modal.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Prompt Modal 6 | 7 | -------------------------------------------------------------------------------- /docs/examples/sidebar.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | ----- 8 | 9 | # Side modal 10 | 11 | An example of displaying a modal window on the side will be described here. For this 12 | you won't need `js` or project setup. We'll make do with just classes 13 | to `css`. 14 | 15 | We can have the following class in `css`: 16 | 17 | ```css 18 | .modal-container:has(.modal-sidebar) { 19 | /*Move modal to right*/ 20 | display: grid; 21 | grid-auto-columns: max-content; 22 | justify-content: end; 23 | } 24 | 25 | /*Change animation only for sidebar modal*/ 26 | .modal-list-enter-from .modal-sidebar, 27 | .modal-list-leave-to .modal-sidebar{ 28 | transform: translateX(100px) !important; 29 | } 30 | ``` 31 | 32 | :::info Naming 33 | 34 | - `.modal-container` is the class in which the modal window is located, it 35 | used by the library. 36 | - `.modal-sidebar` is the name of the class that you set in your 37 | modal component. 38 | - `.modal-list-enter-from` and `.modal-list-leave-to` animation classes, 39 | also created by the library. 40 | ::: 41 | 42 | 43 | Here we have established that the modal window will appear on the right. 44 | Now in the modal window itself we will indicate this class at the very top 45 | level: 46 | 47 | ```vue 48 | 53 | ``` 54 | 55 | It is important not to forget that these `css` properties must be specified in the global 56 | scope, because they should be available not only to the modal 57 | the window, but also the container itself. 58 | -------------------------------------------------------------------------------- /docs/examples/simple-modal.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Simple Modal 6 | 7 | -------------------------------------------------------------------------------- /docs/examples/vue-router-with-modals.md: -------------------------------------------------------------------------------- 1 | # VueRouter with Modals 2 | 3 | It turned out to be problematic on the site to display an example for working with vue-router, but you can familiarize yourself with it by running 4 | `npm run serve` and go to **/vue-router-with-modal**. 5 | 6 | In this example, we'll look at how vue-router integration works and how to use it. 7 | 8 | To work with VueRouter, the library provides the **useModalRouter** method, which is primarily a wrapper for 9 | component. It is with the help of it that our components will be wrapped in modal windows. To use, let's create a simple 10 | config file for vue-router: 11 | ```ts 12 | import {createRouter, createWebHashHistory} from "vue-router" 13 | import {useModalRouter} from "jenesius-vue-mdoal"; 14 | import Modal from "./any-modal.vue"; 15 | 16 | const routes = [ 17 | { 18 | path: "/", 19 | component: SomeComponent 20 | }, 21 | { 22 | path: "/users/:id", 23 | component: useModalRouter(Modal) // Step 1 24 | } 25 | ] 26 | 27 | const router = createRouter({ 28 | routes, 29 | history: createWebHashHistory() 30 | }) 31 | 32 | useModalRouter.init(router); // Step 2 33 | 34 | export default router 35 | ``` 36 | 37 | ### Step 1 38 | In the first step, we wrap the component. In fact, this method does the following: 39 | 1. When switching to the specified route, it will show the passed component. 40 | 2. Render always returns *null* as a result. 41 | 3. When leaving the route, the current modal window will be closed. 42 | 43 | ### Step 2 44 | Before work, we must provide a router so that the library can subscribe to state changes 45 | router. 46 | 47 | ---- 48 | 49 | Now if we navigate to */users/:id* our modal window will be opened. 50 | -------------------------------------------------------------------------------- /docs/guide/close-after-route-changed.md: -------------------------------------------------------------------------------- 1 | # Closing modal with vue-router 2 | 3 | By default, the modal doesn't close when the path changes (vue-router). This was done on purpose, because 4 | your project may not have this library or any other one for working with routes. Also this 5 | the function is not heavy even for an entry-level programmer. 6 | 7 | ## Vue Router 8 | So that when closing a modal window using the **vue-router** library, you need to register the following 9 | hook: 10 | 11 | ```ts 12 | import {getCurrentModal, closeModal} from "jenesius-vue-modal"; 13 | 14 | const router = new VueRouter({...}) 15 | 16 | router.beforeEach(async (to, from, next) => { 17 | // There's some opened modal 18 | if (getCurrentModal()) { // (1) 19 | try { 20 | await closeModal(); // (2) 21 | next(); // (3) 22 | } catch (e) { 23 | // ... 24 | next(false); // (4) 25 | } 26 | } else next(); 27 | }) 28 | ``` 29 | 30 | Let's see what's going on here: 31 | - (1) - If the current modal window is not *undefined* then we need to close all modal windows (2). 32 | - (3) - If the closing was successful, then we confirm the transition to the next route. 33 | - (4) - We disable the transition if the *closeModal* threw an error (the modal window remained open). -------------------------------------------------------------------------------- /docs/guide/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | To change the settings `jenesius-vue-modal` you need to use 4 | function `config`. You can influence the handling of pressing `Esc`, 5 | name of animation, `scroll` lock, etc: 6 | 7 | ```ts 8 | import {config} from "jenesius-vue-modal"; 9 | config({ 10 | // Params 11 | }) 12 | ``` 13 | 14 | The configuration object has the following properties: 15 | 16 | - **`scrollLock`** Default value is `true`. If installed 17 | value `true`, opening a modal window will block `scroll` on 18 | page. 19 | - **`animation`** Default value is `modal-list`. Used 20 | to set the animation name for `transition-group`. Read more 21 | can be found on [this page](./details-animation). 22 | - **`escClose`** Default value is `true`. Controls closing 23 | modal window by pressing `Esc`. 24 | 25 | :::info For Namespace 26 | If you are working with a different `namespace` than the original one, 27 | you need to take care of the closure yourself. 28 | ::: 29 | 30 | - **`backgroundClose`** Default value is `true`. Parameter 31 | is responsible for closing the modal window by clicking on the 32 | darkened background. In 33 | case, if set to `true`, clicking on the back area will 34 | cause the modal window to close. 35 | 36 | - **`skipInitCheck`** Default value is `false`. Is used for 37 | checking for the presence of `container` on the page when the modal is opened 38 | window. When you try to open a modal window you will receive an error 39 | `NotInitilized`. If your project assumes that the container 40 | will create after opening the modal window, you can pass 41 | value `true`. Thus skipping the `container` check procedure. 42 | 43 | - **`store`** Default value is `{}`. Used for storage 44 | modal windows and opening them by key. You can read more in detail 45 | on [here](./store). 46 | 47 | - **`singleShow`** The default value is `false`. Used in case 48 | if you need to show only the last modal window when using several 49 | windows (via `pushModal`). In such a case, if the value is set to true, 50 | When you open a fashion window, all voices will be closed (through the mechanism 51 | `v-show`). -------------------------------------------------------------------------------- /docs/guide/details-animation.md: -------------------------------------------------------------------------------- 1 | # Animation 2 | If you need to change the animation of showing and hiding the modal, you need 3 | to override some properties and styles. Default for animating modal windows uses 4 | **modal-list** as the animation name. To override the animation name, you need 5 | to specify a new one in the configuration: 6 | ```ts 7 | import {config} from "jenesius-vue-modal"; 8 | 9 | config({ 10 | animation: "fade" // Any name 11 | }) 12 | ``` 13 | When changing the animation, it is necessary to take into account that we must 14 | animate both the **.modal-container** and the modal window itself **.modal-item**. 15 | 16 | For example, let's change the animation and the length of the appearance of 17 | the modal window: 18 | 19 | *Instead of fade, you can use modal-list. In this case, you do not need to 20 | redefine the name in the configuration.* 21 | 22 | ```css 23 | /* Don't forget to include the animation time for the start block */ 24 | .fade-enter-active, 25 | .fade-leave-active, 26 | .fade-enter-active .modal-item, 27 | .fade-leave-active .modal-item{ 28 | transition: 1.2s; 29 | } 30 | 31 | .fade-enter-from .modal-item, 32 | .fade-leave-to .modal-item{ 33 | transform: translateX(100px); 34 | } 35 | ``` 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/guide/details-styles.md: -------------------------------------------------------------------------------- 1 | # Styles 2 | 3 | To control the background and position of the modal window, you need to work 4 | with the css class **.modal-container** . When a modal window is opened, it is 5 | placed container with the aforementioned class. It has the following properties: 6 | ```css 7 | .modal-container{ 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | 12 | background-color: #3e3e3e21; 13 | } 14 | ``` 15 | For example, let's change the background of the modal and make it open at the bottom: 16 | ```css 17 | .modal-container{ 18 | align-items: flex-end; 19 | background-color: rgba(62, 175, 124, 0.47); 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/guide/event-close.md: -------------------------------------------------------------------------------- 1 | # Event close 2 | **Each close hook** of a modal accepts one *event-close* parameter. This object provides information about 3 | closing the modal window. It currently has the following properties: 4 | - background (*boolean*) Set to true if the modal is closed by clicking on the background. 5 | - esc (*boolean*) Set to true if the modal close process started with *Escape* 6 | 7 | ```ts 8 | { 9 | beforeModalClose(e) { 10 | // e.background 11 | // e.esc 12 | } 13 | } 14 | ``` 15 | 16 | ### Handle esc closing 17 | This example demonstrates how to cancel the closing of the modal window on pressing *Escape*: 18 | ```ts 19 | const modal = await openModal(SomeComponent); 20 | modal.onclose = (event) => { 21 | if (event.esc) return false; 22 | // ... 23 | } 24 | ``` 25 | 26 | ## Handle background closing 27 | This example demonstrates how to cancel the closing of the modal window on clicking on the background (darkened) background: 28 | ```ts 29 | const modal = await openModal(SomeComponent); 30 | modal.onclose = (event) => { 31 | if (event.background) return false; 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Jenesius Vue Modal is a lightweight and simple library for working with modal windows in Vue3. It integrates deeply with Vue.js and allows you to create modals of any complexity. 4 | 5 | ## Installation 6 | Npm is recommended for installing a package. 7 | ```shell 8 | npm i jenesius-vue-modal 9 | ``` 10 | 11 | ## Connection to page 12 | To get started, we need to initialize modal windows, add a container in which our components will be displayed. We import the container from the library: 13 | ```vue 14 | 18 | 26 | ``` 27 | 28 | ## Let's continue 29 | 30 | Everything is ready to work with modal windows. Go to the [methods](./guide-methods.md) tab to learn how to open and 31 | close modal windows. This library provides a wide range of functions for working with modal windows: 32 | - Opening multiple modal windows 33 | - Return value 34 | - Working with events through the `ModalObject` object 35 | - Integration with `vue-router` 36 | - Availability of `namespace` and the ability to open elements in different spaces. -------------------------------------------------------------------------------- /docs/guide/guide-navigation-guards.md: -------------------------------------------------------------------------------- 1 | # Navigation Guards 2 | 3 | ## Information 4 | 5 | Sometimes it is necessary to catch the closing of a modal and manage 6 | this state. This can be useful to prevent the user from closing the 7 | modal until they have entered input, or to send a request to the 8 | server. 9 | 10 | If the handler returns **false** or **throws an error** , closing 11 | the modal window will be interrupted. 12 | 13 | Jenesius Vue Modal provides three ways to catch closures: 14 | 15 | ## Onclose 16 | 17 | The [openModal](/guide/guide-methods#open-modal) 18 | and [pushModal](/guide/guide-methods#push-modal) 19 | methods return Promise, which, if successful, 20 | will return the [modalObject](/guide/modal-object) object. In order to 21 | catch the closing of 22 | a modal window, you need to add an event **onclose** to this object: 23 | 24 | ```ts 25 | import {openModal} from "jenesius-vue-modal"; 26 | 27 | const modal = await openModal(VueComponent); 28 | let count = 5; 29 | 30 | modal.onclose = () => { 31 | count--; 32 | //The modal window will be closed after five attempts. 33 | if (count > 0) return false; 34 | } 35 | ``` 36 | 37 | ### Example 38 | 39 | If several modal windows are open, and one of them will have an 40 | onclose handler that returns false, you can close only those modal 41 | windows that were opened after it. 42 | 43 | ```ts 44 | import {pushModal, closeModal} from "jenesius-vue-modal"; 45 | 46 | const modal1 = await pushModal(VueComponent); 47 | const modal2 = await pushModal(VueComponent); 48 | const modal3 = await pushModal(VueComponent); 49 | 50 | modal2.onclose = () => false; 51 | 52 | closeModal(); // close only modal3 53 | ``` 54 | 55 | ## In-Component Guards 56 | 57 | Finally, the navigation hook can be specified directly in the 58 | component using the following options: 59 | 60 | - beforeModalClose 61 | 62 | ```ts 63 | const Foo = { 64 | template: "...", 65 | beforeModalClose() { 66 | // has access to the context of the component instance this. 67 | } 68 | } 69 | ``` 70 | 71 | ## Composition Api 72 | 73 | While you can still use built-in functions, Jenesius Vue Modal 74 | provides functions for the Composition API: 75 | 76 | ```ts 77 | import {onBeforeModalClose} from "jenesius-vue-modal" 78 | 79 | export default { 80 | setup() { 81 | onBeforeModalClose(() => { 82 | const answer = window.confirm( 83 | "Do you really want to leave? You have unsaved changes!" 84 | ) 85 | if (!answer) return false 86 | }) 87 | } 88 | } 89 | ``` 90 | 91 | ## Async Guards 92 | 93 | The navigation hook can be asynchronous. The modal window will be 94 | closed only when it finishes its work: 95 | 96 | ```ts 97 | export default { 98 | async beforeModalClose() { 99 | await updateData(); 100 | } 101 | } 102 | ``` 103 | 104 | ```ts 105 | const modal = await openModal(Modal); 106 | 107 | modal.onclose = () => { 108 | return new Promise(resolve => { 109 | setTimeout(resolve, 1000); // The modal will be closed after one second. 110 | }) 111 | } 112 | ``` 113 | 114 | ## Close event 115 | 116 | Each modal close handler get window parameter: event-close 117 | 118 | ```ts 119 | modal.onclose = (event) => { 120 | // ... 121 | } 122 | ``` 123 | 124 | This *event* stores information about how the modal window is closed. 125 | Detailed information about it, about the way to prevent 126 | Closing a modal window by background or by pressing the Esc key can be 127 | read [here](/guide/event-close). 128 | -------------------------------------------------------------------------------- /docs/guide/guide-returned-value.md: -------------------------------------------------------------------------------- 1 | # Return value from Modal 2 | 3 | If we are talking about the return value of modal windows, we must 4 | first understand their essence. By default, modal windows are treated 5 | as a separate layer of logic with their own data model. This approach 6 | is convenient and makes developing a web application with modal 7 | windows safe. This library inherits this concept. 8 | A modal window is a separate logical level that accepts input 9 | parameters and somehow interacts with them. 10 | However, there are times when a modal window is just part of a 11 | process. I sometimes encounter such cases (although I try my best to 12 | avoid them). 13 | I will describe the solutions that I use in my projects. Starting with 14 | the most convenient, ending with those that I would not use: 15 | 16 | ## Using PromptModal 17 | 18 | In vanilla JS, there is a function prompt, which, upon closing the 19 | popup, will wrap the entered value. 20 | 21 | File *ModalCode.vue* 22 | 23 | ```vue 24 | 25 | 28 | 41 | ``` 42 | 43 | How to use: 44 | 45 | ```ts 46 | import {promptModal} from "jenesius-vue-modal" 47 | 48 | const code = await promptModal(ModalCode); 49 | ``` 50 | 51 | What happened in this example: 52 | 53 | - `promptModal` returns a promise that will be fulfilled when 54 | closing the modal window. The result of `promise` will be 55 | the value passed to the `Modal.EVENT_PROMPT` event. 56 | - To describe the modal window, we implemented a simple pass 57 | random value by pressing a button. 58 | 59 | ## Provide Handle 60 | 61 | We can also pass a function to be called inside a modal window ( 62 | Reminds me of the React component logic): 63 | 64 | ```ts 65 | const modal = await openModal(Modal, { 66 | handleRequest: (value) => { 67 | // do something 68 | } 69 | }) 70 | ``` 71 | 72 | And then in the modal: 73 | 74 | ```vue 75 | 76 | 86 | ``` 87 | 88 | ## Emit value and closing 89 | 90 | We can also subscribe to an event and react when it is triggered: 91 | 92 | ```vue 93 | // modal.vue 94 | 97 | ``` 98 | 99 | And when we open modal: 100 | 101 | ```ts 102 | const modal = await openModal(Modal) 103 | modal.on('return', (value) => { 104 | console.log(value); // false 105 | modal.close() 106 | }) 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/guide/integration-installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | - Don't forget to [initialize](./getting-started) Jenesius Vue Modal 4 | - When creating router add modal integration: 5 | ```ts 6 | import { createWebHistory, createRouter} from "vue-router"; 7 | import {useModalRouter} from "jenesius-vue-modal"; 8 | 9 | const routes = [...]; 10 | const router = createRouter({ 11 | history: createWebHistory(), // Or any other 12 | routes, 13 | }); 14 | 15 | useModalRouter.init(router); 16 | ``` 17 | Now the modal handler will react to navigation changes. 18 | - Add new route: 19 | ```ts 20 | import Modal from "Modal.vue" 21 | 22 | const routes = [ 23 | { 24 | path: "/any-route", 25 | component: useModalRouter(Modal) 26 | } 27 | ] 28 | ``` 29 | Now, when switching to **/any-route**, the window that was passed to 30 | **useModalRouter** the modal window will be displayed. 31 | 32 | :::warning router-view 33 | To display modal windows integrated with `vue-router` is not necessary 34 | create `router-view`, because modal windows are displayed in their own 35 | container. 36 | ::: -------------------------------------------------------------------------------- /docs/guide/integration-introduction.md: -------------------------------------------------------------------------------- 1 | #Introduction 2 | 3 | Sometimes there is a need to bind vue-router for display 4 | modal window. To do this you need to write your own 5 | handler for the router and integrate it with 6 | opening/closing a modal window. `jenesius-vue-modal` already has 7 | all this, you just need to connect it. 8 | 9 | For example, a user table was created along the route **/users**. 10 | A modal handler has been added to the **/users/:id** route. Now when 11 | switching to **/users/3** a modal window will open in which 12 | input parameters from the route are available. They will be transferred as 13 | `props`. 14 | 15 | A complete example can be seen at 16 | SandBox([link](https://codesandbox.io/s/vue-modal-router-n9rn94)). 17 | Note the change in `URL`. -------------------------------------------------------------------------------- /docs/guide/integration-practical.md: -------------------------------------------------------------------------------- 1 | # Particular Qualities 2 | You can use **props** to get input parameters. 3 | ```ts 4 | // user/5 5 | { 6 | props: { 7 | id: String // 5 8 | } 9 | } 10 | ``` 11 | Using **beforeRouteEnter** , **beforeRouteUpdate** , **beforeRouteLeave** is not 12 | possible at this stage. I will try to fix this problem shortly. 13 | BeforeModalClose can be used as a temporary solution. 14 | -------------------------------------------------------------------------------- /docs/guide/modal-object.md: -------------------------------------------------------------------------------- 1 | # Modal 2 | 3 | Methods [openModal](./guide-methods#open-modal), 4 | [pushModal](./guide-methods#push-modal) return `Promise` which 5 | is successful, will return a `Modal` object. This object is a class 6 | modal window and serves to receive or manage the current 7 | condition. This page will list all available methods 8 | and properties. 9 | 10 | ## Properties 11 | 12 | ### `id` 13 | 14 | The field is a unique identifier for the modal window. 15 | Assigned at the time of opening. 16 | 17 | ### `instance` 18 | 19 | An instance of a modal window component. Used to 20 | access `$data`, `$props` and other modal window properties. 21 | 22 | ::: warning 23 | If you get **instance** in your project together in 24 | ` 25 | ``` 26 | 27 | Let us immediately pay attention to: 28 | 29 | - getting a queue for `notification`. 30 | - display messages in `v-for`. 31 | - We take the message text from props. **It's important** to remember that props are 32 | `ComputedRef`. 33 | 34 | :::info 35 | If instead of `p` you use your own component, then I recommend 36 | use the following: 37 | ```vue 38 | 46 | ``` 47 | ::: 48 | 49 | This completes the `notification` functionality. It remains to connect this 50 | container in your component, for example `App.vue` and call one of 51 | methods for opening a modal window with `namespace: 'notification'`. -------------------------------------------------------------------------------- /docs/guide/namespace.md: -------------------------------------------------------------------------------- 1 | # Namespace 2 | 3 | This functionality goes beyond the use of modal windows. However, 4 | this mechanism allows you to implement new interface elements, such as 5 | notifications, error display, etc. Using `namespace` you can easily 6 | implement components with similar functionality of modal windows. 7 | 8 | This article will tell you how to use `namespace`, as well as 9 | An example of creating notifications based on a modal window container 10 | will be given. 11 | 12 | ## Initialization 13 | 14 | By default, we create a container without specifying a `namespace`, but we 15 | nothing prevent you from creating a named container: 16 | 17 | ```vue 18 | 21 | 24 | ``` 25 | 26 | Here we have created a container in which all modal windows will be placed 27 | with `namespace` set to `errors`. 28 | 29 | ## Addition 30 | 31 | Methods [openModal](./guide-methods#open-modal), [pushModal](./guide-methods#push-modal), 32 | [promptModal](./guide-methods#prompt-modal) are also expanded. In the option you can 33 | indicate the `namespace` that will be used: 34 | 35 | ```ts 36 | import Modal from "my-modal.vue" 37 | import {pushModal} from "jenesius-vue-modal" 38 | 39 | pushModal(Modal, {}, { namespace: 'errors' }) 40 | ``` 41 | 42 | This modal window will be added to the `errors` space and will 43 | be displayed in the appropriate container. 44 | 45 | ## Closing 46 | 47 | Of course, now you need the ability to close not the last modal 48 | window, and the last window from the desired container. Methods 49 | [closeModal](./guide-methods#close-modal), 50 | [popModal](./guide-methods#pop-modal) have been expanded. Now in the options, 51 | passed as the first parameter, you can specify `namespace`: 52 | 53 | ```ts 54 | import {closeModal} from "jenesius-vue-modal" 55 | closeModal({namespace: 'errors'}) 56 | ``` 57 | 58 | ## Standard interaction 59 | 60 | What you need to understand is that when we don't specify `namespace`, we are working 61 | with `namespace` = `default`. I recommend not specifying it at all if 62 | The `namespace` used is assumed to be the default. 63 | 64 | **It is important** to clarify that only for `namespace` equal to `default` 65 | The `Esc` handler is installed. At the moment the implementation forces 66 | It’s up to you to take care of closing modal windows in containers 67 | other than `default.` 68 | 69 | ## Modal 70 | 71 | In the object returned by the pushModal, openModal, promptModal methods 72 | a `namespace` field has appeared, which will be set automatically 73 | and store the name of the container. 74 | 75 | ```ts 76 | const modal = await pushModal(Modal, {}, { namespace: 'notification' }); 77 | modal.namespace // 'notification' 78 | ``` 79 | 80 | ## Gets the current modal window. 81 | 82 | The `getCurrentModal` method has been extended. Now first and optional 83 | the parameter is the name `namespace` for which you want to find the last one 84 | open modal window. 85 | 86 | ```ts 87 | getCurrentModal() // Return last opened modal from default namespace 88 | getCurrentModal("notifications") // From namespace: "notifications" 89 | ``` 90 | 91 | ## Getting the current queue 92 | 93 | In practice, using the second `modal-container` does not represent 94 | many amenities. Other interface elements are very different from the dialog ones 95 | (modal) windows. Therefore, it will be more convenient for you to use a queue that 96 | stores an array of current elements: 97 | 98 | ```ts 99 | import {getQueueByNamespace} from "jenesius-vue-modal" 100 | 101 | const notifications = getQueueByNamespace("notifications"); 102 | ``` 103 | 104 | ### Options 105 | 106 | - **namespace** Optional parameter specifying the name `namespace`. 107 | 108 | ### Return 109 | 110 | The method returns a `reactive` array that stores modal windows. 111 | ## Example 112 | 113 | -------------------------------------------------------------------------------- /docs/guide/store.md: -------------------------------------------------------------------------------- 1 | # Modal storage 2 | 3 | In older versions of this library, opening a modal window was only possible by passing a component. 4 | The approach remains the main one and sow the day, because. gives you more control and order in your code. However, there is 5 | many situations where this approach is not convenient. For example, when developing a library, the question arises: 6 | how to override the opening of modal windows (how to replace the modal window). This is where storage comes in. 7 | 8 | ## Initialization 9 | 10 | To get started, you need to initialize the storage (save the modal windows we need in it). All 11 | this is done using the configuration function: 12 | 13 | ```ts 14 | import {config} from "jenesius-vue-modal"; 15 | import ModalConfirm from "./ModalConfirm"; 16 | import ModalAlert from "./ModalAlert" 17 | 18 | config({ 19 | store: { 20 | confirm: ModalConfirm, 21 | alert: { 22 | component: "ModalAlert" 23 | } 24 | } 25 | }) 26 | ``` 27 | In this example, we have added two modal windows to the repository: 28 | - by directly transferring components 29 | - in the second case, we specified an object with the `component` property set, this is necessary to set some 30 | properties specifically for this component. 31 | 32 | Now you can open it by name by passing the key there: 33 | ```ts 34 | openModal('confirm'); 35 | openModal('alert'); 36 | ``` 37 | 38 | Of course, *pushModal* and *promptModal* methods also support this functionality. 39 | 40 | ## Checking for a Modal Window 41 | If you are writing a library, for better interoperability, a function has been added that checks for the presence of a modal 42 | windows in storage: 43 | ```ts 44 | import {getComponentFromStore} from "jenesius-vue-modal"; 45 | getComponentFromStore('alert') // undefined 46 | getComponentFromStore('confirm') // Component 47 | ``` 48 | **Returns** the VueComponent if it was previously initialized in the store, *undefined* otherwise. 49 | 50 | ## Extended component transfer 51 | 52 | In this article, we will consider the case when we, together with components, pass an entire object with the described `component` property. 53 | In this case, we can also specify the following properties: 54 | 55 | - `backgroundClose` 56 | - `draggable` 57 | - `beforeEach` 58 | 59 | All these properties correspond to the parameter from [configuration](./config). 60 | 61 | ```ts 62 | config({ 63 | store: { 64 | confirm: { 65 | component: ModalConfirm, 66 | draggable: true, 67 | backgroundClose: false, 68 | beforeEach() { 69 | Logger.write('Attempting to open a confirmation window.') 70 | } 71 | } 72 | } 73 | }) 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jenesius/vue-modal/9177d8fe34af0f5584df120d49c36f1b336ac6f6/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Jenesius vue-modal 5 | titleTemplate: Jenesius Vue Modal & Simple Modal System for Vue 6 | 7 | hero: 8 | name: Jenesius vue-modal 9 | text: Simple Modal System for Vue 10 | tagline: Simple, powerful, and performant. 11 | actions: 12 | - theme: brand 13 | text: Get Started 14 | link: /guide/getting-started 15 | - theme: alt 16 | text: View on GitHub 17 | link: https://github.com/jenesius/vue-modal 18 | features: 19 | - title: So simple 20 | icon: 🚀 21 | details: Just put ModalContainer and then open any components like modal windows. 22 | - title: Built-in and custom 23 | icon: ✍ 24 | details: This library don't add any design rules. You can use your components with your css classes. 25 | - title: Abstraction 26 | icon: 🤖 27 | details: There is no need to embed a modal window in the component. Work with it at a different level of abstraction. 28 | - title: Integration with vue-router 29 | icon: 🎢 30 | details: You can connect this library to vue-router to make a more user-friendly interface. 31 | --- 32 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build" 8 | }, 9 | "devDependencies": { 10 | "vitepress": "workspace:*" 11 | } 12 | } -------------------------------------------------------------------------------- /docs/ru/examples/animation.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Кастомные анимации 6 | 7 | -------------------------------------------------------------------------------- /docs/ru/examples/demo.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Демо 6 | -------------------------------------------------------------------------------- /docs/ru/examples/list.md: -------------------------------------------------------------------------------- 1 | - [Простое модальное окно](simple-modal.md) 2 | - [Возращение значение из модального окна](prompt-modal.md) 3 | - [Множественные модальные окна](multi-modals.md) 4 | - [vue-router с модальными окнами](vue-router-with-modals.md) 5 | - [Демо](demo.md) 6 | - [Анимации](animation.md) 7 | - [Боковое модальное окно](sidebar.md) 8 | -------------------------------------------------------------------------------- /docs/ru/examples/multi-modals.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Множественные модальные окна 6 | 7 | -------------------------------------------------------------------------------- /docs/ru/examples/prompt-modal.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Возвращаемое значение 6 | 7 | -------------------------------------------------------------------------------- /docs/ru/examples/sidebar.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | ----- 8 | 9 | # Боковое окно 10 | 11 | Здесь будет описан пример отображения модального окна сбоку. Для этого 12 | вам не понадобится `js` или настройка проекта. Мы обойдёмся лишь классами 13 | на `css`. 14 | 15 | Мы можем завести следующий класс в `css`: 16 | 17 | ```css 18 | .modal-container:has(.modal-sidebar) { 19 | /*Move modal to right*/ 20 | display: grid; 21 | grid-auto-columns: max-content; 22 | justify-content: end; 23 | } 24 | 25 | /*Change animation only for sidebar modal*/ 26 | .modal-list-enter-from .modal-sidebar, 27 | .modal-list-leave-to .modal-sidebar{ 28 | transform: translateX(100px) !important; 29 | } 30 | ``` 31 | 32 | :::info Именование 33 | 34 | - `.modal-container` класс в котором находится модальное окно, оно 35 | используется библиотекой. 36 | - `.modal-sidebar` название класса, которое вы устанавливаете в своей 37 | модальной компоненте. 38 | - `.modal-list-enter-from` и `.modal-list-leave-to` классы анимации, 39 | также создаваемые библиотекой. 40 | ::: 41 | 42 | 43 | Здесь мы установили то, что модально окно будет появляться справа. 44 | Теперь в самом модальном окне мы укажем этот класс на самом верхнем 45 | уровне: 46 | 47 | ```vue 48 | 53 | ``` 54 | 55 | Важно не забыть, что данные свойства `css` нужно указывать в глобальной 56 | области видимости, т.к. они должны быть доступны не только модальному 57 | окну, но и самому контейнеру. 58 | -------------------------------------------------------------------------------- /docs/ru/examples/simple-modal.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Простое модальное окно 6 | 7 | -------------------------------------------------------------------------------- /docs/ru/examples/vue-router-with-modals.md: -------------------------------------------------------------------------------- 1 | # VueRouter с Modals 2 | 3 | На сайте оказалось проблематично отобразить пример работы с vue-router, но ознакомиться с ним можно, запустив 4 | `npm run serve` и перейдите в **/vue-router-with-modal**. 5 | 6 | В этом примере мы рассмотрим, как работает интеграция vue-router и как ее использовать. 7 | 8 | Для работы с VueRouter библиотека предоставляет метод **useModalRouter**, который в первую очередь является оболочкой для 9 | компонент. Именно с его помощью наши компоненты будут обернуты в модальные окна. Для использования давайте создадим простой 10 | конфигурационный файл для vue-router: 11 | ```ts 12 | import {createRouter, createWebHashHistory} from "vue-router" 13 | import {useModalRouter} from "jenesius-vue-modal"; 14 | import Modal from "./any-modal.vue"; 15 | 16 | const routes = [ 17 | { 18 | path: "/", 19 | component: SomeComponent 20 | }, 21 | { 22 | path: "/users/:id", 23 | component: useModalRouter(Modal) // Step 1 24 | } 25 | ] 26 | 27 | const router = createRouter({ 28 | routes, 29 | history: createWebHashHistory() 30 | }) 31 | 32 | useModalRouter.init(router); // Step 2 33 | 34 | export default router 35 | ``` 36 | 37 | ### Шаг 1 38 | На первом этапе мы оборачиваем компонент. Фактически этот метод делает следующее: 39 | 1. При переходе на указанный маршрут будет отображаться переданный компонент. 40 | 2. Render всегда возвращает в результате *null*. 41 | 3. При выходе с маршрута текущее модальное окно закроется. 42 | 43 | ### Шаг 2 44 | Перед работой мы должны предоставить маршрутизатор, чтобы библиотека могла подписываться на изменение состояния. 45 | маршрутизатор. 46 | 47 | ---- 48 | 49 | Теперь, если мы перейдем к */users/:id*, откроется наше модальное окно, а значение id будет передано в качестве props. 50 | -------------------------------------------------------------------------------- /docs/ru/guide/close-after-route-changed.md: -------------------------------------------------------------------------------- 1 | # Закрытие модального окна с помощью vue-router 2 | 3 | По умолчанию модальное окно не закрывается при изменении пути (vue-router). Это было сделано намеренно, потому что 4 | в вашем проекте может не быть этой библиотеки или какой-либо другой для работы с маршрутами. Также это 5 | функция не тяжёлая даже для программиста начального уровня. 6 | 7 | ## Vue Router 8 | Для закрытия модального окна с помощью библиотеки **vue-router** нужно прописать следующий хук: 9 | 10 | ```ts 11 | import {getCurrentModal, closeModal} from "jenesius-vue-modal"; 12 | 13 | const router = new VueRouter({...}) 14 | 15 | router.beforeEach(async (to, from, next) => { 16 | // Есть ли открытые окна 17 | if (getCurrentModal()) { // (1) 18 | try { 19 | await closeModal(); // (2) 20 | next(); // (3) 21 | } catch (e) { 22 | // ... 23 | next(false); // (4) 24 | } 25 | } else next(); 26 | }) 27 | ``` 28 | 29 | Давайте посмотрим, что здесь происходит: 30 | - (1) - Если текущее модальное окно не *undefined*, нам нужно закрыть все модальные окна (2). 31 | - (3) - Если закрытие прошло успешно, то подтверждаем переход на следующий маршрут. 32 | - (4) - Отключаем переход, если *closeModal* выдал ошибку (модальное окно осталось открытым). -------------------------------------------------------------------------------- /docs/ru/guide/config.md: -------------------------------------------------------------------------------- 1 | # Конфигурация 2 | 3 | Для изменения настроек `jenesius-vue-modal` необходимо воспользоваться 4 | функцией `config`. Вы можете повлиять на обработку нажатия `Esc`, 5 | название анимации, блокировки `scroll` и т.д: 6 | 7 | ```ts 8 | import {config} from "jenesius-vue-modal"; 9 | config({ 10 | // Параметры 11 | }) 12 | ``` 13 | 14 | Объект конфигурации имеет следующие свойства: 15 | 16 | - **`scrollLock`** Значение по умолчанию `true`. Если установлено 17 | значение `true`, открытие модального окна блокирует `scroll` на 18 | странице. 19 | - **`animation`** Значение по умолчанию `modal-list`. Используется 20 | для задания имени анимации для `transition-group`. Подробнее прочитать 21 | можно на [этой странице](./details-animation). 22 | - **`escClose`** Значение по умолчанию `true`. Управляет закрытие 23 | модального окна по нажатию на `Esc`. 24 | 25 | :::info Для Namespace 26 | Если вы работаете с `namespace` отличным от первоначального, вам 27 | необходимо позаботиться о закрытии самостоятельно. 28 | ::: 29 | 30 | - **`backgroundClose`** Значение по умолчанию `true`. Параметр 31 | отвечает за закрытие модального окна по клику на затемнённый фон. В 32 | случае, если установлено значение `true`, клик по задней области будет 33 | приводить к закрытию модального окна. 34 | 35 | - **`skipInitCheck`** Значение по умолчанию `false`. Используется для 36 | проверки наличия `container` на странице в момент открытия модального 37 | окна. При попытке открытия модального окна будет получена ошибка 38 | `NotInitilized`. Если у вас в проекте предполагается, что контейнер 39 | будет создавать после открытия модального окна, вы можете передать 40 | значение `true`. Тем самым пропуская процедуру проверки `container`. 41 | 42 | - **`store`** Значение по умолчанию `{}`. Используется для хранения 43 | модальных окон и открытия их по ключу. Более подробно можно прочитать 44 | на [здесь](./store). 45 | 46 | - **`singleShow`** Значение по умолчанию `false`. Используется в случае, 47 | если нужно показывать лишь последнее модальное окно при использовании нескольких 48 | окон (через `pushModal`). В таком случае, если значение установлено как `true`, 49 | при открытии модального окна, все предыдущие будут скрываться (через механизм 50 | `v-show`). -------------------------------------------------------------------------------- /docs/ru/guide/details-animation.md: -------------------------------------------------------------------------------- 1 | # Анимация 2 | Если вам нужно изменить анимацию показа или скрытия модального окна, вам нужно переопределить некоторые свойства и стили. 3 | По умолчанию для анимации модальных окон используется **modal-list** в качестве имени анимации. Чтобы переопределить имя 4 | анимации, вам нужно указать новые в конфигурации: 5 | 6 | ```ts 7 | import {config} from "jenesius-vue-modal"; 8 | 9 | config({ 10 | animation: "fade" // Любая другая 11 | }) 12 | ``` 13 | При изменении анимации необходимо учитывать, что мы должны анимировать как **.modal-container**, так и само модальное 14 | окно **.modal-item**. 15 | 16 | Например, изменим анимацию и продолжительность появления модальное окно: 17 | 18 | *Вместо **fade** вы можете использовать **modal-list**. В таком случае вам не нужно переопределять имя в конфигурации.* 19 | 20 | ```css 21 | /* Не забудьте указать время анимации для стартового блока. */ 22 | .fade-enter-active, 23 | .fade-leave-active, 24 | .fade-enter-active .modal-item, 25 | .fade-leave-active .modal-item{ 26 | transition: 1.2s; 27 | } 28 | 29 | .fade-enter-from .modal-item, 30 | .fade-leave-to .modal-item{ 31 | transform: translateX(100px); 32 | } 33 | ``` 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/ru/guide/details-styles.md: -------------------------------------------------------------------------------- 1 | # CSS стилизация 2 | 3 | Для управления фоном и положением модального окна нужно поработать 4 | с классом CSS **.modal-container**. Когда открывается модальное окно, оно 5 | размещается в контейнере с вышеупомянутым классом. Он имеет следующие свойства: 6 | ```css 7 | .modal-container{ 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | 12 | background-color: #3e3e3e21; 13 | } 14 | ``` 15 | Например, давайте изменим фон модального окна и сделаем так, чтобы оно открывалось внизу: 16 | ```css 17 | .modal-container{ 18 | align-items: flex-end; 19 | background-color: rgba(62, 175, 124, 0.47); 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/ru/guide/event-close.md: -------------------------------------------------------------------------------- 1 | # Событие close 2 | **Каждый хук закрытия** модального окна принимает один параметр *event-close*. Этот объект предоставляет информацию о 3 | закрытие модального окна. На данный момент он имеет следующие свойства: 4 | - background (*boolean*) Устанавливается в значение **true**, если модальное окно закрывается щелчком по фону. 5 | - esc (*boolean*) Устанавливается в значение true, если процесс модального закрытия начался с *Escape* 6 | 7 | ```ts 8 | { 9 | beforeModalClose(e) { 10 | // e.background 11 | // e.esc 12 | } 13 | } 14 | ``` 15 | 16 | ### Отлавливание закрытия через Escape 17 | В этом примере показано, как отменить закрытие модального окна при нажатии *Escape*: 18 | ```ts 19 | const modal = await openModal(SomeComponent); 20 | modal.onclose = (event) => { 21 | if (event.esc) return false; 22 | // ... 23 | } 24 | ``` 25 | 26 | ## Отлавливание закрытия через нажатие на фон 27 | В этом примере показано, как отменить закрытие модального окна при нажатии на фон (затемненную часть экрана): 28 | ```ts 29 | const modal = await openModal(SomeComponent); 30 | modal.onclose = (event) => { 31 | if (event.background) return false; 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/ru/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Приступим к работе 2 | Jenesius Vue Modal — легкая и простая библиотека для работы с модальными окнами в Vue.js версии **3+**. Она глубоко 3 | интегрируется с Vue.js и позволяет создавать модальные окна любой сложности. 4 | 5 | ## Установка 6 | Для установки пакета рекомендуется использовать Npm. 7 | ```shell 8 | npm i jenesius-vue-modal 9 | ``` 10 | 11 | ## Подключение к странице 12 | Для начала нам необходимо инициализировать модальные окна, добавив контейнер, в котором будут отображаться наши 13 | компоненты. Импортируем контейнер из библиотеки: 14 | ```vue 15 | 19 | 27 | ``` 28 | 29 | ## Продолжим 30 | 31 | Для работы с модальными окнами всё готово. Переходите во вкладку [методы](./guide-methods.md), чтобы узнать, как открывать и 32 | закрывать модальные окна. Данная библиотека предоставляет широкий набор функций по работе с модальными окнами: 33 | - Открытие нескольких модальных окон 34 | - Возвращение значения 35 | - Работы с событиями через объект `ModalObject` 36 | - Интеграция с `vue-router` 37 | - Наличие `Namespace` и возможность открытия элементов в разных пространствах -------------------------------------------------------------------------------- /docs/ru/guide/guide-navigation-guards.md: -------------------------------------------------------------------------------- 1 | # Навигационные хуки 2 | 3 | ## Информация 4 | 5 | Иногда необходимо отловить закрытие модального кона. Это может быть 6 | полезно для предотвращения закрытия пользователем 7 | модально, пока они не введут данные или не отправят запрос на сервер. 8 | 9 | Если обработчик возвращает **false** или **выдает ошибку**, закрытие 10 | модальное окно будет прервано. 11 | 12 | Jenesius Vue Modal предоставляет три способа перехвата закрытия: 13 | 14 | ## Onclose 15 | 16 | Методы [openModal](./guide-methods#open-modal) 17 | and [pushModal](./guide-methods#push-modal) 18 | возвращают Promise, который в случае успеха 19 | вернет объект [modalObject](./modal-object). Чтобы поймать закрытие 20 | модальное окно, вам нужно добавить событие **onclose** к этому 21 | объекту: 22 | 23 | ```ts 24 | import {openModal} from "jenesius-vue-modal"; 25 | 26 | const modal = await openModal(VueComponent); 27 | 28 | let count = 5; 29 | 30 | modal.onclose = () => { 31 | count--; 32 | if (count > 0) return false; // Модальное окно закроется после пяти попыток. 33 | } 34 | ``` 35 | 36 | ### Пример 37 | 38 | Если открыто несколько модальных окон и у одного из них будет 39 | обработчик onclose, возвращающий false, вы сможете 40 | закрыть только те модальные окна, которые были открыты после него. 41 | 42 | ```ts 43 | import {pushModal, closeModal} from "jenesius-vue-modal"; 44 | 45 | const modal1 = await pushModal(VueComponent); 46 | const modal2 = await pushModal(VueComponent); 47 | const modal3 = await pushModal(VueComponent); 48 | 49 | modal2.onclose = () => false; 50 | 51 | closeModal(); // закроется только modal3 52 | ``` 53 | 54 | ## Хук внутри компоненты 55 | 56 | Наконец, навигационный хук можно указать непосредственно в компоненте, 57 | используя следующие параметры: 58 | 59 | - beforeModalClose 60 | 61 | ```ts 62 | const Foo = { 63 | template: "...", 64 | beforeModalClose() { 65 | // имеет доступ к контексту экземпляра компонента this. 66 | } 67 | } 68 | ``` 69 | 70 | ## Composition Api 71 | 72 | Хотя вы по-прежнему можете использовать встроенные функции, Jenesius 73 | Vue Modal предоставляет функции для Composition API: 74 | 75 | ```ts 76 | import {onBeforeModalClose} from "jenesius-vue-modal" 77 | 78 | export default { 79 | setup() { 80 | onBeforeModalClose(() => { 81 | const answer = window.confirm( 82 | "Вы действительно хочешь уйти? У вас есть несохраненные изменения!" 83 | ) 84 | if (!answer) return false 85 | }) 86 | } 87 | } 88 | ``` 89 | 90 | ## Асинхронный хук 91 | 92 | Навигационный хук может быть асинхронным. Модальное окно закроется 93 | только тогда, когда завершит свою работу: 94 | 95 | ```ts 96 | export default { 97 | async beforeModalClose() { 98 | await updateData(); 99 | } 100 | } 101 | ``` 102 | 103 | ```ts 104 | const modal = await openModal(Modal); 105 | 106 | modal.onclose = () => { 107 | return new Promise(resolve => { 108 | setTimeout(resolve, 1000); // Модальное окно закроется через одну секунду. 109 | }) 110 | } 111 | ``` 112 | 113 | ## Событие close 114 | 115 | Каждый обработчик модального закрытия получает параметр окна: 116 | event-close 117 | 118 | ```ts 119 | modal.onclose = (event) => { 120 | // ... 121 | } 122 | ``` 123 | 124 | Это *событие* хранит информацию о том, как закрывается модальное окно. 125 | Подробная информация о нем, о способах предотвращения 126 | закрытия модального окна при нажатии на фон или по нажатию клавиши Esc 127 | можно прочитать [здесь](./event-close). 128 | -------------------------------------------------------------------------------- /docs/ru/guide/guide-returned-value.md: -------------------------------------------------------------------------------- 1 | # Возвращение значения 2 | 3 | Если мы говорим о возвращаемом значении модальных окон, мы должны 4 | сначала понять их суть. 5 | По умолчанию модальные окна рассматриваются как отдельный уровень 6 | логики со своей собственной моделью данных. 7 | Такой подход удобен и делает разработку веб-приложения с модальными 8 | окнами безопасной. Эта библиотека наследует 9 | эту концепцию. Модальное окно — это отдельный логический уровень, 10 | принимающий входные параметры и каким-то образом с 11 | ними взаимодействующий. Однако бывают случаи, когда модальное окно 12 | является лишь частью процесса. Я иногда сталкиваюсь с 13 | такими случаями (хотя стараюсь их избегать). Опишу решения, которые 14 | использую в своих проектах. Начиная с самых удобных, 15 | заканчивая теми, которыми я бы не стал пользоваться: 16 | 17 | ## Использование PromptModal 18 | 19 | В VanillaJS есть функция prompt, которая при закрытии всплывающего 20 | окна возвращает введённое значение. Мы также предоставили метод 21 | [`promptModal`](guide-methods.md#prompt-modal) для схожего 22 | функционала: 23 | 24 | Файл *ModalCode.vue* 25 | 26 | ```vue 27 | 28 | 31 | 44 | ``` 45 | 46 | Как использовать: 47 | 48 | ```ts 49 | import {promptModal} from "jenesius-vue-modal" 50 | 51 | const code = await promptModal(ModalCode); 52 | ``` 53 | 54 | Что произошло в этом примере: 55 | 56 | - `promptModal` возвращает обещание, которое будет выполнено при 57 | закрытии модального окна. Результатом `promise` будет 58 | переданное в событие `Modal.EVENT_PROMPT` значение. 59 | - Чтобы описать модальное окно, мы реализовали простую передачу 60 | случайного значения по нажатию на кнопку. 61 | 62 | ## Предоставить Handle 63 | 64 | Мы также можем передать функцию для вызова внутри модального окна: 65 | 66 | ```ts 67 | const modal = await openModal(Modal, { 68 | handleRequest: (value) => { 69 | // Делаем что-то 70 | } 71 | }) 72 | ``` 73 | 74 | И затем в модальном окне: 75 | 76 | ```vue 77 | 78 | 88 | ``` 89 | 90 | ## Передать значение и после этого закрыть окно 91 | 92 | Мы также можем подписаться на событие и реагировать на его срабатывание: 93 | 94 | ```vue 95 | // modal.vue 96 | 99 | ``` 100 | 101 | И когда мы открываем окно: 102 | 103 | ```ts 104 | const modal = await openModal(Modal) 105 | modal.on('return', (value) => { 106 | console.log(value); // false 107 | modal.close() 108 | }) 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/ru/guide/integration-installation.md: -------------------------------------------------------------------------------- 1 | # Установка 2 | 3 | - Не забудьте [проинициализировать](./getting-started) Jenesius Vue Modal 4 | - При создании роутера добавьте модальную интеграцию: 5 | ```ts 6 | import { createWebHistory, createRouter} from "vue-router"; 7 | import {useModalRouter} from "jenesius-vue-modal"; 8 | 9 | const routes = [...]; 10 | const router = createRouter({ 11 | history: createWebHistory(), 12 | routes, 13 | }); 14 | 15 | useModalRouter.init(router); // ! 16 | ``` 17 | Теперь модальный обработчик будет реагировать на изменения навигации. 18 | - Добавьте новый маршрут обёрнутый в `useModalRouter`: 19 | ```ts 20 | import Modal from "Modal.vue" 21 | 22 | const routes = [ 23 | { 24 | path: "/any-route", 25 | component: useModalRouter(Modal) 26 | } 27 | ] 28 | ``` 29 | Теперь при переключении на **/any-route** окно, переданное в 30 | **useModalRouter** будет отображаться модальное окно. 31 | 32 | 33 | :::warning router-view 34 | Для отображения модальных окон интегрируемых с `vue-router` не нужно 35 | создавать `router-view`, т.к. модальные окна отображаются в своём 36 | контейнере. 37 | ::: -------------------------------------------------------------------------------- /docs/ru/guide/integration-introduction.md: -------------------------------------------------------------------------------- 1 | # Введение 2 | 3 | Иногда возникает необходимость связать vue-router для отображения 4 | модального окна. Чтобы сделать это, вам нужно написать собственный 5 | обработчик для маршрутизатора и интегрировать его с 6 | открытием/закрытием модального окна. В `jenesius-vue-modal` уже есть 7 | все это, вам просто нужно подключить его. 8 | 9 | Например, была создана таблица пользователей по маршруту **/users**. 10 | Модальный обработчик был добавлен в маршрут **/users/:id**. Теперь при 11 | переключении на **/users/3** откроется модальное окно, в котором 12 | доступны входные параметры из маршрута. Они будут переданы в качестве 13 | `props`. 14 | 15 | Полный пример можно посмотреть на 16 | SandBox([ссылка](https://codesandbox.io/s/vue-modal-router-n9rn94)). 17 | Обратите внимание на изменение `URL`. -------------------------------------------------------------------------------- /docs/ru/guide/integration-practical.md: -------------------------------------------------------------------------------- 1 | # Особые качества 2 | Вы можете использовать **props** для получения входных параметров. 3 | ```ts 4 | // user/5 5 | { 6 | props: { 7 | id: String // 5 8 | } 9 | } 10 | ``` 11 | Использование **beforeRouteEnter** , **beforeRouteUpdate** , **beforeRouteLeave** не является 12 | возможно на данном этапе. Я постараюсь исправить эту проблему в ближайшее время. 13 | BeforeModalClose можно использовать как временное решение. 14 | 15 | -------------------------------------------------------------------------------- /docs/ru/guide/modal-object.md: -------------------------------------------------------------------------------- 1 | # Modal 2 | 3 | Методы [openModal](./guide-methods#open-modal), 4 | [pushModal](./guide-methods#push-modal) возвращают `Promise`, которые 5 | в случае успеха вернёт объект `Modal`. Данный объект является классом 6 | модального окна и служит для получения или управления текущим 7 | состоянием. На данной странице будут перечислены все доступные методы 8 | и свойства. 9 | 10 | ## Свойства 11 | 12 | ### `id` 13 | 14 | Поле является уникальным идентификатором модального окна. 15 | Присваивается в момент открытия. 16 | 17 | ### `instance` 18 | 19 | Экземпляр компоненты модального окна. Используется для того, чтобы 20 | получить доступ к `$data`, `$props` и другим свойствам модального окна. 21 | 22 | ::: warning 23 | Если вы получаете **instance** в своем проекте вместе в 24 | ` 25 | ``` 26 | 27 | Сразу же обращаем внимание на: 28 | 29 | - получение очереди для `notification`. 30 | - выводим сообщения в `v-for`. 31 | - Текст сообщений берём из props. **Важно** помнить, что props является 32 | `ComputedRef`. 33 | 34 | :::info 35 | Если вместо `p` вы будете использовать свою компоненту, то я рекомендую 36 | использовать следующее: 37 | ```vue 38 | 46 | ``` 47 | ::: 48 | 49 | На этом функционал `notification` реализован. Осталось подключить этот 50 | контейнер в вашей компоненте, например `App.vue` и вызвать один из 51 | методов открытия модального окна с `namespace: 'notification'`. -------------------------------------------------------------------------------- /docs/ru/guide/namespace.md: -------------------------------------------------------------------------------- 1 | # Namespace 2 | 3 | Данный функционал выходит за рамки использования модальных окон. Однако 4 | этот механизм позволяет реализовать новые элементы интерфейса, такие как 5 | уведомления, отображение ошибок и т.д. Используя `namespace` можно легко 6 | реализовать компоненты со схожим функционалом модальных окон. 7 | 8 | В данной статье будет рассказано, как пользоваться `namespace`, а также 9 | будет приведён пример создания уведомлений на базе контейнера модальных окон. 10 | 11 | ## Инициализация 12 | 13 | По умолчанию мы создаём контейнер не указывая `namespace`, однако нам 14 | ничего не мешает создать именованный контейнер: 15 | 16 | ```vue 17 | 20 | 23 | ``` 24 | 25 | Здесь мы создали контейнер, в который будут помещаться все модальные окна 26 | с установленным `namespace` в `errors`. 27 | 28 | ## Добавление 29 | 30 | Методы [openModal](./guide-methods#open-modal), [pushModal](./guide-methods#push-modal), 31 | [promptModal](./guide-methods#prompt-modal) также расширены. В опции можно 32 | указать тот `namespace`, которые будет использован: 33 | ```ts 34 | import Modal from "my-modal.vue" 35 | import {pushModal} from "jenesius-vue-modal" 36 | 37 | pushModal(Modal, {}, { namespace: 'errors' }) 38 | ``` 39 | 40 | Данное модальное окно будет добавлено в пространство `errors` и будет 41 | отображаться в соответствующем контейнере. 42 | 43 | ## Закрытие 44 | 45 | Разумеется теперь необходима возможность закрыть не последнее модальное 46 | окно, а последнее окно из нужного контейнера. Методы 47 | [closeModal](./guide-methods#close-modal), 48 | [popModal](./guide-methods#pop-modal) были расширены. Теперь в опциях, 49 | передаваемых первым параметром, можно указать `namespace`: 50 | 51 | ```ts 52 | import {closeModal} from "jenesius-vue-modal" 53 | closeModal({namespace: 'errors'}) 54 | ``` 55 | 56 | ## Стандартное взаимодействие 57 | 58 | Нужно понимать то, что когда мы не указываем `namespace`, мы работаем 59 | с `namespace` = `default`. Я рекомендую не указывать его вовсе, если 60 | используемый `namespace` предполагается, как по умолчанию. 61 | 62 | **Важно** уточнить то, что только для `namespace` равный `default` 63 | устанавливается обработчик `Esc`. На данный момент реализация вынуждает 64 | вас самим позаботиться про закрытие модальных окон в контейнерах 65 | отличных от `default.` 66 | 67 | ## Modal 68 | 69 | В объекте возвращаемом методами pushModal, openModal, promptModal 70 | появилось поле `namespace`, которое будет устанавливаться автоматически 71 | и хранить название контейнера. 72 | 73 | ```ts 74 | const modal = await pushModal(Modal, {}, { namespace: 'notification' }); 75 | modal.namespace // 'notification' 76 | ``` 77 | 78 | ## Получение текущего модального окна. 79 | 80 | Метод `getCurrentModal` был расширен. Теперь первый и необязательный 81 | параметр является название `namespace` для которого нужно найти последнее 82 | открытое модальное окно. 83 | 84 | ```ts 85 | getCurrentModal() // Return last opened modal from default namespace 86 | getCurrentModal("notifications") // From namespace: "notifications" 87 | ``` 88 | 89 | ## Получение текущей очереди 90 | 91 | В практике использование второго `modal-container` не представляет 92 | много удобств. Другие элементы интерфейса сильно отличаются от диалоговых 93 | (модальных) окон. По этому вам будет удобнее использовать очередь, которая 94 | хранит массив текущих элементов: 95 | 96 | ```ts 97 | import {getQueueByNamespace} from "jenesius-vue-modal" 98 | 99 | const notifications = getQueueByNamespace("notifications"); 100 | ``` 101 | 102 | ### Параметры 103 | 104 | - **namespace** Необязательный параметр, указывающий название `namespace`. 105 | 106 | ### Return 107 | 108 | Метод возвращает `reactive` массив, который хранит в себе модальные окна. 109 | ## Пример 110 | 111 | -------------------------------------------------------------------------------- /docs/ru/guide/store.md: -------------------------------------------------------------------------------- 1 | # Модальное хранилище 2 | 3 | В старых версиях этой библиотеки открытие модального окна было возможно только путем передачи компонента. 4 | Подход остаётся основным и на текущий день, т.к. дает вам больше контроля и порядка в вашем коде. Однако существует 5 | много ситуаций, когда этот подход не удобен. Например, при разработке библиотеки возникает вопрос: 6 | как переопределить открытие модальных окон (как заменить модальное окно). Здесь на помощь приходит хранилище. 7 | 8 | ## Инициализация 9 | 10 | Для начала необходимо инициализировать хранилище (сохранить в нем нужные нам модальные окна). Все 11 | это делается с помощью функции конфигурации: 12 | 13 | ```ts 14 | import {config} from "jenesius-vue-modal"; 15 | import ModalConfirm from "./ModalConfirm"; 16 | import ModalAlert from "./ModalAlert" 17 | 18 | config({ 19 | store: { 20 | confirm: ModalConfirm, 21 | alert: { 22 | component: "ModalAlert" 23 | } 24 | } 25 | }) 26 | ``` 27 | В этом примере мы добавили в хранилище два модальных окна: 28 | - передав напрямую компоненты 29 | - во втором случае, мы указали объект с установленным свойством `component`, это необходимо для установления некоторых 30 | свойств конкретно для данной компоненты. 31 | 32 | Теперь его можно открыть по имени, передав туда ключ: 33 | ```ts 34 | openModal('confirm'); 35 | openModal('alert'); 36 | ``` 37 | Конечно, методы *pushModal* и *promptModal* также поддерживают эту функциональность. 38 | 39 | ## Проверка модального окна 40 | Если вы пишете библиотеку, для лучшей совместимости добавлена функция, проверяющая наличие модального окна в хранилище: 41 | ```ts 42 | import {getComponentFromStore} from "jenesius-vue-modal"; 43 | getComponentFromStore('alert') // undefined 44 | getComponentFromStore('confirm') // Component 45 | ``` 46 | **Возвращает** компонент VueComponent, если он был ранее инициализирован в хранилище, в противном случае *undefined*. 47 | 48 | ## Расширенная передача компоненты 49 | 50 | В данной артикле рассмотрим случай, когда мы вместе компоненты передаём целый объект с описанным свойством `component`. 51 | В таком случае мы также можем указать следующие свойства: 52 | 53 | - `backgroundClose` 54 | - `draggable` 55 | - `beforeEach` 56 | 57 | Все эти свойства соответствуют параметром из [конфигурации](./config). 58 | 59 | ```ts 60 | config({ 61 | store: { 62 | confirm: { 63 | component: ModalConfirm, 64 | draggable: true, 65 | backgroundClose: false, 66 | beforeEach() { 67 | Logger.write('Попытка открыть окно подтверждения.') 68 | } 69 | } 70 | } 71 | }) 72 | ``` -------------------------------------------------------------------------------- /docs/ru/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Jenesius vue-modal 5 | titleTemplate: Jenesius Vue Modal & Простая Модальная Система для VueJS 6 | 7 | hero: 8 | name: Jenesius vue-modal 9 | text: Простая Модальная Система для VueJS 10 | tagline: Просто, мощно, and быстро. 11 | actions: 12 | - theme: brand 13 | text: Приступим 14 | link: ru/guide/getting-started 15 | - theme: alt 16 | text: Перейти на GitHub 17 | link: https://github.com/jenesius/vue-modal 18 | features: 19 | - title: Лёгкость использования 20 | icon: 🚀 21 | details: Просто подключите ModalContainer и открывайте компоненты как модальные окна. 22 | - title: Создавай и изменяй 23 | icon: ✍ 24 | details: Эта библиотека не добавляет никаких правил дизайна. Вы можете использовать свои компоненты с классами CSS. 25 | - title: Абстракция 26 | icon: 🤖 27 | details: Не нужно встраивать модальное окно в компоненту. Работай с ней на другом уровне абстракции. 28 | - title: Интеграция с vue-router 29 | icon: 🎢 30 | details: Вы можете подключить эту библиотеку к vue-router, чтобы сделать интерфейс более удобным. 31 | --- 32 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | Данная папки содержит примеры работы с модальными окнами. Для такого, чтобы запустить их используйте 4 | 5 | ```shell 6 | npm run serve 7 | ``` -------------------------------------------------------------------------------- /examples/draggable/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | -------------------------------------------------------------------------------- /examples/draggable/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jenesius/vue-modal/9177d8fe34af0f5584df120d49c36f1b336ac6f6/examples/draggable/README.md -------------------------------------------------------------------------------- /examples/draggable/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/draggable/modal-info.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | -------------------------------------------------------------------------------- /examples/draggable/modal-with-header.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | -------------------------------------------------------------------------------- /examples/handle-closing/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /examples/handle-closing/README.md: -------------------------------------------------------------------------------- 1 | # Handle closing 2 | 3 | The current example demonstrates the ability to unclose a modal 4 | windows via `Esc` or by clicking on the background. -------------------------------------------------------------------------------- /examples/handle-closing/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/handle-closing/modal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multi-modal/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /examples/multi-modal/README.md: -------------------------------------------------------------------------------- 1 | # Multi modals 2 | 3 | One of the advantages of this library is the ability to open multiple modal windows on top of each other. This approach 4 | is not common, but allows you to design complex logic using simple tools. 5 | 6 | In this example, let's see how this is done: 7 | 8 | 1. Let's create a modal window that will open its duplicate, open the initial window and close all the remaining ones: 9 | ```vue 10 | 19 | 20 | 25 | ``` 26 | 27 | In this example, one of the buttons has a handler for opening another modal window using `pushModal`. This 28 | the function opens a new modal window on top of the existing ones without closing them. 29 | -------------------------------------------------------------------------------- /examples/multi-modal/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | 5 | createApp(App) 6 | .mount("#app") -------------------------------------------------------------------------------- /examples/multi-modal/modal-multi-duplicate.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /examples/multi-modal/modal-multi.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /examples/open-modal/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /examples/open-modal/README.md: -------------------------------------------------------------------------------- 1 | # Open Modal -------------------------------------------------------------------------------- /examples/open-modal/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/open-modal/modal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /examples/pretty-modal/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 56 | -------------------------------------------------------------------------------- /examples/pretty-modal/README.md: -------------------------------------------------------------------------------- 1 | ## Main Demo 2 | 3 | 1. Открытие профиля персоны 4 | 2. Открытие подтверждения сохранения 5 | 3. Открытие уведомления 6 | 4. Открытие auto-height 7 | 5. Открытие Вопрос-Ответ 8 | -------------------------------------------------------------------------------- /examples/pretty-modal/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/pretty-modal/modals/modal-alert.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /examples/pretty-modal/modals/modal-auto-height.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | -------------------------------------------------------------------------------- /examples/pretty-modal/modals/modal-confirm.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | -------------------------------------------------------------------------------- /examples/pretty-modal/modals/modal-info.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /examples/pretty-modal/modals/modal-profile.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | 36 | -------------------------------------------------------------------------------- /examples/pretty-modal/modals/modal-question.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | 30 | -------------------------------------------------------------------------------- /examples/prompt-modal/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /examples/prompt-modal/README.md: -------------------------------------------------------------------------------- 1 | ## Prompt Modal 2 | 3 | Currently, work with *promptModal* will be considered. 4 | 5 | This method is used to get data from a modal window in a linear fashion. 6 | 7 | For example, we have a modal window that is only needed to get a one-time password. 8 | This window is not a separate logic, but is used only as a temporary step for data entry. 9 | You may find something in common with the `prompt` function from JavaScript, which returns a string. 10 | 11 | Let's see how it works step by step: 12 | 13 | 1. Let's create a modal window *Modal.vue* for one-time password entry: 14 | ```vue 15 | 21 | 37 | ``` 38 | 39 | Nothing complicated, two interceptors for pressing Enter and a button and calling `$emit`. 40 | 41 | 2. Open it using *promptModal*: 42 | ```ts 43 | const value = await promptModal(ModalCode) 44 | ``` -------------------------------------------------------------------------------- /examples/prompt-modal/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/prompt-modal/modal-code.vue: -------------------------------------------------------------------------------- 1 | 9 | 25 | -------------------------------------------------------------------------------- /examples/side-modal/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | -------------------------------------------------------------------------------- /examples/side-modal/README.md: -------------------------------------------------------------------------------- 1 | # Sidebar modal -------------------------------------------------------------------------------- /examples/side-modal/components/modal-bottom.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /examples/side-modal/components/modal-sidebar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /examples/side-modal/components/other-modal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /examples/side-modal/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/simple-animation/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | -------------------------------------------------------------------------------- /examples/simple-animation/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | import {config} from "../../src/utils/config"; 4 | 5 | config({ 6 | animation: "custom-fade" 7 | }) 8 | 9 | createApp(App) 10 | .mount("#app") -------------------------------------------------------------------------------- /examples/use-namespace/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | 36 | -------------------------------------------------------------------------------- /examples/use-namespace/README.md: -------------------------------------------------------------------------------- 1 | # Namespace 2 | 3 | This example demonstrates the possibility of using containers. 4 | This functionality can be convenient in cases when you want 5 | divide application logic into `namespace`. Also, the possibility of receiving 6 | modal window queue states for a specific `namespace` gives 7 | the ability to create your own containers with the logic you need. 8 | 9 | The `App.vue` file shows three containers: 10 | 11 | - The main container that you usually register. 12 | - Name container `` 13 | - Container for notification `` 14 | 15 | ## Main container 16 | 17 | By default, the `openModal`, `pushModal` and other methods work with 18 | him. 19 | 20 | ## Named container 21 | 22 | To make the container named, you need to add the property 23 | `namespace`. Also, when adding a modal window, you need to specify 24 | additional option: 25 | ```ts 26 | openModal(Modal, {}, { 27 | namespace: "notification" 28 | }) 29 | ``` 30 | 31 | Please note that the mechanism does not work in this container 32 | closing by pressing `Esc`. This is intentional, but in the future 33 | subject to change. 34 | 35 | ## Notification container 36 | 37 | Go inside this component `notification-container.vue`. In it 38 | we get the current queue using the `getQueueByNamespace` method. 39 | Next, we display notifications the way we want. 40 | -------------------------------------------------------------------------------- /examples/use-namespace/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /examples/use-namespace/modal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /examples/use-namespace/notification-container.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 23 | 32 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/README.md: -------------------------------------------------------------------------------- 1 | # VueRouter with Modal 2 | 3 | Currently, using vue-router merged with jenesius-vue-modal. Added a function for this 4 | **useModalRouter** which allows you to bind a modal window to a route. -------------------------------------------------------------------------------- /examples/vue-router-with-modal/components/FlowersList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/components/SomeInterface.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/components/UserList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/components/modals/FlowersModal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/components/modals/Modal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /examples/vue-router-with-modal/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | 5 | createApp(App) 6 | .use(router) 7 | .mount("#app") -------------------------------------------------------------------------------- /examples/vue-router-with-modal/router.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, RouterView, createWebHistory} from "vue-router" 2 | import FlowersList from "./components/FlowersList.vue" 3 | import useModalRouter from "../../src/routerIntegration"; 4 | import Modal from "./components/modals/Modal.vue"; 5 | import FlowersModal from "./components/modals/FlowersModal.vue"; 6 | import UserList from "./components/UserList.vue" 7 | 8 | const routes = [ 9 | 10 | { 11 | path: "/users", 12 | component: UserList, 13 | children: [ 14 | { 15 | path: "/users/:id", 16 | component: useModalRouter(Modal) 17 | } 18 | ] 19 | }, 20 | { 21 | path: "/flowers", 22 | component: FlowersList, 23 | children: [ 24 | { 25 | path: ":id", 26 | component: useModalRouter(FlowersModal) 27 | } 28 | ] 29 | } 30 | ] 31 | 32 | const router = createRouter({ 33 | routes, 34 | history: createWebHistory("/vue-router-with-modal") 35 | }) 36 | 37 | useModalRouter.init(router); 38 | 39 | export default router -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageProvider: 'v8', 3 | "moduleFileExtensions": [ 4 | "js", 5 | "json", 6 | "ts", 7 | "vue" 8 | ], 9 | transform: { 10 | "^.+\\.jsx?$": "babel-jest", 11 | '^.+\\.ts?$': 'ts-jest', 12 | '^.+\\.vue?$': '@vue/vue3-jest', 13 | }, 14 | testEnvironment: 'jsdom', 15 | testEnvironmentOptions: { 16 | customExportConditions: ["node", "node-addons"], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jenesius-vue-modal", 3 | "version": "1.11.9", 4 | "private": false, 5 | "description": "The progressive and simple modal system for Vue.js v3", 6 | "author": "Jenesius", 7 | "scripts": { 8 | "serve": "vue-cli-service serve --mode development", 9 | "build": "vite build --config vite.config.mjs", 10 | "lint": "vue-cli-service lint", 11 | "test": "jest", 12 | "full": "npm run build && npm publish", 13 | "docs:dev": "vitepress dev docs", 14 | "docs:build": "vitepress build docs", 15 | "docs:serve": "vitepress serve docs", 16 | "serve-with-debug-windows": "set DEBUG=jenesius-vue-modal*& vue-cli-service serve --mode development" 17 | }, 18 | "main": "dist/jenesius-vue-modal.cjs.js", 19 | "module": "dist/jenesius-vue-modal.es.js", 20 | "files": [ 21 | "README.MD", 22 | "dist", 23 | ".eslintrc.cjs", 24 | "babel.config.cjs", 25 | "jest.config.cjs", 26 | "LICENSE", 27 | "tests" 28 | ], 29 | "types": "dist/types/index.d.ts", 30 | "peerDependencies": { 31 | "vue": "^3.0.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.22.11", 35 | "@babel/plugin-transform-runtime": "^7.22.10", 36 | "@babel/preset-env": "^7.22.10", 37 | "@babel/preset-typescript": "7.22.11", 38 | "@rollup/plugin-typescript": "11.1.2", 39 | "@testing-library/vue": "8.0.0", 40 | "@types/debug": "^4.1.12", 41 | "@types/jest": "^29.2.6", 42 | "@vitejs/plugin-vue": "4.3.1", 43 | "@vue/cli-plugin-babel": "~5.0.0", 44 | "@vue/cli-plugin-typescript": "~5.0.0", 45 | "@vue/cli-service": "~5.0.0", 46 | "@vue/test-utils": "2.2.10", 47 | "@vue/vue3-jest": "29.2.2", 48 | "core-js": "^3.8.3", 49 | "generate-changelog": "^1.8.0", 50 | "jenesius-vue-form": "3.0.0", 51 | "jest": "29.6.4", 52 | "jest-environment-jsdom": "^29.6.4", 53 | "ts-jest": "^29.0.4", 54 | "typescript": "~4.7.4", 55 | "vite": "4.4.9", 56 | "vite-plugin-css-injected-by-js": "^3.1.0", 57 | "vite-tsconfig-paths": "4.2.0", 58 | "vitepress": "1.0.0-rc.44", 59 | "vue-router": "^4.0.12" 60 | }, 61 | "browserslist": [ 62 | "> 1%", 63 | "last 2 versions", 64 | "not dead" 65 | ], 66 | "email": "lokargenia@gmail.com", 67 | "homepage": "https://modal.jenesius.com", 68 | "keywords": [ 69 | "vue", 70 | "vue3", 71 | "modal", 72 | "dialog" 73 | ], 74 | "license": "MIT", 75 | "repository": { 76 | "type": "git", 77 | "url": "https://github.com/Jenesius/vue-modal.git" 78 | }, 79 | "url": "https://github.com/Jenesius/vue-modal/issues", 80 | "dependencies": { 81 | "debug": "^4.3.4" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /project/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@vue/cli-plugin-babel/preset" 4 | ] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /project/default-style.css: -------------------------------------------------------------------------------- 1 | button, input { 2 | border: 1px solid #e3e3e3; 3 | padding: 4px 8px; 4 | cursor: pointer; 5 | transition: 0.1s; 6 | } 7 | button:hover { 8 | transform: scale(0.96); 9 | } 10 | .modal-window{ 11 | padding: 10px; 12 | background-color: white; 13 | border-radius: 4px; 14 | } 15 | .modal-title{ 16 | transition: 0.1s; 17 | } 18 | .modal-title:hover { 19 | color: #42b883; 20 | } 21 | .button_active{ 22 | background-color: #42b883; 23 | color: white; 24 | } 25 | input { 26 | padding: 7px 10px; 27 | } 28 | input:focus { 29 | outline: none; 30 | border: 1px solid #42b883; 31 | } -------------------------------------------------------------------------------- /project/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jenesius/vue-modal/9177d8fe34af0f5584df120d49c36f1b336ac6f6/project/public/favicon.ico -------------------------------------------------------------------------------- /project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | jenesius-vue-modal 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /project/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 26 | -------------------------------------------------------------------------------- /project/src/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /project/src/test/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 30 | -------------------------------------------------------------------------------- /project/src/test/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import App from "./App.vue"; 3 | 4 | createApp(App) 5 | .mount("#app") -------------------------------------------------------------------------------- /project/src/vue-shim.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } -------------------------------------------------------------------------------- /src/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"] 4 | ] 5 | } -------------------------------------------------------------------------------- /src/components/WidgetModalContainer.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 51 | -------------------------------------------------------------------------------- /src/components/WidgetModalContainerItem.vue: -------------------------------------------------------------------------------- 1 | 13 | 110 | 132 | -------------------------------------------------------------------------------- /src/hooks/onBeforeModalClose.ts: -------------------------------------------------------------------------------- 1 | import {getCurrentInstance} from "vue"; 2 | import guards from "../utils/guards"; 3 | import {GuardFunction} from "../utils/types"; 4 | 5 | export default function onBeforeModalClose(callback: GuardFunction){ 6 | 7 | const a = getCurrentInstance(); 8 | const attrModalId = String(a?.props?.modalId || a?.props?.["modal-id"] || a?.attrs?.modalId); 9 | 10 | const modalId = attrModalId.replace(/[^0-9]/g, ""); 11 | 12 | guards.add(Number(modalId), "close", callback); 13 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import closeModal from "./methods/closeModal"; 2 | import popModal from "./methods/popModal"; 3 | import pushModal from "./methods/pushModal"; 4 | import openModal from "./methods/openModal"; 5 | import {getNamespace} from "./utils/state"; 6 | import {config} from "./utils/config"; 7 | import onBeforeModalClose from "./hooks/onBeforeModalClose"; 8 | import getCurrentModal from "./methods/getCurrentModal"; 9 | import closeById from "./methods/closeById"; 10 | import promptModal from "./methods/prompt-modal"; 11 | import Modal from "./utils/Modal"; 12 | import getComponentFromStore from "./methods/get-component-from-store"; 13 | // @ts-ignore 14 | import WidgetModalContainer from "./components/WidgetModalContainer.vue"; 15 | import useModalRouter from "./routerIntegration" 16 | 17 | const modalQueue = getNamespace().queue; 18 | 19 | /** 20 | * @description The function returns a reactive array of modal windows. The array will be created if it does not 21 | * exist for the passed namespace to the store. 22 | * 23 | * @param {String} namespace - name of namespace. Default value: "default" 24 | */ 25 | function getQueueByNamespace(namespace?: string) { 26 | return getNamespace(namespace).queue 27 | } 28 | 29 | export { 30 | WidgetModalContainer as container, 31 | Modal, 32 | closeModal, 33 | popModal, 34 | pushModal, 35 | openModal, 36 | promptModal, 37 | config, 38 | modalQueue, 39 | getQueueByNamespace, 40 | onBeforeModalClose, 41 | useModalRouter, 42 | getCurrentModal, 43 | closeById, 44 | getComponentFromStore 45 | } 46 | -------------------------------------------------------------------------------- /src/methods/addModal.ts: -------------------------------------------------------------------------------- 1 | import Modal, {ModalOptions} from "../utils/Modal"; 2 | import {configuration, getNamespace, isStoreComponentConfiguration} from "../utils/state"; 3 | import ModalError from "../utils/ModalError"; 4 | import {Component, markRaw} from "vue"; 5 | import {getComponentFromStore} from "../index"; 6 | import NamespaceStore from "../utils/NamespaceStore"; 7 | import {DTOModalOptions} from "../utils/dto"; 8 | 9 | /** 10 | * Sync function for adding modal window. 11 | * Two check: 12 | * - Application was initialized (ModalContainer was mounted). 13 | * - Component is required. 14 | * */ 15 | 16 | export default async function _addModal(component: string | Component, params: any, modalOptions: Partial):Promise{ 17 | 18 | const options = DTOModalOptions(modalOptions); 19 | const namespaceState = getNamespace(options.namespace); 20 | 21 | /** 22 | * @description Проверка только для namespace по умолчанию. Это сделано из-за того, что дополнительные namespace 23 | * подлежат инициализации. Однако пользователю намеренно не доступен этот метод. 24 | * */ 25 | if (options.namespace === NamespaceStore.DEFAULT_NAMESPACE && !namespaceState.initialized && !configuration.skipInitCheck) 26 | throw ModalError.NotInitialized(options.namespace); 27 | 28 | if ((await configuration.beforeEach()) === false) 29 | throw ModalError.RejectedByBeforeEach(); 30 | 31 | // If component is string. In this case we get the component from store. 32 | if (typeof component === "string") { 33 | const storeElement = getComponentFromStore(component); 34 | if (!storeElement) throw ModalError.ModalNotExistsInStore(component); 35 | 36 | if (isStoreComponentConfiguration(storeElement)) { 37 | component = storeElement.component; 38 | Object.assign(options, storeElement); 39 | 40 | if (storeElement.beforeEach && (await storeElement.beforeEach()) === false) 41 | throw ModalError.RejectedByBeforeEach(); 42 | 43 | } else component = storeElement; 44 | } 45 | if (!component) throw ModalError.ModalComponentNotProvided(); 46 | 47 | 48 | const modal = new Modal(component, params, options); 49 | 50 | /** 51 | * modalQueue.value.push(Object.freeze(modal)) - фундаментальная ошибка! 52 | * Таким способом мы запрещаем изменение любых свойств объекта - что является 53 | * недопустим исключением, ведь объект может хранить, например, свойство 54 | * `version`, которое по итогу будет не изменяемым. 55 | * 56 | * computed свойство 'closed' так-же потеряет реактивность в таком случае 57 | * 58 | * modalQueue.value.push(modal) - ошибка! 59 | * Т.к. modalQueue является реактивным объектом и создаётся при помощи ref. 60 | * В итоге все элементы, добавленные в неё, становятся реактивными полностью. 61 | * Так же получим небольшие проблемы с computed свойствами, поскольку они 62 | * И так уже находятся в реактивном объекте и разложатся. 63 | * 64 | * markRaw - пометка для vue, что к данному элементу не надо добавлять никак 65 | * ой реактивности. 66 | * 67 | * */ 68 | //modalQueue.value.push(modal); 69 | namespaceState.queue.push(markRaw(modal)); 70 | 71 | return modal; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/methods/closeById.ts: -------------------------------------------------------------------------------- 1 | import {getNamespace} from "../utils/state"; 2 | import ModalError from "../utils/ModalError"; 3 | import {guardToPromiseFn, runGuardQueue} from "../utils/guards"; 4 | import Modal from "../utils/Modal"; 5 | import {GuardFunction} from "../utils/types"; 6 | import guards from "../utils/guards" 7 | import {DTOEventClose, IEventClose} from "../utils/dto"; 8 | 9 | /** 10 | * @description Closing modal window by id. Only this method allows you to change the properties of the event-close. 11 | * */ 12 | export default function closeById(id: number, options: Partial = {}) { 13 | const modal = Modal.STORE.get(id); 14 | 15 | if (!modal) return Promise.reject(ModalError.ModalNotFoundByID(id)); 16 | 17 | const namespaceState = getNamespace(modal.namespace); 18 | 19 | const indexRemoveElement: number 20 | = namespaceState.queue.findIndex((item: Modal) => item.id === id); 21 | 22 | //Modal with id not found 23 | if (indexRemoveElement === -1) 24 | return Promise.reject(ModalError.Undefined(id)); 25 | 26 | const arr = 27 | guards.get(id, "close") 28 | .map((guard: GuardFunction) => guardToPromiseFn(guard, id, DTOEventClose(options))); 29 | 30 | return runGuardQueue(arr) 31 | .then(() => { 32 | namespaceState.queue.splice(indexRemoveElement, 1); 33 | }) 34 | .then(() => { 35 | guards.get(id, "destroy") 36 | .forEach((guard: GuardFunction) => guard(DTOEventClose(options))); 37 | }) 38 | .then(() => { 39 | guards.delete(id) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/methods/closeModal.ts: -------------------------------------------------------------------------------- 1 | import {runGuardQueue} from "../utils/guards"; 2 | import {getNamespace} from "../utils/state"; 3 | import Modal from "../utils/Modal"; 4 | import {IModalCloseOptions} from "../utils/types"; 5 | import {DTOModalCloseOptions} from "../utils/dto"; 6 | 7 | /** 8 | * @description Try to close all modals windows. Throw error if some modal has onClose hook with returned false value. 9 | * */ 10 | export default function closeModal(options?: Partial):Promise{ 11 | options = DTOModalCloseOptions(options); 12 | 13 | return runGuardQueue(getNamespace(options.namespace).queue.map((modalObject:Modal) => () => modalObject.close())) 14 | } 15 | -------------------------------------------------------------------------------- /src/methods/get-component-from-store.ts: -------------------------------------------------------------------------------- 1 | import {configuration} from "../utils/state"; 2 | import {Component} from "vue"; 3 | import {IStoreElement} from "../utils/types"; 4 | 5 | /** 6 | * @description Method using for checking exist modal in store. 7 | * */ 8 | export default function getComponentFromStore(modalName: string): IStoreElement | undefined { 9 | return configuration.store[modalName] || undefined 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/methods/getCurrentModal.ts: -------------------------------------------------------------------------------- 1 | import {getNamespace} from "../utils/state"; 2 | import {INamespaceKey} from "../utils/NamespaceStore"; 3 | /** 4 | * @return {Modal} Last opened modal in active status. Return undefined if there is not opened modal. 5 | * */ 6 | export default function getCurrentModal(namespace?: INamespaceKey) { 7 | const namespaceState = getNamespace(namespace); 8 | const modalQueue = namespaceState.queue; 9 | 10 | if (modalQueue.length === 0) return undefined; 11 | 12 | return modalQueue[modalQueue.length - 1]; 13 | } -------------------------------------------------------------------------------- /src/methods/openModal.ts: -------------------------------------------------------------------------------- 1 | import {getNamespace} from "../utils/state"; 2 | import closeModal from "./closeModal"; 3 | import pushModal from "./pushModal"; 4 | import Modal, {ModalOptions} from "../utils/Modal"; 5 | import ModalError from "../utils/ModalError"; 6 | import {WrapComponent} from "../utils/types"; 7 | 8 | /** 9 | * @description OpenModal that was provided as component. 10 | * Before opening try to close all previous modals. 11 | * @param {Object} component Any Vue component 12 | * @param {Object} props Props that will be passed to the component 13 | * @param {Object} options Params for Modal. Like backgroundClose and other 14 | * 15 | * @return {Promise} ModalObject 16 | */ 17 | export default function openModal< P extends WrapComponent>(component: P | string, props: any = {}, options: Partial = {}):Promise 18 | { 19 | return closeModal({ 20 | namespace: options.namespace 21 | }) 22 | .then(() => { 23 | const namespaceState = getNamespace(options.namespace); 24 | if (namespaceState.queue.length) throw ModalError.QueueNoEmpty(); 25 | }) 26 | .then(() => pushModal(component, props, options)) 27 | } -------------------------------------------------------------------------------- /src/methods/popModal.ts: -------------------------------------------------------------------------------- 1 | import getCurrentModal from "./getCurrentModal"; 2 | import {IModalCloseOptions} from "../utils/types"; 3 | import {DTOModalCloseOptions} from "../utils/dto"; 4 | 5 | /** 6 | * @description Try to close the last opened modal window. 7 | * */ 8 | export default function popModal(options?: Partial):Promise{ 9 | options = DTOModalCloseOptions(options); 10 | 11 | const modal = getCurrentModal(options.namespace); 12 | if (!modal) return Promise.resolve(); 13 | return modal.close(); 14 | } -------------------------------------------------------------------------------- /src/methods/prompt-modal.ts: -------------------------------------------------------------------------------- 1 | import pushModal from "./pushModal"; 2 | import Modal, {ModalOptions} from "../utils/Modal"; 3 | import {WrapComponent} from "../utils/types"; 4 | 5 | /** 6 | * @description Method push modalComponent with provided options and then wait until current component will trigger event 7 | * Modal.EVENT_PROMPT. After triggering will close the modal window and return provided data to event. 8 | * @warning In new version I will change declaration of current method. Type declaration will be added for returned value. 9 | */ 10 | 11 | export default async function promptModal

(component: P | string, props: any = {}, options: Partial = {}) { 12 | const modal = await pushModal(component, props, options); 13 | let isPrompted = false; 14 | 15 | return new Promise(resolve => { 16 | 17 | /** 18 | * @description Переключатель, используемый для отлавливания того, что событие EVENT_PROMPT было вызвано. 19 | */ 20 | 21 | modal.on(Modal.EVENT_PROMPT, async data => { 22 | isPrompted = true; 23 | return modal.close() 24 | .then(() => resolve(data)) 25 | .catch(() => isPrompted = false) 26 | }); 27 | /** 28 | * Концептуальная проблема: просто повесить обработчик на onclose не получится. Т.к. в случае, если закрытие 29 | * модального окно будет перехвачено другим(следующим после) обработчиком и отклонено - модальное окно не будет 30 | * закрыто, но при этом Promise будет выполнен со значением null. Для решения данной задачи(Issue #109) необходимо 31 | * добавить событие afterClose(ondestroy), которое будет срабатывать в момент полного закрытия модального окна. 32 | * Именно в таком случае, можно будет с полной уверенностью заявить о том, что модальное окно закрыто, а Promise 33 | * выполнен. 34 | */ 35 | modal.ondestroy = () => { 36 | if (!isPrompted) resolve(null); 37 | } 38 | }) 39 | } -------------------------------------------------------------------------------- /src/methods/pushModal.ts: -------------------------------------------------------------------------------- 1 | import _addModal from "./addModal"; 2 | import Modal, {ModalOptions} from "../utils/Modal"; 3 | import {WrapComponent} from "../utils/types"; 4 | 5 | /** 6 | * @description Method push modal to queue. Using this method you can open multiple windows. For closing use popModal 7 | * */ 8 | export default function pushModal< P extends WrapComponent>(component: P | string, props: any = {}, options: Partial = {}):Promise { 9 | return Promise.resolve().then(() => _addModal(component, props, options)); 10 | } -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "strict": true, 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "declaration": true, 15 | "declarationDir": "../dist/types", 16 | "lib": [ 17 | "dom", 18 | "es5", 19 | "scripthost", 20 | "es2015.promise" 21 | ] 22 | }, 23 | } -------------------------------------------------------------------------------- /src/utils/ModalError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * last change: 25.11.2021 3 | * */ 4 | import {ModalID} from "./Modal"; 5 | import {INamespaceKey} from "./NamespaceStore"; 6 | 7 | export default class ModalError extends Error{ 8 | readonly isModalError:boolean = true; 9 | details:any 10 | 11 | constructor(message: string, details: any = null) { 12 | super(); 13 | 14 | this.message = message; 15 | this.details = details; 16 | } 17 | 18 | static Undefined(id:number) { 19 | return new ModalError(`Modal with id: ${id} not founded. The modal window may have been closed earlier.`); 20 | } 21 | static UndefinedGuardName(name:string) { 22 | return new ModalError(`Guard's name ${name} is not declaration.`); 23 | } 24 | 25 | static NextReject(id:number){ 26 | return new ModalError(`Guard returned false. Modal navigation was stopped. Modal id ${id}`); 27 | } 28 | 29 | static GuardDeclarationType(func:Function){ 30 | return new ModalError("Guard's type should be a function. Provided:", func); 31 | } 32 | static RejectedByBeforeEach() { 33 | return new ModalError("The opening of the modal was stopped in beforeEach"); 34 | } 35 | static ConfigurationType(config:object) { 36 | return new ModalError("Configuration type must be an Object. Provided", config) 37 | } 38 | static ConfigurationUndefinedParam(param:string, availableParams:Array) { 39 | return new ModalError(`In configuration founded unknown parameter: ${param}. Available are ${availableParams.join(", ")} `) 40 | } 41 | 42 | static QueueNoEmpty(){ 43 | return new ModalError("Modal's queue is not empty. Probably some modal reject closing by onClose hook.") 44 | } 45 | static EmptyModalQueue(){ 46 | return new ModalError("Modal queue is empty."); 47 | } 48 | 49 | static NotInitialized(namespace: INamespaceKey){ 50 | return new ModalError(`Modal Container not found. Put container from jenesius-vue-modal in App's template. Namespace: ${namespace}. Check documentation for more information https://modal.jenesius.com/docs.html/installation#getting-started.`); 51 | } 52 | 53 | static ModalComponentNotProvided(){ 54 | return new ModalError("The first parameter(VueComponent) was not specified."); 55 | } 56 | 57 | static DuplicatedRouterIntegration(){ 58 | return new ModalError('useModalRouter.init should escaped only once.') 59 | } 60 | 61 | static ModalRouterIntegrationNotInitialized(){ 62 | return new ModalError("The integration was not initialized. Please, use useModalRouter.init(router). For more information: https://modal.jenesius.com/docs.html/integration-vue-router#installation") 63 | } 64 | 65 | static ModalEventNameMustBeString(eventName: string) { 66 | return new ModalError( 67 | `Event name must be a string. Provided: ${eventName}` 68 | ) 69 | } 70 | static ModalNotFoundByID(id: ModalID) { 71 | return new ModalError(`Modal with ID ${id} was not found.`) 72 | } 73 | static ModalNotExistsInStore(modalName: string) { 74 | return new ModalError( 75 | `Provided name(${modalName}) don't exist in the store. Has the given name been added to the store?` 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/NamespaceStore.ts: -------------------------------------------------------------------------------- 1 | import Modal from "./Modal"; 2 | import {reactive} from "vue"; 3 | 4 | export default class NamespaceStore { 5 | static readonly DEFAULT_NAMESPACE: INamespaceKey = "default" 6 | static instance: NamespaceStore 7 | 8 | constructor() { 9 | NamespaceStore.instance = this 10 | } 11 | 12 | state = new Map(); 13 | 14 | /** 15 | * @description Метод возвращает состояние для переданного namespace. Если для переданного namespace состояния не 16 | * существует, оно будет создано со значениями по умолчанию. 17 | * 18 | * @param {String} namespace - Имя namespace, если значение не передано, оно устанавливается как "default" 19 | * */ 20 | getByName(namespace: INamespaceKey = NamespaceStore.DEFAULT_NAMESPACE) { 21 | if (!this.state.has(namespace)) { 22 | this.state.set(namespace, { 23 | queue: reactive([]), 24 | initialized: false 25 | }) 26 | } 27 | 28 | return this.state.get(namespace) as INamespaceState; 29 | } 30 | 31 | /** 32 | * @description Is Dev method. Using for cleaning all namespace without run quards. 33 | * */ 34 | forceClean() { 35 | this.state.forEach(item => { 36 | item.queue.splice(0, item.queue.length) 37 | }) 38 | } 39 | } 40 | 41 | export interface INamespaceState { 42 | queue: Modal[], 43 | initialized: boolean 44 | } 45 | export type INamespaceKey = number | string; -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * last change: 18.02.2022 3 | * */ 4 | import ModalError from "./ModalError"; 5 | import {configuration} from "./state"; 6 | import {ConfigInterface} from "./state"; 7 | 8 | /** 9 | * @description Method for changing default configuration. 10 | * @param {object} options - The Configuration Options of Modal System. 11 | * @param {boolean} options.scrollLock - if true: Disable scrolling in time when modal is open. 12 | * @param {string} options.animation - Animation name for transition-group. 13 | * @param {boolean} options.backgroundClose - Closing on click back area of modal. 14 | * @param {boolean} options.escClose - Closing on press ESC key 15 | * */ 16 | export function config (options: Partial){ 17 | if (typeof options !== "object") throw ModalError.ConfigurationType(options); 18 | 19 | Object.assign(configuration, options) 20 | } -------------------------------------------------------------------------------- /src/utils/create-debug.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | export default function createDebug(scope: string) { 4 | return debug(`jenesius-vue-modal:${scope}`); 5 | } -------------------------------------------------------------------------------- /src/utils/dto.ts: -------------------------------------------------------------------------------- 1 | import {ModalOptions} from "./Modal"; 2 | import {configuration} from "./state" 3 | import {IModalCloseOptions} from "./types"; 4 | import NamespaceStore from "./NamespaceStore"; 5 | 6 | 7 | 8 | export function DTOModalOptions(options: Partial): ModalOptions { 9 | const output: ModalOptions = { 10 | backgroundClose: configuration.backgroundClose, 11 | isRoute: false, 12 | namespace: NamespaceStore.DEFAULT_NAMESPACE, 13 | draggable: configuration.draggable 14 | }; 15 | 16 | if (options.backgroundClose !== undefined) 17 | output.backgroundClose = options.backgroundClose; 18 | 19 | if (options.isRoute) output.isRoute = options.isRoute; 20 | if (options.namespace) output.namespace = options.namespace; 21 | 22 | if (options.draggable !== undefined) output.draggable = options.draggable; 23 | 24 | return output; 25 | } 26 | export function DTOEventClose(obj: Partial = {}): IEventClose{ 27 | const defaultValues: IEventClose = { 28 | background: false, 29 | esc: false, 30 | } 31 | 32 | return Object.assign(defaultValues, obj); 33 | } 34 | export function DTOModalCloseOptions(options?: Partial) { 35 | if (!options) options = {}; 36 | 37 | return options; 38 | } 39 | /** 40 | * @param background - Attempt to close modal by clicking on the background. 41 | * @param esc - Modal was closed by pressed `Esc` 42 | */ 43 | export interface IEventClose { 44 | background: boolean, 45 | esc: boolean 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/utils/guards.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * last change: 25.11.2021 3 | * */ 4 | 5 | import ModalError from "./ModalError"; 6 | import {GuardFunction, GuardFunctionPromisify} from "./types"; 7 | import {getModalById, ModalID} from "./Modal"; 8 | 9 | type AvailableKeys = 'close' | 'destroy' 10 | interface GuardsInterface{ 11 | store: { 12 | [index: number]: { 13 | [key in AvailableKeys]?: Array 14 | } 15 | }, 16 | add(id: number, name: AvailableKeys, func: GuardFunction):void, 17 | get(id: number, name: AvailableKeys): Array, 18 | delete(id: number):void 19 | } 20 | 21 | const guards:GuardsInterface = { 22 | 23 | store: {}, 24 | 25 | add(modalId, name, func){ 26 | 27 | if (typeof func !== "function") throw ModalError.GuardDeclarationType(func); 28 | 29 | if (!this.store[modalId]) this.store[modalId] = { 30 | [name]: [] 31 | }; 32 | 33 | if (!this.store[modalId][name]) this.store[modalId][name] = []; 34 | 35 | this.store[modalId][name]?.push(func); 36 | }, 37 | 38 | get(id, name) { 39 | 40 | if (!(id in this.store)) return []; 41 | if (!(name in this.store[id])) return []; 42 | 43 | return this.store[id][name] || []; 44 | }, 45 | 46 | delete(id:number){ 47 | if (!(id in this.store)) return; 48 | 49 | delete this.store[id]; 50 | } 51 | } 52 | 53 | export default guards 54 | 55 | /** 56 | * Accumulator for guard queue 57 | * */ 58 | export function runGuardQueue(guards:Array): Promise { 59 | return guards.reduce( 60 | (promiseAccumulator, guard) => 61 | promiseAccumulator.then( 62 | () => guard() 63 | ), Promise.resolve()); 64 | } 65 | 66 | /** 67 | * @description Function just only for one guard. 68 | * @return {Promise} promisify guard. 69 | * 70 | * If guard return void or true value - resolve. 71 | * Otherwise, reject(err) 72 | * */ 73 | export function guardToPromiseFn(guard:GuardFunction, id: ModalID, props?: any): GuardFunctionPromisify{ 74 | return () => new Promise((resolve, reject) => { 75 | /** 76 | * Next - hook for returned value from guard. 77 | * */ 78 | const next = (valid:boolean | void = true):void => { 79 | if (valid === false) reject(ModalError.NextReject(id)); 80 | resolve(); 81 | }; 82 | 83 | Promise.resolve(guard.call(getModalById(id)?.instance, props)) 84 | .then(next) 85 | .catch(err => reject(err)); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /src/utils/initialize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * last change: 13.08.2022 3 | * */ 4 | 5 | import {configuration} from "./state"; 6 | import getCurrentModal from "../methods/getCurrentModal"; 7 | import closeById from "../methods/closeById"; 8 | import NamespaceStore from "./NamespaceStore"; 9 | 10 | /** 11 | * @description Function run when ModalContainer was mounted in user's interface. 12 | * Set the key 'initialized' to true and handle the 'keyup' event. 13 | * */ 14 | export default function initialize(namespace = NamespaceStore.DEFAULT_NAMESPACE){ 15 | 16 | const namespaceState = NamespaceStore.instance.getByName(namespace); 17 | namespaceState.initialized = true; 18 | 19 | if (namespace !== NamespaceStore.DEFAULT_NAMESPACE) return; 20 | 21 | document.addEventListener("keyup", e => { 22 | // Closing the last modal window when user pressed Escape 23 | if (configuration.escClose && (e.key === "Escape" || e.code === "Escape")) { 24 | const modal = getCurrentModal(namespace); 25 | if (!modal) return; 26 | /** 27 | * Моя Дорогая Анна, с Любовью, твой Евгений!. 28 | * */ 29 | closeById(modal.id, {esc: true}).catch(() => {}) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * last change: 25.11.2021 3 | * 4 | * STATE ПРЕДНАЗНАЧЕН ДЛЯ ВНУТРЕННЕГО ХРАНИЛИЩА ДАННЫХ 5 | * НЕПУТАТЬ С КОНФИГУРАЦИЕЙ, ЕЁ ЗАДАЁТ ПОЛЬЗОВАТЕЛЬ 6 | * 7 | * initialized - параметра принимает true, когда приложение было проинициализировано, то есть WidgetModalContainer 8 | * был добавлен на страницу 9 | * 10 | * */ 11 | 12 | import {watch} from "vue"; 13 | import NamespaceStore, {INamespaceKey} from "./NamespaceStore"; 14 | import {IBeforeEachCallback, IStoreComponentConfiguration, IStoreElement} from "./types"; 15 | 16 | const modalState = (function () { 17 | 18 | const namespaceStore = new NamespaceStore(); 19 | const configuration: ConfigInterface = { 20 | scrollLock: true, // Disable scrolling in time when modal is open. 21 | animation: "modal-list", // Animation name for transition-group. 22 | backgroundClose: true, // Closing on click back area of modal. 23 | escClose: true, // Closing on press ESC key 24 | store: {}, 25 | skipInitCheck: false, 26 | draggable: false, 27 | appear: true, 28 | singleShow: false, 29 | beforeEach: () => {} 30 | } 31 | 32 | // Default queue. 33 | const modalQueue = namespaceStore.getByName().queue; 34 | 35 | watch(() => modalQueue, () => { 36 | 37 | if (!configuration.scrollLock) return; 38 | 39 | if (modalQueue.length) document.body.style.overflowY = "hidden"; 40 | else document.body.style.overflowY = "auto"; 41 | }, {deep: true}) 42 | 43 | return { 44 | namespaceStore, 45 | configuration, 46 | } 47 | })() 48 | 49 | /** 50 | * @description Метод для получения Namespace. 51 | * */ 52 | export function getNamespace(name?: INamespaceKey) { 53 | return modalState.namespaceStore.getByName(name); 54 | } 55 | 56 | 57 | export const configuration = modalState.configuration; 58 | 59 | export interface ConfigInterface { 60 | /** 61 | * @description Disable scrolling in time when modal is open. 62 | * */ 63 | scrollLock: boolean, 64 | /** 65 | * @description Animation name for transition-group. 66 | * */ 67 | animation : string, 68 | /** 69 | * @description Closing on click back area of modal. 70 | * */ 71 | backgroundClose : boolean, 72 | /** 73 | * @description Closing on press ESC key 74 | * */ 75 | escClose : boolean, 76 | /** 77 | * @description Disable throwing the error when the container has not been initialized. 78 | * */ 79 | skipInitCheck: boolean, 80 | /** 81 | * @description If set to true, all modal windows will be draggable. If a string is set, then this string will be 82 | * used as the name of the css class by default for all modular windows. Which will be used to move the window. 83 | */ 84 | draggable: boolean | string, 85 | /** 86 | * @description TransitionGroup appear prop. For more details https://vuejs.org/guide/built-ins/transition.html#transition-on-appear 87 | */ 88 | appear: boolean, 89 | /** 90 | * @description A hook that will fire before opening each modal window. In this hook, you can cancel closing by 91 | * passing the value false. 92 | */ 93 | beforeEach: IBeforeEachCallback, 94 | /** 95 | * @description If the value is set to true, then only one modal window will be shown. If several are opened 96 | * (using pushModal), only the last one will be shown, and the rest will be hidden using `v-show`. 97 | */ 98 | singleShow: boolean, 99 | store: Record 100 | } 101 | 102 | /** 103 | * @description The function determines whether the transferred data is an extended configuration of a storage element 104 | */ 105 | export function isStoreComponentConfiguration(element: unknown): element is IStoreComponentConfiguration { 106 | return !!element && typeof element === 'object' && !!(element as IStoreComponentConfiguration).component; 107 | } 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import {IEventClose} from "./dto"; 2 | import {Component, ComputedRef, ExtractPropTypes, Ref, UnwrapRef} from "vue"; 3 | import {INamespaceKey} from "./NamespaceStore"; 4 | import {ConfigInterface} from "./state"; 5 | 6 | export type GuardFunction = (e: IEventClose) => void | boolean | Promise 7 | export type GuardFunctionPromisify = () => Promise 8 | 9 | export interface IModalCloseOptions { 10 | namespace?: INamespaceKey 11 | } 12 | 13 | export type WrapComponent = Component & { 14 | props?: T, 15 | } 16 | export type WrapComponentProps

= P["props"] | UnwrapRef | Ref | ComputedRef | ExtractPropTypes 17 | 18 | export type IStoreElement = IStoreComponentConfiguration | Component 19 | 20 | export interface IStoreComponentConfiguration extends Partial>{ 21 | component: Component, 22 | } 23 | 24 | /** 25 | * @description Callback, which will be called before each attempt to open the modal window. 26 | */ 27 | export type IBeforeEachCallback = () => any; -------------------------------------------------------------------------------- /tests/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", {"targets": {"node": "current"}, "modules": "commonjs"}], 4 | "@babel/preset-typescript" 5 | ] 6 | } -------------------------------------------------------------------------------- /tests/IntegrationRouter/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/ContainerUsers.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/ModalGuard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/ModalRoute.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/ModalUser.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/router-async.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ОСНОВНАЯ ПРОБЛЕМА: 3 | * ПАУЗА В ВЫЗОВАХ, НЕОБХОДИМО ПОСЛЕ КАЖДОГО ROUTER.PUSH ДЕЛАТЬ ПАУЗУ 4 | * ДАННЫЕ ТЕСТЫ ПРЕДНАЗНАЧЕНЫ ДЛЯ ПРОВЕРКИ ДАННЫХ СИТУАЦИЙ В РАБОТЕ С ROUTER-INTEGRATION 5 | * */ 6 | import router from "./router"; 7 | import {mount} from "@vue/test-utils"; 8 | import {useModalRouter, container, getQueueByNamespace} from "./../../src/index"; 9 | import wait from "../wait"; 10 | import NamespaceStore from "../../src/utils/NamespaceStore"; 11 | 12 | const modalQueue = getQueueByNamespace(); 13 | beforeEach(async () => { 14 | NamespaceStore.instance.forceClean() 15 | }) 16 | useModalRouter.init(router); 17 | 18 | describe("Router async", () => { 19 | 20 | test("Changing modalQueue length", async () => { 21 | await mount(container, {global: {plugins: [router]}}); 22 | 23 | await router.push('/'); 24 | // After this line, router is ready 25 | await router.isReady(); 26 | 27 | await router.push("/router-simple-modal"); 28 | await wait(); 29 | 30 | expect(modalQueue.length).toBe(1); 31 | }) 32 | 33 | test("Entering modal", async () => { 34 | const wrapper = await mount(container, {global: {plugins: [router]}}); 35 | 36 | await router.push('/'); 37 | // After this line, router is ready 38 | await router.isReady(); 39 | 40 | await router.push("/router-simple-modal"); 41 | await wait(); 42 | 43 | expect(wrapper.text()).toBe("Modal router"); 44 | }) 45 | 46 | 47 | }) 48 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/router.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import router from "./router"; 3 | import {nextTick} from "vue"; 4 | import {getCurrentModal, useModalRouter} from "../../src/index"; 5 | import wait from "../wait"; 6 | import App from "./App.vue"; 7 | import Modal from "../../src/utils/Modal"; 8 | import {render} from "@testing-library/vue"; 9 | import NamespaceStore from "../../src/utils/NamespaceStore"; 10 | 11 | beforeEach(async () => { 12 | NamespaceStore.instance.forceClean() 13 | await router.push("/"); 14 | await router.isReady(); 15 | await wait() 16 | }) 17 | 18 | useModalRouter.init(router); 19 | 20 | describe("Integration with VueRouter", () => { 21 | 22 | it("Default", async () => { 23 | await router.push('/'); 24 | // After this line, router is ready 25 | await router.isReady(); 26 | 27 | const wrapper = await mount(App, {global: {plugins: [router]}}); 28 | 29 | expect(wrapper.text()).toBe("Test"); 30 | }) 31 | it("Opening a window on a simple match", async () => { 32 | await router.push("/simple-modal"); 33 | await router.isReady(); 34 | 35 | // @ts-ignore 36 | const wrapper = await mount(App, { stubs: { 37 | transition: false 38 | }, global: {plugins: [router]}}); 39 | 40 | await nextTick(); 41 | await wait(1000) 42 | expect(wrapper.text()).toBe("Modal router"); 43 | }) 44 | it("Opening and the closing", async () => { 45 | 46 | await router.push("/simple-modal"); 47 | await router.isReady(); 48 | 49 | const wrapper = await mount(App, {global: {plugins: [router]}}); 50 | await wait(1000) 51 | 52 | expect(wrapper.text()).toBe("Modal router"); 53 | await router.push("/"); 54 | await wait(1000) 55 | expect(wrapper.text()).toBe("Test"); 56 | 57 | }) 58 | it("Open child routeModal with params", async () => { 59 | await router.push("/users/3"); 60 | await router.isReady(); 61 | 62 | const wrapper = await mount(App, {global: {plugins: [router]}}); 63 | 64 | await nextTick(); 65 | await wait(1000) 66 | expect(wrapper.text()).toBe("user-3"); 67 | }) 68 | it("Open a list of child routeModal", async () => { 69 | await router.push("/users/3"); 70 | await router.isReady(); 71 | 72 | const wrapper = await mount(App, {global: {plugins: [router]}}); 73 | 74 | for(let i = 0; i < 5; i++) { 75 | await router.push("/users/"+i); 76 | await nextTick(); 77 | await wait(); 78 | 79 | 80 | expect(wrapper.text()).toBe(`user-${i}`); 81 | } 82 | }) 83 | it("Closing modal with guard", async () => { 84 | await router.push("/guard"); 85 | await router.isReady(); 86 | 87 | expect(router.currentRoute.value.path).toBe("/guard"); 88 | 89 | try { 90 | await router.push("/"); 91 | 92 | } catch (e) { 93 | console.log("error with open route /"); 94 | } 95 | await router.isReady(); 96 | 97 | expect(router.currentRoute.value.path).toBe("/guard"); 98 | }) 99 | it("Push", async () => { 100 | await router.push("/"); 101 | await router.isReady(); 102 | 103 | const wrapper = await mount(App, {global: {plugins: [router]}}); 104 | 105 | await router.push("/a"); 106 | await nextTick(); 107 | 108 | await router.push("/b"); 109 | await nextTick(); 110 | 111 | await nextTick(); 112 | await router.push("/users/3"); 113 | 114 | await nextTick(); 115 | await wait(); 116 | expect(wrapper.text()).toBe("user-3"); 117 | 118 | await router.push("/"); 119 | await wait(); 120 | 121 | expect(wrapper.text()).toBe("Test"); 122 | }) 123 | /** 124 | * Sometimes on this test start throwing error. Check that before each new test you add modalQueue.value = [] 125 | * */ 126 | it("Back", async () => { 127 | /** 128 | * Shallow: create and test component only (no children) 129 | * Mount: same as shallow but mounts with children and parent/host component, allows lifecycle methods 130 | * Render: outputs the html given by the component, including children 131 | * */ 132 | const wrapper = await render(App, {global: {plugins: [router]}}) 133 | await router.push("/users/3"); 134 | 135 | await wait(); 136 | 137 | expect(wrapper.container.textContent).toBe("user-3"); 138 | await router.back(); 139 | await wait(100); 140 | expect(wrapper.container.textContent).toBe("Test"); 141 | }) 142 | 143 | it("Modal.isRoute should be true, when modal was opened by RouterIntegration", async () => { 144 | await router.push("/"); 145 | await router.isReady(); 146 | const wrapper = await mount(App, {global: {plugins: [router]}}); 147 | await nextTick(); 148 | await wait(); 149 | 150 | await router.push("/router-simple-modal"); 151 | await wait(); 152 | const modal = getCurrentModal() as Modal; 153 | expect(modal.isRoute).toBe(true); 154 | }) 155 | 156 | }); 157 | -------------------------------------------------------------------------------- /tests/IntegrationRouter/router.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory} from "vue-router"; 2 | import {useModalRouter} from "../../src/index"; 3 | import ModalRoute from "./ModalRoute.vue"; 4 | import ContainerUsers from "./ContainerUsers.vue"; 5 | import ModalUser from "./ModalUser.vue"; 6 | import ModalGuard from "./ModalGuard.vue"; 7 | 8 | const router = createRouter({ 9 | history: createWebHashHistory(), 10 | routes: [ 11 | { 12 | path: "/router-simple-modal", 13 | component: useModalRouter(ModalRoute) 14 | }, 15 | { 16 | path: "/simple-modal", 17 | component: useModalRouter(ModalRoute) 18 | }, 19 | { 20 | path: "/users", 21 | component: ContainerUsers, 22 | children: [ 23 | { 24 | path: ":id", 25 | components: { 26 | modal: useModalRouter(ModalUser) 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | path: '/a', 33 | component: { 34 | template: 'A' 35 | } 36 | }, 37 | { 38 | path: '/b', 39 | component: { 40 | template: 'B' 41 | } 42 | }, 43 | { 44 | path: '/c', 45 | component: { 46 | template: 'C' 47 | } 48 | }, 49 | { 50 | path: '/', 51 | component: { 52 | template: 'Test' 53 | } 54 | }, 55 | { 56 | path: "/guard", 57 | component: useModalRouter(ModalGuard) 58 | } 59 | ] 60 | }) 61 | 62 | export default router; -------------------------------------------------------------------------------- /tests/assets/trigger-click-close.ts: -------------------------------------------------------------------------------- 1 | import {VueWrapper} from "@vue/test-utils"; 2 | 3 | /** 4 | * @description The function is used to simulate a click on the background 5 | */ 6 | export default function triggerClickClose(wrapper: VueWrapper) { 7 | return wrapper.find(".modal-container").trigger("pointerdown"); 8 | } -------------------------------------------------------------------------------- /tests/before-each.spec.ts: -------------------------------------------------------------------------------- 1 | import {container, config, pushModal} from "../src/index"; 2 | import ModalTitle from "./components/modal-title.vue"; 3 | import NamespaceStore from "../src/utils/NamespaceStore"; 4 | import {render} from "@testing-library/vue"; 5 | 6 | 7 | beforeEach(async () => { 8 | NamespaceStore.instance.forceClean() 9 | }) 10 | 11 | afterEach(() => { 12 | config({ 13 | beforeEach() { 14 | return true; 15 | }, 16 | }) 17 | }) 18 | 19 | describe("beforeEach", () => { 20 | 21 | test("Reject all modals", async () => { 22 | await render(container); 23 | config({ 24 | beforeEach() { 25 | return false; 26 | }, 27 | }) 28 | 29 | await pushModal(ModalTitle).catch(() => {}); 30 | await pushModal(ModalTitle).catch(() => {}); 31 | await pushModal(ModalTitle).catch(() => {}); 32 | 33 | expect(NamespaceStore.instance.getByName().queue.length).toBe(0) 34 | 35 | }) 36 | test("Default return pushModal", async () => { 37 | 38 | let count = 0; 39 | 40 | await render(container); 41 | config({ 42 | beforeEach() { 43 | count++; 44 | 45 | return count > 2 46 | }, 47 | }) 48 | 49 | await pushModal(ModalTitle).catch(() => {}); 50 | await pushModal(ModalTitle).catch(() => {}); 51 | await pushModal(ModalTitle).catch(() => {}); 52 | await pushModal(ModalTitle).catch(() => {}); 53 | await pushModal(ModalTitle).catch(() => {}); 54 | 55 | expect(NamespaceStore.instance.getByName().queue.length).toBe(3) 56 | 57 | }) 58 | }) -------------------------------------------------------------------------------- /tests/before-modal-close.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {closeModal, container, getQueueByNamespace, openModal} from "../src/index"; 3 | import wait from "./wait"; 4 | import NamespaceStore from "./../src/utils/NamespaceStore"; 5 | 6 | let modalQueue = getQueueByNamespace(); 7 | beforeEach(async () => { 8 | NamespaceStore.instance.forceClean() 9 | }) 10 | 11 | 12 | describe("beforeModalClose", () => { 13 | const component = { 14 | beforeModalClose: () => {}, 15 | template: `

3

`, 16 | data: () => ({title: "Test"}) 17 | } 18 | it("beforeModalClose run", async () => { 19 | await mount(container); 20 | 21 | let isClosed = false; 22 | 23 | component.beforeModalClose = function(){ 24 | isClosed = true; 25 | } 26 | 27 | const modal = await openModal(component); 28 | await modal.close(); 29 | 30 | expect(isClosed).toBeTruthy(); 31 | 32 | }) 33 | 34 | it("beforeModalClose next(false)", async () => { 35 | await mount(container); 36 | 37 | component.beforeModalClose = function(){ 38 | return false; 39 | } 40 | 41 | await openModal(component); 42 | try { 43 | expect(await closeModal()).toThrow() 44 | 45 | } catch (e) { 46 | 47 | } 48 | 49 | expect(modalQueue.length).toBe(1); 50 | }) 51 | it("beforeModalClose next(true)", async () => { 52 | await mount(container); 53 | 54 | const testComponent = { 55 | template: "

a

", 56 | beforeModalClose(){ 57 | return true; 58 | } 59 | } 60 | 61 | await openModal(testComponent); 62 | await closeModal(); 63 | 64 | expect(modalQueue.length).toBe(0); 65 | }) 66 | it("beforeModalClose retrun undefined", async () => { 67 | await mount(container); 68 | 69 | component.beforeModalClose = function(){ 70 | 71 | } 72 | 73 | await openModal(component); 74 | await closeModal(); 75 | 76 | expect(modalQueue.length).toBe(0); 77 | }) 78 | 79 | it("beforeModalClose async next(false)", async () => { 80 | await mount(container); 81 | 82 | component.beforeModalClose = async function(){ 83 | await wait() 84 | 85 | return false; 86 | } 87 | 88 | await openModal(component); 89 | try { 90 | expect(await closeModal()).toThrow() 91 | 92 | } catch (e) { 93 | 94 | } 95 | 96 | expect(modalQueue.length).toBe(1); 97 | }) 98 | it("beforeModalClose async next(true)", async () => { 99 | await mount(container); 100 | 101 | 102 | await openModal({ 103 | ...component, 104 | beforeModalClose: async function(){ 105 | await wait() 106 | return true; 107 | } 108 | }); 109 | await closeModal(); 110 | expect(modalQueue.length).toBe(0); 111 | }) 112 | 113 | it("Access to this", async () => { 114 | await mount(container); 115 | 116 | let value = null; 117 | 118 | component.beforeModalClose = async function(this: {title: string}){ 119 | value = this.title; 120 | } 121 | 122 | await openModal(component); 123 | await closeModal(); 124 | 125 | expect(value).toBe("Test"); 126 | }) 127 | 128 | }) -------------------------------------------------------------------------------- /tests/components/modal-button.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /tests/components/modal-prompt-value-with-handler.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 36 | -------------------------------------------------------------------------------- /tests/components/modal-prompt-value.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | -------------------------------------------------------------------------------- /tests/components/modal-title.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /tests/configuration-store-element.spec.ts: -------------------------------------------------------------------------------- 1 | import {container, config, pushModal} from "../src/index"; 2 | import ModalTitle from "./components/modal-title.vue"; 3 | import NamespaceStore from "../src/utils/NamespaceStore"; 4 | import {mount} from "@vue/test-utils"; 5 | 6 | 7 | beforeEach(async () => { 8 | NamespaceStore.instance.forceClean() 9 | }) 10 | 11 | afterEach(() => { 12 | config({ 13 | store: {} 14 | }) 15 | }) 16 | 17 | describe("Configuration using Extend Store Element", () => { 18 | test("Default opening modal", async () => { 19 | const wrapper = await mount(container); 20 | config({ 21 | store: { 22 | Foo: { 23 | component: ModalTitle 24 | } 25 | } 26 | }) 27 | await pushModal('Foo'); 28 | await pushModal('Foo'); 29 | 30 | expect(NamespaceStore.instance.getByName().queue.length).toBe(2) 31 | 32 | }) 33 | 34 | test("Opening modal by name should add options from configuration", async () => { 35 | const wrapper = await mount(container); 36 | config({ 37 | store: { 38 | Foo: { 39 | component: ModalTitle, 40 | backgroundClose: false, 41 | draggable: true, 42 | } 43 | } 44 | }) 45 | const modal = await pushModal('Foo'); 46 | const modal_2 = await pushModal(ModalTitle); 47 | 48 | expect(modal.backgroundClose).toBe(false); 49 | expect(modal.draggable).toBe(true); 50 | 51 | expect(modal_2.backgroundClose).toBe(true); 52 | expect(modal_2.draggable).toBe(false); 53 | }) 54 | 55 | test("Configuration with beforeEach", async () => { 56 | await mount(container); 57 | let count = 3; 58 | config({ 59 | store: { 60 | Foo: { 61 | component: ModalTitle, 62 | beforeEach() { 63 | if (count-- > 0) return false; 64 | } 65 | } 66 | } 67 | }) 68 | await pushModal('Foo').catch(() => {}); 69 | await pushModal('Foo').catch(() => {}); 70 | await pushModal('Foo').catch(() => {}); 71 | await pushModal('Foo'); 72 | await pushModal('Foo'); 73 | 74 | expect(NamespaceStore.instance.getByName().queue.length).toBe(2) 75 | 76 | }) 77 | }) -------------------------------------------------------------------------------- /tests/configuration.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {closeModal, config, container, getQueueByNamespace, openModal, popModal, pushModal} from "../src/index"; 3 | import wait from "./wait"; 4 | import ModalTitle from "./components/modal-title.vue"; 5 | import NamespaceStore from "../src/utils/NamespaceStore"; 6 | import ModalError from "../src/utils/ModalError"; 7 | import triggerClickClose from "./assets/trigger-click-close"; 8 | import WidgetModalContainerItem from "../src/components/WidgetModalContainerItem.vue"; 9 | 10 | const modalQueue = getQueueByNamespace(); 11 | beforeEach(async () => { 12 | NamespaceStore.instance.forceClean() 13 | NamespaceStore.instance.getByName().initialized = false; 14 | }) 15 | /** 16 | * ТЕСТЫ ДЛЯ ФУНКЦИИ CONFIG 17 | * + backgroundClose 18 | * + escClose 19 | * - scrollLock 20 | * ? animation - не знаю, как проверить этот параметр 21 | * 22 | * */ 23 | describe("Configuration function", () => { 24 | 25 | /** 26 | * Тест для проверки закрытия модального окна по клику на задний фон, в случае дефолтного значения параметра 27 | * backClose: true 28 | * */ 29 | test("backgroundClose (true)", async () => { 30 | 31 | const wrapper = await mount(container); 32 | 33 | await openModal(ModalTitle, { 34 | 35 | }); 36 | 37 | expect(modalQueue.length).toBe(1); 38 | 39 | await triggerClickClose(wrapper); 40 | 41 | expect(modalQueue.length).toBe(0); 42 | }) 43 | /** 44 | * В случае клика на заднюю область, и установлении параметра backClos 45 | * */ 46 | test("backgroundClose (false)", async () => { 47 | const wrapper = await mount(container); 48 | config({backgroundClose: false}); 49 | 50 | await openModal(ModalTitle); 51 | 52 | expect(modalQueue.length).toBe(1); 53 | 54 | await wrapper.find(".modal-container").trigger('click'); 55 | 56 | expect(modalQueue.length).toBe(1); 57 | }) 58 | /** 59 | * BackClose: false, закрытие при помощи closeModal 60 | * */ 61 | test("backgroundClose (false) => closeModal", async () => { 62 | await mount(container); 63 | config({backgroundClose: false}); 64 | 65 | await openModal(ModalTitle); 66 | 67 | expect(modalQueue.length).toBe(1); 68 | 69 | await closeModal(); 70 | 71 | expect(modalQueue.length).toBe(0); 72 | }) 73 | /** 74 | * BackClose: false, закрытие при помощи popModal 75 | * */ 76 | test("backgroundClose (false) => popModal", async () => { 77 | await mount(container); 78 | config({backgroundClose: false}); 79 | 80 | await openModal(ModalTitle); 81 | 82 | expect(modalQueue.length).toBe(1); 83 | 84 | await popModal(); 85 | 86 | expect(modalQueue.length).toBe(0); 87 | }) 88 | 89 | /** 90 | * Неопознанный параметр в конфигурации 91 | * Нет необходимости в данном тесте. 92 | * */ 93 | /* 94 | test("Undefined params in config function", () => { 95 | expect(() =>config({backgroundClose___1: false})).toThrow() 96 | }) 97 | */ 98 | /** 99 | * Нажатие на ESC -> закрытие модального окна 100 | * */ 101 | test("escClose:true", async () => { 102 | 103 | await mount(container); 104 | 105 | await openModal(ModalTitle); 106 | 107 | const event = new KeyboardEvent('keyup', {'key': "Escape"}); 108 | document.dispatchEvent(event); 109 | await wait(10); 110 | 111 | expect(modalQueue.length).toBe(0); 112 | }) 113 | /** 114 | * Нажатие на ESC в случае, когда escClose: false 115 | * Модальное окно не должно быть закрыто 116 | * */ 117 | 118 | test("escClose: false", async () => { 119 | await mount(container); 120 | 121 | config({ 122 | escClose: false 123 | }) 124 | 125 | await openModal(ModalTitle); 126 | 127 | const event = new KeyboardEvent('keyup', {'key': "Escape"}); 128 | document.dispatchEvent(event); 129 | await wait(10); 130 | 131 | expect(modalQueue.length).toBe(1); 132 | }) 133 | 134 | test("disableInitializationCheck should provide way to addModal without container", async () => { 135 | await expect(openModal(ModalTitle)).rejects.toThrowError(ModalError.NotInitialized(NamespaceStore.DEFAULT_NAMESPACE)); 136 | config({ 137 | skipInitCheck: true 138 | }) 139 | await expect(openModal(ModalTitle)).resolves.not.toThrow(); 140 | }) 141 | 142 | test("SingleShow in configuration", async () => { 143 | const app = await mount(container); 144 | config({ 145 | singleShow: true 146 | }) 147 | 148 | await pushModal(ModalTitle) 149 | await pushModal(ModalTitle) 150 | await pushModal(ModalTitle) 151 | await pushModal(ModalTitle) 152 | 153 | expect(modalQueue.length).toBe(4); 154 | 155 | 156 | const arrayModalContainer = app.findAllComponents(WidgetModalContainerItem); 157 | 158 | arrayModalContainer.map(item => item.props('show')) 159 | .forEach((value, index, arr) => expect(value).toBe(index === arr.length - 1)) 160 | 161 | }) 162 | 163 | }) 164 | -------------------------------------------------------------------------------- /tests/esc-closing.spec.ts: -------------------------------------------------------------------------------- 1 | import {config, container} from "../src/index"; 2 | import {fireEvent, render} from "@testing-library/vue"; 3 | import openModal from "../src/methods/openModal"; 4 | import ModalTitle from "./components/modal-title.vue"; 5 | import wait from "./wait"; 6 | 7 | afterEach(() => { 8 | config({ 9 | escClose: true 10 | }) 11 | }) 12 | describe("Test Esc closing", () => { 13 | test("Modal should close after pressed Esc", async () => { 14 | await render(container); 15 | const modal = await openModal(ModalTitle); 16 | await fireEvent.keyUp(document, {key: "Escape"}); 17 | await wait(5); 18 | expect(modal.closed.value).toBe(true); 19 | }) 20 | test("Modal shouldn't close, if using config.escClose = false", async () => { 21 | await render(container); 22 | const modal = await openModal(ModalTitle); 23 | config({ 24 | escClose: false 25 | }) 26 | await fireEvent.keyUp(document, {key: "Escape"}); 27 | await wait(5); 28 | expect(modal.closed.value).toBe(false); 29 | }) 30 | }) -------------------------------------------------------------------------------- /tests/event-close.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {fireEvent, render} from "@testing-library/vue"; 3 | import {container, openModal} from "../src/index"; 4 | import ModalTitle from "./components/modal-title.vue"; 5 | import wait from "./wait"; 6 | import triggerClickClose from "./assets/trigger-click-close"; 7 | 8 | describe("Testing event close", () => { 9 | test("event.background should be true, after background click", async () => { 10 | const wrap = mount(container); 11 | const modal = await openModal(ModalTitle); 12 | let backgroundValue = null; 13 | modal.onclose = (v) => { 14 | backgroundValue = v.background; 15 | } 16 | 17 | await triggerClickClose(wrap); 18 | await wait(); 19 | expect(backgroundValue).toBe(true) 20 | }) 21 | test("event.background should be false by default", async () => { 22 | mount(container); 23 | const modal = await openModal(ModalTitle); 24 | let backgroundValue = null; 25 | modal.onclose = (v) => { 26 | backgroundValue = v.background; 27 | } 28 | await modal.close() 29 | await wait() 30 | expect(backgroundValue).toBe(false); 31 | }) 32 | test("event.esc should be false by default", async () => { 33 | const wrap = await mount(container); 34 | const modal = await openModal(ModalTitle); 35 | 36 | let escValue = null; 37 | 38 | modal.onclose = (v) => { 39 | escValue = v.esc; 40 | } 41 | await modal.close() 42 | expect(escValue).toBe(false); 43 | }) 44 | test("event.esc should be true if modal was closed by pressed Esc", async () => { 45 | const wrap = await render(container); 46 | const modal = await openModal(ModalTitle); 47 | let escValue = null; 48 | 49 | modal.onclose = (v) => { 50 | escValue = v.esc; 51 | } 52 | await fireEvent.keyUp(document, {key: "Escape"}); 53 | await wait(10) 54 | expect(escValue).toBe(true) 55 | }) 56 | }) -------------------------------------------------------------------------------- /tests/initialization.spec.ts: -------------------------------------------------------------------------------- 1 | import {container, openModal} from "../src/index"; 2 | import ModalTitle from "./components/modal-title.vue"; 3 | import {render} from "@testing-library/vue"; 4 | 5 | describe("Test init library", () => { 6 | test("Throw error without mounting container", async () => { 7 | await expect(openModal(ModalTitle)).rejects.toThrow(); 8 | }) 9 | test("Normal working. Container was mounted", async () => { 10 | render(container); 11 | await expect(openModal(ModalTitle)).resolves.not.toThrow() 12 | }) 13 | 14 | }) -------------------------------------------------------------------------------- /tests/modal-class.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {container, openModal} from "../src/index"; 3 | import Modal from "../src/utils/Modal"; 4 | import pushModal from "../src/methods/pushModal"; 5 | import ModalTitle from "./components/modal-title.vue"; 6 | import NamespaceStore from "./../src/utils/NamespaceStore"; 7 | beforeEach(async () => { 8 | NamespaceStore.instance.forceClean() 9 | }) 10 | 11 | describe("ModalClass", () => { 12 | 13 | test("Default return openModal", async () => { 14 | 15 | await mount(container); 16 | 17 | const modal = await openModal(ModalTitle); 18 | 19 | expect(modal instanceof Modal).toBeTruthy(); 20 | 21 | }) 22 | test("Default return pushModal", async () => { 23 | 24 | await mount(container); 25 | 26 | const modal = await pushModal(ModalTitle); 27 | 28 | expect(modal instanceof Modal).toBeTruthy(); 29 | 30 | }) 31 | }) -------------------------------------------------------------------------------- /tests/modal-object.spec.ts: -------------------------------------------------------------------------------- 1 | import {container, openModal} from "../src/index"; 2 | import ModalTitle from "./components/modal-title.vue"; 3 | import {mount} from "@vue/test-utils"; 4 | import wait from "./wait"; 5 | import NamespaceStore from "./../src/utils/NamespaceStore"; 6 | 7 | let wrapper = null; 8 | 9 | beforeAll(async () => { 10 | wrapper = await mount(container); 11 | }) 12 | beforeEach(async () => { 13 | NamespaceStore.instance.forceClean() 14 | }) 15 | 16 | describe("ModalObject test", () => { 17 | 18 | test("target test, when modal opened", async () => { 19 | 20 | const modal = await openModal(ModalTitle, {title: "Test", age: 15}); 21 | 22 | expect(modal.instance.title + modal.instance.age).toBe("Test15"); 23 | }) 24 | 25 | /** 26 | * @description Тест был ранее добавлен, когда instance очищался при закрытии модального окна. Однако теперь он 27 | * встроен в класс Modal. А поскольку ссылка на modal осталась, то и компонента не очистилась. 28 | * */ 29 | test("target test, when modal closed", async () => { 30 | const modal = await openModal(ModalTitle, {title: "Test", age: 15}); 31 | await modal.close(); 32 | expect(modal.instance).toEqual({title: "Test", age: 15}); 33 | }) 34 | 35 | test('ModalObject.closed, when modal open', async () => { 36 | const modal = await openModal(ModalTitle, {title: "Test", age: 15}); 37 | expect(modal.closed.value).toBe(false); 38 | }) 39 | 40 | test('ModalObject.closed, when modal closed', async () => { 41 | 42 | const modal = await openModal(ModalTitle); 43 | await modal.close(); 44 | await wait(); 45 | 46 | expect(modal.closed.value).toBe(true); 47 | 48 | }) 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /tests/modal-on.spec.ts: -------------------------------------------------------------------------------- 1 | import {container, openModal} from "../src/index"; 2 | import {fireEvent, render} from "@testing-library/vue"; 3 | import ModalButton from "./components/modal-button.vue"; 4 | 5 | describe("Test with on method of Modal", () => { 6 | 7 | test('Default', async () => { 8 | 9 | const wrap = await render(container) 10 | const value = Math.random(); 11 | let output = null; 12 | 13 | const modal = await openModal(ModalButton, {value}); 14 | 15 | modal.on('update', function (v) { 16 | output = v; 17 | }) 18 | 19 | const button = await wrap.findByRole("button") 20 | await fireEvent.click(button); 21 | 22 | expect(output).toBe(value); 23 | 24 | }) 25 | test("Event should not be handled, after unsubscribe", async() => { 26 | const wrap = await render(container); 27 | let output = 0; 28 | 29 | const modal = await openModal(ModalButton); 30 | 31 | const off = modal.on('update', () => { 32 | output++; 33 | }) 34 | const button = await wrap.findByRole("button"); 35 | await fireEvent.click(button); 36 | expect(output).toBe(1); 37 | off(); 38 | await fireEvent.click(button); 39 | expect(output).toBe(1); 40 | 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/modal-onclose.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {closeModal, container, getQueueByNamespace, openModal} from "../src/index"; 3 | import {nextTick} from "vue"; 4 | import ModalTitle from "./components/modal-title.vue"; 5 | import NamespaceStore from "./../src/utils/NamespaceStore"; 6 | let wrapper:any = null; 7 | 8 | const modalQueue = getQueueByNamespace(); 9 | beforeAll(async () => { 10 | wrapper = await mount(container); 11 | }) 12 | 13 | beforeEach(() => { 14 | NamespaceStore.instance.forceClean() 15 | }) 16 | 17 | describe("onclose test", () => { 18 | 19 | it("Without onclose", async () => { 20 | await openModal(ModalTitle, {title: "test"}); 21 | 22 | expect(wrapper.text()).toBe("test"); 23 | 24 | await closeModal(); 25 | 26 | expect(modalQueue.length).toBe(0); 27 | }); 28 | 29 | it("With onclose without return", async () => { 30 | const modal = await openModal(ModalTitle, {title: "test"}); 31 | 32 | let testValue = 0; 33 | 34 | modal.onclose = () => { 35 | testValue = 5; 36 | } 37 | await modal.close(); 38 | 39 | expect(testValue).toBe(5); 40 | expect(modalQueue.length).toBe(0) 41 | }) 42 | 43 | it("With onclose returned false", async() => { 44 | 45 | const modal = await openModal(ModalTitle, {title: "test-2"}); 46 | modal.onclose =() => false; 47 | 48 | await (expect(modal.close())).rejects.toThrow() 49 | 50 | expect(modalQueue.length).toBe(1); 51 | expect(wrapper.text()).toBe("test-2"); 52 | }) 53 | 54 | it("With onclose returned true", async () => { 55 | const modal = await openModal(ModalTitle, {title: "test-2"}); 56 | modal.onclose =() => true; 57 | 58 | await modal.close(); 59 | 60 | await nextTick(); 61 | 62 | expect(modalQueue.length).toBe(0); 63 | expect(wrapper.text()).toBe(""); 64 | }) 65 | 66 | 67 | 68 | test("Closing after 3 attempts.", async () => { 69 | 70 | const modal = await openModal(ModalTitle); 71 | let count = 3; 72 | modal.onclose = () => { 73 | count--; 74 | if (count > 0) return false; 75 | } 76 | 77 | await closeModal().catch(() => {}) 78 | await closeModal().catch(() => {}) 79 | 80 | expect(modalQueue.length).toBe(1); 81 | 82 | await closeModal(); 83 | expect(modalQueue.length).toBe(0); 84 | }) 85 | it("Opening modal, before prev window can't be closed", async () => { 86 | const modal = await openModal(ModalTitle, {title: "1"}); 87 | modal.onclose = () => false; 88 | 89 | await expect(openModal(ModalTitle, {title: "2"})).rejects.toThrow() 90 | 91 | 92 | expect(wrapper.text()).toBe("1"); 93 | }) 94 | it("Multi onclose", async () => { 95 | 96 | let count = 0; 97 | const modal = await openModal(ModalTitle); 98 | 99 | const add = () => { 100 | count++; 101 | }; 102 | 103 | modal.onclose = add; 104 | modal.onclose = add; 105 | modal.onclose = add; 106 | 107 | await modal.close(); 108 | 109 | expect(modalQueue.length).toBe(0); 110 | 111 | expect(count).toBe(3); 112 | 113 | }) 114 | 115 | 116 | 117 | }) -------------------------------------------------------------------------------- /tests/modal-options.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import { 3 | config, 4 | container, 5 | openModal, 6 | } from "../src/index"; 7 | import wait from "./wait"; 8 | import ModalTitle from "./components/modal-title.vue"; 9 | import NamespaceStore from "./../src/utils/NamespaceStore"; 10 | import triggerClickClose from "./assets/trigger-click-close"; 11 | 12 | let wrapper:any = null; 13 | 14 | beforeAll(async () => { 15 | wrapper = await mount(container); 16 | }) 17 | beforeEach(async () => { 18 | NamespaceStore.instance.forceClean() 19 | await wait() 20 | }) 21 | 22 | describe("Testing modal options", () => { 23 | 24 | test('options {backgroundClose: false} ', async () => { 25 | const modal = await openModal(ModalTitle, {}, {backgroundClose: false}) 26 | 27 | wrapper.find(".modal-container").trigger('click'); 28 | await wait(); 29 | 30 | expect(modal.closed.value).toBe(false) 31 | }) 32 | test('modal.backGroundClose = false', async () => { 33 | const modal = await openModal(ModalTitle, {}) 34 | modal.backgroundClose = false; 35 | 36 | wrapper.find(".modal-container").trigger('click'); 37 | await wait(); 38 | 39 | expect(modal.closed.value).toBe(false) 40 | }) 41 | 42 | test('modal.backGroundClose = true, configuration = false', async () => { 43 | config({backgroundClose: false}); 44 | 45 | const modal = await openModal(ModalTitle, {}, {backgroundClose: true}) 46 | 47 | await triggerClickClose(wrapper); 48 | await wait(); 49 | 50 | expect(modal.closed.value).toBe(true) 51 | }) 52 | test('Modal.isRoute should be false by default', async () => { 53 | const modal = await openModal(ModalTitle); 54 | expect(modal.isRoute).toBe(false); 55 | }) 56 | 57 | 58 | }) 59 | -------------------------------------------------------------------------------- /tests/modal-props.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {container, openModal, pushModal} from "../src/index"; 3 | import ModalTitle from "./components/modal-title.vue" 4 | import {computed, reactive, ref} from "vue"; 5 | import wait from "./wait"; 6 | import NamespaceStore from "./../src/utils/NamespaceStore"; 7 | 8 | let wrapper = null; 9 | 10 | beforeAll(async () => { 11 | wrapper = await mount(container); 12 | }) 13 | beforeEach(async () => { 14 | NamespaceStore.instance.forceClean() 15 | await wait() 16 | }) 17 | 18 | describe('Props of Modal', () => { 19 | 20 | /** 21 | * Проверка на передачу простых аргументов в модальное окно 22 | * */ 23 | test('Simple props', async () => { 24 | const title = "Text" 25 | const modal = await openModal(ModalTitle, {title}); 26 | 27 | expect(modal.instance.title).toBe(title) 28 | }) 29 | /** 30 | * Проверка на передачу простых аргументов в модальное окно 31 | * */ 32 | test('Simple props with push', async () => { 33 | const title = "Text" 34 | const modal = await pushModal(ModalTitle, {title}); 35 | 36 | expect(modal.instance.title).toBe(title) 37 | }) 38 | /** 39 | * Проверка на объект целиком 40 | * */ 41 | test('Ref props', async () => { 42 | const state = { title: 'Hello'}; 43 | const modal = await openModal(ModalTitle, state); 44 | 45 | expect(modal.instance.title).toBe('Hello') 46 | }) 47 | /** 48 | * Проверка на передачу ref 49 | * */ 50 | test('Ref props', async () => { 51 | const state = ref({ title: 'Hello'}); 52 | const modal = await openModal(ModalTitle, state); 53 | 54 | expect(modal.instance.title).toBe('Hello') 55 | }) 56 | 57 | /** 58 | * Проверка на передачу reactive 59 | * */ 60 | test('Reactive props', async () => { 61 | const state = reactive({title: 'base'}); 62 | const modal = await openModal(ModalTitle, state); 63 | 64 | expect(modal.instance.title).toBe('base'); 65 | }) 66 | 67 | /** 68 | * Проверка на передачу вычисляемого объекта, в качестве props целиком 69 | * */ 70 | test('Computed props', async () => { 71 | const refState = ref({title: 'ref'}); 72 | const state = computed(() => refState.value); 73 | 74 | const modal = await openModal(ModalTitle, state); 75 | expect(modal.instance.title).toBe('ref'); 76 | }) 77 | 78 | /** 79 | * Передача props, одно из свойств которого является вычисляемое значение 80 | * */ 81 | test('Computed prop in object', async () => { 82 | 83 | const modal = await openModal(ModalTitle, { 84 | title: "Hello", 85 | age: 1 86 | }) 87 | expect(modal.instance.title).toBe('Hello') 88 | }) 89 | 90 | /** 91 | * ПРОВЕРКА НА ИЗМЕНЕНИЕ PROPS: REF, REACTIVE, COMPUTED 92 | * */ 93 | 94 | test('Changing ref in props', async () => { 95 | const state = ref({ title: 'Hello', age: 1}); 96 | const modal = await openModal(ModalTitle, state); 97 | 98 | state.value.title = 'New value'; 99 | 100 | await wait(); 101 | 102 | expect(modal.instance.title).toBe('New value') 103 | }) 104 | test('Changing reactive in props', async () => { 105 | const state = reactive({title: 'base', age: 4}); 106 | const modal = await openModal(ModalTitle, state); 107 | 108 | state.title = 'New value'; 109 | 110 | await wait(); 111 | 112 | expect(modal.instance.title).toBe('New value'); 113 | }) 114 | test('Changing computed in props', async () => { 115 | const refState = ref({title: 'ref'}); 116 | const state = computed(() => refState.value); 117 | 118 | const modal = await openModal(ModalTitle, state); 119 | 120 | refState.value.title = 'New value'; 121 | await wait(); 122 | expect(modal.instance.title).toBe('New value'); 123 | }) 124 | 125 | 126 | }) 127 | -------------------------------------------------------------------------------- /tests/on-before-modal-close.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {closeModal, container, getQueueByNamespace, onBeforeModalClose, openModal} from "../src/index"; 3 | import wait from "./wait"; 4 | import NamespaceStore from "./../src/utils/NamespaceStore"; 5 | 6 | let wrap; 7 | const modalQueue = getQueueByNamespace(); 8 | beforeAll(async () => { 9 | wrap = await mount(container); 10 | }) 11 | beforeEach(async () => { 12 | NamespaceStore.instance.forceClean() 13 | }) 14 | 15 | describe("onBeforeModalClose", () => { 16 | 17 | test("onBeforeModalClose run", async () => { 18 | let isClosed = false; 19 | 20 | const component = { 21 | setup() { 22 | onBeforeModalClose(() => { 23 | isClosed = true; 24 | }) 25 | } 26 | } 27 | 28 | const modal = await openModal(component); 29 | await modal.close(); 30 | 31 | expect(isClosed).toBeTruthy(); 32 | }) 33 | 34 | test("onBeforeModalClose next(false)", async () => { 35 | const component = { 36 | template: "

Test

", 37 | setup() { 38 | onBeforeModalClose(() => { 39 | return false; 40 | }) 41 | } 42 | } 43 | 44 | await openModal(component); 45 | await closeModal() 46 | .catch(() => { 47 | }) 48 | 49 | expect(modalQueue.length).toBe(1); 50 | }) 51 | test("onBeforeModalClose next(true)", async () => { 52 | const component = { 53 | setup() { 54 | onBeforeModalClose(() => { 55 | return true; 56 | }) 57 | } 58 | } 59 | 60 | await openModal(component); 61 | await closeModal(); 62 | 63 | expect(modalQueue.length).toBe(0); 64 | }) 65 | 66 | test("onBeforeModalClose async next(false)", async () => { 67 | 68 | const component = { 69 | setup() { 70 | onBeforeModalClose(async () => { 71 | await wait(100); 72 | 73 | return false; 74 | }) 75 | } 76 | } 77 | 78 | await openModal(component); 79 | try { 80 | expect(await closeModal()).toThrow() 81 | 82 | } catch (e) { 83 | 84 | } 85 | 86 | expect(modalQueue.length).toBe(1); 87 | }) 88 | test("onBeforeModalClose async next(true)", async () => { 89 | 90 | const component = { 91 | setup() { 92 | onBeforeModalClose(async () => { 93 | await wait(100); 94 | 95 | return true; 96 | }) 97 | } 98 | } 99 | 100 | await openModal(component); 101 | await closeModal(); 102 | 103 | expect(modalQueue.length).toBe(0); 104 | }) 105 | 106 | 107 | }) -------------------------------------------------------------------------------- /tests/prompt-modal.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {container, promptModal, closeModal} from "../src/index"; 3 | import ModalPromptValue from "./components/modal-prompt-value.vue"; 4 | import wait from "./wait"; 5 | import NamespaceStore from "./../src/utils/NamespaceStore"; 6 | import ModalPromptValueWithHandler from "./components/modal-prompt-value-with-handler.vue"; 7 | import guards from "./../src/utils/guards"; 8 | 9 | const app = mount(container); 10 | beforeEach(async () => { 11 | NamespaceStore.instance.forceClean() 12 | guards.store = {} 13 | await wait() 14 | }) 15 | 16 | describe("Testing prompt-modal", () => { 17 | 18 | it('Test for opened modal window', async function () { 19 | 20 | const value = '123'; 21 | 22 | promptModal(ModalPromptValue, {value}) 23 | 24 | await wait(1); 25 | 26 | expect(app.text()).toEqual(value); 27 | }); 28 | it("Should be returned with provided value", async function () { 29 | 30 | const value = Math.random(); 31 | const prResult = promptModal(ModalPromptValue, { 32 | value 33 | }) 34 | await wait() 35 | app.find('button').trigger('click') 36 | 37 | await wait() 38 | 39 | expect(await prResult).toEqual(value) 40 | }) 41 | 42 | it('If modal was prompted and then just closed, promise should be resolved with value null', async () => { 43 | 44 | const value = Math.random(); 45 | const prResult = promptModal(ModalPromptValue, { 46 | value 47 | }) 48 | await wait() 49 | await closeModal() 50 | 51 | expect(await prResult).toEqual(null) 52 | }) 53 | 54 | 55 | it("Should not be executed, if on of close's handler stop closing process", async () => { 56 | const value = Math.random(); 57 | let isResolved = false 58 | const prResult = promptModal(ModalPromptValueWithHandler, { 59 | value 60 | }).then(data => { 61 | isResolved = true; 62 | return data; 63 | }) 64 | await wait() 65 | await app.find('button').trigger('click') 66 | await wait() 67 | await app.find('button').trigger('click') 68 | await wait() 69 | expect(await prResult).toEqual(value) 70 | }) 71 | }) -------------------------------------------------------------------------------- /tests/unit-test/add-modal-by-name.spec.ts: -------------------------------------------------------------------------------- 1 | import {container, openModal, promptModal, pushModal, config} from "../../src/index"; 2 | import {mount} from "@vue/test-utils"; 3 | import ModalError from "../../src/utils/ModalError"; 4 | import ModalTitle from "../components/modal-title.vue"; 5 | import wait from "../wait"; 6 | import NamespaceStore from "./../../src/utils/NamespaceStore"; 7 | 8 | beforeEach(() => { 9 | config({ 10 | store: {} 11 | }) 12 | NamespaceStore.instance.forceClean(); 13 | }) 14 | describe("Add modal by name", () => { 15 | test("Add modal by name that don't exist", async () => { 16 | await mount(container); 17 | 18 | const name = 'confirm'; 19 | await expect(() => pushModal(name)).rejects.toThrowError(ModalError.ModalNotExistsInStore(name)); 20 | }) 21 | test("Add modal by name that exists in store", async () => { 22 | const wrapper = await mount(container); 23 | const name = 'confirm'; 24 | 25 | config({ 26 | store: { 27 | [name]: ModalTitle 28 | } 29 | }) 30 | await pushModal(name, { 31 | title: "Jenesius", 32 | age: 24 33 | }); 34 | expect(wrapper.text()).toBe("Jenesius 24"); 35 | }) 36 | 37 | test("Open modal by name that exists in store", async () => { 38 | const wrapper = await mount(container); 39 | const name = 'confirm'; 40 | 41 | config({ 42 | store: { 43 | [name]: ModalTitle 44 | } 45 | }) 46 | await openModal(name, { 47 | title: "Burdin", 48 | age: 17 49 | }); 50 | expect(wrapper.text()).toBe("Burdin 17"); 51 | }) 52 | 53 | test("Prompt modal by name that exists in store", async () => { 54 | const wrapper = await mount(container); 55 | const name = 'confirm'; 56 | 57 | config({ 58 | store: { 59 | [name]: ModalTitle 60 | } 61 | }) 62 | promptModal(name, { 63 | title: "Mother", 64 | age: 54 65 | }); 66 | await wait(); 67 | 68 | expect(wrapper.text()).toBe("Mother 54"); 69 | }) 70 | }) -------------------------------------------------------------------------------- /tests/unit-test/get-component-from-store.spec.ts: -------------------------------------------------------------------------------- 1 | import {config} from "../../src/index"; 2 | import getComponentFromStore from "../../src/methods/get-component-from-store"; 3 | import ModalTitle from "../components/modal-title.vue"; 4 | 5 | // Clean store 6 | beforeEach(() => { 7 | config({ 8 | store: {} 9 | }) 10 | }) 11 | 12 | describe("Checking modal in the store", () => { 13 | test("Be default is undefined", () => { 14 | expect(getComponentFromStore('any')).toBe(undefined) 15 | }) 16 | test('Add new component, but getting by wrong name', () => { 17 | config({ 18 | store: { 19 | 'alert': ModalTitle 20 | } 21 | }) 22 | expect(getComponentFromStore('aler')).toBe(undefined) 23 | }) 24 | test('Add new component and get it', () => { 25 | config({ 26 | store: { 27 | 'alert': ModalTitle 28 | } 29 | }) 30 | expect(getComponentFromStore('alert')).toBe(ModalTitle) 31 | }) 32 | }) -------------------------------------------------------------------------------- /tests/wait.ts: -------------------------------------------------------------------------------- 1 | export default (n = 10) => { 2 | return new Promise(resolve => { 3 | setTimeout(resolve, n); 4 | }) 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "noEmit": true, 17 | "types": [ 18 | "webpack-env", 19 | "jest" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "project/**/*.ts", 35 | "project/**/*.tsx", 36 | "project/**/*.vue", 37 | "tests/**/*.ts", 38 | "tests/**/*.tsx" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite'; 3 | import { resolve } from 'node:path' 4 | import pkg from './package.json' 5 | import typescript from '@rollup/plugin-typescript'; 6 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' 7 | 8 | const NAME = pkg.name; 9 | const VERSION = pkg.version; 10 | 11 | const banner = `/*! 12 | * ${NAME} v${VERSION} 13 | * (c) ${new Date().getFullYear()} Jenesius 14 | * @license MIT 15 | */` 16 | const srcFolder = 'src'; 17 | export default defineConfig({ 18 | plugins: [ 19 | { 20 | ...typescript({ tsconfig: resolve(srcFolder, "tsconfig.json"), }), 21 | apply: 'build' 22 | }, 23 | cssInjectedByJsPlugin(), 24 | vue(), 25 | ], 26 | build: { 27 | outDir: resolve( "dist"), 28 | lib: { 29 | entry: resolve(__dirname, srcFolder, "index.ts"), 30 | name: "JenesiusVueModal", 31 | formats: [`cjs`, 'umd', 'es'], 32 | fileName(format) { 33 | return [NAME, format, 'js'].join('.') 34 | } 35 | }, 36 | rollupOptions: { 37 | external: ["vue"], 38 | output: { 39 | globals: { 40 | vue: 'Vue', 41 | }, 42 | sourcemap: true, 43 | banner 44 | } 45 | }, 46 | 47 | } 48 | }) -------------------------------------------------------------------------------- /vue.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pages: { 3 | index: { 4 | entry: 'project/src/main.ts' 5 | }, 6 | "prompt-modal": { 7 | entry: "examples/prompt-modal/main.ts" 8 | }, 9 | "multi-modals": { 10 | entry: "examples/multi-modal/main.ts" 11 | }, 12 | "open-modal": { 13 | entry: "examples/open-modal/main.ts" 14 | }, 15 | "vue-router-with-modal": { 16 | entry: "examples/vue-router-with-modal/main.ts" 17 | }, 18 | "demo": { 19 | entry: "examples/pretty-modal/main.ts" 20 | }, 21 | "simple-animation": { 22 | entry: "examples/simple-animation/main.ts" 23 | }, 24 | "example-router": { 25 | entry: "examples/vue-router-with-modal/main.ts" 26 | }, 27 | "sidebar-modal": { 28 | entry: "examples/side-modal/main.ts" 29 | }, 30 | "handle-closing": { 31 | entry: "examples/handle-closing/main.ts" 32 | }, 33 | "namespace": { 34 | entry: "examples/use-namespace/main.ts" 35 | }, 36 | "draggable": { 37 | entry: "examples/draggable/main.ts" 38 | }, 39 | "test": { 40 | entry: "project/src/test/main.ts" 41 | } 42 | } 43 | } --------------------------------------------------------------------------------