├── .browserslistrc ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── docs ├── zh-CN │ ├── browser-compatibility.md │ ├── README.md │ ├── params.md │ ├── quick-start.md │ └── examples.md ├── browser-compatibility.md ├── .vuepress │ ├── enhanceApp.js │ ├── styles │ │ └── index.styl │ ├── config.js │ └── public │ │ └── vue-directive-window.umd.min.js ├── README.md ├── quick-start.md ├── params.md └── examples.md ├── .prettierrc.js ├── src ├── config │ ├── constant.js │ └── default-params.js ├── main.js └── libs │ ├── validate.js │ ├── maximize.js │ ├── event-binding.js │ ├── move.js │ ├── common.js │ └── resize.js ├── dist ├── demo.html └── vue-directive-window.umd.min.js ├── babel.config.js ├── .gitignore ├── .eslintrc.js ├── script ├── deploy.sh ├── dist-copy.js └── file-watch.js ├── vue.config.js ├── LICENSE ├── test.html ├── package.json ├── README.zh-CN.md └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | not ie <= 8 3 | last 2 versions 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Array-Huang/vue-directive-window/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs/zh-CN/browser-compatibility.md: -------------------------------------------------------------------------------- 1 | # 浏览器兼容性 2 | | IE10 | IE11 | Chrome | 3 | | :---: | :---: | :---: | 4 | | OK | OK | OK | -------------------------------------------------------------------------------- /docs/browser-compatibility.md: -------------------------------------------------------------------------------- 1 | # Browser Compatibility 2 | | IE10 | IE11 | Chrome | 3 | | :---: | :---: | :---: | 4 | | OK | OK | OK | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | bracketSpacing: true, 8 | arrowParens: 'avoid', 9 | }; 10 | -------------------------------------------------------------------------------- /src/config/constant.js: -------------------------------------------------------------------------------- 1 | export default { 2 | BORDER_SCOPE: 10, // resize区域的宽度 3 | AVAILABLE_CLICK_MAX_MOVE_DISTANCE: 4, // 在move中,超出这个距离的话将把click事件吞掉 4 | AVAILABLE_CLICK_MAX_RESIZE_DISTANCE: 4, // 在resize中,超出这个距离的话将把click事件吞掉 5 | }; 6 | -------------------------------------------------------------------------------- /dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | vue-directive-window demo 3 | 4 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | /* https://v1.vuepress.vuejs.org/zh/guide/basic-config.html#%E5%BA%94%E7%94%A8%E7%BA%A7%E5%88%AB%E7%9A%84%E9%85%8D%E7%BD%AE */ 2 | 3 | export default ({ 4 | Vue, // VuePress 正在使用的 Vue 构造函数 5 | options, // 附加到根实例的一些选项 6 | router, // 当前应用的路由实例 7 | siteData, // 站点元数据 8 | }) => { 9 | // ...做一些其他的应用级别的优化 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@vue/app', 5 | { 6 | // polyfills: ['es6.promise', 'es6.symbol', 'es7.object.entries'], 7 | // useBuiltIns: 'usage', 8 | targets: { 9 | browsers: ['> 1%', 'not ie < 9', 'last 2 versions'], 10 | }, 11 | }, 12 | ], 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | 22 | dist.webpack.config.js 23 | docs/.vuepress/dist/ 24 | *.hot-update.* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/prettier'], 7 | rules: { 8 | 'no-console': 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | 'no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 11 | }, 12 | parserOptions: { 13 | parser: 'babel-eslint', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /docs/zh-CN/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: vue-directive-window 4 | tagline: 让你的模态框轻而易举地支持类窗口操作 5 | actionText: 快速上手 → 6 | actionLink: /zh-CN/quick-start/ 7 | features: 8 | - title: 节约成本 9 | details: 旨在以极少的改造成本,让一个现有的模态框或任何合适的HTMLElement轻松支持拖拽移动、调整大小、最大化等类视窗操作。 10 | - title: 使用便捷 11 | details: 同时提供Vue自定义指令以及一般js类库两种调用方式。 12 | - title: 可插拔 13 | details: 可以随时为某个系统里已存在的模块添上/去除类视窗操作的功能,而不会影响该模块原有的功能。 14 | footer: MIT Licensed | Copyright © 2019-present Array Huang 15 | --- -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 生成静态文件 7 | vuepress build docs 8 | 9 | # 进入生成的文件夹 10 | cd docs/.vuepress/dist 11 | 12 | # 如果是发布到自定义域名 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # 如果发布到 https://.github.io 20 | # git push -f git@github.com:/.github.io.git master 21 | 22 | # 如果发布到 https://.github.io/ 23 | git push -f git@github.com:Array-Huang/vue-directive-window.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | .window { 2 | z-index: 100; 3 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); 4 | border-radius: 4px; 5 | border: 1px solid #ebeef5; 6 | background-color: #fff; 7 | overflow: hidden; 8 | color: #303133; 9 | } 10 | 11 | .window__header { 12 | padding: 18px 20px; 13 | border-bottom: 1px solid #ebeef5; 14 | box-sizing: border-box; 15 | } 16 | 17 | .window__body { 18 | padding: 20px; 19 | } 20 | 21 | /* 主题重置 */ 22 | .content__default.theme-default-content { 23 | max-width: none; 24 | } 25 | .page .page-edit, .page .page-nav { 26 | max-width: none; 27 | } 28 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'core-js/es7/object'; 2 | import 'core-js/es6/symbol'; 3 | import { eventBinding } from './libs/event-binding'; 4 | /* vue-directive-window,提供Vue.use方式安装,安装后将可使用v-title这一自定义指令 */ 5 | function install(Vue) { 6 | Vue.directive('window', { 7 | bind(el, binding) { 8 | const customParams = binding.value; // 从指令绑定值取来参数 9 | eventBinding(el, customParams); 10 | }, 11 | }); 12 | } 13 | /* EnhancedWindow,提供普通函数的方式来调用 */ 14 | function enhanceWindow(el, customParams) { 15 | eventBinding(el, customParams); 16 | } 17 | 18 | export default { 19 | install, 20 | enhanceWindow, 21 | }; 22 | -------------------------------------------------------------------------------- /script/dist-copy.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const shell = require('shelljs'); 3 | const DIST_FILE_PATH = [ 4 | path.resolve(__filename, '../../dist/vue-directive-window.umd.min.js'), 5 | ]; 6 | const DIST_DIR = path.resolve(__filename, '../../docs/.vuepress/public'); 7 | 8 | function afterWebpackBuildCb() { 9 | for (let path of DIST_FILE_PATH) { 10 | shell.cp('-f', path, DIST_DIR); 11 | console.log(`${path} synced`); 12 | } 13 | } 14 | 15 | class AfterBuildCbPlugin { 16 | apply(compiler) { 17 | compiler.plugin('done', afterWebpackBuildCb); 18 | } 19 | } 20 | 21 | module.exports = AfterBuildCbPlugin; 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-directive-window 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const AfterBuildCbPlugin = require('./script/dist-copy'); 3 | const pkg = require('./package.json'); 4 | 5 | const banner = ` 6 | ${pkg.name} 7 | ${pkg.description}\n 8 | @version v${pkg.version} 9 | @homepage ${pkg.homepage} 10 | @repository ${pkg.repository.url}\n 11 | (c) 2019 Array-Huang 12 | Released under the MIT License. 13 | hash: [hash] 14 | `; 15 | 16 | module.exports = { 17 | chainWebpack: config => { 18 | config.output.libraryExport('default'); 19 | config.plugin('banner').use(webpack.BannerPlugin, [ 20 | { 21 | banner, 22 | entryOnly: true, 23 | }, 24 | ]); 25 | config.plugin('after-build').use(AfterBuildCbPlugin); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: vue-directive-window 4 | tagline: Vue.js directive that enhance your Modal Window, support drag, resize and maximize. 5 | actionText: Quick Start → 6 | actionLink: /quick-start/ 7 | features: 8 | - title: Cost Saving 9 | details: Born to enhance your existed Modal Window or any other approprite HTMLElement, support drag, resize and miximize, with minimum cost. 10 | - title: Easy to Use 11 | details: We provide two ways to use, Vue Custom Directive, and general javascript class library. 12 | - title: Pluggable 13 | details: Annytime to add or remove Window-Like enhancement to any existed module in your system, without any negative impact. 14 | footer: MIT Licensed | Copyright © 2019-present Array Huang 15 | --- -------------------------------------------------------------------------------- /src/config/default-params.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minWidth: 100, // resize最小宽度 3 | maxWidth: null, // resize最大宽度 4 | minHeight: 100, // resize最小高度 5 | maxHeight: null, // resize最大高度 6 | movable: true, // 是否开启拖拽移动功能,默认开启 7 | resizable: true, // 是否开启resize功能,true表示开启,false表示关闭;另外还可接受数组类型参数,指定在哪些方向上开启resize,包括:left-top/left-bottom/left/right-top/right-bottom/right/top/bottom 8 | customMoveHandler: null, // 自定义的拖拽移动handler,可接受选择器形式的参数,或是Element;为空则以窗口自身为handler 9 | customMaximizeHandler: null, // 自定义的最大化handler,可接受选择器形式的参数,或是Element;为空则不开启最大化的功能 10 | maximizeCallback: () => {}, // 最大化后的回调函数 11 | moveStartCallback: () => {}, // 拖拽移动开始的回调函数 12 | movingCallback: () => {}, // 拖拽移动过程中的回调函数,在每次拖拽过程中会被执行多次 13 | moveEndCallback: () => {}, // 拖拽移动结束的回调函数 14 | }; 15 | -------------------------------------------------------------------------------- /script/file-watch.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar'); 2 | const path = require('path'); 3 | const shell = require('shelljs'); 4 | 5 | const WATCH_PATH = [ 6 | path.resolve(__filename, '../../dist/vue-directive-window.umd.min.js'), 7 | path.resolve(__filename, '../../dist/vue-directive-window.umd.min.js.map'), 8 | ]; 9 | const DIST_DIR = path.resolve(__filename, '../../docs/.vuepress/public'); 10 | 11 | function changeCb(path) { 12 | shell.cp('-f', path, DIST_DIR); 13 | console.log(`${path} changed, file synced`); 14 | } 15 | const watcher = chokidar.watch(WATCH_PATH, { 16 | persistent: true, 17 | interval: 500, 18 | }); 19 | 20 | watcher 21 | .on('ready', () => { 22 | console.log('ready for dist change'); 23 | }) 24 | .on('add', changeCb) 25 | .on('change', changeCb); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 George Raptis 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 | -------------------------------------------------------------------------------- /docs/zh-CN/params.md: -------------------------------------------------------------------------------- 1 | # 参数 2 | 3 | ## minWidth 4 | - 类型: `Number` 5 | - 默认值: `100` 6 | - 说明: 窗口可被调整至的最小宽度(px) 7 | 8 | ## maxWidth 9 | - 类型: `Number` 10 | - 说明: 窗口可被调整至的最大宽度(px) 11 | 12 | ## minHeight 13 | - 类型: `Number` 14 | - 默认值: `100` 15 | - 说明: 窗口可被调整至的最小高度(px) 16 | 17 | ## maxHeight 18 | - 类型: `Number` 19 | - 说明: 窗口可被调整至的最大高度(px) 20 | 21 | ## movable 22 | - 类型: `Boolean`/`String` 23 | - 默认值: `true` 24 | - 可选值:`true`/`false`/`'horizontal'`/`'vertical'` 25 | - 说明: 是否开启拖拽移动功能;`'horizontal'`表示只允许水平方向的拖拽移动,`'vertical'`表示只允许垂直方向的拖拽移动,`true`表示水平垂直方向的拖拽移动均被允可。 26 | 27 | ## resizable 28 | - 类型: `Boolean`/`Array` 29 | - 默认值: `true` 30 | - 可选值: `left-top`/`left-bottom`/`left`/`right-top`/`right-bottom`/`right`/`top`/`bottom` 31 | - 说明: 是否开启调整窗口尺寸的功能。参数为`true`表示八个方向均可调整窗口尺寸;但如果传入的是字符串数组,如`['left', 'left-top']`,则只有参数指定的方向可以调整窗口尺寸;各个方向的标识如“可选值”列里所示。 32 | 33 | ## customMoveHandler 34 | - 类型: `String`/`Element` 35 | - 说明: 自定义的拖拽移动handler。如果传入字符串类型参数,系统则将采用`document.querySelector(customMoveHandler)`来获取handler。 36 | 37 | ## customMaximizeHandler 38 | - 类型: `String`/`Element` 39 | - 说明: 自定义的最大化handler。如果传入字符串类型参数,系统则将采用`document.querySelector(customMoveHandler)`来获取handler。 40 | 41 | ## maximizeCallback 42 | - 类型: `Function` 43 | - 说明: 窗口最大化的回调函数。回调参数为:当前是否最大化(Boolean)。 44 | 45 | ## moveStartCallback 46 | - 类型: `Function` 47 | - 说明: 拖拽移动开始时触发。 48 | 49 | ## movingCallback 50 | - 类型: `Function` 51 | - 说明: 拖拽移动过程中会多次触发。 52 | 53 | ## moveEndCallback 54 | - 类型: `Function` 55 | - 说明: 拖拽移动结束时触发。 -------------------------------------------------------------------------------- /docs/zh-CN/quick-start.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | ::: warning 4 | 注意 请确保你的 Node.js 版本 >= 8。 5 | ::: 6 | 7 | ## 引入vue-directive-window 8 | `vue-directive-window`支持静态文件及npm两种方式引入。 9 | 10 | ### 静态文件方式引入 11 | ```html 12 | 13 | ``` 14 | 15 | ### npm方式引入 16 | ```bash 17 | npm install vue-directive-window 18 | ``` 19 | 20 | ## 开始使用 21 | `vue-directive-window`支持Vue自定义指令及一般js类两种方式来使用。 22 | 23 | ### Vue自定义指令 24 | ```vue 25 | 30 | 44 | ``` 45 | 46 | ### 一般js类 47 | ```html 48 |
49 | 50 |
51 | ``` 52 | 53 | ```javascript 54 | import { enhanceWindow } from 'vue-directive-window'; // 如果是以静态文件方式引入的话,则是const enhanceWindow = window['vue-directive-window'].enhanceWindow; 55 | 56 | const windowParams = { 57 | movable: false 58 | resizable: ['left', 'left-top'] 59 | }; 60 | 61 | enhanceWindow(document.querySelector('.demo-window'), windowParams); 62 | ``` 63 | 64 | ## 结语 65 | 到此,您已引入`vue-directive-window`并可以简单使用了,想要了解更多请参考[使用案例](/examples.md)和[参数](/params.md)章节。 66 | -------------------------------------------------------------------------------- /src/libs/validate.js: -------------------------------------------------------------------------------- 1 | import Schema from 'micro-schema-validator'; 2 | const RULES = { 3 | windowSelector: { 4 | type: 'string', 5 | required: false, 6 | }, 7 | minWidth: { 8 | type: 'number', 9 | size: { min: 1 }, 10 | }, 11 | maxWidth: { 12 | type: 'number', 13 | }, 14 | minHeight: { 15 | type: 'number', 16 | size: { min: 1 }, 17 | }, 18 | maxHeight: { 19 | type: 'number', 20 | }, 21 | resizeHandlerClassName: { 22 | type: 'string', 23 | }, 24 | customMoveHandler: { 25 | type: 'string', 26 | }, 27 | customMaximizeHandler: { 28 | type: 'string', 29 | }, 30 | movable: { 31 | type: 'boolean|string', 32 | required: false, 33 | }, 34 | resizable: { 35 | type: 'boolean|string', 36 | required: false, 37 | }, 38 | maximizeCallback: { 39 | type: 'function', 40 | }, 41 | moveStartCallback: { 42 | type: 'function', 43 | required: false, 44 | }, 45 | movingCallback: { 46 | type: 'function', 47 | required: false, 48 | }, 49 | moveEndCallback: { 50 | type: 'function', 51 | required: false, 52 | }, 53 | }; 54 | export function validate(customParams = {}) { 55 | const schema = new Schema(RULES); 56 | const validateResult = schema.validate(customParams); 57 | if (validateResult.status) { 58 | return; 59 | } else { 60 | console.warn( 61 | 'There are some mistakes in your params to vue-directive-window, please fix them. Otherwise, it will act not like what you expected.' 62 | ); 63 | if (Array.isArray(validateResult.errors)) { 64 | validateResult.errors.forEach(error => { 65 | console.warn(error.msg); 66 | /* 参数有误则立刻抛出异常 */ 67 | throw 'Params validation failed, so vue-directive-window stopped.'; 68 | }); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-directive-window demo 6 | 7 | 8 | 9 | 40 | 41 | 42 |
43 |
44 | 函数方式生成的窗口 45 |
46 |
47 |

