├── static ├── .gitkeep └── dragger.js ├── .eslintignore ├── test ├── unit │ ├── setup.js │ ├── .eslintrc │ ├── specs │ │ └── HelloWorld.spec.js │ └── jest.conf.js └── e2e │ ├── specs │ └── test.js │ ├── custom-assertions │ └── elementCount.js │ ├── nightwatch.conf.js │ └── runner.js ├── imgs └── attribute.png ├── src ├── assets │ ├── logo.png │ └── css │ │ └── dragger.css ├── router │ └── index.js ├── App.vue ├── main.js └── components │ └── HelloWorld.vue ├── config ├── prod.env.js ├── test.env.js ├── dev.env.js └── index.js ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── .babelrc ├── index.html ├── .eslintrc.js ├── package.json └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /imgs/attribute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munan2/box-dragger/HEAD/imgs/attribute.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munan2/box-dragger/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import HelloWorld from '@/components/HelloWorld' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'HelloWorld', 12 | component: HelloWorld 13 | } 14 | ] 15 | }) 16 | -------------------------------------------------------------------------------- /test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 拖拽 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: main.js 3 | * @Author: zy 4 | * @LastEditors: zy 5 | * @Date: 2019-04-02 17:50:38 6 | * @LastEditTime: 2019-04-08 14:32:08 7 | */ 8 | // The Vue build version to load with the `import` command 9 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 10 | import Vue from 'vue' 11 | import App from './App' 12 | import router from './router' 13 | import './assets/css/dragger.css' 14 | Vue.config.productionTip = false 15 | 16 | /* eslint-disable no-new */ 17 | new Vue({ 18 | el: '#app', 19 | router, 20 | components: { App }, 21 | template: '' 22 | }) 23 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | testPathIgnorePatterns: [ 18 | '/test/e2e' 19 | ], 20 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 21 | setupFiles: ['/test/unit/setup'], 22 | mapCoverage: true, 23 | coverageDirectory: '/test/unit/coverage', 24 | collectCoverageFrom: [ 25 | 'src/**/*.{js,vue}', 26 | '!src/main.js', 27 | '!src/router/index.js', 28 | '!**/node_modules/**' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 18 | 51 | 52 | 53 | 111 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/assets/css/dragger.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | #pannel { 6 | position: absolute; 7 | } 8 | .dragger-rotate-point { 9 | position: absolute; 10 | left: 50%; 11 | width: 5px; 12 | height: 5px; 13 | margin-left: -3px; 14 | border-radius: 5px; 15 | border: 1px solid #000; 16 | top: -30px; 17 | cursor: url(http://hr.xesv5.com/docs/imgs/mouserotate.ico) 10 10, default; 18 | } 19 | .dragger-rotate-point span { 20 | position: absolute; 21 | top: 6px; 22 | left: -6px; 23 | } 24 | .dragger-rotate-point:after { 25 | content: ''; 26 | display: inline-block; 27 | height: 23px; 28 | width: 1px; 29 | position: absolute; 30 | left: 2px; 31 | top: 6px; 32 | background: #000; 33 | cursor: auto; 34 | } 35 | .dragger-rotate-angle { 36 | position: absolute; 37 | left: 52%; 38 | top: -20px; 39 | } 40 | .dragger-position { 41 | position: absolute; 42 | right: 0; 43 | padding: 0 5px; 44 | background: #fafafa; 45 | border: 1px solid #ccc; 46 | bottom: -35px; 47 | border-radius: 2px; 48 | } 49 | .dragger-base-piont { 50 | position: absolute; 51 | width: 5px; 52 | height: 5px; 53 | border: 1px solid #000; 54 | border-radius: 5px; 55 | background: #fff; 56 | } 57 | .dragger-zoom1 { 58 | left: -3px; 59 | top: -3px; 60 | cursor: nw-resize; 61 | } 62 | .dragger-zoom2 { 63 | left: -3px; 64 | bottom: -3px; 65 | cursor: sw-resize; 66 | } 67 | .dragger-zoom3 { 68 | right: -3px; 69 | top: -3px; 70 | cursor: ne-resize; 71 | } 72 | .dragger-zoom4 { 73 | right: -3px; 74 | bottom: -3px; 75 | cursor: se-resize; 76 | } 77 | .dragger-pull5 { 78 | left: 50%; 79 | margin-left: -3px; 80 | top: -3px; 81 | cursor: row-resize; 82 | } 83 | .dragger-pull6 { 84 | top: 50%; 85 | margin-top: -3px; 86 | left: -3px; 87 | cursor: col-resize; 88 | } 89 | .dragger-pull7 { 90 | left: 50%; 91 | margin-left: -3px; 92 | bottom: -3px; 93 | cursor: row-resize; 94 | } 95 | .dragger-pull8 { 96 | top: 50%; 97 | margin-top: -3px; 98 | right: -3px; 99 | cursor: col-resize; 100 | } 101 | .dragger-base-line { 102 | position: absolute; 103 | } 104 | .dragger-line1 { 105 | width: 1px; 106 | height: 100%; 107 | background: #000; 108 | left: 0; 109 | top: 0; 110 | } 111 | .dragger-line2 { 112 | height: 1px; 113 | width: 100%; 114 | left: 0; 115 | background: #000; 116 | top: 0; 117 | } 118 | .dragger-line3 { 119 | width: 1px; 120 | height: 100%; 121 | background: #000; 122 | right: 0; 123 | top: 0; 124 | } 125 | .dragger-line4 { 126 | height: 1px; 127 | width: 100%; 128 | left: 0; 129 | background: #000; 130 | bottom: 0; 131 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y", 3 | "version": "1.0.0", 4 | "description": "y", 5 | "author": "y", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 14 | "build": "node build/build.js" 15 | }, 16 | "dependencies": { 17 | "style-loader": "^0.23.1", 18 | "vue": "^2.5.2", 19 | "vue-router": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^7.1.2", 23 | "babel-core": "^6.22.1", 24 | "babel-eslint": "^8.2.1", 25 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 26 | "babel-jest": "^21.0.2", 27 | "babel-loader": "^7.1.1", 28 | "babel-plugin-dynamic-import-node": "^1.2.0", 29 | "babel-plugin-syntax-jsx": "^6.18.0", 30 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-plugin-transform-vue-jsx": "^3.5.0", 33 | "babel-preset-env": "^1.3.2", 34 | "babel-preset-stage-2": "^6.22.0", 35 | "babel-register": "^6.22.0", 36 | "chalk": "^2.0.1", 37 | "chromedriver": "^2.27.2", 38 | "copy-webpack-plugin": "^4.0.1", 39 | "cross-spawn": "^5.0.1", 40 | "css-loader": "^0.28.11", 41 | "eslint": "^4.15.0", 42 | "eslint-config-standard": "^10.2.1", 43 | "eslint-friendly-formatter": "^3.0.0", 44 | "eslint-loader": "^1.7.1", 45 | "eslint-plugin-import": "^2.7.0", 46 | "eslint-plugin-node": "^5.2.0", 47 | "eslint-plugin-promise": "^3.4.0", 48 | "eslint-plugin-standard": "^3.0.1", 49 | "eslint-plugin-vue": "^4.0.0", 50 | "extract-text-webpack-plugin": "^3.0.0", 51 | "file-loader": "^1.1.4", 52 | "friendly-errors-webpack-plugin": "^1.6.1", 53 | "html-webpack-plugin": "^2.30.1", 54 | "jest": "^22.0.4", 55 | "jest-serializer-vue": "^0.3.0", 56 | "nightwatch": "^0.9.12", 57 | "node-notifier": "^5.1.2", 58 | "optimize-css-assets-webpack-plugin": "^3.2.0", 59 | "ora": "^1.2.0", 60 | "portfinder": "^1.0.13", 61 | "postcss-import": "^11.0.0", 62 | "postcss-loader": "^2.0.8", 63 | "postcss-url": "^7.2.1", 64 | "rimraf": "^2.6.0", 65 | "selenium-server": "^3.0.1", 66 | "semver": "^5.3.0", 67 | "shelljs": "^0.7.6", 68 | "uglifyjs-webpack-plugin": "^1.1.1", 69 | "url-loader": "^0.5.8", 70 | "vue-jest": "^1.0.2", 71 | "vue-loader": "^13.3.0", 72 | "vue-style-loader": "^3.0.1", 73 | "vue-template-compiler": "^2.5.2", 74 | "webpack": "^3.6.0", 75 | "webpack-bundle-analyzer": "^2.9.0", 76 | "webpack-dev-server": "^2.9.1", 77 | "webpack-merge": "^4.1.0" 78 | }, 79 | "engines": { 80 | "node": ">= 6.0.0", 81 | "npm": ">= 3.0.0" 82 | }, 83 | "browserslist": [ 84 | "> 1%", 85 | "last 2 versions", 86 | "not ie <= 8" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 效果图如下: 2 | ![](https://user-gold-cdn.xitu.io/2019/4/17/16a2b3731bd4e631?w=892&h=760&f=gif&s=1427136) 3 | github地址如下: 4 | [github地址](https://github.com/munan2/boxDragger) 5 | ## 使用方法 6 | 引入js和对应的css 7 | 8 | ```js 9 | import Drag from '../../static/dragger.js' 10 | import './assets/css/dragger.css' 11 | ``` 12 | 之后,实例化 13 | 14 | ```js 15 | new Drag({ 16 | id: 'box-dragger', 17 | showAngle: true, 18 | isScale: false, 19 | showBorder: false 20 | }) 21 | new Drag({ 22 | id: 'box-dragger2', 23 | canZoom: false, 24 | canRotate: false 25 | }) 26 | new Drag({ 27 | id: 'img-box', 28 | showAngle: true, 29 | showPosition: true 30 | }) 31 | new Drag({ 32 | id: 'test' 33 | }) 34 | ``` 35 | ## 具体实现(封装细节) 36 | ### 功能细节整理: 37 | - 旋转 38 | - 缩放 39 | - 平移 40 | 41 | ### 技术难点: 42 | - 旋转时要注意盒子每一个点的位置发生了变化 43 | - 针对拖拽后的盒子的left和top都有变化,计算其left和top时需将其按照中心轴旋转摆正,再进行计算 44 | - 当且仅有一个盒子是被选中的盒子,点击哪个选中哪个。(当前页面多个实例化Drag对象时,如何保证操作互不影响) 45 | - 实现的两种不同方式: 46 | - 可以选中某元素,直接给该元素内部加上操作的点 47 | - 有一个pannel,选中某元素时,将这个pannel定位到该元素的位置上 48 | 49 | 这两种方式都实现过一次,第一种比较简单,但是第一种,不好控制选中某个元素才让操作点展示。 50 | 51 | ### 如何封装: 52 | 考虑如何让用户快速上手使用,可参考的点: 53 | - 用户需要传入什么必须的参数 54 | - 暴露给用户什么可设置的参数和方法 55 | 56 | ### 实现过程 57 | #### 可配置参数 58 | 字段| 说明| 是否必填 | 默认值 59 | ---|---|---| --- 60 | id | 目标元素id | 是 | 无 61 | container| 父容器id | 否 | body 62 | canRotate | 是否可以旋转 | 否 | true 63 | canZoom | 是否可以缩放| 否 | true 64 | canPull | 是否可以拉升 | 否 | true 65 | canMove | 是否可以平移 | 否 | true 66 | showAngle | 展示角度 | 否 | false 67 | showPosition | 展示位置 | 否 | false 68 | isScale | 是否等比例缩放 | 否 | true 69 | showBorder | 是否展示pannel的border | 否 | false 70 | 71 | #### 属性 72 | - canRotate 73 | - canZoom 74 | - canPull 75 | - canMove 76 | - showAngle 77 | - isScale 78 | - id 79 | - container 80 | - targetObj 81 | - pannelDom 操作divdom 82 | - ... 83 | 84 | 具体看图: 85 | ![](https://user-gold-cdn.xitu.io/2019/4/17/16a2be9b0c2cdde0?w=579&h=761&f=png&s=155256) 86 | 87 | #### 实现过程 88 | 1. 初始化参数 89 | 2. 初始化目标dom对象的位置:记录其: 90 | + left平距左 91 | + top 92 | + width 93 | + height 94 | + angle 95 | + rightBottomPoint 目标dom对象右下坐标 96 | + rightTopPoint 目标dom对象右上坐标 97 | + leftTopPoint 目标dom对象左上坐标 98 | + leftBottomPoint 目标dom对象左下坐标 99 | + leftMiddlePoint 目标dom对象左中坐标 100 | + rightMiddlePoint 目标dom对象右中坐标 101 | + topMiddlePoint 目标dom对象上中坐标 102 | + bottomMiddlePoint 目标dom对象下中坐标 103 | + centerPos 目标dom对象中心点坐标 104 | 3. 初始化pannel结构 105 | **当前的父容器中只有一个pannel结构**,每次实例化对象时,会判断一下如果当前这个父容器里已经存在id为pannel的结构,就将其子节点清空,按照当前实例化对象传进来的属性重新渲染pannel子结构。如果没有id为pannel的结构,就创建。 106 | 4. 初始化事件 107 | + 给pannelDom和targetObj绑定mousedown事件 108 | + 给document绑定mousemove和mouseup事件 109 | 110 | ```js 111 | initEvent () { 112 | document.addEventListener('mousemove', e => { 113 | e.preventDefault && e.preventDefault() 114 | this.moveChange(e, this.targetObj) 115 | }) 116 | document.addEventListener('mouseup', e => { 117 | this.moveLeave(this.targetObj) 118 | }) 119 | if (this.canMove) { 120 | // 外层给this.pannelDom添加mousedown事件,是在所有实例化结束后,panneldom被展示在最后一个实例化对象上,鼠标按下它时,触发moveInit事件 121 | this.pannelDom.onmousedown = e => { 122 | e.stopPropagation() 123 | this.moveInit(9, e, this.targetObj) 124 | } 125 | this.targetObj.onmousedown = e => { 126 | e.stopPropagation() 127 | this.moveInit(9, e, this.targetObj) 128 | this.initPannel() 129 | // 在点击其他未被选中元素时,pannel定位到该元素上,重写pannelDom事件,因为此时的this.pannelDom已经根据新的目标元素被重写 130 | this.pannelDom.onmousedown= e => { 131 | this.moveInit(9, e, this.targetObj) 132 | } 133 | } 134 | } 135 | } 136 | ``` 137 | 5. dom操作 138 | + 旋转操作 139 | + 鼠标按下时,记录当前鼠标位置距离box中心位置的y/x的反正切函数A1。 140 | 141 | ```js 142 | this.mouseInit = { 143 | x: Math.floor(e.clientX), 144 | y: Math.floor(e.clientY) 145 | } 146 | this.preRadian = Math.atan2(this.mouseInit.y - this.centerPos.y, this.mouseInit.x - this.centerPos.x) 147 | ``` 148 | + 鼠标移动时,记录再次计算鼠标位置距离box中心位置的y/x的反正切函数A2。 149 | 150 | ```js 151 | this.rotateCurrent = { 152 | x: Math.floor(e.clientX), 153 | y: Math.floor(e.clientY) 154 | } 155 | this.curRadian = Math.atan2(this.rotateCurrent.y - this.centerPos.y, this.rotateCurrent.x - this.centerPos.x) 156 | ``` 157 | + 求A2-A1,求出移动的弧度 158 | 159 | ```js 160 | this.tranformRadian = this.curRadian - this.preRadian 161 | ``` 162 | + 求出最后box的旋转角度,this.getRotate(target)是js中获取某dom元素的旋转角度的方法(粘贴过来的,亲测好使) 163 | 164 | ```js 165 | this.angle = this.getRotate(target) + Math.round(this.tranformRadian * 180 / Math.PI) 166 | this.preRadian = this.curRadian //鼠标移动的每一下都计算这个角度,所以每一下移动前的弧度值都上一次移动后的弧度值 167 | ``` 168 | + 计算旋转后box每个点的坐标,根据余弦公式,传入:旋转前每点坐标,旋转中心坐标和旋转角度 169 | 170 | ```js 171 | let disAngle = this.angle - this.initAngle 172 | this.rightBottomPoint = this.getRotatedPoint(this.initRightBottomPoint, this.centerPos, disAngle) 173 | this.rightTopPoint = this.getRotatedPoint(this.initRightTopPoint, this.centerPos, disAngle) 174 | this.leftTopPoint = this.getRotatedPoint(this.initLeftTopPoint, this.centerPos, disAngle) 175 | this.leftBottomPoint = this.getRotatedPoint(this.initLeftBottomPoint, this.centerPos, disAngle) 176 | this.leftMiddlePoint = this.getRotatedPoint(this.initLeftMiddlePoint, this.centerPos, disAngle) 177 | this.rightMiddlePoint = this.getRotatedPoint(this.initRightMiddlePoint, this.centerPos, disAngle) 178 | this.topMiddlePoint = this.getRotatedPoint(this.initTopMiddlePoint, this.centerPos, disAngle) 179 | this.bottomMiddlePoint = this.getRotatedPoint(this.initBottomMiddlePoint, this.centerPos, disAngle) 180 | ``` 181 | + 沿着一个方向拉升操作。 182 | + 沿着一个角缩放操作。 183 | 这两个操作,主要参考了一个大佬的拖拽思想实现的 [github wiki地址](https://github.com/shenhudong/snapping-demo/wiki) 184 | 185 | 6. 优化,mousemove事件添加节流函数 186 | 187 | ```js 188 | function throttle(fn, interval) { 189 | let canRun = true; 190 | return function () { 191 | if (!canRun) return; 192 | canRun = false; 193 | setTimeout(() => { 194 | fn.apply(this, arguments); 195 | canRun = true; 196 | }, interval); 197 | }; 198 | } 199 | let that = this 200 | document.addEventListener('mousemove', throttle(function (e) { 201 | e.preventDefault && e.preventDefault() 202 | that.moveChange(e, that.targetObj) 203 | }, 10)) 204 | ``` -------------------------------------------------------------------------------- /static/dragger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: dragger 3 | * @Author: zy 4 | * @LastEditors: zy 5 | * @Date: 2019-04-04 16:35:59 6 | * @LastEditTime: 2019-04-18 15:01:43 7 | */ 8 | /** 9 | * @description: 拖拽类 10 | * @param { 11 | * canRotate: true, 可旋转 12 | * canZoom: true, 可缩放 13 | * canPull: true, 可拉升 14 | * canMove: true, 可平移 15 | * showAngle: false, 展示角度 16 | * showPosition: false, 展示位置 17 | * isScale: true 是否等比例缩放 18 | * } 19 | * @return: 20 | */ 21 | export default class Drag { 22 | constructor (obj) { 23 | this.id = obj.id 24 | this.initParameter(obj) 25 | } 26 | /** 27 | * @description: 初始化参数 28 | * @param {type} 29 | * @return: 30 | */ 31 | initParameter (obj) { 32 | this.canRotate = obj.canRotate === undefined ? true : obj.canRotate 33 | this.canZoom = obj.canZoom === undefined ? true : obj.canZoom 34 | this.canPull = obj.canPull === undefined ? true : obj.canPull 35 | this.canMove = obj.canMove === undefined ? true : obj.canMove 36 | this.showBorder = obj.showBorder === undefined ? true : obj.showBorder 37 | this.showAngle = obj.showAngle 38 | this.showPosition = obj.showPosition 39 | this.isScale = obj.isScale === undefined ? true : obj.isScale 40 | this.container = obj.container ? document.getElementById(obj.container) : document.body 41 | this.targetObj = document.getElementById(obj.id) 42 | this.initPos() 43 | this.initPannel() 44 | this.initEvent() 45 | } 46 | /** 47 | * @description: 判断是否有pannel面板 48 | * @param {type} 49 | * @return: 50 | */ 51 | initPannel () { 52 | this.pannelDom = document.querySelector('#pannel') 53 | if (!this.pannelDom) { 54 | this.pannelDom = document.createElement('div') 55 | this.pannelDom.id = 'pannel' 56 | this.container.appendChild(this.pannelDom) 57 | } else { 58 | this.pannelDom.innerHTML = '' 59 | } 60 | this.initPannelDom() 61 | } 62 | /** 63 | * @description: 初始化事件 64 | * @param {type} 65 | * @return: 66 | */ 67 | initEvent () { 68 | function throttle(fn, interval) { 69 | let canRun = true; 70 | return function () { 71 | if (!canRun) return; 72 | canRun = false; 73 | setTimeout(() => { 74 | fn.apply(this, arguments); 75 | canRun = true; 76 | }, interval); 77 | }; 78 | } 79 | let that = this 80 | document.addEventListener('mousemove', throttle(function (e) { 81 | e.preventDefault && e.preventDefault() 82 | that.moveChange(e, that.targetObj) 83 | }, 10)) 84 | document.addEventListener('mouseup', e => { 85 | this.moveLeave(this.targetObj) 86 | }) 87 | // this.container.addEventListener('mousedown', e => { 88 | // this.mergeBox.flag = true 89 | // this.initPannel() 90 | // this.mergeBox.initPos = { 91 | // x: Math.floor(e.clientX), 92 | // y: Math.floor(e.clientY) 93 | // } 94 | // this.container.addEventListener('mousemove', e => { 95 | // if (this.mergeBox.flag) { 96 | // this.mergeBox.left = this.mergeBox.initPos.x 97 | // this.mergeBox.top = this.mergeBox.initPos.y 98 | // this.pannelDom.style.left = `${this.mergeBox.initPos.x}px` 99 | // this.pannelDom.style.top = `${this.mergeBox.initPos.y}px` 100 | // this.pannelDom.style.width = `${Math.floor(e.clientX) - this.mergeBox.initPos.x}px` 101 | // this.pannelDom.style.height = `${Math.floor(e.clientY) - this.mergeBox.initPos.y}px` 102 | // } 103 | // }) 104 | // }) 105 | if (this.canMove) { 106 | // 外层给this.pannelDom添加mousedown事件,是在所有实例化结束后,panneldom被展示在最后一个实例化对象上,鼠标按下它时,触发moveInit事件 107 | this.pannelDom.onmousedown = e => { 108 | e.stopPropagation() 109 | this.moveInit(9, e, this.targetObj) 110 | } 111 | this.targetObj.onmousedown = e => { 112 | e.stopPropagation() 113 | this.moveInit(9, e, this.targetObj) 114 | this.initPannel() 115 | // 在点击其他未被选中元素时,pannel定位到该元素上,重写pannelDom事件,因为此时的this.pannelDom已经根据新的目标元素被重写 116 | this.pannelDom.onmousedown= e => { 117 | this.moveInit(9, e, this.targetObj) 118 | } 119 | } 120 | } 121 | } 122 | /** 123 | * @description: 初始化Pannel内部dom结构 124 | * @param {} 125 | * @return: null 126 | */ 127 | initPannelDom () { 128 | this.pannelDom.style.left = `${this.left}px` 129 | this.pannelDom.style.top = `${this.top}px` 130 | this.pannelDom.style.width = `${this.width}px` 131 | this.pannelDom.style.height = `${this.height}px` 132 | this.pannelDom.style.transform = `rotate(${this.angle}deg)` 133 | if (this.canRotate) { 134 | let rotatePoint = document.createElement('span') 135 | rotatePoint.className = `${this.id}-dragger-rotate-point dragger-rotate-point` 136 | if (this.showAngle) { 137 | this.angleDom = document.createElement('span') 138 | this.angleDom.className = `dragger-rotate-angle ${this.id}-dragger-rotate-angle` 139 | this.angleDom.style.display = 'none' 140 | this.pannelDom.appendChild(this.angleDom) 141 | } 142 | this.pannelDom.appendChild(rotatePoint) 143 | rotatePoint.addEventListener('mousedown', e => { 144 | e.stopPropagation() 145 | this.moveInit(0, e, this.targetObj) 146 | }) 147 | } 148 | if (this.showBorder) { 149 | for (let i = 1; i < 5; i++) { 150 | let baseLine = document.createElement('span') 151 | baseLine.className = `${this.id}-dragger-base-line dragger-base-line dragger-line${i}` 152 | this.pannelDom.appendChild(baseLine) 153 | } 154 | } 155 | if (this.canZoom) { 156 | for (let i = 1; i < 5; i++) { 157 | let zoomPoint = document.createElement('span') 158 | zoomPoint.className = `${this.id}-dragger-base-piont dragger-base-piont dragger-zoom${i}` 159 | this.pannelDom.appendChild(zoomPoint) 160 | zoomPoint.addEventListener('mousedown', e => { 161 | e.stopPropagation() 162 | this.moveInit(i, e, this.targetObj) 163 | }) 164 | } 165 | } 166 | if (this.canPull) { 167 | for (let i = 5; i < 9; i++) { 168 | let pullPoint = document.createElement('span') 169 | pullPoint.className = `${this.id}-dragger-base-piont dragger-base-piont dragger-pull${i}` 170 | this.pannelDom.appendChild(pullPoint) 171 | pullPoint.addEventListener('mousedown', e => { 172 | e.stopPropagation() 173 | this.moveInit(i, e, this.targetObj) 174 | }) 175 | } 176 | } 177 | if (this.canMove && this.showPosition) { 178 | this.positionDom = document.createElement('span') 179 | this.positionDom.className = 'dragger-position' 180 | this.pannelDom.appendChild(this.positionDom) 181 | this.positionDom.style.display = 'none' 182 | } 183 | } 184 | /** 185 | * @description: 初始化targetObj的位置 186 | * @param {type} 187 | * @return: 188 | */ 189 | initPos () { 190 | this.left = this.targetObj.offsetLeft 191 | this.top = this.targetObj.offsetTop 192 | this.width = this.targetObj.offsetWidth 193 | this.height = this.targetObj.offsetHeight 194 | this.angle = this.getRotate(this.targetObj) 195 | // 记录初始盒子位置右下 196 | this.rightBottomPoint = { 197 | x: this.width + this.left, 198 | y: this.height + this.top 199 | } 200 | // 记录初始盒子右上角位置 201 | this.rightTopPoint = { 202 | x: this.width + this.left, 203 | y: this.top 204 | } 205 | // 记录左上角的位置 206 | this.leftTopPoint = { 207 | x: this.left, 208 | y: this.top 209 | } 210 | // 左下 211 | this.leftBottomPoint = { 212 | x: this.left, 213 | y: this.top + this.height 214 | } 215 | // 左中 216 | this.leftMiddlePoint = { 217 | x: this.left, 218 | y: this.top + this.height / 2 219 | } 220 | // 右中 221 | this.rightMiddlePoint = { 222 | x: this.left + this.width, 223 | y: this.top + this.height / 2 224 | } 225 | // 上中 226 | this.topMiddlePoint = { 227 | x: this.left + this.width / 2, 228 | y: this.top 229 | } 230 | // 下中 231 | this.bottomMiddlePoint= { 232 | x: this.left + this.width / 2, 233 | y: this.top + this.height 234 | } 235 | // 记录中心位置 236 | this.centerPos = { 237 | x: this.left + this.width / 2, 238 | y: this.top + this.height / 2 239 | } 240 | } 241 | moveInit (type, e, target) { 242 | this.type = Number(type) 243 | this.mouseInit = { 244 | x: Math.floor(e.clientX), 245 | y: Math.floor(e.clientY) 246 | } 247 | this.scale = target.offsetWidth / target.offsetHeight 248 | this.initAngle = this.angle 249 | this.initRightBottomPoint = this.rightBottomPoint 250 | this.initRightTopPoint = this.rightTopPoint 251 | this.initLeftTopPoint = this.leftTopPoint 252 | this.initLeftBottomPoint = this.leftBottomPoint 253 | this.initLeftMiddlePoint = this.leftMiddlePoint 254 | this.initRightMiddlePoint = this.rightMiddlePoint 255 | this.initTopMiddlePoint = this.topMiddlePoint 256 | this.initBottomMiddlePoint = this.bottomMiddlePoint 257 | this.initCenterPos = this.centerPos 258 | this.initPosition = { 259 | x: this.left, 260 | y: this.top 261 | } 262 | if (type === 0) { 263 | this.rotateFlag = true 264 | this.preRadian = Math.atan2(this.mouseInit.y - this.centerPos.y, this.mouseInit.x - this.centerPos.x) 265 | } else if (type < 10) { 266 | this.canChange = true 267 | } 268 | } 269 | moveChange (e, target) { 270 | let newHeight, newWidth, newRightBottomPoint, newLeftTopPoint, newLeftBottomPoint, newRightTopPoint, rotateCurrentPos 271 | switch (this.type) { 272 | case 0: 273 | if (this.rotateFlag) { 274 | this.rotateCurrent = { 275 | x: Math.floor(e.clientX), 276 | y: Math.floor(e.clientY) 277 | } 278 | this.curRadian = Math.atan2(this.rotateCurrent.y - this.centerPos.y, this.rotateCurrent.x - this.centerPos.x) 279 | this.tranformRadian = this.curRadian - this.preRadian 280 | this.angle = this.getRotate(target) + Math.round(this.tranformRadian * 180 / Math.PI) 281 | target.style.transform = `rotate(${this.angle}deg)` 282 | this.pannelDom.style.transform = `rotate(${this.angle}deg)` 283 | if (this.showAngle) { 284 | this.angleDom.innerHTML = this.angle 285 | this.angleDom.style.display = 'block' 286 | } 287 | this.preRadian = this.curRadian 288 | // 重新计算旋转后四个点的坐标变化 289 | let disAngle = this.angle - this.initAngle 290 | this.rightBottomPoint = this.getRotatedPoint(this.initRightBottomPoint, this.centerPos, disAngle) 291 | this.rightTopPoint = this.getRotatedPoint(this.initRightTopPoint, this.centerPos, disAngle) 292 | this.leftTopPoint = this.getRotatedPoint(this.initLeftTopPoint, this.centerPos, disAngle) 293 | this.leftBottomPoint = this.getRotatedPoint(this.initLeftBottomPoint, this.centerPos, disAngle) 294 | this.leftMiddlePoint = this.getRotatedPoint(this.initLeftMiddlePoint, this.centerPos, disAngle) 295 | this.rightMiddlePoint = this.getRotatedPoint(this.initRightMiddlePoint, this.centerPos, disAngle) 296 | this.topMiddlePoint = this.getRotatedPoint(this.initTopMiddlePoint, this.centerPos, disAngle) 297 | this.bottomMiddlePoint = this.getRotatedPoint(this.initBottomMiddlePoint, this.centerPos, disAngle) 298 | } 299 | case 1: 300 | if (this.canChange) { 301 | this.centerPos = { 302 | x: Math.floor((e.clientX + this.rightBottomPoint.x) / 2), 303 | y: Math.floor((e.clientY + this.rightMiddlePoint.y) / 2) 304 | } 305 | // 计算旋转为水平角度的两点坐标 306 | newLeftTopPoint = this.getRotatedPoint({ 307 | x: e.clientX, 308 | y: e.clientY 309 | }, this.centerPos, -this.initAngle) 310 | newRightBottomPoint = this.getRotatedPoint(this.rightBottomPoint, this.centerPos, -this.initAngle) 311 | newWidth = newRightBottomPoint.x - newLeftTopPoint.x 312 | newHeight = newRightBottomPoint.y - newLeftTopPoint.y 313 | if (this.isScale) { 314 | if (newWidth / newHeight > this.scale) { 315 | newLeftTopPoint.x = newLeftTopPoint.x + Math.abs(newWidth - newHeight * this.scale) 316 | newWidth = newHeight * this.scale 317 | } else { 318 | newLeftTopPoint.y = newLeftTopPoint.y + Math.abs(newHeight - newWidth / this.scale) 319 | newHeight = newWidth / this.scale 320 | } 321 | // 计算出左上角等比角度变换后水平坐标后,再计算旋转后的角度 322 | var rotateLeftTopPoint = this.getRotatedPoint(newLeftTopPoint, this.centerPos, this.initAngle) 323 | this.centerPos = { 324 | x: Math.floor((rotateLeftTopPoint.x + this.rightBottomPoint.x) / 2), 325 | y: Math.floor((rotateLeftTopPoint.y + this.rightBottomPoint.y) / 2) 326 | } 327 | newLeftTopPoint = this.getRotatedPoint(rotateLeftTopPoint, this.centerPos, -this.initAngle) 328 | newRightBottomPoint = this.getRotatedPoint(this.rightBottomPoint, this.centerPos, -this.initAngle) 329 | newWidth = newRightBottomPoint.x - newLeftTopPoint.x 330 | newHeight = newRightBottomPoint.y - newLeftTopPoint.y 331 | } 332 | if (newWidth <= 12) { 333 | newWidth = 12 334 | newHeight = Math.floor(newWidth / this.scale) 335 | newLeftTopPoint.x = newRightBottomPoint.x - newWidth 336 | newLeftTopPoint.y = newRightBottomPoint.y - newHeight 337 | } 338 | if (newHeight <= 12) { 339 | newHeight = 12 340 | newWidth = Math.floor(newHeight * this.scale) 341 | newLeftTopPoint.y = newRightBottomPoint.y - newHeight 342 | newLeftTopPoint.x = newRightBottomPoint.x - newWidth 343 | } 344 | if (newHeight > 12 && newWidth > 12) { 345 | this.left = newLeftTopPoint.x 346 | this.top = newLeftTopPoint.y 347 | this.width = newWidth 348 | this.height = newHeight 349 | target.style.left = `${this.left}px` 350 | target.style.top = `${this.top}px` 351 | target.style.width = `${this.width}px` 352 | target.style.height = `${this.height}px` 353 | this.pannelDom.style.left = `${this.left}px` 354 | this.pannelDom.style.top = `${this.top}px` 355 | this.pannelDom.style.width = `${this.width}px` 356 | this.pannelDom.style.height = `${this.height}px` 357 | } 358 | } 359 | break; 360 | case 2: 361 | if (this.canChange) { 362 | this.centerPos = { 363 | x: Math.floor((e.clientX + this.rightTopPoint.x) / 2), 364 | y: Math.floor((e.clientY + this.rightTopPoint.y) / 2) 365 | } 366 | newLeftBottomPoint = this.getRotatedPoint({ 367 | x: e.clientX, 368 | y: e.clientY 369 | }, this.centerPos, -this.initAngle) 370 | newRightTopPoint = this.getRotatedPoint(this.rightTopPoint, this.centerPos, -this.initAngle) 371 | newWidth = newRightTopPoint.x - newLeftBottomPoint.x 372 | newHeight = newLeftBottomPoint.y - newRightTopPoint.y 373 | if (this.isScale) { 374 | if (newWidth / newHeight > this.scale) { 375 | newLeftBottomPoint.x = newLeftBottomPoint.x + Math.abs(newWidth - newHeight * this.scale) 376 | newWidth = newHeight * this.scale 377 | } else { 378 | newLeftBottomPoint.y = newLeftBottomPoint.y - Math.abs(newHeight - newWidth / this.scale) 379 | newHeight = newWidth / this.scale 380 | } 381 | var rotatedLeftBottomPoint = this.getRotatedPoint(newLeftBottomPoint, this.centerPos, this.initAngle) 382 | this.centerPos = { 383 | x: Math.floor((rotatedLeftBottomPoint.x + this.rightTopPoint.x) / 2), 384 | y: Math.floor((rotatedLeftBottomPoint.y + this.rightTopPoint.y) / 2) 385 | } 386 | newLeftBottomPoint = this.getRotatedPoint(rotatedLeftBottomPoint, this.centerPos, -this.initAngle) 387 | newRightTopPoint = this.getRotatedPoint(this.rightTopPoint, this.centerPos, -this.initAngle) 388 | newWidth = newRightTopPoint.x - newLeftBottomPoint.x 389 | newHeight = newLeftBottomPoint.y - newRightTopPoint.y 390 | } 391 | if (newHeight <= 12) { 392 | newHeight = 12 393 | newWidth = Math.floor(newHeight * this.scale) 394 | newLeftBottomPoint = { 395 | x: newRightTopPoint.x - newWidth, 396 | y: newRightTopPoint.y + newHeight 397 | } 398 | } 399 | if (newWidth <= 12) { 400 | newWidth = 12 401 | newHeight = Math.floor(newWidth / this.scale) 402 | newLeftBottomPoint = { 403 | x: newRightTopPoint.x - newWidth, 404 | y: newRightTopPoint.y + newHeight 405 | } 406 | } 407 | if (newHeight > 12 && newHeight > 12) { 408 | this.left = newLeftBottomPoint.x 409 | this.top = newRightTopPoint.y 410 | this.width = newWidth 411 | this.height = newHeight 412 | target.style.left = `${this.left}px` 413 | target.style.top = `${this.top}px` 414 | target.style.width = `${this.width}px` 415 | target.style.height = `${this.height}px` 416 | this.pannelDom.style.left = `${this.left}px` 417 | this.pannelDom.style.top = `${this.top}px` 418 | this.pannelDom.style.width = `${this.width}px` 419 | this.pannelDom.style.height = `${this.height}px` 420 | } 421 | } 422 | break; 423 | case 3: 424 | if (this.canChange) { 425 | this.centerPos = { 426 | x: Math.floor((e.clientX + this.leftBottomPoint.x) / 2), 427 | y: Math.floor((e.clientY + this.leftBottomPoint.y) / 2) 428 | } 429 | newRightTopPoint = this.getRotatedPoint({ 430 | x: e.clientX, 431 | y: e.clientY 432 | }, this.centerPos, -this.initAngle) 433 | newLeftBottomPoint = this.getRotatedPoint(this.leftBottomPoint, this.centerPos, -this.initAngle) 434 | newWidth = newRightTopPoint.x - newLeftBottomPoint.x 435 | newHeight = newLeftBottomPoint.y - newRightTopPoint.y 436 | if (this.isScale) { 437 | if (newWidth / newHeight > this.scale) { 438 | newRightTopPoint.x = newRightTopPoint.x - Math.abs(newWidth - newHeight * this.scale) 439 | newWidth = newHeight * this.scale 440 | } else { 441 | newRightTopPoint.y = newRightTopPoint.y + Math.abs(newHeight - newWidth / this.scale) 442 | newHeight = newWidth / this.scale 443 | } 444 | let rotatedRightTopPoint = this.getRotatedPoint(newRightTopPoint, this.centerPos, this.initAngle) 445 | this.centerPos = { 446 | x: Math.floor((rotatedRightTopPoint.x + this.leftBottomPoint.x) / 2), 447 | y: Math.floor((rotatedRightTopPoint.y + this.leftBottomPoint.y) / 2) 448 | } 449 | newLeftBottomPoint = this.getRotatedPoint(this.leftBottomPoint, this.centerPos, -this.initAngle) 450 | newRightTopPoint = this.getRotatedPoint(rotatedRightTopPoint, this.centerPos, -this.initAngle) 451 | newWidth = newRightTopPoint.x - newLeftBottomPoint.x 452 | newHeight = newLeftBottomPoint.y - newRightTopPoint.y 453 | } 454 | if (newWidth <= 12) { 455 | newWidth = 12 456 | newHeight = Math.floor(newWidth / this.scale) 457 | newRightTopPoint = { 458 | x: newLeftBottomPoint.x + newWidth, 459 | y: newLeftBottomPoint.y - newHeight 460 | } 461 | } 462 | if (newHeight <= 12) { 463 | newHeight = 12 464 | newWidth = Math.floor(newHeight * this.scale) 465 | newRightTopPoint = { 466 | x: newLeftBottomPoint.x + newWidth, 467 | y: newLeftBottomPoint.y - newHeight 468 | } 469 | } 470 | if (newWidth > 12 && newHeight > 12) { 471 | this.left = newLeftBottomPoint.x 472 | this.top = newRightTopPoint.y 473 | this.width = newWidth 474 | this.height = newHeight 475 | target.style.left = `${this.left}px` 476 | target.style.top = `${this.top}px` 477 | target.style.width = `${this.width}px` 478 | target.style.height = `${this.height}px` 479 | this.pannelDom.style.left = `${this.left}px` 480 | this.pannelDom.style.top = `${this.top}px` 481 | this.pannelDom.style.width = `${this.width}px` 482 | this.pannelDom.style.height = `${this.height}px` 483 | } 484 | } 485 | break; 486 | case 4: 487 | if (this.canChange) { 488 | this.centerPos = { 489 | x: Math.floor((e.clientX + this.leftTopPoint.x) / 2), 490 | y: Math.floor((e.clientY + this.leftTopPoint.y) / 2) 491 | } 492 | newRightBottomPoint = this.getRotatedPoint({ 493 | x: e.clientX, 494 | y: e.clientY 495 | }, this.centerPos, -this.initAngle) 496 | newLeftTopPoint = this.getRotatedPoint(this.leftTopPoint, this.centerPos, -this.initAngle) 497 | newWidth = newRightBottomPoint.x - newLeftTopPoint.x 498 | newHeight = newRightBottomPoint.y - newLeftTopPoint.y 499 | if (this.isScale) { 500 | if (newWidth / newHeight > this.scale) { 501 | newRightBottomPoint.x = newRightBottomPoint.x - Math.abs(newWidth - newHeight * this.scale) 502 | newWidth = newHeight * this.scale 503 | } else { 504 | newRightBottomPoint.y = newRightBottomPoint.y - Math.abs(newHeight - newWidth / this.scale) 505 | newHeight = newWidth / this.scale 506 | } 507 | let rotatedRightBottomPoint = this.getRotatedPoint(newRightBottomPoint, this.centerPos, this.initAngle) 508 | this.centerPos = { 509 | x: Math.floor((rotatedRightBottomPoint.x + this.leftTopPoint.x) / 2), 510 | y: Math.floor((rotatedRightBottomPoint.y + this.leftTopPoint.y) / 2) 511 | } 512 | newLeftTopPoint = this.getRotatedPoint(this.leftTopPoint, this.centerPos, -this.initAngle) 513 | newRightBottomPoint = this.getRotatedPoint(rotatedRightBottomPoint, this.centerPos, -this.initAngle) 514 | newWidth = newRightBottomPoint.x - newLeftTopPoint.x 515 | newHeight = newRightBottomPoint.y - newLeftTopPoint.y 516 | } 517 | if (newWidth <= 12) { 518 | newWidth = 12 519 | newHeight = Math.floor(newWidth / this.scale) 520 | newRightBottomPoint = { 521 | x: newLeftTopPoint.x + newWidth, 522 | y: newLeftTopPoint.y + newHeight 523 | } 524 | } 525 | if (newHeight <= 12) { 526 | newHeight = 12 527 | newWidth = Math.floor(newHeight * this.scale) 528 | newRightBottomPoint = { 529 | x: newLeftTopPoint.x + newWidth, 530 | y: newLeftTopPoint.y + newHeight 531 | } 532 | } 533 | if (newWidth > 12 && newHeight > 12) { 534 | this.left = newLeftTopPoint.x 535 | this.top = newLeftTopPoint.y 536 | this.width = newWidth 537 | this.height = newHeight 538 | target.style.left = `${this.left}px` 539 | target.style.top = `${this.top}px` 540 | target.style.width = `${this.width}px` 541 | target.style.height = `${this.height}px` 542 | this.pannelDom.style.left = `${this.left}px` 543 | this.pannelDom.style.top = `${this.top}px` 544 | this.pannelDom.style.width = `${this.width}px` 545 | this.pannelDom.style.height = `${this.height}px` 546 | } 547 | } 548 | break; 549 | case 5: 550 | if (this.canChange) { 551 | // 计算出鼠标现在所在的点,经过以bottommiddle点反向旋转后的位置,从而得到其y轴坐标点与topmiddle的x轴坐标结合,求出旋转后图形的topmiddle 552 | rotateCurrentPos = this.getRotatedPoint({ 553 | x: e.clientX, 554 | y: e.clientY 555 | }, this.bottomMiddlePoint, -this.initAngle) 556 | let rotatedTopMiddlePoint = { 557 | x: this.bottomMiddlePoint.x, 558 | y: rotateCurrentPos.y 559 | } 560 | newHeight = this.bottomMiddlePoint.y - rotatedTopMiddlePoint.y 561 | 562 | // newHeight = Math.sqrt(Math.pow((this.topMiddlePoint.x - this.bottomMiddlePoint.x), 2) + Math.pow((this.topMiddlePoint.y - this.bottomMiddlePoint.y), 2)) 563 | if (newHeight <= 12) { 564 | newHeight = 12 565 | rotatedTopMiddlePoint.y = this.bottomMiddlePoint.y - 12 566 | } 567 | // 计算转回去的topmiddle点坐标 568 | this.topMiddlePoint = this.getRotatedPoint(rotatedTopMiddlePoint, this.bottomMiddlePoint, this.initAngle) 569 | this.centerPos = { 570 | x: (this.topMiddlePoint.x + this.bottomMiddlePoint.x) / 2, 571 | y: (this.topMiddlePoint.y + this.bottomMiddlePoint.y) / 2 572 | } 573 | this.left = this.centerPos.x - target.offsetWidth / 2 574 | this.top = this.centerPos.y - newHeight / 2 575 | this.height = newHeight 576 | target.style.left = `${this.left}px` 577 | target.style.height = `${this.height}px` 578 | target.style.top = `${this.top}px` 579 | this.pannelDom.style.left = `${this.left}px` 580 | this.pannelDom.style.height = `${this.height}px` 581 | this.pannelDom.style.top = `${this.top}px` 582 | } 583 | break; 584 | case 6: 585 | if (this.canChange) { 586 | rotateCurrentPos = this.getRotatedPoint({ 587 | x: e.clientX, 588 | y: e.clientY 589 | }, this.rightMiddlePoint, -this.initAngle) 590 | let rotatedLeftMiddlePonit = { 591 | x: rotateCurrentPos.x, 592 | y: this.rightMiddlePoint.y 593 | } 594 | newWidth = this.rightMiddlePoint.x - rotatedLeftMiddlePonit.x 595 | if (newWidth <= 12) { 596 | newWidth = 12 597 | rotatedLeftMiddlePonit.x = this.rightMiddlePoint.x - 12 598 | } 599 | this.leftMiddlePoint = this.getRotatedPoint(rotatedLeftMiddlePonit, this.rightMiddlePoint, this.initAngle) 600 | this.centerPos = { 601 | x: Math.floor((this.leftMiddlePoint.x + this.rightMiddlePoint.x) / 2), 602 | y: Math.floor((this.leftMiddlePoint.y + this.rightMiddlePoint.y) / 2) 603 | } 604 | this.left = this.centerPos.x - newWidth / 2 605 | this.top = this.centerPos.y - target.offsetHeight / 2 606 | this.width = newWidth 607 | target.style.left = `${this.left}px` 608 | target.style.top = `${this.top}px` 609 | target.style.width = `${this.width}px` 610 | this.pannelDom.style.left = `${this.left}px` 611 | this.pannelDom.style.top = `${this.top}px` 612 | this.pannelDom.style.width = `${this.width}px` 613 | } 614 | break; 615 | case 7: 616 | if (this.canChange) { 617 | rotateCurrentPos = this.getRotatedPoint({ 618 | x: e.clientX, 619 | y: e.clientY 620 | }, this.topMiddlePoint, -this.initAngle) 621 | let rotatedBottomMiddlePoint = { 622 | x: this.topMiddlePoint.x, 623 | y: rotateCurrentPos.y 624 | } 625 | newHeight = rotatedBottomMiddlePoint.y - this.topMiddlePoint.y 626 | if (newHeight <= 12) { 627 | newHeight = 12 628 | rotatedBottomMiddlePoint.y = this.topMiddlePoint.y - 12 629 | } 630 | this.bottomMiddlePoint = this.getRotatedPoint(rotatedBottomMiddlePoint, this.topMiddlePoint, this.initAngle) 631 | this.centerPos = { 632 | x: Math.floor((this.bottomMiddlePoint.x + this.topMiddlePoint.x) / 2), 633 | y: Math.floor((this.bottomMiddlePoint.y + this.topMiddlePoint.y) / 2) 634 | } 635 | this.left = this.centerPos.x - target.offsetWidth / 2 636 | this.top = this.centerPos.y - newHeight / 2 637 | this.height = newHeight 638 | target.style.left = `${this.left}px` 639 | target.style.top = `${this.top}px` 640 | target.style.height = `${this.height}px` 641 | this.pannelDom.style.left = `${this.left}px` 642 | this.pannelDom.style.top = `${this.top}px` 643 | this.pannelDom.style.height = `${this.height}px` 644 | } 645 | break; 646 | case 8: 647 | if (this.canChange) { 648 | rotateCurrentPos = this.getRotatedPoint({ 649 | x: e.clientX, 650 | y: e.clientY 651 | }, this.leftMiddlePoint, -this.initAngle) 652 | let rotatedRightMiddlePoint = { 653 | x: rotateCurrentPos.x, 654 | y: this.leftMiddlePoint.y 655 | } 656 | newWidth = rotatedRightMiddlePoint.x - this.leftMiddlePoint.x 657 | if (newWidth <= 12) { 658 | newWidth = 12 659 | rotatedRightMiddlePoint.x = this.leftMiddlePoint.x + 12 660 | } 661 | this.rightMiddlePoint = this.getRotatedPoint(rotatedRightMiddlePoint, this.leftMiddlePoint, this.initAngle) 662 | this.centerPos = { 663 | x: Math.floor((this.leftMiddlePoint.x + this.rightMiddlePoint.x) / 2), 664 | y: Math.floor((this.leftMiddlePoint.y + this.rightMiddlePoint.y) / 2) 665 | } 666 | this.left = this.centerPos.x - newWidth / 2 667 | this.top = this.centerPos.y - target.offsetHeight / 2 668 | this.width = newWidth 669 | target.style.left = `${this.left}px` 670 | target.style.top = `${this.top}px` 671 | target.style.width = `${newWidth}px` 672 | this.pannelDom.style.left = `${this.left}px` 673 | this.pannelDom.style.top = `${this.top}px` 674 | this.pannelDom.style.width = `${this.width}px` 675 | } 676 | break; 677 | case 9: 678 | if (this.canChange) { 679 | let dis = { 680 | x: Math.floor(e.clientX - this.mouseInit.x), 681 | y: Math.floor(e.clientY - this.mouseInit.y) 682 | } 683 | this.left = this.initPosition.x + dis.x 684 | this.top = this.initPosition.y + dis.y 685 | target.style.left = `${this.left}px` 686 | target.style.top = `${this.top}px` 687 | this.pannelDom.style.left = `${this.left}px` 688 | this.pannelDom.style.top = `${this.top}px` 689 | this.pannelDom.style.width = `${this.width}px` 690 | this.pannelDom.style.height = `${this.height}px` 691 | this.centerPos = { 692 | x: this.initCenterPos.x + dis.x, 693 | y: this.initCenterPos.y + dis.y 694 | } 695 | if (this.showPosition) { 696 | this.positionDom.style.display = 'inline-block' 697 | this.positionDom.innerHTML = `X: ${this.left} Y: ${this.top}` 698 | } 699 | } 700 | break; 701 | } 702 | } 703 | moveLeave () { 704 | if (this.canChange || this.rotateFlag) { 705 | this.rotateFlag = false 706 | this.canChange = false 707 | if (this.angleDom) this.angleDom.style.display = 'none' 708 | if (this.positionDom) this.positionDom.style.display = 'none' 709 | this.getTransferPosition(this.left, this.top, this.width, this.height, this.angle, this.centerPos) 710 | } 711 | } 712 | getRotate (target) { 713 | var st = window.getComputedStyle(target, null); 714 | var tr = st.getPropertyValue("-webkit-transform") || 715 | st.getPropertyValue("-moz-transform") || 716 | st.getPropertyValue("-ms-transform") || 717 | st.getPropertyValue("-o-transform") || 718 | st.getPropertyValue("transform") || "FAIL"; 719 | // With rotate(30deg)... 720 | // matrix(0.866025, 0.5, -0.5, 0.866025, 0px, 0px) 721 | if (tr !== 'none') { 722 | var values = tr.split('(')[1].split(')')[0].split(','); 723 | var a = values[0]; 724 | var b = values[1]; 725 | var c = values[2]; 726 | var d = values[3]; 727 | var scale = Math.sqrt(a * a + b * b); 728 | // arc sin, convert from radians to degrees, round 729 | var sin = b / scale; 730 | // next line works for 30deg but not 130deg (returns 50); 731 | // var angle = Math.round(Math.asin(sin) * (180/Math.PI)); 732 | var angle = Math.round(Math.atan2(b, a) * (180 / Math.PI)) 733 | if (angle < 0) { 734 | angle = 360 + angle 735 | } 736 | return angle 737 | } else { 738 | return 0 739 | } 740 | } 741 | getRotatedPoint (curPos, centerPos, angle) { 742 | return { 743 | x: Math.floor((curPos.x - centerPos.x) * Math.cos(Math.PI / 180 * angle) - (curPos.y - centerPos.y) * Math.sin(Math.PI / 180 * angle) + centerPos.x), 744 | y: Math.floor((curPos.x - centerPos.x) * Math.sin(Math.PI / 180 * angle) + (curPos.y - centerPos.y) * Math.cos(Math.PI / 180 * angle) + centerPos.y) 745 | } 746 | } 747 | getTransferPosition (left, top, width, height, angle, center) { 748 | // 计算变换后的方框四个角的位置 749 | var a1 = { 750 | x: left, 751 | y: top 752 | } 753 | var a2 = { 754 | x: left, 755 | y: top + height 756 | } 757 | var a3 = { 758 | x: left + width, 759 | y: top 760 | } 761 | var a4 = { 762 | x: left + width, 763 | y: top + height 764 | } 765 | var a5 = { 766 | x: left, 767 | y: top + height / 2 768 | } 769 | var a6 = { 770 | x: left + width, 771 | y: top + height / 2 772 | } 773 | var a7 = { 774 | x: left + width / 2, 775 | y: top 776 | } 777 | var a8 = { 778 | x: left + width / 2, 779 | y: top + height 780 | } 781 | this.leftTopPoint = this.getRotatedPoint(a1, center, angle) 782 | this.leftBottomPoint = this.getRotatedPoint(a2, center, angle) 783 | this.rightTopPoint = this.getRotatedPoint(a3, center, angle) 784 | this.rightBottomPoint = this.getRotatedPoint(a4, center, angle) 785 | this.leftMiddlePoint = this.getRotatedPoint(a5, center, angle) 786 | this.rightMiddlePoint = this.getRotatedPoint(a6, center, angle) 787 | this.topMiddlePoint = this.getRotatedPoint(a7, center, angle) 788 | this.bottomMiddlePoint = this.getRotatedPoint(a8, center, angle) 789 | } 790 | } 791 | --------------------------------------------------------------------------------