├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── stale.yml └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── build ├── webpack.base.config.js ├── webpack.client-no-css.config.js ├── webpack.client.config.js ├── webpack.ssr-no-css.config.js ├── webpack.ssr.config.js └── webpack.test.config.js ├── demo ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── components │ │ ├── Modal_Adaptive.vue │ │ ├── Modal_Autosize.vue │ │ ├── Modal_Conditional.vue │ │ ├── Modal_CustomComponent.vue │ │ ├── Modal_Dogge.vue │ │ ├── Modal_Draggable.vue │ │ ├── Modal_Error.vue │ │ ├── Modal_Login.vue │ │ └── Modal_Resizable.vue │ └── main.js ├── static │ ├── .gkeep │ ├── cute_dog.gif │ └── panorama.jpg └── webpack.config.js ├── deploy.sh ├── dist ├── index.js ├── index.nocss.js ├── ssr.index.js ├── ssr.nocss.js └── styles.css ├── docs ├── .vuepress │ ├── config.js │ └── sidebar.js ├── Events.md ├── Examples.md ├── Installation.md ├── Intro.md ├── Other.md ├── Properties.md ├── README.md ├── Slots.md └── Usage.md ├── examples └── nuxt │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── components │ ├── ExampleModal.vue │ └── Logo.vue │ ├── layouts │ └── default.vue │ ├── nuxt.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ └── index.vue │ └── plugins │ └── vue-js-modal.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── Plugin.js ├── PluginCore.js ├── components │ ├── Dialog.vue │ ├── Modal.vue │ ├── ModalsContainer.vue │ ├── Resizer.vue │ └── VNode.vue ├── index.js └── utils │ ├── errors.js │ ├── focusTrap.js │ ├── index.js │ ├── numbers.js │ ├── parser.js │ ├── parser.test.js │ ├── resizeObserver.js │ └── types.js └── types ├── index.d.ts └── test ├── index.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true 6 | }, 7 | 8 | plugins: ['babel'], 9 | 10 | extends: [ 11 | 'plugin:babel', 12 | 'plugin:vue/essential', 13 | 'eslint:recommended', 14 | '@vue/prettier' 15 | ], 16 | 17 | parserOptions: { 18 | ecmaVersion: 2020 19 | }, 20 | 21 | rules: { 22 | 'no-console': 'off', 23 | 'no-debugger': 'off', 24 | 'babel/camelcase': 1 25 | }, 26 | 27 | overrides: [ 28 | { 29 | files: [ 30 | '**/*.spec.{j,t}s?(x)' 31 | // '**/tests/unit/**/*.spec.{j,t}s?(x)' 32 | ], 33 | env: { 34 | jest: true 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: euvl 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Problem: 2 | 3 | 4 | 5 | #### Version: 6 | 7 | 8 | 9 | #### Example & screenshots: 10 | 11 | 12 | 13 | 14 | 15 | > I have checked stackoverflow for solutions and 100% sure that this issue is not not related to my code. 16 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - bug 10 | - todo 11 | - enhancement 12 | 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | 16 | # Comment to post when marking an issue as stale. Set to `false` to disable 17 | markComment: > 18 | This issue has been automatically marked as stale because it has not had 19 | recent activity. It will be closed if no further activity occurs. Thank you 20 | for your contributions. 21 | 22 | # Comment to post when closing a stale issue. Set to `false` to disable 23 | closeComment: > 24 | This issue has been automatically closed. 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Runs a single command using the runners shell 26 | - name: Run a one-line script 27 | run: echo Hello, world! 28 | 29 | # Runs a set of commands using the runners shell 30 | - name: Run a multi-line script 31 | run: | 32 | echo Add other actions to build, 33 | echo test, and deploy your project. 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | .idea/ 4 | *.map 5 | node_modules/ 6 | npm-debug.log 7 | demo/dist/* 8 | docs/.vuepress/dist 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | *.map 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v 2.0.0 2 | 3 | * Using ResizeObserver instead of MutationObserver 4 | * Using transition states to coordinate modal & overlay transitions (previously if overlay transition was longer than 5 | modal transition weird stuff was happening) 6 | * Using prettier 7 | * Overlay is not a parent of the modal anymore 8 | * Renamed Event.stop => Event.cancel 9 | * Removed `v--modal-background-click` element 10 | * Removed `v--modal` default class 11 | * Removed "delay" property - component is relying on modal & overlay transition durations 12 | * Added naive implementation of focus trap 13 | * Added source-maps 14 | * Added `hideAll` for dynamic modals 15 | * Fix: dialogs not working when componentName is changed 16 | * Fix: ActiveElement is blurred after before-open is fired - not it is possible to cache document.activeElement -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at yev.dev@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yev Vlasenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Vue.js modal 4 | 5 | Easy to use, highly customizable Vue.js modal library. 6 | 7 | ### 😎 [Examples](http://vue-js-modal.yev.io/) 8 | 9 | ### 🤓 [Documentation](https://euvl.github.io/vue-js-modal/) 10 | 11 | ### 🤖 [Changelog](https://github.com/euvl/vue-js-modal/releases/tag/2.0.0-rc4) 12 | 13 | ### 🙌 [Looking for maintainers](https://github.com/euvl/vue-js-modal/issues/588) 14 | 15 | ### 💰 [Sponsorship](https://github.com/sponsors/euvl) 16 | 17 | [![npm version](https://badge.fury.io/js/vue-js-modal.svg)](https://badge.fury.io/js/vue-js-modal) 18 | [![npm](https://img.shields.io/npm/dm/vue-js-modal.svg)](https://www.npmjs.com/package/vue-js-modal) 19 | [![npm](https://img.shields.io/npm/dt/vue-js-modal.svg)](https://www.npmjs.com/package/vue-js-modal) 20 | 21 | # ⚠️⚠️⚠️ 2.0.0-rc.X version: 22 | 23 | Version 2.0.0 release candidates will have breaking changes until it is 2.0.1. If you need a more stable version, please use 1.3.34. 24 | 25 | If you notice any bugs or regressings please do not hesitate to report any issues. 26 | 27 |

28 | 29 | screen shot 2018-03-01 at 10 33 39 30 | 31 |

32 | 33 |

34 | 35 |

36 | 37 | --- 38 | 39 | # Sponsorship & support 40 | 41 | **If you are using this project please consider sponsoring it's further development & bug fixes** 42 | 43 | Links: https://github.com/sponsors/euvl, https://www.buymeacoffee.com/yev 44 | 45 | ## This library is contributor-driven 46 | 47 | **This library is contributor-driven**. It is not backed by any company, which means that all contributions are voluntary and done by the people who need them. If you need something improved, added, or fixed, please contribute it yourself. Please keep in mind that maintainers volunteer their free time to work on this project and have no obligation to reply on the issues, tailor library for specific use-cases or perform customer support. 48 | 49 | ## Other projects 50 | 51 | Check out my other projects: 52 | 53 | - https://github.com/euvl/vue-notification 54 | - https://github.com/euvl/vue-js-toggle-button 55 | - https://github.com/euvl/vue-js-popover 56 | - https://github.com/euvl/v-clipboard 57 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(babel) { 2 | babel.cache(true) 3 | 4 | return { 5 | presets: ['@babel/preset-env'], 6 | plugins: [ 7 | '@babel/plugin-proposal-object-rest-spread', 8 | '@babel/plugin-proposal-optional-chaining' 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 4 | const { VueLoaderPlugin } = require('vue-loader') 5 | 6 | module.exports = { 7 | mode: 'production', 8 | entry: path.resolve(__dirname, '../src/index.js'), 9 | devtool: 'source-map', 10 | output: { 11 | library: 'vue-js-modal', 12 | libraryTarget: 'umd', 13 | path: path.resolve(__dirname, '../dist'), 14 | publicPath: '/dist/' 15 | }, 16 | resolve: { 17 | extensions: ['.ts', '.js'] 18 | }, 19 | optimization: { 20 | minimizer: [ 21 | new UglifyJsPlugin({ 22 | cache: true, 23 | parallel: true, 24 | sourceMap: true 25 | // compress: { 26 | // pure_funcs: ['console.log'] 27 | // } 28 | }), 29 | new OptimizeCSSAssetsPlugin({ 30 | canPrint: true 31 | }) 32 | ] 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.vue$/, 38 | loader: 'vue-loader' 39 | }, 40 | { 41 | test: /\.js$/, 42 | loader: 'babel-loader', 43 | exclude: /node_modules/ 44 | }, 45 | { 46 | test: /\.ts$/, 47 | loader: 'ts-loader', 48 | exclude: /node_modules/, 49 | options: { 50 | appendTsSuffixTo: [/\.vue$/] 51 | } 52 | }, 53 | { 54 | test: /\.css$/, 55 | use: ['vue-style-loader', 'css-loader'] 56 | } 57 | ] 58 | }, 59 | plugins: [new VueLoaderPlugin()] 60 | } 61 | -------------------------------------------------------------------------------- /build/webpack.client-no-css.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 6 | 7 | module.exports = { 8 | mode: 'production', 9 | entry: path.resolve(__dirname, '../src/index.js'), 10 | devtool: 'source-map', 11 | output: { 12 | library: 'vue-js-modal', 13 | libraryTarget: 'umd', 14 | path: path.resolve(__dirname, '../dist'), 15 | publicPath: '/dist/', 16 | filename: 'index.nocss.js' 17 | }, 18 | resolve: { 19 | extensions: ['.js'] 20 | }, 21 | optimization: { 22 | minimizer: [ 23 | new UglifyJsPlugin({ 24 | cache: true, 25 | parallel: true, 26 | sourceMap: true 27 | }), 28 | new OptimizeCSSAssetsPlugin({ 29 | canPrint: true 30 | }) 31 | ] 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.vue$/, 37 | loader: 'vue-loader' 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | exclude: /node_modules/ 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new VueLoaderPlugin(), 52 | new MiniCssExtractPlugin({ 53 | filename: 'styles.css' 54 | }) 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const base = require('./webpack.base.config') 3 | 4 | module.exports = merge(base, { 5 | output: { 6 | filename: 'index.js' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /build/webpack.ssr-no-css.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | 6 | module.exports = { 7 | mode: 'production', 8 | target: 'node', 9 | entry: path.resolve(__dirname, '../src/index.js'), 10 | output: { 11 | path: path.resolve(__dirname, '../dist'), 12 | publicPath: '/dist/', 13 | library: 'vue-js-modal', 14 | libraryTarget: 'umd', 15 | filename: 'ssr.nocss.js' 16 | }, 17 | optimization: { 18 | minimizer: [ 19 | new UglifyJsPlugin({ 20 | cache: true, 21 | parallel: true 22 | }) 23 | ] 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.vue$/, 29 | loader: 'vue-loader', 30 | options: { 31 | optimizeSSR: false 32 | } 33 | }, 34 | { 35 | test: /\.js$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.css$/, 41 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 42 | } 43 | ] 44 | }, 45 | plugins: [ 46 | new VueLoaderPlugin(), 47 | new MiniCssExtractPlugin({ 48 | filename: 'styles.css' 49 | }) 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /build/webpack.ssr.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const base = require('./webpack.base.config') 3 | 4 | module.exports = merge(base, { 5 | target: 'node', 6 | output: { 7 | filename: 'ssr.index.js' 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var baseConfig = require('./webpack.base.config') 3 | 4 | var webpackConfig = merge(baseConfig, { 5 | devtool: '#inline-source-map' 6 | }) 7 | 8 | delete webpackConfig.entry 9 | delete webpackConfig.output 10 | 11 | module.exports = webpackConfig 12 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Modal Examples 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 | 28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 6 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 7 | }, 8 | "dependencies": { 9 | "vue": "^2.2.6" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.2.0", 13 | "@babel/preset-env": "^7.2.0", 14 | "babel-loader": "^8.0.4", 15 | "cross-env": "^3.0.0", 16 | "css-loader": "^3.5.2", 17 | "file-loader": "^0.9.0", 18 | "node-sass": "^4.13.0", 19 | "optimize-css-assets-webpack-plugin": "^5.0.1", 20 | "sass-loader": "^8.0.0", 21 | "uglifyjs-webpack-plugin": "^2.0.1", 22 | "vue": "^2.6.11", 23 | "vue-hot-reload-api": "^2.0.8", 24 | "vue-loader": "^15.0.0", 25 | "vue-style-loader": "^4.1.2", 26 | "vue-template-compiler": "^2.6.11", 27 | "webpack": "^4.27.1", 28 | "webpack-dev-server": "^3.11.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 216 | 217 | 331 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Adaptive.vue: -------------------------------------------------------------------------------- 1 | 14 | 19 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Autosize.vue: -------------------------------------------------------------------------------- 1 | 33 | 67 | 83 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Conditional.vue: -------------------------------------------------------------------------------- 1 | 14 | 29 | -------------------------------------------------------------------------------- /demo/src/components/Modal_CustomComponent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Dogge.vue: -------------------------------------------------------------------------------- 1 | 19 | 24 | 59 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Draggable.vue: -------------------------------------------------------------------------------- 1 | 21 | 26 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Error.vue: -------------------------------------------------------------------------------- 1 | 26 | 62 | 93 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Login.vue: -------------------------------------------------------------------------------- 1 | 39 | 63 | 234 | -------------------------------------------------------------------------------- /demo/src/components/Modal_Resizable.vue: -------------------------------------------------------------------------------- 1 | 27 | 32 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueJsModal from 'plugin' 3 | import App from './App.vue' 4 | 5 | Vue.use(VueJsModal, { 6 | dialog: true, 7 | dynamicDefaults: { 8 | draggable: true 9 | } 10 | }) 11 | 12 | new Vue({ 13 | el: '#app', 14 | render: h => h(App) 15 | }) 16 | -------------------------------------------------------------------------------- /demo/static/.gkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euvl/vue-js-modal/2dcc67984a65ec6ad351f8a2b13bb84582a0800d/demo/static/.gkeep -------------------------------------------------------------------------------- /demo/static/cute_dog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euvl/vue-js-modal/2dcc67984a65ec6ad351f8a2b13bb84582a0800d/demo/static/cute_dog.gif -------------------------------------------------------------------------------- /demo/static/panorama.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euvl/vue-js-modal/2dcc67984a65ec6ad351f8a2b13bb84582a0800d/demo/static/panorama.jpg -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/main.js', 7 | output: { 8 | path: path.resolve(__dirname, './dist'), 9 | publicPath: '/dist/', 10 | filename: 'build.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.vue$/, 16 | loader: 'vue-loader' 17 | }, 18 | { 19 | test: /\.js$/, 20 | loader: 'babel-loader', 21 | exclude: /node_modules/ 22 | }, 23 | { 24 | test: /\.(png|jpg|gif|svg)$/, 25 | loader: 'file-loader', 26 | options: { 27 | name: '[name].[ext]?[hash]' 28 | } 29 | }, 30 | { 31 | test: /\.scss$/, 32 | use: ['vue-style-loader', 'css-loader', 'sass-loader'] 33 | }, 34 | { 35 | test: /\.css$/, 36 | use: ['vue-style-loader', 'css-loader'] 37 | } 38 | ] 39 | }, 40 | resolve: { 41 | alias: { 42 | vue$: 'vue/dist/vue.esm.js', 43 | plugin: path.resolve(__dirname, '../dist/index.js') 44 | } 45 | }, 46 | devServer: { 47 | historyApiFallback: true, 48 | noInfo: true 49 | }, 50 | devtool: '#eval-source-map', 51 | plugins: [new VueLoaderPlugin()] 52 | } 53 | /* 54 | if (process.env.NODE_ENV === 'production') { 55 | module.mode = 'production' 56 | module.exports.devtool = '#source-map' 57 | // http://vue-loader.vuejs.org/en/workflow/production.html 58 | module.exports.plugins = (module.exports.plugins || []).concat([ 59 | new webpack.DefinePlugin({ 60 | 'process.env': { 61 | NODE_ENV: '"production"' 62 | } 63 | }), 64 | new webpack.LoaderOptionsPlugin({ 65 | minimize: true 66 | }) 67 | ]) 68 | } 69 | */ 70 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | npm run docs:build 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | git init 13 | git add -A 14 | git commit -m 'Deplying docs' 15 | 16 | git push -f git@github.com:euvl/vue-js-modal.git master:gh-pages 17 | 18 | cd - -------------------------------------------------------------------------------- /dist/index.nocss.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports["vue-js-modal"]=e():t["vue-js-modal"]=e()}(window,function(){return n={},o.m=i=[function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,M,e){"use strict";(function(t){var n=function(){if("undefined"!=typeof Map)return Map;function n(t,i){var n=-1;return t.some(function(t,e){return t[0]===i&&(n=e,!0)}),n}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(t){var e=n(this.__entries__,t),i=this.__entries__[e];return i&&i[1]},t.prototype.set=function(t,e){var i=n(this.__entries__,t);~i?this.__entries__[i][1]=e:this.__entries__.push([t,e])},t.prototype.delete=function(t){var e=this.__entries__,i=n(e,t);~i&&e.splice(i,1)},t.prototype.has=function(t){return!!~n(this.__entries__,t)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(t,e){void 0===e&&(e=null);for(var i=0,n=this.__entries__;ithis.viewportWidth||t.clientX<0)&&!(t.clientY>this.viewportHeight||t.clientY<0)&&e){switch(this.targetClass){case"vue-modal-right":i-=e.offsetLeft,n=r;break;case"vue-modal-left":n=r,i=o+(this.initialX-t.clientX);break;case"vue-modal-top":i=o,n=r+(this.initialY-t.clientY);break;case"vue-modal-bottom":i=o,n-=e.offsetTop;break;case"vue-modal-bottomRight":i-=e.offsetLeft,n-=e.offsetTop;break;case"vue-modal-topRight":i-=e.offsetLeft,n=r+(this.initialY-t.clientY);break;case"vue-modal-bottomLeft":i=o+(this.initialX-t.clientX),n-=e.offsetTop;break;case"vue-modal-topLeft":i=o+(this.initialX-t.clientX),n=r+(this.initialY-t.clientY);break;default:console.error("Incorrrect/no resize direction.")}var s=Math.min(u(),this.maxWidth),a=Math.min(window.innerHeight,this.maxHeight);i=h(this.minWidth,s,i),n=h(this.minHeight,a,n),this.initialX=t.clientX,this.initialY=t.clientY,this.size={width:i,height:n};var l={width:i-o,height:n-r};e.style.width=i+"px",e.style.height=n+"px",this.$emit("resize",{element:e,size:this.size,direction:this.targetClass,dimGrowth:l})}}}};i(4);function d(t,e,i,n,o,r,s,a){var l,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=i,u._compiled=!0),n&&(u.functional=!0),r&&(u._scopeId="data-v-"+r),s?(l=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),o&&o.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(s)},u._ssrRegister=l):o&&(l=a?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(t,e){return l.call(e),c(t,e)}}else{var d=u.beforeCreate;u.beforeCreate=d?[].concat(d,l):[l]}return{exports:t,options:u}}var m=d(l,o,[],!1,null,null,null);m.options.__file="src/components/Resizer.vue";var p=m.exports;function b(t){return(b="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function y(t){switch(b(t)){case"number":return{type:"px",value:t};case"string":return function(e){if("auto"===e)return{type:e,value:0};var t=_.find(function(t){return t.regexp.test(e)});return t?{type:t.name,value:parseFloat(e)}:{type:"",value:e}}(t);default:return{type:"",value:t}}}function g(t){if("string"!=typeof t)return 0<=t;var e=y(t);return("%"===e.type||"px"===e.type)&&0=this.viewportHeight?Math.max(this.minHeight,this.viewportHeight)+"px":"auto"},containerClass:function(){return["vm--container",this.scrollable&&this.isAutoHeight&&"scrollable"]},modalClass:function(){return["vm--modal",this.classes]},stylesProp:function(){return"string"==typeof this.styles?a(this.styles):this.styles},modalStyle:function(){return[this.stylesProp,{top:this.position.top+"px",left:this.position.left+"px",width:this.trueModalWidth+"px",height:this.isAutoHeight?this.autoHeight:this.trueModalHeight+"px"}]},isComponentReadyToBeDestroyed:function(){return this.overlayTransitionState===A&&this.modalTransitionState===A}},watch:{isComponentReadyToBeDestroyed:function(t){t&&(this.visible=!1)}},methods:{startTransitionEnter:function(){this.visibility.overlay=!0,this.visibility.modal=!0},startTransitionLeave:function(){this.visibility.overlay=!1,this.visibility.modal=!1},beforeOverlayTransitionEnter:function(){this.overlayTransitionState=R},afterOverlayTransitionEnter:function(){this.overlayTransitionState=C},beforeOverlayTransitionLeave:function(){this.overlayTransitionState=H},afterOverlayTransitionLeave:function(){this.overlayTransitionState=A},beforeModalTransitionEnter:function(){var t=this;this.modalTransitionState=R,this.$nextTick(function(){t.resizeObserver.observe(t.$refs.modal)})},afterModalTransitionEnter:function(){this.modalTransitionState=C,this.draggable&&this.addDraggableListeners(),this.focusTrap&&this.$focusTrap.enable(this.$refs.modal);var t=this.createModalEvent({state:"opened"});this.$emit("opened",t)},beforeModalTransitionLeave:function(){this.modalTransitionState=H,this.resizeObserver.unobserve(this.$refs.modal),this.$focusTrap.enabled()&&this.$focusTrap.disable()},afterModalTransitionLeave:function(){this.modalTransitionState=A;var t=this.createModalEvent({state:"closed"});this.$emit("closed",t)},onToggle:function(t,e,i){if(this.name===t){var n=void 0===e?!this.visible:e;this.toggle(n,i)}},setInitialSize:function(){var t=y(this.width),e=y(this.height);this.modal.width=t.value,this.modal.widthType=t.type,this.modal.height=e.value,this.modal.heightType=e.type},onEscapeKeyUp:function(t){27===t.which&&this.visible&&this.$modal.hide(this.name)},onWindowResize:function(){this.viewportWidth=u(),this.viewportHeight=window.innerHeight,this.ensureShiftInWindowBounds()},createModalEvent:function(t){var e=0this.viewportWidth||t.clientX<0)&&!(t.clientY>this.viewportHeight||t.clientY<0)&&e){switch(this.targetClass){case"vue-modal-right":i-=e.offsetLeft,n=r;break;case"vue-modal-left":n=r,i=o+(this.initialX-t.clientX);break;case"vue-modal-top":i=o,n=r+(this.initialY-t.clientY);break;case"vue-modal-bottom":i=o,n-=e.offsetTop;break;case"vue-modal-bottomRight":i-=e.offsetLeft,n-=e.offsetTop;break;case"vue-modal-topRight":i-=e.offsetLeft,n=r+(this.initialY-t.clientY);break;case"vue-modal-bottomLeft":i=o+(this.initialX-t.clientX),n-=e.offsetTop;break;case"vue-modal-topLeft":i=o+(this.initialX-t.clientX),n=r+(this.initialY-t.clientY);break;default:console.error("Incorrrect/no resize direction.")}var s=Math.min(u(),this.maxWidth),a=Math.min(window.innerHeight,this.maxHeight);i=h(this.minWidth,s,i),n=h(this.minHeight,a,n),this.initialX=t.clientX,this.initialY=t.clientY,this.size={width:i,height:n};var l={width:i-o,height:n-r};e.style.width=i+"px",e.style.height=n+"px",this.$emit("resize",{element:e,size:this.size,direction:this.targetClass,dimGrowth:l})}}}},o,[],!1,function(t){var e=i(3);e.__inject__&&e.__inject__(t)},null,"3d775b36");d.options.__file="src/components/Resizer.vue";var m=d.exports;function p(t){return(p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function b(t){switch(p(t)){case"number":return{type:"px",value:t};case"string":return function(e){if("auto"===e)return{type:e,value:0};var t=_.find(function(t){return t.regexp.test(e)});return t?{type:t.name,value:parseFloat(e)}:{type:"",value:e}}(t);default:return{type:"",value:t}}}function y(t){if("string"!=typeof t)return 0<=t;var e=b(t);return("%"===e.type||"px"===e.type)&&0=this.viewportHeight?Math.max(this.minHeight,this.viewportHeight)+"px":"auto"},containerClass:function(){return["vm--container",this.scrollable&&this.isAutoHeight&&"scrollable"]},modalClass:function(){return["vm--modal",this.classes]},stylesProp:function(){return"string"==typeof this.styles?a(this.styles):this.styles},modalStyle:function(){return[this.stylesProp,{top:this.position.top+"px",left:this.position.left+"px",width:this.trueModalWidth+"px",height:this.isAutoHeight?this.autoHeight:this.trueModalHeight+"px"}]},isComponentReadyToBeDestroyed:function(){return this.overlayTransitionState===ot&&this.modalTransitionState===ot}},watch:{isComponentReadyToBeDestroyed:function(t){t&&(this.visible=!1)}},methods:{startTransitionEnter:function(){this.visibility.overlay=!0,this.visibility.modal=!0},startTransitionLeave:function(){this.visibility.overlay=!1,this.visibility.modal=!1},beforeOverlayTransitionEnter:function(){this.overlayTransitionState=nt},afterOverlayTransitionEnter:function(){this.overlayTransitionState=it},beforeOverlayTransitionLeave:function(){this.overlayTransitionState=rt},afterOverlayTransitionLeave:function(){this.overlayTransitionState=ot},beforeModalTransitionEnter:function(){var t=this;this.modalTransitionState=nt,this.$nextTick(function(){t.resizeObserver.observe(t.$refs.modal)})},afterModalTransitionEnter:function(){this.modalTransitionState=it,this.draggable&&this.addDraggableListeners(),this.focusTrap&&this.$focusTrap.enable(this.$refs.modal);var t=this.createModalEvent({state:"opened"});this.$emit("opened",t)},beforeModalTransitionLeave:function(){this.modalTransitionState=rt,this.resizeObserver.unobserve(this.$refs.modal),this.$focusTrap.enabled()&&this.$focusTrap.disable()},afterModalTransitionLeave:function(){this.modalTransitionState=ot;var t=this.createModalEvent({state:"closed"});this.$emit("closed",t)},onToggle:function(t,e,i){if(this.name===t){var n=void 0===e?!this.visible:e;this.toggle(n,i)}},setInitialSize:function(){var t=b(this.width),e=b(this.height);this.modal.width=t.value,this.modal.widthType=t.type,this.modal.height=e.value,this.modal.heightType=e.type},onEscapeKeyUp:function(t){27===t.which&&this.visible&&this.$modal.hide(this.name)},onWindowResize:function(){this.viewportWidth=u(),this.viewportHeight=window.innerHeight,this.ensureShiftInWindowBounds()},createModalEvent:function(t){var e=0 40 | 43 | Hello, {{ name }}! 44 | 45 | 46 | 68 | ``` 69 | 70 | Dynamic modal: 71 | 72 | 73 | ```html 74 | 110 | 111 | ``` -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Todo -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ```bash 4 | npm install vue-js-modal --save 5 | ``` 6 | 7 | ```bash 8 | yarn add vue-js-modal 9 | ``` 10 | 11 | ## Client 12 | 13 | Import plugin in your main file: 14 | 15 | ```js 16 | import VModal from 'vue-js-modal' 17 | 18 | OR 19 | 20 | import VModal from 'vue-js-modal/dist/index.nocss.js' 21 | import 'vue-js-modal/dist/styles.css' 22 | ``` 23 | 24 | ```js 25 | Vue.use(VModal) 26 | ``` 27 | 28 | ## SSR 29 | 30 | To use this plugin with Nuxt.js you need to create a plugin file and reference it in the `nuxt.config.js` file. 31 | 32 | ```js 33 | // nuxt.config.js 34 | export default { 35 | ... 36 | /* 37 | ** Plugins to load before mounting the App 38 | */ 39 | plugins: [ 40 | '~plugins/vue-js-modal.js' 41 | ], 42 | } 43 | ``` 44 | 45 | ```js 46 | // plugins/vue-js-modal.js 47 | import Vue from 'vue' 48 | import VModal from 'vue-js-modal/dist/ssr.nocss' 49 | 50 | import 'vue-js-modal/dist/styles.css' 51 | 52 | Vue.use(VModal, { ... }) 53 | 54 | export default function(_, inject) { 55 | inject('modal', Vue.prototype.$modal) 56 | } 57 | ``` 58 | 59 | ::: tip Extracted CSS 60 | The `/dist` directory contains a version of the build with extracted CSS files. This is useful for SSR but also can be used with the purely client-side implementation when you need more flexibility in controlling your stylesheets. 61 | 62 | - `ssr.index.js` - SSR build with inline CSS 63 | - `ssr.nocss.js` - SSR build without inline CSS 64 | - `index.nocss.js` - Client build without inline CSS 65 | - `styles.css` - Stylesheet 66 | 67 | ::: 68 | -------------------------------------------------------------------------------- /docs/Intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The library supports 2 types of modals - static and dynamic. 4 | 5 | - **Static** are defined explicitly though the template. 6 | - **Dynamic** are generated based on the configuration passed into the "show modal" function. 7 | 8 | ## Static modals 9 | 10 | Modals are defined by simply using the `` component. To control it's visibility - use `$modal.show` / `$modal.hide` functions, for example: 11 | 12 | ```html{2,4,12,15} 13 | 18 | 19 | 35 | ``` 36 | 37 | ## Dynamic modals 38 | 39 | In order to instantiate modals at runtime (for lazy-loading or de-cluttering templates), it is possible to create modals dynamically. 40 | 41 | To show dynamic modal you can use the same `$modal.show` function with extended API: 42 | 43 | ```js 44 | this.$modal.show( 45 | component, 46 | component_properties, 47 | modal_properties, 48 | modal_events 49 | ) 50 | ``` 51 | 52 | - `component` - inline or imported Vue component definition 53 | - `component_properties` - any properties that are used within the `component_definition` 54 | - `modal_properties` -modal component properties (see Properties section) 55 | - `modal_events` - modal event handlers (see Events section) 56 | 57 | Using **imported** component definition: 58 | 59 | ```js 60 | import MyComponent from './MyComponent.vue' 61 | 62 | ... 63 | 64 | this.$modal.show( 65 | MyComponent, 66 | { text: 'This text is passed as a property' }, 67 | { draggable: true } 68 | ) 69 | ``` 70 | 71 | --- 72 | 73 | Using **inline** component definition: 74 | 75 | ```js 76 | this.$modal.show( 77 | { 78 | template: ` 79 |
80 |

This is created inline

81 |

{{ text }}

82 |
83 | `, 84 | props: ['text'] 85 | }, 86 | { text: 'This text is passed as a property' }, 87 | { height: 'auto' }, 88 | { 'before-close': event => {} } 89 | ) 90 | ``` 91 | 92 | Other than defining the `name` modal parameter, it's also possible to close dynamic modals by emitting `'close'` event: 93 | 94 | ```js{5} 95 | this.$modal.show({ 96 | template: ` 97 |
98 |

Close using this button:

99 | 100 |
101 | ` 102 | }) 103 | ``` 104 | 105 | It is possible to set default property values for dynamic modals: 106 | 107 | ```js{4-8} 108 | import VModal from 'vue-js-modal' 109 | 110 | Vue.use(VModal, { 111 | dynamicDefaults: { 112 | draggable: true, 113 | resizable: true, 114 | height: 'auto' 115 | } 116 | }) 117 | ``` 118 | 119 | ## Dialogs 120 | 121 | Dialog is a simplified version of the modal which has most parameters set by default and is useful for quick prototyping, alerts, etc. Dialog is merely an example of how easy it is to create modals that fit your project needs. Nevertheless, you can use it if you set `dialog: true` in plugin configuration: 122 | 123 | ```js 124 | Vue.use(VModal, { dialog: true }) 125 | ``` 126 | 127 | And include this component in your project: 128 | 129 | ```html 130 | 131 | ``` 132 | 133 | To show modal follow this example (all params except of “text” are optional): 134 | 135 | ```js 136 | this.$modal.show('dialog', { 137 | title: 'The standard Lorem Ipsum passage', 138 | text: 'Lorem ipsum dolor sit amet, ...', 139 | buttons: [ 140 | { 141 | title: 'Cancel', 142 | handler: () => { 143 | this.$modal.hide('dialog') 144 | } 145 | }, 146 | { 147 | title: 'Like', 148 | handler: () => { 149 | alert('Like action') 150 | } 151 | }, 152 | { 153 | title: 'Repost', 154 | handler: () => { 155 | alert('Repost action') 156 | } 157 | } 158 | ] 159 | }) 160 | ``` 161 | Note that the name should be `'dialog'`! 162 | 163 | ![](https://user-images.githubusercontent.com/1577802/85934434-0dac5300-b8da-11ea-9db5-34fa5a0b7fe0.png) 164 | -------------------------------------------------------------------------------- /docs/Other.md: -------------------------------------------------------------------------------- 1 | # Other 2 | 3 | ## Sponsorship 4 | 5 | Please consider sponsoring this project through [Github Sponsorship](https://github.com/sponsors/euvl) :pray: 6 | 7 | ## Development 8 | 9 | ```bash 10 | # Clone repo 11 | git clone https://github.com/euvl/vue-js-modal.git 12 | 13 | # Run unit tests 14 | npm run unit 15 | 16 | # Run linter 17 | npm run lint 18 | 19 | # Build main library for client & SSR 20 | cd vue-js-modal 21 | npm install 22 | npm run build 23 | 24 | # Build and run demo 25 | cd demo 26 | npm install 27 | npm run dev 28 | ``` 29 | 30 | ## Check out 31 | 32 | Check out my other projects: 33 | 34 | * https://github.com/euvl/vue-notification 35 | * https://github.com/euvl/vue-js-toggle-button 36 | * https://github.com/euvl/vue-js-popover 37 | * https://github.com/euvl/v-clipboard 38 | 39 | ## IE support 40 | 41 | This plugin uses arrow functions, resize observer and other modern features. 42 | 43 | To be able to use this plugin in IE you need to make sure you transpile the code properly. Please read this [stackoverflow](https://stackoverflow.com/questions/56446904/transpiling-es6-for-ie11-with-babel). 44 | -------------------------------------------------------------------------------- /docs/Properties.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # Properties 6 | 7 | ## Properties 8 | 9 | #### `name: String` **required** 10 | 11 | Name of the modal, it is required property. 12 | 13 | --- 14 | 15 | #### `resizable: Boolean` 16 | 17 | Enables resizing of the modal. 18 | 19 | --- 20 | 21 | #### `resizeEdges: Array` `default: ['r', 'br', 'b', 'bl', 'l', 'tl', 't', 'tr']` 22 | 23 | Can contain an array with the edges on which you want the modal to be able to resize on. 24 | | string | corner | 25 | | ------- | ------------- | 26 | | r | right | 27 | | br | bottom right | 28 | | b | bottom | 29 | | bl | bottom left | 30 | | l | left | 31 | | t | top left | 32 | | t | top | 33 | | tr | top right | 34 | 35 | --- 36 | 37 | #### `resizeIndicator: Boolean` `default: true` 38 | 39 | Enables the resize triangle at the bottom right of a modal when Resizable is enabled. 40 | 41 | --- 42 | 43 | #### `centerResize: Boolean` `default: true` 44 | 45 | Enables automatic centering of the modal when resizing, if disabled modals will resize and remain in a fixed position similar to how Windows applications are resized. 46 | 47 | --- 48 | 49 | #### `adaptive: Boolean` 50 | 51 | Enable responsive behavior, modal will try to adapt to the screen size when possible. Properties `maxHeight`, `maxWidth`, `minHeight`, `minWidth` can set the boundaries for the automatic resizing. 52 | 53 | --- 54 | 55 | #### `draggable: Boolean | String` 56 | 57 | Allows dragging the modal within the boundaries of the screen. 58 | 59 | Draggable property can accept string parameter - a CSS selector to **an element which will be used as a "handler" for dragging**. 60 | 61 | ```html 62 | 63 |
DRAG ME HERE
64 |
65 | Example 66 |
67 |
68 | ``` 69 | 70 | --- 71 | 72 | #### `scrollable: Boolean` 73 | 74 | Enables scroll within the modal when the height of the modal is greater than the screen. 75 | 76 | ::: warning Note 77 | This feature only works when `height` is set to `"auto"` 78 | ::: 79 | 80 | ::: details Show me some gifs 81 | Auto height 82 | 83 |

84 | 85 |

86 | 87 | Scrollable content & auto height 88 | 89 |

90 | 91 |

92 | 93 | ::: 94 | 95 | --- 96 | 97 | #### `focusTrap: Boolean` 98 | 99 | Enables focus trap meaning that only inputs/buttons that are withing the modal window can be focused by pressing Tab (plugin uses very naive implementation of the focus trap) 100 | 101 | --- 102 | 103 | #### `reset: Boolean` 104 | 105 | Resets position and size before showing 106 | 107 | --- 108 | 109 | #### `clickToClose: Boolean` `default: true` 110 | 111 | If set to `false`, it will not be possible to close modal by clicking on the background or by pressing Esc key. 112 | 113 | --- 114 | 115 | #### `transition: String` 116 | 117 | CSS transition applied to the modal window. 118 | 119 | --- 120 | 121 | #### `overlayTransition: String` 122 | 123 | CSS transition applied to the overlay (background). 124 | 125 | --- 126 | 127 | #### `classes: String | Array` 128 | 129 | List of class that will be applied to the modal window (not overlay, just the box). 130 | 131 | --- 132 | 133 | #### `styles: String | Array | Object` 134 | 135 | Style that will be applied to the modal window. 136 | 137 | ::: warning Note 138 | To be able to support string definition of styles there are some hacks in place. 139 | 140 | Vue.js does not allow merging string css definition with an object/array style definition. There are very few cases where you might need to use this property, but if you do - write tests :) 141 | ::: 142 | 143 | --- 144 | 145 | #### `width: String | Number` `default: 600` 146 | 147 | Width in pixels or percents (50, "50px", "50%"). 148 | 149 | Supported string values are `%` and `px` 150 | 151 | ::: warning Note 152 | This is not CSS size value, it does not support `em`, `pem`, etc. Plugin requires pixels to recalculate position and size for draggable, resaziable modal. 153 | If you need to use more value types, please consider contributing to the parser [here](https://github.com/euvl/vue-js-modal/blob/master/src/utils/parser.js). 154 | ::: 155 | 156 | --- 157 | 158 | #### `height: String | Number` `default: 300` 159 | 160 | Height in pixels or percents (50, "50px", "50%") or `"auto"`. 161 | 162 | Supported string values are `%`, `px` and `auto`. Setting height to `"auto"` makes it automatically change the height when the content size changes (this works well with `scrollable` feature). 163 | 164 | ::: warning Note 165 | This is not CSS size value, it does not support `em`, `pem`, etc. Plugin requires pixels to recalculate position and size for draggable, resaziable modal. 166 | If you need to use more value types, please consider contributing to the parser [here](https://github.com/euvl/vue-js-modal/blob/master/src/utils/parser.js). 167 | ::: 168 | 169 | --- 170 | 171 | #### `minWidth: Number (pixels)` `default: 0` 172 | 173 | The minimum width to which modal can be resized. 174 | 175 | --- 176 | 177 | #### `minHeight: Number (pixels)` `default: 0` 178 | 179 | The minimum height to which modal can be resized. 180 | 181 | --- 182 | 183 | #### `maxWidth: Number (pixels)` `default: Infinity` 184 | 185 | The maximum width of the modal (if the value is greater than window width, window width will be used instead. 186 | 187 | --- 188 | 189 | #### `maxHeight: Number (pixels)` `default: Infinity` 190 | 191 | The maximum height of the modal (if the value is greater than window height, window height will be used instead. 192 | 193 | --- 194 | 195 | #### `shiftX: Number (between 0 and 1.0)` `default: 0.5` 196 | 197 | Horizontal position in `%`, default is `0.5` (meaning that modal box will be in the middle (50% from left) of the window 198 | 199 | --- 200 | 201 | #### `shiftY: Number (between 0 and 1.0)` `default: 0.5` 202 | 203 | Vertical position in `%`, default is `0.5` (meaning that modal box will be in the middle (50% from top) of the window. 204 | 205 | --- 206 | 207 | ## Example 208 | 209 | ```html 210 | 218 | 223 | ``` 224 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Installation.md -------------------------------------------------------------------------------- /docs/Slots.md: -------------------------------------------------------------------------------- 1 | # Slots 2 | 3 | ## Close button 4 | 5 | If you want to have a Close (x) button in the top-right corner, you can use "top-right" slot for it. There is no pre-defined Close button style - you will have to implement your own button. 6 | 7 | ```html{3-7} 8 | 18 | ``` -------------------------------------------------------------------------------- /docs/Usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # Usage 6 | 7 | ## Configuration 8 | 9 | Configuration options can be passed as a second argument to `Vue.use`. 10 | 11 | ```js 12 | import VModal from 'vue-js-modal' 13 | 14 | Vue.use(VModal, { ... }) 15 | ``` 16 | 17 | #### `dialog: Boolean` 18 | 19 | Enables [dialogs](Intro#dialogs). 20 | 21 | 22 | --- 23 | 24 | 25 | #### `componentName: String` 26 | 27 | Changes component name from "Modal" to any other string value. It is useful when there is already a global "modal" component. 28 | 29 | ```js 30 | Vue.use(VModal, { componentName: 'Foo' }) 31 | ``` 32 | ```html 33 | This is a modal 34 | ``` 35 | 36 | 37 | --- 38 | 39 | 40 | #### `dynamicDefaults: object` 41 | 42 | Default properties that are injected into dynamic modals. 43 | 44 | ```js 45 | Vue.use(VModal, { dynamicDefaults: { draggable: true, resizable: true } }) 46 | ``` 47 | 48 | ## API 49 | 50 | Plugin API can be called within any component through `this.$modal`: 51 | 52 | 53 | --- 54 | 55 | 56 | #### `$modal.show(name, params)` 57 | 58 | **Arguments:** 59 | 60 | `name: string` - Name of the modal 61 | 62 | `params?: object` - Any data that you would want to pass into the modal (`@before-open` event handler will contain `params` in the event) 63 | 64 | **Description:** 65 | 66 | Shows a static modal. Modal component can be defined anywhere within the same or any ancestor component. 67 | 68 | ```html 69 | 72 | 73 | 81 | ``` 82 | 83 | #### `$modal.show(component, componentProps, modalProps, modalEvents)` 84 | 85 | used to show a dynamic modal at runtime 86 | 87 | #### `$modal.hide(name)` 88 | 89 | hide the modal with the given `name` property 90 | 91 | #### `$modal.hideAll()` 92 | 93 | hide all modals in the application 94 | -------------------------------------------------------------------------------- /examples/nuxt/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint', 9 | }, 10 | extends: [ 11 | '@nuxtjs', 12 | 'prettier', 13 | 'prettier/vue', 14 | 'plugin:prettier/recommended', 15 | 'plugin:nuxt/recommended', 16 | ], 17 | plugins: ['prettier'], 18 | rules: {}, 19 | } 20 | -------------------------------------------------------------------------------- /examples/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /examples/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # nuxt 2 | 3 | ## Build Setup 4 | 5 | ```bash 6 | # install dependencies 7 | $ yarn install 8 | 9 | # serve with hot reload at localhost:3000 10 | $ yarn dev 11 | 12 | # build for production and launch server 13 | $ yarn build 14 | $ yarn start 15 | 16 | # generate static project 17 | $ yarn generate 18 | ``` 19 | 20 | For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 21 | -------------------------------------------------------------------------------- /examples/nuxt/components/ExampleModal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/nuxt/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /examples/nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 57 | -------------------------------------------------------------------------------- /examples/nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /* 3 | ** Nuxt rendering mode 4 | ** See https://nuxtjs.org/api/configuration-mode 5 | */ 6 | mode: 'universal', 7 | /* 8 | ** Nuxt target 9 | ** See https://nuxtjs.org/api/configuration-target 10 | */ 11 | target: 'server', 12 | /* 13 | ** Headers of the page 14 | ** See https://nuxtjs.org/api/configuration-head 15 | */ 16 | head: { 17 | title: process.env.npm_package_name || '', 18 | meta: [ 19 | { charset: 'utf-8' }, 20 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 21 | { 22 | hid: 'description', 23 | name: 'description', 24 | content: process.env.npm_package_description || '', 25 | }, 26 | ], 27 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 28 | }, 29 | /* 30 | ** Global CSS 31 | */ 32 | css: [], 33 | /* 34 | ** Plugins to load before mounting the App 35 | ** https://nuxtjs.org/guide/plugins 36 | */ 37 | plugins: ['~/plugins/vue-js-modal.js'], 38 | /* 39 | ** Auto import components 40 | ** See https://nuxtjs.org/api/configuration-components 41 | */ 42 | components: true, 43 | /* 44 | ** Nuxt.js dev-modules 45 | */ 46 | buildModules: [ 47 | // Doc: https://github.com/nuxt-community/eslint-module 48 | '@nuxtjs/eslint-module', 49 | ], 50 | /* 51 | ** Nuxt.js modules 52 | */ 53 | modules: [], 54 | /* 55 | ** Build configuration 56 | ** See https://nuxtjs.org/api/configuration-build/ 57 | */ 58 | build: {}, 59 | } 60 | -------------------------------------------------------------------------------- /examples/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "export": "nuxt export", 10 | "serve": "nuxt serve", 11 | "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .", 12 | "lint": "yarn lint:js" 13 | }, 14 | "dependencies": { 15 | "nuxt": "^2.13.0" 16 | }, 17 | "devDependencies": { 18 | "@nuxtjs/eslint-config": "^3.0.0", 19 | "@nuxtjs/eslint-module": "^2.0.0", 20 | "babel-eslint": "^10.1.0", 21 | "eslint": "^7.2.0", 22 | "eslint-config-prettier": "^6.11.0", 23 | "eslint-plugin-nuxt": "^1.0.0", 24 | "eslint-plugin-prettier": "^3.1.4", 25 | "prettier": "^2.0.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | 63 | -------------------------------------------------------------------------------- /examples/nuxt/plugins/vue-js-modal.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Modal from '../../../dist/ssr.nocss' 3 | 4 | import '../../../dist/styles.css' 5 | 6 | Vue.use(Modal) 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "vue" 5 | ], 6 | "moduleNameMapper": { 7 | "^@/(.*)$": "/src/$1" 8 | }, 9 | "transform": { 10 | "^.+\\.js$": "/node_modules/babel-jest", 11 | ".*\\.(vue)$": "/node_modules/vue-jest" 12 | }, 13 | "snapshotSerializers": [ 14 | "/node_modules/jest-serializer-vue" 15 | ] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-js-modal", 3 | "description": "Modal Component for Vue.js", 4 | "version": "2.0.1", 5 | "author": "euvl ", 6 | "main": "dist/index.js", 7 | "publishConfig": { 8 | "registry": "https://registry.npmjs.org" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/euvl/vue-js-modal.git" 13 | }, 14 | "types": "types/index.d.ts", 15 | "keywords": [ 16 | "front-end", 17 | "web", 18 | "vue", 19 | "vuejs", 20 | "vue-js", 21 | "dialog", 22 | "alert", 23 | "modal", 24 | "vue-js-modal", 25 | "vue-modal" 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/euvl/vue-js-modal/issues" 29 | }, 30 | "scripts": { 31 | "build:client": "webpack --config ./build/webpack.client.config.js --progress --hide-modules", 32 | "build:client-no-css": "webpack --config ./build/webpack.client-no-css.config.js --progress --hide-modules", 33 | "build:ssr": "webpack --config ./build/webpack.ssr.config.js --progress --hide-modules", 34 | "build:ssr-no-css": "webpack --config ./build/webpack.ssr-no-css.config.js --progress --hide-modules", 35 | "lint:js": "eslint --ext js --ext jsx --ext vue src", 36 | "lint:js:fix": "npm run lint:js -- --fix", 37 | "test": "jest", 38 | "build": "npm run build:client && npm run build:client-no-css && npm run build:ssr && npm run build:ssr-no-css", 39 | "watch": "webpack --config ./build/webpack.client.config.js --progress --hide-modules --watch", 40 | "test:types": "tsc -p types/test", 41 | "docs:dev": "vuepress dev docs", 42 | "docs:build": "vuepress build docs" 43 | }, 44 | "license": "MIT", 45 | "browserslist": "> 0.25%, not dead", 46 | "devDependencies": { 47 | "@babel/core": "^7.2.0", 48 | "@babel/preset-env": "^7.2.0", 49 | "@babel/plugin-proposal-optional-chaining": "^7.10.3", 50 | "@vue/eslint-config-prettier": "^6.0.0", 51 | "@vue/test-utils": "1.0.0-beta.31", 52 | "babel-jest": "^23.6.0", 53 | "babel-loader": "^8.0.4", 54 | "css-loader": "^1.0.1", 55 | "eslint": "^6.7.2", 56 | "eslint-plugin-babel": "^5.3.0", 57 | "eslint-plugin-prettier": "^3.1.1", 58 | "eslint-plugin-vue": "^6.2.2", 59 | "file-loader": "^0.9.0", 60 | "jest": "^23.6.0", 61 | "jest-serializer-vue": "^2.0.2", 62 | "mini-css-extract-plugin": "^0.4.5", 63 | "node-sass": "^4.13.1", 64 | "optimize-css-assets-webpack-plugin": "^5.0.1", 65 | "sass-loader": "^7.1.0", 66 | "ts-loader": "^6.2.2", 67 | "uglifyjs-webpack-plugin": "^2.2.0", 68 | "vue": "2.6.11", 69 | "vue-hot-reload-api": "^2.0.8", 70 | "vue-jest": "^3.0.1", 71 | "vue-loader": "^15.0.0", 72 | "vue-style-loader": "^4.1.2", 73 | "vue-template-compiler": "2.6.11", 74 | "vue-test-utils": "^1.0.0-beta.11", 75 | "vuepress": "^1.5.2", 76 | "webpack": "^4.42.1", 77 | "webpack-cli": "^3.3.11", 78 | "webpack-merge": "^4.2.2" 79 | }, 80 | "peerDependencies": { 81 | "vue": "^2.6.11" 82 | }, 83 | "dependencies": { 84 | "resize-observer-polyfill": "^1.5.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Plugin.js: -------------------------------------------------------------------------------- 1 | import Modal from './components/Modal.vue' 2 | import Dialog from './components/Dialog.vue' 3 | import PluginCore from './PluginCore' 4 | 5 | const Plugin = { 6 | install(Vue, options = {}) { 7 | if (Vue.prototype.$modal) { 8 | return 9 | } 10 | 11 | const plugin = new PluginCore(Vue, options) 12 | 13 | Object.defineProperty(Vue.prototype, '$modal', { 14 | get: function() { 15 | /** 16 | * The "this" scope is the scope of the component that calls this.$modal 17 | */ 18 | const caller = this 19 | /** 20 | * The this.$modal can be called only from inside the vue components so this check is not really needed... 21 | */ 22 | if (caller instanceof Vue) { 23 | const root = caller.$root 24 | 25 | if (!plugin.context.root) { 26 | plugin.setDynamicModalContainer(root) 27 | } 28 | } 29 | 30 | return plugin 31 | } 32 | }) 33 | 34 | /** 35 | * Sets custom component name (if provided) 36 | */ 37 | Vue.component(plugin.context.componentName, Modal) 38 | 39 | /** 40 | * Registration of component 41 | */ 42 | if (options.dialog) { 43 | const componentName = options.dialogComponentName || 'VDialog'; 44 | Vue.component(componentName, Dialog); 45 | } 46 | } 47 | } 48 | 49 | export default Plugin 50 | -------------------------------------------------------------------------------- /src/PluginCore.js: -------------------------------------------------------------------------------- 1 | import { UNSUPPORTED_ARGUMENT_ERROR } from './utils/errors' 2 | import { createDivInBody } from './utils' 3 | import ModalsContainer from './components/ModalsContainer.vue' 4 | 5 | const PluginCore = (Vue, options = {}) => { 6 | const subscription = new Vue() 7 | 8 | const context = { 9 | root: null, 10 | componentName: options.componentName || 'Modal' 11 | } 12 | 13 | const showStaticModal = (name, params) => { 14 | subscription.$emit('toggle', name, true, params) 15 | } 16 | 17 | const showDynamicModal = ( 18 | component, 19 | componentProps, 20 | componentSlots, 21 | modalProps = {}, 22 | modalEvents 23 | ) => { 24 | const container = context.root?.__modalContainer 25 | const defaults = options.dynamicDefaults || {} 26 | 27 | container?.add( 28 | component, 29 | componentProps, 30 | componentSlots, 31 | { ...defaults, ...modalProps }, 32 | modalEvents 33 | ) 34 | } 35 | 36 | /** 37 | * Creates a container for modals in the root Vue component. 38 | * 39 | * @param {Vue} parent 40 | */ 41 | const setDynamicModalContainer = parent => { 42 | context.root = parent 43 | 44 | const element = createDivInBody() 45 | 46 | new Vue({ 47 | parent, 48 | render: h => h(ModalsContainer) 49 | }).$mount(element) 50 | } 51 | 52 | const show = (...args) => { 53 | const [modal] = args 54 | 55 | switch (typeof modal) { 56 | case 'string': 57 | showStaticModal(...args) 58 | break 59 | 60 | case 'object': 61 | case 'function': 62 | showDynamicModal(...args) 63 | break 64 | 65 | default: 66 | console.warn(UNSUPPORTED_ARGUMENT_ERROR, modal) 67 | } 68 | } 69 | 70 | const hide = (name, params) => { 71 | subscription.$emit('toggle', name, false, params) 72 | } 73 | 74 | const hideAll = () => { 75 | subscription.$emit('hide-all') 76 | } 77 | 78 | const toggle = (name, params) => { 79 | subscription.$emit('toggle', name, undefined, params) 80 | } 81 | 82 | return { 83 | context, 84 | subscription, 85 | show, 86 | hide, 87 | hideAll, 88 | toggle, 89 | setDynamicModalContainer 90 | } 91 | } 92 | 93 | export default PluginCore 94 | -------------------------------------------------------------------------------- /src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 100 | 159 | -------------------------------------------------------------------------------- /src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 55 | 842 | 843 | 930 | -------------------------------------------------------------------------------- /src/components/ModalsContainer.vue: -------------------------------------------------------------------------------- 1 | 23 | 83 | -------------------------------------------------------------------------------- /src/components/Resizer.vue: -------------------------------------------------------------------------------- 1 | 13 | 186 | 279 | -------------------------------------------------------------------------------- /src/components/VNode.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Plugin from './Plugin' 2 | 3 | export { default as Modal } from './components/Modal.vue' 4 | export { default as Dialog } from './components/Dialog.vue' 5 | // export { default as ModalsContainer } from './components/ModalsContainer.vue' 6 | 7 | export const version = '__VERSION__' 8 | 9 | // Install by default if using the script tag 10 | // if (typeof window !== 'undefined' && window.Vue) { 11 | // window.Vue.use(install) 12 | // } 13 | 14 | export default Plugin 15 | -------------------------------------------------------------------------------- /src/utils/errors.js: -------------------------------------------------------------------------------- 1 | export const DYNAMIC_MODAL_DISABLED_ERROR = 2 | '[vue-js-modal] ' + 3 | '$modal() received object as a first argument, but dynamic modals are ' + 4 | 'switched off. https://github.com/euvl/vue-js-modal/#dynamic-modals' 5 | 6 | export const UNSUPPORTED_ARGUMENT_ERROR = 7 | '[vue-js-modal] ' + 8 | '$modal() received an unsupported argument as a first argument.' 9 | 10 | export const HIDE_ALL_RESTRICTION_ERROR = 11 | '[vue-js-modal] ' + 12 | '$modal.hideAll() call will be ignored because dynamic modals are not enabled.' 13 | -------------------------------------------------------------------------------- /src/utils/focusTrap.js: -------------------------------------------------------------------------------- 1 | const FOCUSABLE_ELEMENTS_QUERY = 2 | 'button:not([disabled]), ' + 3 | 'select:not([disabled]), ' + 4 | 'a[href]:not([disabled]), ' + 5 | 'area[href]:not([disabled]), ' + 6 | '[contentEditable=""]:not([disabled]), ' + 7 | '[contentEditable="true"]:not([disabled]), ' + 8 | '[contentEditable="TRUE"]:not([disabled]), ' + 9 | 'textarea:not([disabled]), ' + 10 | 'iframe:not([disabled]), ' + 11 | 'input:not([disabled]), ' + 12 | 'summary:not([disabled]), ' + 13 | '[tabindex]:not([tabindex="-1"])' 14 | 15 | const isTabPressed = event => { 16 | return event.key === 'Tab' || event.keyCode === 9 17 | } 18 | 19 | const querySelectorAll = (element, selector) => { 20 | return [...(element.querySelectorAll(selector) || [])] 21 | } 22 | 23 | const queryFocusableElements = element => { 24 | return querySelectorAll(element, FOCUSABLE_ELEMENTS_QUERY) 25 | } 26 | 27 | const isFocused = element => { 28 | return element == document.activeElement 29 | } 30 | 31 | const isNothingFocused = () => { 32 | return !document.activeElement 33 | } 34 | 35 | class FocusTrap { 36 | constructor() { 37 | this.root = null 38 | this.elements = [] 39 | 40 | this.onKeyDown = this.onKeyDown.bind(this) 41 | this.enable = this.enable.bind(this) 42 | this.disable = this.disable.bind(this) 43 | this.firstElement = this.firstElement.bind(this) 44 | this.lastElement = this.lastElement.bind(this) 45 | } 46 | 47 | lastElement() { 48 | return this.elements[this.elements.length - 1] || null 49 | } 50 | 51 | firstElement() { 52 | return this.elements[0] || null 53 | } 54 | 55 | onKeyDown(event) { 56 | if (!isTabPressed(event)) { 57 | return 58 | } 59 | 60 | // SHIFT + TAB 61 | if (event.shiftKey && isFocused(this.firstElement())) { 62 | this.lastElement().focus() 63 | event.preventDefault() 64 | return 65 | } 66 | 67 | // TAB 68 | if (isNothingFocused() || (!event.shiftKey && isFocused(this.lastElement()))) { 69 | this.firstElement().focus() 70 | event.preventDefault() 71 | return 72 | } 73 | } 74 | 75 | enabled() { 76 | return !!this.root 77 | } 78 | 79 | enable(root) { 80 | if (!root) { 81 | return 82 | } 83 | 84 | this.root = root 85 | this.elements = queryFocusableElements(this.root) 86 | 87 | const firstElement = this.firstElement() 88 | 89 | if (firstElement) { 90 | firstElement.focus() 91 | } 92 | 93 | this.root.addEventListener('keydown', this.onKeyDown) 94 | } 95 | 96 | disable() { 97 | this.root.removeEventListener('keydown', this.onKeyDown) 98 | this.root = null 99 | } 100 | } 101 | 102 | export default FocusTrap 103 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './numbers' 2 | 3 | const INPUT_NODE_NAMES = ['INPUT', 'TEXTAREA', 'SELECT'] 4 | 5 | export const generateId = ((index = 0) => () => (index++).toString())() 6 | /** 7 | * @param {Number} from Lower limit 8 | * @param {Number} to Upper limit 9 | * @param {Number} value Checked number value 10 | * 11 | * @return {Number} Either source value itself or limit value if range limits 12 | * are exceeded 13 | */ 14 | 15 | export const createDivInBody = () => { 16 | const div = document.createElement('div') 17 | document.body.appendChild(div) 18 | 19 | return div 20 | } 21 | 22 | export const blurActiveElement = () => { 23 | if ( 24 | typeof document !== 'undefined' && 25 | document.activeElement && 26 | document.activeElement.tagName !== 'BODY' && 27 | document.activeElement.blur 28 | ) { 29 | document.activeElement.blur() 30 | } 31 | } 32 | // Different browsers handle innerWidth/clientWidth differently, 33 | // this function tries to return the smallest width (assuming that it excludes 34 | // scrollbar width) 35 | export const windowWidthWithoutScrollbar = () => { 36 | const { innerWidth } = window 37 | const { clientWidth } = document.documentElement 38 | 39 | if (innerWidth && clientWidth) { 40 | return Math.min(innerWidth, clientWidth) 41 | } 42 | 43 | return clientWidth || innerWidth 44 | } 45 | 46 | export const stringStylesToObject = styles => { 47 | const lines = styles 48 | .split(';') 49 | .map(line => line.trim()) 50 | .filter(Boolean) 51 | 52 | const entries = lines.map(line => line.split(':')) 53 | 54 | return entries.reduce((styles, [key, value]) => { 55 | return { 56 | ...styles, 57 | [key]: value 58 | } 59 | }, {}) 60 | } 61 | 62 | export const isInput = element => { 63 | return element && INPUT_NODE_NAMES.indexOf(element.nodeName) !== -1 64 | } 65 | 66 | export const getTouchEvent = event => { 67 | return event.touches && event.touches.length > 0 ? event.touches[0] : event 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/numbers.js: -------------------------------------------------------------------------------- 1 | export const inRange = (from, to, value) => { 2 | return value < from ? from : value > to ? to : value 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/parser.js: -------------------------------------------------------------------------------- 1 | const floatRegexp = '[-+]?[0-9]*.?[0-9]+' 2 | 3 | const types = [ 4 | { 5 | name: 'px', 6 | regexp: new RegExp(`^${floatRegexp}px$`) 7 | }, 8 | { 9 | name: '%', 10 | regexp: new RegExp(`^${floatRegexp}%$`) 11 | }, 12 | /** 13 | * Fallback option 14 | * If no suffix specified, assigning "px" 15 | */ 16 | { 17 | name: 'px', 18 | regexp: new RegExp(`^${floatRegexp}$`) 19 | } 20 | ] 21 | 22 | const getType = value => { 23 | if (value === 'auto') { 24 | return { 25 | type: value, 26 | value: 0 27 | } 28 | } 29 | 30 | const type = types.find(type => type.regexp.test(value)) 31 | 32 | if (type) { 33 | return { 34 | type: type.name, 35 | value: parseFloat(value) 36 | } 37 | } 38 | 39 | return { 40 | type: '', 41 | value: value 42 | } 43 | } 44 | 45 | export const parseNumber = value => { 46 | switch (typeof value) { 47 | case 'number': 48 | return { type: 'px', value } 49 | case 'string': 50 | return getType(value) 51 | default: 52 | return { type: '', value } 53 | } 54 | } 55 | 56 | export const validateNumber = value => { 57 | if (typeof value === 'string') { 58 | let num = parseNumber(value) 59 | 60 | return (num.type === '%' || num.type === 'px') && num.value > 0 61 | } 62 | 63 | return value >= 0 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/parser.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var { parseNumber } = require('./parser') 3 | 4 | describe('parser.js', () => { 5 | describe('#parse', () => { 6 | describe('Correct types', () => { 7 | it('Should parse numbers', () => { 8 | let object = parseNumber(10) 9 | 10 | expect(object.value).to.be.a('number') 11 | expect(object.type).to.be.a('string') 12 | 13 | expect(object.value).to.equal(10) 14 | expect(object.type).to.equal('px') 15 | }) 16 | 17 | it('Should parse strings', () => { 18 | let object = parseNumber('10') 19 | 20 | expect(object.value).to.be.a('number') 21 | expect(object.type).to.be.a('string') 22 | 23 | expect(object.value).to.equal(10) 24 | expect(object.type).to.equal('px') 25 | }) 26 | 27 | it ('Should parse "auto" string, auto => {type: "auto", value: 0}', () => { 28 | let object = parseNumber('auto') 29 | 30 | expect(object.value).to.equal(0) 31 | expect(object.type).to.equal('auto') 32 | }) 33 | 34 | it ('Should parse wrong types', () => { 35 | let nullValue = parseNumber(null) 36 | let booleanValue = parseNumber(false) 37 | 38 | expect(nullValue.value).to.equal(null) 39 | expect(nullValue.type).to.equal('') 40 | 41 | expect(booleanValue.value).to.equal(false) 42 | expect(booleanValue.type).to.equal('') 43 | }) 44 | }) 45 | 46 | describe('Parsing suffixed string', () => { 47 | it ('Should parse "px"', () => { 48 | let object = parseNumber('10px') 49 | 50 | expect(object.value).to.equal(10) 51 | expect(object.type).to.equal('px') 52 | }) 53 | 54 | it ('Should parse "%"', () => { 55 | let object = parseNumber('10%') 56 | 57 | expect(object.value).to.equal(10) 58 | expect(object.type).to.equal('%') 59 | }) 60 | 61 | it ('Should not parse "%px"', () => { 62 | let object = parseNumber('10%px') 63 | 64 | expect(object.value).to.be.a('string') 65 | expect(object.type).to.equal('') 66 | }) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/utils/resizeObserver.js: -------------------------------------------------------------------------------- 1 | import ResizeObserverPolyfill from 'resize-observer-polyfill' 2 | 3 | const observer = 4 | typeof window !== 'undefined' && window.ResizeObserver 5 | ? ResizeObserver 6 | : ResizeObserverPolyfill 7 | 8 | export default observer 9 | -------------------------------------------------------------------------------- /src/utils/types.js: -------------------------------------------------------------------------------- 1 | export const isString = value => { 2 | return typeof value === 'string' 3 | } 4 | 5 | export const isObject = value => { 6 | return typeof value === 'object' 7 | } 8 | 9 | export const isFn = value => { 10 | return typeof value === 'function' 11 | } 12 | 13 | export const isFunction = value => { 14 | return typeof value === 'function' 15 | } 16 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PluginObject, ComponentOptions, AsyncComponent } from 'vue' 2 | 3 | export declare interface VueJSModalOptions { 4 | componentName?: string 5 | dialog?: boolean 6 | dialogComponentName?: string 7 | dynamicDefaults?: object 8 | } 9 | 10 | declare const VueJSModal: PluginObject 11 | 12 | declare interface VModal { 13 | show(name: string, params?: object): void 14 | show( 15 | component: typeof Vue | ComponentOptions | AsyncComponent, 16 | componentProps?: object, 17 | componentSlots?: object, 18 | modalProps?: object, 19 | modalEvents?: object 20 | ): void 21 | hide(name: string, params?: object): void 22 | hideAll(): void 23 | toggle(name: string, params?: object): void 24 | } 25 | 26 | declare module 'vue/types/vue' { 27 | interface Vue { 28 | $modal: VModal 29 | } 30 | } 31 | 32 | export default VueJSModal 33 | -------------------------------------------------------------------------------- /types/test/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueJSModal, { VueJSModalOptions } from "../index"; 3 | 4 | Vue.use(VueJSModal); 5 | Vue.use(VueJSModal, { 6 | componentName: "another-modal-name", 7 | dialog: false 8 | }); 9 | 10 | const vm = new Vue({ 11 | template: `` 12 | }).$mount("#app"); 13 | 14 | vm.$modal.show("awesome-modal"); 15 | vm.$modal.hide("awesome-modal", { customeEvent: "customEventParam" }); 16 | vm.$modal.toggle("awesome-modal"); 17 | -------------------------------------------------------------------------------- /types/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noEmit": true 8 | }, 9 | "include": [ 10 | "*.ts", 11 | "../index.d.ts" 12 | ] 13 | } 14 | --------------------------------------------------------------------------------