窗口内容1

48 |

窗口内容2

49 |

窗口内容3

50 |
51 |
52 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: '/vue-directive-window/', 3 | head: [ 4 | ['script', { src: '/vendor/vue.min.js' }], 5 | [ 6 | 'script', 7 | { src: '/vue-directive-window.umd.min.js' }, 8 | ], 9 | ], 10 | locales: { 11 | // 键名是该语言所属的子路径 12 | // 作为特例,默认语言可以使用 '/' 作为其路径。 13 | '/': { 14 | lang: 'en-US', // 将会被设置为 的 lang 属性 15 | title: 'vue-directive-window', 16 | description: 17 | 'Vue.js directive that enhance your Modal Window, support drag, resize and maximize.', 18 | }, 19 | '/zh-CN/': { 20 | lang: 'zh-CN', 21 | title: 'vue-directive-window', 22 | description: '让你的模态框轻而易举地支持类窗口操作', 23 | }, 24 | }, 25 | themeConfig: { 26 | /* 导航栏链接 */ 27 | nav: [ 28 | { 29 | text: 'Github', 30 | link: 'https://github.com/Array-Huang/vue-directive-window', 31 | }, 32 | { 33 | text: 'npm', 34 | link: 'https://www.npmjs.com/package/vue-directive-window', 35 | }, 36 | ], 37 | /* 侧边栏 */ 38 | sidebarDepth: 2, 39 | /* 最后更新时间 */ 40 | lastUpdated: 'Last Updated', 41 | /* 活动的标题链接 */ 42 | activeHeaderLinks: true, // 默认值:true 43 | /* 多语言 */ 44 | locales: { 45 | '/': { 46 | selectText: 'Languages', 47 | label: 'English', 48 | sidebar: ['quick-start', 'browser-compatibility', 'examples', 'params'], 49 | }, 50 | '/zh-CN/': { 51 | selectText: 'Languages', 52 | label: '简体中文', 53 | /* 侧边栏 */ 54 | sidebar: { 55 | '/zh-CN/': [ 56 | 'quick-start', 57 | 'browser-compatibility', 58 | 'examples', 59 | 'params', 60 | ], 61 | }, 62 | }, 63 | }, 64 | }, 65 | chainWebpack(config, isServer) { 66 | config.resolve.alias.set('@pwd', process.cwd()); // process.cwd() 是当前执行node命令时候的文件夹地址 ——工作目录,保证了文件在不同的目录下执行时,路径始终不变 67 | }, 68 | plugins: ['demo-block'], 69 | }; 70 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ::: warning 4 | Please make sure your Node.js version >= 8. 5 | ::: 6 | 7 | ## Installation 8 | There are two ways of installation, from CDN and from npm, you can choose which you like. 9 | 10 | ### CDN 11 | ```html 12 | 13 | ``` 14 | 15 | ### npm 16 | ```bash 17 | npm install vue-directive-window 18 | ``` 19 | 20 | ## Hello World 21 | `vue-directive-window` provides two ways to use: 22 | - Vue Custom Directive 23 | - general javascript class library 24 | 25 | ### Vue Custom Directive 26 | ```vue 27 | 32 | 46 | ``` 47 | 48 | ### Javascript Class Library 49 | ```html 50 |
51 | 52 |
53 | ``` 54 | 55 | ```javascript 56 | import { enhanceWindow } from 'vue-directive-window'; // When you take the CDN way, you may use `const enhanceWindow = window['vue-directive-window'].enhanceWindow;` instead. 57 | 58 | const windowParams = { 59 | movable: false 60 | resizable: ['left', 'left-top'] 61 | }; 62 | 63 | enhanceWindow(document.querySelector('.demo-window'), windowParams); 64 | ``` 65 | 66 | ## Ready for More? 67 | At this point, you already install `vue-directive-window` and create a Hello World case. If you are interested in `vue-directive-window` and want to know more about it, you may refer to chapter [examples](/examples.md) and [params](/params.md). 68 | -------------------------------------------------------------------------------- /src/libs/maximize.js: -------------------------------------------------------------------------------- 1 | import { getPositionOffset, getSize } from './common'; 2 | /** 3 | * 添加窗口最大化的事件 4 | * 5 | * @export 6 | * @param {Element} handler 7 | */ 8 | export function addMaximizeEvent(handler) { 9 | const target = this.window; 10 | const params = this.params; 11 | let positionOffset; // 记录最大化前的位置偏移(top/left) 12 | let size; // 记录最大化前的大小(width/height) 13 | let position; // 记录最大化前的position值 14 | let isMaximize = false; // 记录当前是否为最大化的状态,方便判定切换状态 15 | /* 设置位置偏移值 */ 16 | function _setPositionOffset(left, top, right, bottom) { 17 | if (typeof left === 'number') { 18 | target.style.left = left + 'px'; 19 | } 20 | if (typeof top === 'number') { 21 | target.style.top = top + 'px'; 22 | } 23 | if (typeof right === 'number') { 24 | target.style.right = right + 'px'; 25 | } 26 | if (typeof bottom === 'number') { 27 | target.style.bottom = bottom + 'px'; 28 | } 29 | } 30 | /* 设置大小 */ 31 | function _setSize(width, height) { 32 | target.style.width = width; 33 | target.style.height = height; 34 | } 35 | /* 最大化窗口,其原理是取浏览器窗口的宽高来设置在窗口上 */ 36 | function _setTargetMaximize() { 37 | _setPositionOffset(0, 0, 0, 0); 38 | _setSize('auto', 'auto'); 39 | } 40 | 41 | /* 在最大化的handler绑定click事件回调 */ 42 | handler.addEventListener('click', event => { 43 | if (!isMaximize) { 44 | positionOffset = getPositionOffset(target); // 记录最大化前的位置偏移 45 | size = getSize(target); // 记录最大化前的窗口大小 46 | 47 | _setTargetMaximize(); // 最大化窗口 48 | isMaximize = true; 49 | } else { 50 | // 如果当前是最大化状态... 51 | target.style.position = position; 52 | _setPositionOffset(positionOffset.x, positionOffset.y); 53 | _setSize(size.width, size.height); 54 | window.removeEventListener('resize', _setTargetMaximize); 55 | isMaximize = false; 56 | } 57 | 58 | if ( 59 | !!params.maximizeCallback && 60 | typeof params.maximizeCallback === 'function' 61 | ) { 62 | params.maximizeCallback(isMaximize); 63 | } 64 | 65 | event.stopPropagation(); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-directive-window", 3 | "version": "0.8.0", 4 | "description": "Vue.js directive that enhance your Modal Window, support drag, resize and maximize.", 5 | "author": "Array Huang", 6 | "license": "MIT", 7 | "files": [ 8 | "src/", 9 | "dist/" 10 | ], 11 | "homepage": "https://github.com/Array-Huang/vue-directive-window", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Array-Huang/vue-directive-window.git", 15 | "bugs": { 16 | "url": "https://github.com/Array-Huang/vue-directive-window/issues" 17 | } 18 | }, 19 | "keywords": [ 20 | "vue", 21 | "directive", 22 | "resize", 23 | "drag", 24 | "maximize" 25 | ], 26 | "private": false, 27 | "main": "./dist/vue-directive-window.common.js", 28 | "scripts": { 29 | "build": "vue-cli-service build --target lib --name vue-directive-window ./src/main.js", 30 | "watch": "vue-cli-service build --target lib --name vue-directive-window ./src/main.js --watch", 31 | "lint": "vue-cli-service lint", 32 | "inspect": "vue inspect > dist.webpack.config.js", 33 | "start": "npm run docs:dev", 34 | "docs:dev": "vuepress dev docs", 35 | "docs:deploy": "sh script/deploy.sh", 36 | "npm:deploy": "npm version patch && git push --follow-tags && npm publish" 37 | }, 38 | "dependencies": { 39 | "core-js": "^2.6.11", 40 | "current-script-polyfill": "^1.0.0", 41 | "micro-schema-validator": "^0.1.2", 42 | "validate": "^4.5.1", 43 | "vue": "^2.6.11", 44 | "vue-router": "^3.0.7", 45 | "vuex": "^3.1.2" 46 | }, 47 | "devDependencies": { 48 | "@vue/cli-plugin-babel": "^3.12.1", 49 | "@vue/cli-plugin-eslint": "^3.12.1", 50 | "@vue/cli-service": "^3.12.1", 51 | "@vue/eslint-config-prettier": "^4.0.1", 52 | "babel-eslint": "^10.0.2", 53 | "cz-conventional-changelog": "^3.0.2", 54 | "eslint": "^5.16.0", 55 | "eslint-plugin-vue": "^5.2.3", 56 | "shelljs": "^0.8.3", 57 | "stylus": "^0.54.5", 58 | "stylus-loader": "^3.0.2", 59 | "vue-template-compiler": "^2.6.11", 60 | "vuepress": "^1.2.0", 61 | "vuepress-plugin-demo-block": "^0.7.2" 62 | }, 63 | "config": { 64 | "commitizen": { 65 | "path": "./node_modules/cz-conventional-changelog" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/params.md: -------------------------------------------------------------------------------- 1 | # Parameters 2 | 3 | ## minWidth 4 | - Type: `Number` 5 | - Default: `100` 6 | - Description: window's minimum width(px) 7 | 8 | ## maxWidth 9 | - Type: `Number` 10 | - Description: window's maximum width(px) 11 | 12 | ## minHeight 13 | - Type: `Number` 14 | - Default: `100` 15 | - Description: window's minimum height(px) 16 | 17 | ## maxHeight 18 | - Type: `Number` 19 | - Description: window's maximum height(px) 20 | 21 | ## movable 22 | - Type: `Boolean`/`String` 23 | - Default: `true` 24 | - Accepted Values:`true`/`false`/`'horizontal'`/`'vertical'` 25 | - Description: Is drag feature available; when set `movable` to `'vertical'`, users will be only allow to make vertical drag; for the same reason, `'horizontal'` means only allow horizontal drag; when set `true`, both directions are available. 26 | 27 | ## resizable 28 | - Type: `Boolean`/`Array` 29 | - Default: `true` 30 | - Accepted Values: `left-top`/`left-bottom`/`left`/`right-top`/`right-bottom`/`right`/`top`/`bottom` 31 | - Description: is resize feature available; when it is `true`, it means you could resize the window from every eight directions; when it is an Array value which contain String value, like `['left', 'left-top']` you could resize the window only from targeted directions. 32 | 33 | ## customMoveHandler 34 | - Type: `String`/`Element` 35 | - Description: custom drag handler. When it is `null`, you could move the window by dragging every inch of this window. Otherwise, when it is a String value, `vue-directive-window` will use `document.querySelector(customMoveHandler)` to get the handler's Element; in that case, you could move the window only by dragging the handler. 36 | 37 | ## customMaximizeHandler 38 | - Type: `String`/`Element` 39 | - Description: maximize feature's handler. When it is a String value, `vue-directive-window` will use `document.querySelector(customMoveHandler)` to get the handler. 40 | 41 | ## maximizeCallback 42 | - Type: `Function` 43 | - Description: window maximizeCallback function; there is one parameter, which means if it is current maximize(Boolean). 44 | 45 | ## moveStartCallback 46 | - Type: `Function` 47 | - Description: triggers when drag&move start. 48 | 49 | ## movingCallback 50 | - Type: `Function` 51 | - Description: triggers multiple times during drag&move going. 52 | 53 | ## moveEndCallback 54 | - Type: `Function` 55 | - Description: triggers when drag&move end. -------------------------------------------------------------------------------- /src/libs/event-binding.js: -------------------------------------------------------------------------------- 1 | import { 2 | startEvent, 3 | moveEvent, 4 | endEvent, 5 | ignoreIframe, 6 | recoverIframe, 7 | } from './common'; 8 | import { handleStartEventForResize, cursorChange } from './resize'; 9 | import { handleStartEventForMove } from './move'; 10 | import { addMaximizeEvent } from './maximize'; 11 | import { validate } from './validate'; 12 | import DEFAULT_PARAMS from '../config/default-params'; 13 | 14 | function _prepareParams(customParams) { 15 | validate(customParams); 16 | return Object.assign({}, DEFAULT_PARAMS, customParams); 17 | } 18 | 19 | function getMoveHandler(finalParams, el) { 20 | const customMoveHandler = finalParams.customMoveHandler; 21 | if (customMoveHandler) { 22 | if (typeof customMoveHandler === 'string') { 23 | return el.querySelector(customMoveHandler); 24 | } else { 25 | return customMoveHandler; 26 | } 27 | } else { 28 | return el; 29 | } 30 | } 31 | 32 | function getMaximizeHandler(finalParams, el) { 33 | const customMaximizeHandler = finalParams.customMaximizeHandler; 34 | if (customMaximizeHandler) { 35 | if (typeof customMaximizeHandler === 'string') { 36 | return el.querySelector(customMaximizeHandler); 37 | } else { 38 | return customMaximizeHandler; 39 | } 40 | } 41 | 42 | return null; 43 | } 44 | 45 | function isMoveHandlerEqualWindow(window, moveHandler) { 46 | return window === moveHandler; 47 | } 48 | 49 | export function eventBinding(el, customParams) { 50 | /* 传入参数校验,参数有误则立刻停止执行 */ 51 | try { 52 | var finalParams = _prepareParams(customParams); 53 | } catch (exception) { 54 | console.warn(exception); 55 | return; 56 | } 57 | 58 | el = finalParams.windowSelector 59 | ? el.querySelector(finalParams.windowSelector) 60 | : el; 61 | const moveHandler = getMoveHandler(finalParams, el); 62 | const maximizeHandler = getMaximizeHandler(finalParams, el); 63 | const instance = { 64 | window: el, 65 | params: finalParams, 66 | moveHandler, 67 | maximizeHandler, 68 | isMoveHandlerEqualWindow: isMoveHandlerEqualWindow(el, moveHandler), 69 | }; 70 | 71 | /* 一些杂项的处理 */ 72 | el.addEventListener(startEvent, () => { 73 | ignoreIframe(el); // 由于iframe会把moveEvent吃掉,因此需要忽略掉iframe; 74 | el.addEventListener( 75 | endEvent, 76 | () => { 77 | recoverIframe(el); // 恢复iframe的功能 78 | }, 79 | { once: true } 80 | ); 81 | }); 82 | 83 | /* 拖拽移动相关 */ 84 | if (finalParams.movable) { 85 | moveHandler.addEventListener( 86 | startEvent, 87 | handleStartEventForMove.bind(instance) 88 | ); 89 | 90 | /* 当处在moving状态的时候,吞掉click事件 */ 91 | moveHandler.addEventListener('click', event => { 92 | if (moveHandler.className.indexOf('moving') > -1) { 93 | event.stopImmediatePropagation(); 94 | } 95 | }); 96 | } 97 | 98 | /* resize相关 */ 99 | if (finalParams.resizable) { 100 | el.addEventListener(startEvent, handleStartEventForResize.bind(instance)); 101 | el.addEventListener(moveEvent, cursorChange.bind(instance)); 102 | } 103 | 104 | /* 最大化相关 */ 105 | if (maximizeHandler) { 106 | addMaximizeEvent.call(instance, maximizeHandler); 107 | } 108 | 109 | /* 当处在resizing/moving状态的时候,吞掉click事件 */ 110 | el.addEventListener('click', event => { 111 | if ( 112 | el.className.indexOf('moving') > -1 || 113 | el.className.indexOf('resizing') > -1 114 | ) { 115 | event.stopImmediatePropagation(); 116 | } 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /src/libs/move.js: -------------------------------------------------------------------------------- 1 | import { 2 | moveEvent, 3 | endEvent, 4 | getPositionOffset, 5 | getClientPosition, 6 | isOutOfBrowser, 7 | judgeResizeType, 8 | getSize, 9 | setSize, 10 | calDistance, 11 | } from './common'; 12 | import constant from '../config/constant'; 13 | 14 | /** 15 | * 计算拖拽移动过程中窗口的位置,在移动过程中每一小段就会触发本方法 16 | * 17 | * @param {Object} touchStartPoint 鼠标/手势起始点 18 | * @param {Object} touchEndPoint 鼠标/手势当前点 19 | * @param {Object} windowOriginPosition 窗口在拖拽移动前的位置 20 | * @param {Object} movable 用户传入的拖拽移动开关/类型 21 | */ 22 | function _calWindowCurrentPosition( 23 | touchStartPoint, 24 | touchEndPoint, 25 | windowOriginPosition, 26 | movableParam 27 | ) { 28 | const supposePosition = { 29 | x: touchEndPoint.x - touchStartPoint.x + windowOriginPosition.x, 30 | y: touchEndPoint.y - touchStartPoint.y + windowOriginPosition.y, 31 | }; 32 | 33 | switch (movableParam) { 34 | case 'horizontal': 35 | return { 36 | x: supposePosition.x, 37 | y: windowOriginPosition.y, 38 | }; 39 | 40 | case 'vertical': 41 | return { 42 | x: windowOriginPosition.x, 43 | y: supposePosition.y, 44 | }; 45 | 46 | case true: 47 | default: 48 | return supposePosition; 49 | } 50 | } 51 | 52 | export function handleStartEventForMove(event) { 53 | function _handleEndEventForMove(event) { 54 | /* 提供拖拽移动结束的钩子 */ 55 | moveEndCallback(); 56 | nowInMoving = false; 57 | 58 | document.removeEventListener(moveEvent, _handleMoveEventForMove, false); // 拖拽结束,清除移动的事件回调 59 | 60 | event.preventDefault(); 61 | 62 | /* 撤销moving状态,但由于此状态值主要用于吞掉click事件,因此使用setTimeout延长moving状态至click事件结束 */ 63 | setTimeout(() => { 64 | window.className = window.className.replace(/ ?moving/, ''); 65 | }, 0); 66 | } 67 | 68 | function _handleMoveEventForMove(event) { 69 | /* 判断鼠标是否已出浏览器窗口,是的话就限制移动,避免整个window出浏览器窗口 */ 70 | if (isOutOfBrowser(event)) { 71 | return false; 72 | } 73 | 74 | const position = getClientPosition(event); // 获取鼠标/手指的位置 75 | /* 计算位置偏移值 */ 76 | const positionOffset = _calWindowCurrentPosition( 77 | startPoint, 78 | position, 79 | originPositionOffset, 80 | movableParam 81 | ); 82 | 83 | window.style.top = positionOffset.y + 'px'; // 设置纵坐标,即top 84 | window.style.left = positionOffset.x + 'px'; // 设置横坐标,left 85 | window.style.bottom = 'auto'; // 必须设置为auto,否则就会把高度撑起来 86 | window.style.right = 'auto'; // 必须设置为auto,否则就会把宽度撑起来 87 | 88 | /* 设置moving状态,主要用于吞掉click事件 */ 89 | if ( 90 | calDistance({ 91 | x1: position.x, 92 | y1: position.y, 93 | x2: startPoint.x, 94 | y2: startPoint.y, 95 | }) > constant.AVAILABLE_CLICK_MAX_MOVE_DISTANCE && 96 | window.className.indexOf('moving') === -1 97 | ) { 98 | window.className += ' moving'; 99 | 100 | /* 提供拖拽移动相关的钩子 */ 101 | if (!nowInMoving) { 102 | moveStartCallback(); 103 | nowInMoving = true; // 保证在一次完整的拖拽移动过程中只触发一次moveStartCallback 104 | } 105 | movingCallback(); 106 | } 107 | } 108 | 109 | const window = this.window; 110 | const startPoint = getClientPosition(event); // 记录本次拖拽的起点位置 111 | const movableParam = this.params.movable; 112 | const moveStartCallback = this.params.moveStartCallback; 113 | const movingCallback = this.params.movingCallback; 114 | const moveEndCallback = this.params.moveEndCallback; 115 | let nowInMoving = false; 116 | 117 | /* 当窗口本体作为MoveHandler且启用resize特性时,需要判断拖拽的位置是否与resize重复 */ 118 | if ( 119 | this.params.resizable && 120 | this.isMoveHandlerEqualWindow && 121 | judgeResizeType(startPoint, window) !== 'middle' 122 | ) { 123 | return; 124 | } 125 | 126 | /* 固定窗口宽高 */ 127 | let size = getSize(window); 128 | size = { 129 | width: parseInt(size.width), 130 | height: parseInt(size.height), 131 | }; 132 | setSize(window, size.width, size.height); 133 | 134 | document.addEventListener(moveEvent, _handleMoveEventForMove, false); // 应在拖拽开始后才绑定移动的事件回调 135 | document.addEventListener(endEvent, _handleEndEventForMove); 136 | 137 | const originPositionOffset = getPositionOffset(window); // 获取当前的位置偏移值 138 | 139 | event.preventDefault(); 140 | } 141 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

vue-directive-window 👋

2 |

3 | 4 | 5 | 6 | 7 | Maintenance 8 | 9 | npm bundle size 10 | 11 | Documentation 12 | 13 | 14 | License: MIT 15 | 16 | 17 |

18 | 19 | > 让你的模态框轻而易举地支持类窗口操作。 20 | 21 | - [Github](https://github.com/Array-Huang/vue-directive-window) 22 | - [English README](https://github.com/Array-Huang/vue-directive-window/blob/master/README.md) 23 | - [English Document](https://array-huang.github.io/vue-directive-window/) 24 | 25 | > 注意 请确保你的 Node.js 版本 >= 8。 26 | 27 | ## 引入vue-directive-window 28 | `vue-directive-window`支持静态文件及npm两种方式引入。 29 | 30 | ### 静态文件方式引入 31 | ```html 32 | 33 | ``` 34 | 35 | ### npm方式引入 36 | ```bash 37 | npm install vue-directive-window 38 | ``` 39 | 40 | ## 打开本地文档 41 | ```bash 42 | npm start 43 | ``` 44 | 45 | ## 开始使用 46 | `vue-directive-window`支持Vue自定义指令及一般js类两种方式来使用。 47 | 48 | ### Vue自定义指令 49 | ```vue 50 | 55 | 69 | ``` 70 | 71 | ### 一般js类 72 | ```html 73 |
74 | 75 |
76 | ``` 77 | 78 | ```javascript 79 | import { enhanceWindow } from 'vue-directive-window'; // 如果是以静态文件方式引入的话,则是const enhanceWindow = window['vue-directive-window'].enhanceWindow; 80 | 81 | const windowParams = { 82 | movable: false 83 | resizable: ['left', 'left-top'] 84 | }; 85 | 86 | enhanceWindow(document.querySelector('.demo-window'), windowParams); 87 | ``` 88 | 89 | ## 浏览器兼容性 90 | | IE10 | IE11 | Chrome | 91 | | :---: | :---: | :---: | 92 | | OK | OK | OK | 93 | 94 | ## 参数 95 | 96 | ### minWidth 97 | - 类型: `Number` 98 | - 默认值: `100` 99 | - 说明: 窗口可被调整至的最小宽度(px) 100 | 101 | ### maxWidth 102 | - 类型: `Number` 103 | - 说明: 窗口可被调整至的最大宽度(px) 104 | 105 | ### minHeight 106 | - 类型: `Number` 107 | - 默认值: `100` 108 | - 说明: 窗口可被调整至的最小高度(px) 109 | 110 | ### maxHeight 111 | - 类型: `Number` 112 | - 说明: 窗口可被调整至的最大高度(px) 113 | 114 | ### movable 115 | - 类型: `Boolean`/`String` 116 | - 默认值: `true` 117 | - 可选值:`true`/`false`/`'horizontal'`/`'vertical'` 118 | - 说明: 是否开启拖拽移动功能;`'horizontal'`表示只允许水平方向的拖拽移动,`'vertical'`表示只允许垂直方向的拖拽移动,`true`表示水平垂直方向的拖拽移动均被允可。 119 | 120 | ### resizable 121 | - 类型: `Boolean`/`Array` 122 | - 默认值: `true` 123 | - 可选值: `left-top`/`left-bottom`/`left`/`right-top`/`right-bottom`/`right`/`top`/`bottom` 124 | - 说明: 是否开启调整窗口尺寸的功能。参数为`true`表示八个方向均可调整窗口尺寸;但如果传入的是字符串数组,如`['left', 'left-top']`,则只有参数指定的方向可以调整窗口尺寸;各个方向的标识如“可选值”列里所示。 125 | 126 | ### customMoveHandler 127 | - 类型: `String`/`Element` 128 | - 说明: 自定义的拖拽移动handler。如果传入字符串类型参数,系统则将采用`document.querySelector(customMoveHandler)`来获取handler。 129 | 130 | ### customMaximizeHandler 131 | - 类型: `String`/`Element` 132 | - 说明: 自定义的最大化handler。如果传入字符串类型参数,系统则将采用`document.querySelector(customMoveHandler)`来获取handler。 133 | 134 | ### maximizeCallback 135 | - 类型: `Function` 136 | - 说明: 窗口最大化的回调函数。回调参数为:当前是否最大化(Boolean)。 137 | 138 | ### moveStartCallback 139 | - 类型: `Function` 140 | - 说明: 拖拽移动开始时触发。 141 | 142 | ### movingCallback 143 | - 类型: `Function` 144 | - 说明: 拖拽移动过程中会多次触发。 145 | 146 | ### moveEndCallback 147 | - 类型: `Function` 148 | - 说明: 拖拽移动结束时触发。 149 | 150 | ## 喜欢的话,请给个星吧⭐️ 151 | 152 | ## 📝 License 153 | 154 | Copyright © 2019 [Array Huang](https://github.com/Array-Huang).
155 | This project is [MIT](https://github.com/Array-Huang/vue-directive-window/blob/master/LICENSE) licensed. 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to vue-directive-window 👋

2 |

3 | 4 | 5 | 6 | 7 | Maintenance 8 | 9 | npm bundle size 10 | 11 | Documentation 12 | 13 | 14 | License: MIT 15 | 16 | 17 |

18 | 19 | > Vue.js directive that enhance your Modal Window, support drag, resize and maximize. 20 | 21 | - [Github](https://github.com/Array-Huang/vue-directive-window) 22 | - [中文 README](https://github.com/Array-Huang/vue-directive-window/blob/master/README.zh-CN.md) 23 | - [中文 Document](https://array-huang.github.io/vue-directive-window/zh-CN/) 24 | 25 | > `vue-directive-window` requires your Node.js version >= 8. 26 | 27 | ## Installation 28 | There are two ways of installation, from CDN and from npm, you can choose which you like. 29 | 30 | ### CDN 31 | ```html 32 | 33 | ``` 34 | 35 | ### npm 36 | ```bash 37 | npm install vue-directive-window 38 | ``` 39 | 40 | ## Local document 41 | ```bash 42 | npm start 43 | ``` 44 | 45 | ## Hello World 46 | `vue-directive-window` provides two ways to use: 47 | - Vue Custom Directive 48 | - general javascript class library 49 | 50 | ### Vue Custom Directive 51 | ```vue 52 | 57 | 71 | ``` 72 | 73 | ### Javascript Class Library 74 | ```html 75 |
76 | 77 |
78 | ``` 79 | 80 | ```javascript 81 | import { enhanceWindow } from 'vue-directive-window'; // When you take the CDN way, you may use `const enhanceWindow = window['vue-directive-window'].enhanceWindow;` instead. 82 | 83 | const windowParams = { 84 | movable: false 85 | resizable: ['left', 'left-top'] 86 | }; 87 | 88 | enhanceWindow(document.querySelector('.demo-window'), windowParams); 89 | ``` 90 | 91 | ## Browser Compatibility 92 | | IE10 | IE11 | Chrome | 93 | | :---: | :---: | :---: | 94 | | OK | OK | OK | 95 | 96 | ## Parameters 97 | 98 | ### minWidth 99 | - Type: `Number` 100 | - Default: `100` 101 | - Description: window's minimum width(px) 102 | 103 | ### maxWidth 104 | - Type: `Number` 105 | - Description: window's maximum width(px) 106 | 107 | ### minHeight 108 | - Type: `Number` 109 | - Default: `100` 110 | - Description: window's minimum height(px) 111 | 112 | ### maxHeight 113 | - Type: `Number` 114 | - Description: window's maximum height(px) 115 | 116 | ### movable 117 | - Type: `Boolean`/`String` 118 | - Default: `true` 119 | - Accepted Values:`true`/`false`/`'horizontal'`/`'vertical'` 120 | - Description: Is drag feature available; when set `movable` to `'vertical'`, users will be only allow to make vertical drag; for the same reason, `'horizontal'` means only allow horizontal drag; when set `true`, both directions are available. 121 | 122 | ### resizable 123 | - Type: `Boolean`/`Array` 124 | - Default: `true` 125 | - Accepted Values: `left-top`/`left-bottom`/`left`/`right-top`/`right-bottom`/`right`/`top`/`bottom` 126 | - Description: is resize feature available; when it is `true`, it means you could resize the window from every eight directions; when it is an Array value which contain String value, like `['left', 'left-top']` you could resize the window only from targeted directions. 127 | 128 | ### customMoveHandler 129 | - Type: `String`/`Element` 130 | - Description: custom drag handler. When it is `null`, you could move the window by dragging every inch of this window. Otherwise, when it is a String value, `vue-directive-window` will use `document.querySelector(customMoveHandler)` to get the handler's Element; in that case, you could move the window only by dragging the handler. 131 | 132 | ### customMaximizeHandler 133 | - Type: `String`/`Element` 134 | - Description: maximize feature's handler. When it is a String value, `vue-directive-window` will use `document.querySelector(customMoveHandler)` to get the handler. 135 | 136 | ### maximizeCallback 137 | - Type: `Function` 138 | - Description: window maximizeCallback function; there is one parameter, which means if it is current maximize(Boolean). 139 | 140 | ### moveStartCallback 141 | - Type: `Function` 142 | - Description: triggers when drag&move start. 143 | 144 | ### movingCallback 145 | - Type: `Function` 146 | - Description: triggers multiple times during drag&move going. 147 | 148 | ### moveEndCallback 149 | - Type: `Function` 150 | - Description: triggers when drag&move end. 151 | 152 | ## Author 153 | 154 | 👤 **Array Huang** 155 | 156 | - Github: [@Array-Huang](https://github.com/Array-Huang) 157 | 158 | ## Give a ⭐️ if this project helped you! 159 | 160 | ## 📝 License 161 | 162 | Copyright © 2019 [Array Huang](https://github.com/Array-Huang).
163 | This project is [MIT](https://github.com/Array-Huang/vue-directive-window/blob/master/LICENSE) licensed. 164 | -------------------------------------------------------------------------------- /src/libs/common.js: -------------------------------------------------------------------------------- 1 | import constant from '../config/constant'; 2 | 3 | /* 判断当前应该采用mouse相关事件还是touch相关事件 */ 4 | export const isTouchEvent = 'ontouchstart' in window; 5 | export const startEvent = isTouchEvent ? 'touchstart' : 'mousedown'; 6 | export const moveEvent = isTouchEvent ? 'touchmove' : 'mousemove'; 7 | export const endEvent = isTouchEvent ? 'touchend' : 'mouseup'; 8 | 9 | function _refillPx(target) { 10 | if (typeof target === 'number') { 11 | return target + 'px'; 12 | } 13 | 14 | return target; 15 | } 16 | 17 | /** 18 | * 判断当前用户是否使用IE浏览器访问 19 | * 20 | * @returns {Boolean} 21 | */ 22 | function isIE() { 23 | if (!!window.ActiveXObject || 'ActiveXObject' in window) return true; 24 | else return false; 25 | } 26 | 27 | /** 28 | * 从Event对象中获取当前鼠标/手指的位置 29 | * 30 | * @param {Event} event 31 | * @returns {Object} 32 | */ 33 | export function getClientPosition(event) { 34 | const clientX = isTouchEvent ? event.targetTouches[0].clientX : event.clientX; 35 | const clientY = isTouchEvent ? event.targetTouches[0].clientY : event.clientY; 36 | 37 | return { 38 | x: clientX, 39 | y: clientY, 40 | }; 41 | } 42 | 43 | /** 44 | * 获取当前的位置偏移值(left、top) 45 | * 46 | * @export 47 | * @param {Node} node 48 | * @returns {Object} 49 | */ 50 | export function getPositionOffset(node) { 51 | const styleLeft = parseInt(getStyle(node, 'left')); 52 | const styleTop = parseInt(getStyle(node, 'top')); 53 | 54 | return { 55 | x: styleLeft ? styleLeft : 0, 56 | y: styleTop ? styleTop : 0, 57 | }; 58 | } 59 | 60 | export function setPositionOffset(node, left, top, right, bottom) { 61 | if (!!left || left === 0) { 62 | node.style.left = _refillPx(left); 63 | } 64 | if (!!top || top === 0) { 65 | node.style.top = _refillPx(top); 66 | } 67 | if (!!right || right === 0) { 68 | node.style.right = _refillPx(right); 69 | } 70 | if (!!bottom || bottom === 0) { 71 | node.style.bottom = _refillPx(bottom); 72 | } 73 | } 74 | 75 | /** 76 | * 获取node的宽高 77 | * 78 | * @export 79 | * @param {Node} node 80 | */ 81 | export function getSize(node) { 82 | const computedStyle = window.getComputedStyle(node); 83 | return { 84 | width: computedStyle.getPropertyValue('width'), 85 | height: computedStyle.getPropertyValue('height'), 86 | }; 87 | } 88 | 89 | export function setSize(node, width, height) { 90 | node.style.width = _refillPx(width); 91 | node.style.height = _refillPx(height); 92 | } 93 | 94 | /** 95 | * 判断鼠标是否已出浏览器窗口 96 | * @param {Event} event 97 | * @return {Boolean} 98 | */ 99 | export function isOutOfBrowser(event) { 100 | if ( 101 | event.clientX > window.innerWidth || 102 | event.clientX < 0 || 103 | event.clientY < 0 || 104 | event.clientY > window.innerHeight 105 | ) { 106 | return true; 107 | } 108 | 109 | return false; 110 | } 111 | /** 112 | * 判断目标Element是否在拖拽移动的handler上 113 | * 114 | * @export 115 | * @param {Node} targetEl 116 | * @param {String} customMoveHandler 117 | * @returns 118 | */ 119 | export function isInMoveHandler(targetEl, { customMoveHandler }) { 120 | if (!customMoveHandler) { 121 | return false; 122 | } 123 | const handler = document.querySelector(customMoveHandler); 124 | if (!handler) { 125 | return false; 126 | } 127 | 128 | return handler.contains(targetEl); 129 | } 130 | /** 131 | * 判断目标Element是否在最大化的handler上 132 | * 133 | * @export 134 | * @param {Node} targetEl 135 | * @param {String} customMoveHandler 136 | * @returns 137 | */ 138 | export function isInMaximizeHandler(targetEl, { customMaximizeHandler }) { 139 | if (!customMaximizeHandler) { 140 | return false; 141 | } 142 | const handler = document.querySelector(customMaximizeHandler); 143 | if (!handler) { 144 | return false; 145 | } 146 | 147 | return handler.contains(targetEl); 148 | } 149 | 150 | export function getStyle(el, prop) { 151 | const computedStyle = window.getComputedStyle(el); 152 | const styleValue = computedStyle.getPropertyValue(prop); 153 | /* 154 | 需要对IE下的`getComputedStyle()`进行兼容,目前已知在css里设置`right: 0`的时候, 155 | 再用`getComputedStyle()`取left属性的时候只取到`auto` 156 | */ 157 | if (isIE()) { 158 | if (prop === 'left' && styleValue === 'auto') { 159 | const elWidth = computedStyle.getPropertyValue('width'); 160 | const elRight = computedStyle.getPropertyValue('right'); 161 | console.log('left:', window.innerWidth - parseFloat(elWidth) + 'px'); 162 | return ( 163 | window.innerWidth - parseFloat(elWidth) - parseFloat(elRight) + 'px' 164 | ); 165 | } 166 | 167 | if (prop === 'top' && styleValue === 'auto') { 168 | const elHeight = computedStyle.getPropertyValue('height'); 169 | const elBottom = computedStyle.getPropertyValue('bottom'); 170 | console.log('top:', window.innerHeight - parseFloat(elHeight) + 'px'); 171 | return ( 172 | window.innerHeight - parseFloat(elHeight) - parseFloat(elBottom) + 'px' 173 | ); 174 | } 175 | } 176 | 177 | return styleValue; 178 | } 179 | 180 | export function judgeResizeType(cursorPoint, target) { 181 | const borderScope = constant.BORDER_SCOPE; 182 | const x = cursorPoint.x; 183 | const y = cursorPoint.y; 184 | const offsetTop = target.offsetTop; 185 | const offsetLeft = target.offsetLeft; 186 | const offsetWidth = target.offsetWidth; 187 | const offsetHeight = target.offsetHeight; 188 | // console.log( 189 | // `x:${x};y:${y};offsetTop:${offsetTop};offsetLeft:${offsetLeft};offsetWidth:${offsetWidth};offsetHeight:${offsetHeight};` 190 | // ); 191 | if (Math.abs(offsetLeft - x) <= borderScope) { 192 | if (Math.abs(offsetTop - y) <= borderScope) { 193 | return 'left-top'; 194 | } else if (Math.abs(offsetTop + offsetHeight - y) <= borderScope) { 195 | return 'left-bottom'; 196 | } else { 197 | return 'left'; 198 | } 199 | } 200 | 201 | if (Math.abs(offsetLeft + offsetWidth - x) <= borderScope) { 202 | if (Math.abs(offsetTop - y) <= borderScope) { 203 | return 'right-top'; 204 | } else if (Math.abs(offsetTop + offsetHeight - y) <= borderScope) { 205 | return 'right-bottom'; 206 | } else { 207 | return 'right'; 208 | } 209 | } 210 | 211 | if (Math.abs(offsetTop - y) <= borderScope) { 212 | return 'top'; 213 | } else if (Math.abs(offsetTop + offsetHeight - y) <= borderScope) { 214 | return 'bottom'; 215 | } 216 | 217 | return 'middle'; 218 | } 219 | 220 | function _iframeWalk(window, func) { 221 | const iframeEls = window.querySelectorAll('iframe'); 222 | if (!!iframeEls && iframeEls.length > 0) { 223 | Array.prototype.forEach.call(iframeEls, iframe => { 224 | func(iframe); 225 | }); 226 | } 227 | } 228 | 229 | export function ignoreIframe(window) { 230 | _iframeWalk(window, iframe => { 231 | iframe.style['pointer-events'] = 'none'; 232 | }); 233 | } 234 | 235 | export function recoverIframe(window) { 236 | _iframeWalk(window, iframe => { 237 | iframe.style['pointer-events'] = 'auto'; 238 | }); 239 | } 240 | /* 计算两点间距离 */ 241 | export function calDistance({ x1, y1, x2, y2 }) { 242 | const result = Math.pow(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2), 0.5); 243 | // console.log({ x1, y1, x2, y2 }, result); 244 | return result; 245 | } 246 | -------------------------------------------------------------------------------- /src/libs/resize.js: -------------------------------------------------------------------------------- 1 | import { 2 | moveEvent, 3 | endEvent, 4 | getClientPosition, 5 | isOutOfBrowser, 6 | getSize, 7 | getPositionOffset, 8 | setSize, 9 | setPositionOffset, 10 | isInMoveHandler, 11 | isInMaximizeHandler, 12 | judgeResizeType, 13 | getStyle, 14 | calDistance, 15 | } from './common'; 16 | import constant from '../config/constant'; 17 | 18 | function _isOnOtherHandler(el, { moveHandler, maximizeHandler }) { 19 | return el === moveHandler || el === maximizeHandler; 20 | } 21 | 22 | function _setCursor(window, el, positionType) { 23 | let cursor; 24 | switch (positionType) { 25 | case 'top': 26 | case 'bottom': 27 | cursor = 'n-resize'; 28 | break; 29 | 30 | case 'left': 31 | case 'right': 32 | cursor = 'e-resize'; 33 | break; 34 | 35 | case 'left-top': 36 | cursor = 'nw-resize'; 37 | break; 38 | 39 | case 'left-bottom': 40 | cursor = 'sw-resize'; 41 | break; 42 | 43 | case 'right-top': 44 | cursor = 'ne-resize'; 45 | break; 46 | 47 | case 'right-bottom': 48 | cursor = 'se-resize'; 49 | break; 50 | } 51 | window.style.cursor = cursor; 52 | } 53 | 54 | function _resetCursor(window) { 55 | if (getStyle(window, 'cursor').indexOf('resize') > -1) { 56 | window.style.cursor = ''; 57 | } 58 | } 59 | 60 | function _isDirectionResizable(direction) { 61 | const resizableParams = this.params.resizable; 62 | if (resizableParams === true) return true; 63 | if ( 64 | Array.isArray(resizableParams) && 65 | resizableParams.indexOf(direction) > -1 66 | ) { 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | function _calWidthAndOffset({ 74 | type, 75 | originSize, 76 | originOffset, 77 | nowPosition, 78 | startPoint, 79 | minWidth, 80 | maxWidth, 81 | minHeight, 82 | maxHeight, 83 | }) { 84 | /* 获取最小/最大宽度限制下的实际宽度 */ 85 | function _getLimitWidth({ minWidth, maxWidth, currentWidth }) { 86 | /* 最小宽度限制 */ 87 | if (!!minWidth && currentWidth < minWidth) { 88 | currentWidth = minWidth; 89 | } else if (currentWidth < 0) { 90 | currentWidth = 0; 91 | } 92 | /* 最大宽度限制 */ 93 | if (!!maxWidth && currentWidth > maxWidth) { 94 | currentWidth = maxWidth; 95 | } 96 | 97 | return currentWidth; 98 | } 99 | /* 获取最小/最大高度限制下的实际高度 */ 100 | function _getLimitHeight({ minHeight, maxHeight, currentHeight }) { 101 | /* 最小宽度限制 */ 102 | if (!!minHeight && currentHeight < minHeight) { 103 | currentHeight = minHeight; 104 | } else if (currentHeight < 0) { 105 | currentHeight = 0; 106 | } 107 | /* 最大宽度限制 */ 108 | if (!!maxHeight && currentHeight > maxHeight) { 109 | currentHeight = maxHeight; 110 | } 111 | 112 | return currentHeight; 113 | } 114 | 115 | const optionWidth = nowPosition.x - startPoint.x + originSize.width; 116 | const optionHeight = nowPosition.y - startPoint.y + originSize.height; 117 | let calWidth = originSize.width; 118 | let calHeight = originSize.height; 119 | let calTop = originOffset.y; 120 | let calLeft = originOffset.x; 121 | /* 左边的拖拽调整大小 */ 122 | if (type.indexOf('left') > -1) { 123 | calWidth = startPoint.x - nowPosition.x + originSize.width; // 根据拖拽移动的水平位移决定宽度 124 | calWidth = _getLimitWidth({ minWidth, maxWidth, currentWidth: calWidth }); // 根据最大/最小宽度限制来决定最终的宽度 125 | calLeft = originOffset.x - (calWidth - originSize.width); // 根据宽度的变化量来决定窗口的left属性 126 | } 127 | /* 上边的拖拽调整大小 */ 128 | if (type.indexOf('top') > -1) { 129 | calHeight = startPoint.y - nowPosition.y + originSize.height; // 根据拖拽移动的垂直位移决定高度 130 | // 根据最大/最小高度限制来决定最终的高度 131 | calHeight = _getLimitHeight({ 132 | minHeight, 133 | maxHeight, 134 | currentHeight: calHeight, 135 | }); 136 | calTop = originOffset.y - (calHeight - originSize.height); // 根据高度的变化量来决定窗口的top属性 137 | } 138 | /* 右边的拖拽调整大小 */ 139 | if (type.indexOf('right') > -1) { 140 | calWidth = _getLimitWidth({ 141 | minWidth, 142 | maxWidth, 143 | currentWidth: optionWidth, 144 | }); 145 | } 146 | /* 下边的拖拽调整大小 */ 147 | if (type.indexOf('bottom') > -1) { 148 | calHeight = _getLimitHeight({ 149 | minHeight, 150 | maxHeight, 151 | currentHeight: optionHeight, 152 | }); 153 | } 154 | 155 | return { calWidth, calHeight, calTop, calLeft }; 156 | } 157 | 158 | /** 159 | * 为了拖拽调整大小,绑定事件; 160 | * 与拖拽移动不一样的是,由于鼠标在移动过程中会超出window的范围,因此moveEvent需要绑定在document上 161 | * 162 | * @param {Event} event 163 | * @returns 164 | */ 165 | export function handleStartEventForResize(startEvent) { 166 | function _handleMoveEventForResize(moveEvent) { 167 | /* 判断鼠标是否已出浏览器窗口,是的话就限制拖拽,避免整个window出浏览器窗口 */ 168 | if (isOutOfBrowser(moveEvent)) { 169 | return false; 170 | } 171 | const nowPosition = getClientPosition(moveEvent); // 获取鼠标/手指的位置 172 | const { calWidth, calHeight, calTop, calLeft } = _calWidthAndOffset({ 173 | type, 174 | originSize, 175 | originOffset, 176 | nowPosition, 177 | startPoint, 178 | minWidth: params.minWidth, 179 | maxWidth: params.maxWidth, 180 | minHeight: params.minHeight, 181 | maxHeight: params.maxHeight, 182 | }); 183 | setSize(target, calWidth, calHeight); 184 | setPositionOffset(target, calLeft, calTop); 185 | moveEvent.stopPropagation(); 186 | 187 | /* 设置resizing状态,主要用于吞掉click事件 */ 188 | if ( 189 | calDistance({ 190 | x1: nowPosition.x, 191 | y1: nowPosition.y, 192 | x2: startPoint.x, 193 | y2: startPoint.y, 194 | }) > constant.AVAILABLE_CLICK_MAX_RESIZE_DISTANCE && 195 | target.className.indexOf('resizing') === -1 196 | ) { 197 | target.className += ' resizing'; 198 | } 199 | } 200 | 201 | function _handleEndEventForResize(endEvent) { 202 | document.removeEventListener(moveEvent, _handleMoveEventForResize, false); // 拖拽结束,清除移动的事件回调 203 | 204 | endEvent.preventDefault(); 205 | endEvent.stopPropagation(); 206 | 207 | /* 撤销moving状态,但由于此状态值主要用于吞掉click事件,因此使用setTimeout延长moving状态至click事件结束 */ 208 | setTimeout(() => { 209 | target.className = target.className.replace(/ ?resizing/, ''); 210 | }, 0); 211 | } 212 | 213 | const eventEl = startEvent.target; 214 | const params = this.params; 215 | const target = this.window; 216 | const startPoint = getClientPosition(startEvent); // 本次拖拽的起点位置 217 | const type = judgeResizeType(startPoint, target); // 获取本次点击位于窗口的哪个区域 218 | /* 点击位置位于窗口中央,不做任何处理 */ 219 | if (type === 'middle') { 220 | return; 221 | } 222 | /* 该方向上的resize是否启用 */ 223 | if (!_isDirectionResizable.call(this, type)) { 224 | return; 225 | } 226 | if (isInMoveHandler(eventEl, params)) { 227 | /* 判断是否点击在拖拽移动的handler上,是的话就不做处理 */ 228 | return; 229 | } 230 | /* 判断是否点击在最大化的handler上,是的话就不做处理 */ 231 | if (isInMaximizeHandler(eventEl, params)) { 232 | return; 233 | } 234 | let originSize = getSize(target); 235 | originSize = { 236 | width: parseInt(originSize.width), 237 | height: parseInt(originSize.height), 238 | }; 239 | 240 | const originOffset = getPositionOffset(target); 241 | 242 | document.addEventListener(moveEvent, _handleMoveEventForResize, false); // 应在拖拽开始后才绑定移动的事件回调 243 | document.addEventListener(endEvent, _handleEndEventForResize); // 绑定endEvent 244 | startEvent.preventDefault(); 245 | startEvent.stopPropagation(); 246 | } 247 | 248 | export function cursorChange(event) { 249 | const target = this.window; 250 | const currentPoint = getClientPosition(event); // 本次拖拽的起点位置 251 | const type = judgeResizeType(currentPoint, target); // 获取本次点击位于窗口的哪个区域 252 | 253 | /* 点击位置位于窗口中央,重置cursor */ 254 | if ( 255 | _isOnOtherHandler(event.target, this) || 256 | type === 'middle' || 257 | !_isDirectionResizable.call(this, type) 258 | ) { 259 | _resetCursor(this.window); 260 | } else { 261 | _setCursor(this.window, event.target, type); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /docs/zh-CN/examples.md: -------------------------------------------------------------------------------- 1 | # 使用案例 2 | 3 | ## Vue 自定义指令 v-window 的基本使用案例 4 | 5 | 本案例包含 3 个特性:拖拽移动、调整大小、窗口最大化。 6 | 7 | 注意: 8 | - `