├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── lib
├── assets
│ └── styles
│ │ └── main.css
├── components
│ ├── Hotzone.vue
│ └── Zone.vue
├── directives
│ ├── addItem.js
│ ├── changeSize.js
│ └── dragItem.js
├── index.js
└── utils
│ └── index.js
├── package.json
├── postcss.config.js
├── public
├── favicon.png
└── index.html
├── src
├── App.vue
└── main.js
├── tests
└── unit
│ ├── components
│ ├── hotzone.spec.js
│ └── zone.spec.js
│ └── utils.spec.js
├── vue.config.js
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | jest: true
6 | },
7 | 'extends': [
8 | 'plugin:vue/essential'
9 | ],
10 | rules: {
11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
13 | },
14 | parserOptions: {
15 | parser: 'babel-eslint'
16 | },
17 | overrides: [
18 | {
19 | files: [
20 | '**/__tests__/*.{j,t}s?(x)',
21 | '**/tests/unit/**/*.spec.{j,t}s?(x)'
22 | ],
23 | env: {
24 | jest: true
25 | }
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # Coverage directory used by tools like istanbul
6 | coverage
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | __tests__
2 | coverage
3 | dist
4 | publish
5 | src
6 | .eslintrc
7 | .gitignore
8 | .babelrc
9 | .postcssrc.js
10 | babel.config.js
11 | jest.config.js
12 | vue.config.js
13 | yarn.lock
14 | README.md
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | install:
5 | - yarn
6 | script:
7 | - yarn lint
8 | - yarn test:unit
9 | after_success:
10 | - codecov
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present, OrangeXC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Introduction
12 |
13 | A vue2 hotzone component
14 |
15 | ### [Demo](https://vue-hotzone.orangexc.xyz/) | [案例](https://vue-hotzone.orangexc.xyz/)
16 |
17 | ## Install
18 |
19 | ```bash
20 | npm i vue-hotzone --save
21 | # or
22 | yarn add vue-hotzone
23 | ```
24 |
25 | ## Use
26 |
27 | ```js
28 | // Use in component
29 | import hotzone from 'vue-hotzone'
30 |
31 | export default {
32 | components: {
33 | hotzone
34 | }
35 | }
36 |
37 | // Use in global
38 | import hotzone from 'vue-hotzone'
39 |
40 | Vue.component(hotzone.name, hotzone)
41 |
42 | // or
43 | Vue.use(hotzone)
44 | ```
45 |
46 | ```html
47 |
48 | ```
49 |
50 | ## Options
51 |
52 | ### Attributes
53 | You can set them to your data function
54 |
55 | | Attribute | Type | Description | Keys |
56 | |:----------|:-------|:---------------------------------|:-------------------------------------------|
57 | | image | String | image of hotzone(required: true) | |
58 | | max | Number | max number of zones | |
59 | | zonesInit | Array | init zones | item(heightPer, leftPer, topPer, widthPer) |
60 |
61 | ### Events
62 |
63 | | Event Name | Description | Parameters |
64 | |:-----------|:-------------------------------------------------------------------------|:--------------------------------|
65 | | change | triggers when the zones changes | the array of the zones |
66 | | add | triggers when the zone add | the add zone item |
67 | | remove | triggers when the zone remove | the index of the remove zone |
68 | | overRange | triggers when zones number > max | the index of the overRange zone |
69 | | erase | triggers when add zone overRange or smaller than the minimum area(48*48) | the index of the erase zone |
70 |
71 | ## Develop
72 |
73 | ```bash
74 | $ git clone https://github.com/OrangeXC/vue-hotzone.git
75 |
76 | $ cd vue-hotzone
77 |
78 | $ yarn
79 |
80 | $ yarn serve
81 | ```
82 |
83 | ## License
84 |
85 | Vue-hotzone is [MIT licensed](https://github.com/OrangeXC/vue-hotzone/blob/master/LICENSE).
86 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@vue/cli-plugin-unit-jest'
3 | }
4 |
--------------------------------------------------------------------------------
/lib/assets/styles/main.css:
--------------------------------------------------------------------------------
1 | .hz-m-wrap {
2 | position: relative;
3 | /*overflow: hidden;*/
4 | }
5 | .hz-m-wrap .hz-u-img {
6 | display: block;
7 | width: 100%;
8 | max-width: 100%;
9 | height: auto;
10 | max-height: 100%;
11 | user-select: none;
12 | }
13 | .hz-m-wrap .hz-m-area {
14 | position: absolute;
15 | top: 0;
16 | right: 0;
17 | bottom: 0;
18 | left: 0;
19 | cursor: crosshair;
20 | }
21 | .hz-m-wrap .hz-m-item {
22 | position: absolute;
23 | display: block;
24 | }
25 | .hz-m-wrap .hz-m-box {
26 | position: relative;
27 | width: 100%;
28 | height: 100%;
29 | box-shadow: 0 0 6px #000;
30 | background-color: #e31414;
31 | font-size: 12px;
32 | cursor: pointer;
33 | color: #fff;
34 | opacity: 0.8;
35 | }
36 | .hz-m-wrap .hz-m-box > li {
37 | position: absolute;
38 | text-align: center;
39 | user-select: none;
40 | }
41 | .hz-m-wrap .hz-m-box.hz-z-hidden > li {
42 | display: none;
43 | }
44 | .hz-m-wrap .hz-m-box.hz-m-hoverbox:hover {
45 | box-shadow: 0 0 0 2px #373950;
46 | }
47 | .hz-m-wrap .hz-m-box.hz-m-hoverbox .hz-icon:hover {
48 | background-color: #373950;
49 | }
50 | .hz-m-wrap .hz-m-box .hz-icon {
51 | width: 24px;
52 | height: 24px;
53 | line-height: 24px;
54 | font-size: 20px;
55 | text-align: center;
56 | }
57 | .hz-m-wrap .hz-m-box .hz-icon:hover {
58 | background-color: #e31414;
59 | opacity: 0.8;
60 | }
61 | .hz-m-wrap .hz-m-box .hz-u-index {
62 | top: 0;
63 | left: 0;
64 | width: 24px;
65 | height: 24px;
66 | line-height: 24px;
67 | background-color: #000;
68 | }
69 | .hz-m-wrap .hz-m-box .hz-u-close {
70 | top: 0;
71 | right: 0;
72 | }
73 | .hz-m-wrap .hz-m-box .hz-m-copy {
74 | display: inline-block;
75 | }
76 | .hz-m-wrap .hz-m-box .hz-small-icon {
77 | border: 0;
78 | border-radius: 0;
79 | }
80 | .hz-m-wrap .hz-m-box .hz-u-square {
81 | width: 8px;
82 | height: 8px;
83 | opacity: 0.8;
84 | }
85 | .hz-m-wrap .hz-m-box .hz-u-square:after {
86 | content: '';
87 | position: absolute;
88 | top: 2px;
89 | left: 2px;
90 | width: 4px;
91 | height: 4px;
92 | border-radius: 4px;
93 | background-color: #fff;
94 | }
95 | .hz-m-wrap .hz-m-box .hz-u-square-tl {
96 | top: -4px;
97 | left: -4px;
98 | cursor: nw-resize;
99 | }
100 | .hz-m-wrap .hz-m-box .hz-u-square-tc {
101 | top: -4px;
102 | left: 50%;
103 | transform: translateX(-50%);
104 | cursor: n-resize;
105 | }
106 | .hz-m-wrap .hz-m-box .hz-u-square-tr {
107 | top: -4px;
108 | right: -4px;
109 | cursor: ne-resize;
110 | }
111 | .hz-m-wrap .hz-m-box .hz-u-square-cl {
112 | top: 50%;
113 | left: -4px;
114 | transform: translateY(-50%);
115 | cursor: w-resize;
116 | }
117 | .hz-m-wrap .hz-m-box .hz-u-square-cr {
118 | top: 50%;
119 | right: -4px;
120 | transform: translateY(-50%);
121 | cursor: w-resize;
122 | }
123 | .hz-m-wrap .hz-m-box .hz-u-square-bl {
124 | bottom: -4px;
125 | left: -4px;
126 | cursor: sw-resize;
127 | }
128 | .hz-m-wrap .hz-m-box .hz-u-square-bc {
129 | bottom: -4px;
130 | left: 50%;
131 | transform: translateX(-50%);
132 | cursor: s-resize;
133 | }
134 | .hz-m-wrap .hz-m-box .hz-u-square-br {
135 | bottom: -4px;
136 | right: -4px;
137 | cursor: se-resize;
138 | }
139 | /* reset */
140 | .hz-m-modal, .hz-m-wrap {
141 | font-size: 12px;
142 | /* 清除内外边距 */
143 | /* 重置列表元素 */
144 | /* 重置文本格式元素 */
145 | /* 初始化 input */
146 | }
147 | .hz-m-modal ul, .hz-m-wrap ul, .hz-m-modal ol, .hz-m-wrap ol, .hz-m-modal li, .hz-m-wrap li {
148 | margin: 0;
149 | padding: 0;
150 | }
151 | .hz-m-modal ul, .hz-m-wrap ul, .hz-m-modal ol, .hz-m-wrap ol {
152 | list-style: none;
153 | }
154 | .hz-m-modal a, .hz-m-wrap a {
155 | text-decoration: none;
156 | }
157 | .hz-m-modal a:hover, .hz-m-wrap a:hover {
158 | text-decoration: underline;
159 | }
160 | .hz-m-modal p, .hz-m-wrap p {
161 | -webkit-margin-before: 0;
162 | -webkit-margin-after: 0;
163 | }
164 | .hz-m-modal input[type="checkbox"], .hz-m-wrap input[type="checkbox"] {
165 | cursor: pointer;
166 | }
167 | /* basic */
168 | /* modal 样式 */
169 | .hz-m-modal {
170 | position: fixed;
171 | top: 0;
172 | right: 0;
173 | bottom: 0;
174 | left: 0;
175 | z-index: 1000;
176 | overflow-y: auto;
177 | -webkit-overflow-scrolling: touch;
178 | touch-action: cross-slide-y pinch-zoom double-tap-zoom;
179 | text-align: center;
180 | overflow: hidden;
181 | }
182 | .hz-m-modal:before {
183 | content: "";
184 | display: inline-block;
185 | vertical-align: middle;
186 | height: 100%;
187 | }
188 | .hz-m-modal .hz-modal_dialog {
189 | display: inline-block;
190 | vertical-align: middle;
191 | text-align: left;
192 | border-radius: 3px;
193 | }
194 | .hz-m-modal .hz-modal_title {
195 | margin: 0;
196 | }
197 | .hz-m-modal .hz-modal_close {
198 | float: right;
199 | margin: -6px -4px 0 0;
200 | }
201 | @media (max-width: 767px) {
202 | .hz-m-modal .hz-modal_dialog {
203 | width: auto;
204 | }
205 | }
206 | html.z-modal, html.z-modal body {
207 | overflow: hidden;
208 | }
209 | .hz-m-modal {
210 | background: rgba(0, 0, 0, 0.6);
211 | }
212 | .hz-m-modal .hz-modal_dialog {
213 | width: 450px;
214 | background: #fff;
215 | -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125);
216 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125);
217 | }
218 | .hz-m-modal .hz-modal_hd {
219 | padding: 15px;
220 | border-bottom: 1px solid #f4f4f4;
221 | }
222 | .hz-m-modal .hz-modal_title {
223 | font-size: 18px;
224 | }
225 | .hz-m-modal .hz-modal_close {
226 | margin: -15px -15px 0 0;
227 | padding: 6px;
228 | color: #bbb;
229 | cursor: pointer;
230 | }
231 | .hz-m-modal .hz-modal_close:hover {
232 | color: #888;
233 | }
234 | .hz-m-modal .hz-modal_close .hz-u-icon-close {
235 | font-size: 18px;
236 | transition: transform 500ms ease-in-out;
237 | transform: rotate(0deg);
238 | width: 18px;
239 | text-align: center;
240 | }
241 | .hz-m-modal .hz-modal_close:hover .hz-u-icon-close {
242 | transform: rotate(270deg);
243 | }
244 | .hz-m-modal .hz-modal_bd {
245 | padding: 15px 15px 0 15px;
246 | min-height: 10px;
247 | }
248 | .hz-m-modal .hz-modal_ft {
249 | padding: 15px;
250 | text-align: center;
251 | border-top: 1px solid #f4f4f4;
252 | }
253 | .hz-m-modal .hz-modal_ft .hz-u-btn {
254 | margin: 0 10px;
255 | }
256 | @media (max-width: 767px) {
257 | .hz-m-modal .hz-modal_dialog {
258 | margin: 10px;
259 | }
260 | }
261 | /* 基本按钮样式 btn */
262 | .hz-u-btn {
263 | -webkit-user-select: none;
264 | -moz-user-select: none;
265 | -ms-user-select: none;
266 | user-select: none;
267 | -webkit-appearance: none;
268 | border: none;
269 | overflow: visible;
270 | font: inherit;
271 | text-transform: none;
272 | text-decoration: none;
273 | cursor: pointer;
274 | -webkit-box-sizing: border-box;
275 | -moz-box-sizing: border-box;
276 | box-sizing: border-box;
277 | background: none;
278 | display: inline-block;
279 | vertical-align: middle;
280 | text-align: center;
281 | font-size: 12px;
282 | }
283 | .hz-u-btn:hover, .hz-u-btn:focus {
284 | outline: none;
285 | text-decoration: none;
286 | }
287 | .hz-u-btn:disabled {
288 | cursor: not-allowed;
289 | }
290 | .hz-u-btn-block {
291 | display: block;
292 | width: 100%;
293 | }
294 | .hz-u-btn {
295 | padding: 0 16px;
296 | height: 28px;
297 | line-height: 26px;
298 | background: #f4f4f4;
299 | color: #444;
300 | border: 1px solid #ddd;
301 | -moz-border-radius: 3px;
302 | border-radius: 3px;
303 | }
304 | .hz-u-btn:hover, .hz-u-btn:focus {
305 | background: #e5e5e5;
306 | border: 1px solid #adadad;
307 | }
308 | .hz-u-btn:active {
309 | background: #e5e5e5;
310 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
311 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
312 | }
313 | .hz-u-btn:disabled {
314 | background: #fff;
315 | border: 1px solid #ccc;
316 | filter: alpha(opacity=65);
317 | opacity: 0.65;
318 | -webkit-box-shadow: none;
319 | box-shadow: none;
320 | }
321 | /* 按钮类型 */
322 | .hz-u-btn-primary {
323 | background: #67739b;
324 | color: #fff;
325 | border: 1px solid #67739b;
326 | }
327 | .hz-u-btn-primary:hover, .hz-u-btn-primary:focus {
328 | background: #31384b;
329 | color: #fff;
330 | border: 1px solid #31384b;
331 | }
332 | .hz-u-btn-primary:active {
333 | background: #367fa9;
334 | color: #fff;
335 | border: 1px solid #367fa9;
336 | }
337 | .hz-u-btn-primary:disabled {
338 | background: #444;
339 | color: #fff;
340 | border: 1px solid #444;
341 | }
342 | /* input */
343 | .hz-u-input {
344 | -webkit-box-sizing: border-box;
345 | -moz-box-sizing: border-box;
346 | box-sizing: border-box;
347 | margin: 0;
348 | border: 0;
349 | padding: 0;
350 | border-radius: 0;
351 | font: inherit;
352 | color: inherit;
353 | vertical-align: middle;
354 | }
355 | .hz-u-input {
356 | position: relative;
357 | z-index: 0;
358 | padding: 5px 6px;
359 | border: 1px solid #d2d6de;
360 | color: #555;
361 | background: #fff;
362 | -moz-border-radius: 3px;
363 | border-radius: 3px;
364 | }
365 | .hz-u-input::-webkit-input-placeholder {
366 | color: #bbb;
367 | filter: alpha(opacity=100);
368 | opacity: 1;
369 | }
370 | .hz-u-input::-moz-placeholder {
371 | color: #bbb;
372 | filter: alpha(opacity=100);
373 | opacity: 1;
374 | }
375 | .hz-u-input:-moz-placeholder {
376 | color: #bbb;
377 | filter: alpha(opacity=100);
378 | opacity: 1;
379 | }
380 | .hz-u-input:-ms-placeholder {
381 | color: #bbb;
382 | filter: alpha(opacity=100);
383 | opacity: 1;
384 | }
385 | .hz-u-input:focus {
386 | outline: 0;
387 | background: #fff;
388 | color: #555;
389 | border: 1px solid #3c8dbc;
390 | }
391 | .hz-u-input:disabled {
392 | cursor: not-allowed;
393 | background: #eee;
394 | color: #999;
395 | border: 1px solid #d2d6de;
396 | }
397 | .hz-u-input {
398 | width: 280px;
399 | height: 34px;
400 | }
401 | .hz-u-input.hz-u-input-success {
402 | color: #00a65a;
403 | border-color: #00a65a;
404 | }
405 | .hz-u-input.hz-u-input-warning {
406 | color: #f39c12;
407 | border-color: #f39c12;
408 | }
409 | .hz-u-input.hz-u-input-error {
410 | color: #dd4b39;
411 | border-color: #dd4b39;
412 | }
413 | .hz-u-input.hz-u-input-blank {
414 | border-color: transparent;
415 | border-style: dashed;
416 | background: none;
417 | }
418 | .hz-u-input.hz-u-input-blank:focus {
419 | border-color: #ddd;
420 | }
421 | /* formItem */
422 | .hz-u-formitem {
423 | display: inline-block;
424 | *zoom: 1;
425 | margin-bottom: 1em;
426 | }
427 | .hz-u-formitem:before, .hz-u-formitem:after {
428 | display: table;
429 | content: "";
430 | line-height: 0;
431 | }
432 | .hz-u-formitem:after {
433 | clear: both;
434 | }
435 | .hz-u-formitem .hz-formitem_tt {
436 | display: block;
437 | float: left;
438 | text-align: right;
439 | }
440 | .hz-u-formitem .hz-formitem_ct {
441 | display: block;
442 | }
443 | .hz-u-formitem .hz-formitem_rqr {
444 | line-height: 28px;
445 | color: #dd4b39;
446 | }
447 | .hz-u-formitem .hz-formitem_tt {
448 | line-height: 34px;
449 | width: 100px;
450 | }
451 | .hz-u-formitem .hz-formitem_ct {
452 | line-height: 34px;
453 | margin-left: 108px;
454 | }
455 | /* icon */
456 | .hz-u-icon {
457 | display: inline-block;
458 | font: normal normal normal 14px/1 FontAwesome;
459 | font-size: inherit;
460 | text-rendering: auto;
461 | -webkit-font-smoothing: antialiased;
462 | -moz-osx-font-smoothing: grayscale;
463 | }
464 | /* label */
465 | .hz-u-label {
466 | display: inline-block;
467 | cursor: pointer;
468 | }
469 | /* margin */
470 | .hz-f-ml0 {
471 | margin-bottom: 0;
472 | }
473 | /* replicator */
474 | .hz-u-copy input[data-for-copy] {
475 | transform: translateZ(0);
476 | position: fixed;
477 | bottom: 0;
478 | right: 0;
479 | width: 1px;
480 | height: 1px;
481 | opacity: 0;
482 | overflow: hidden;
483 | z-index: -999;
484 | color: transparent;
485 | background-color: transparent;
486 | border: none;
487 | outline: none;
488 | }
489 | @font-face {
490 | font-family: 'iconfont';
491 | /* project id 525460 */
492 | src: url('//at.alicdn.com/t/font_525460_d0ysfwzacahsemi.eot');
493 | src: url('//at.alicdn.com/t/font_525460_d0ysfwzacahsemi.eot?#iefix') format('embedded-opentype'), url('//at.alicdn.com/t/font_525460_d0ysfwzacahsemi.woff') format('woff'), url('//at.alicdn.com/t/font_525460_d0ysfwzacahsemi.ttf') format('truetype'), url('//at.alicdn.com/t/font_525460_d0ysfwzacahsemi.svg#iconfont') format('svg');
494 | }
495 | .hz-icon {
496 | font-family: "iconfont" !important;
497 | font-size: 20px;
498 | font-style: normal;
499 | text-align: center;
500 | user-select: none;
501 | -webkit-font-smoothing: antialiased;
502 | -moz-osx-font-smoothing: grayscale;
503 | }
504 | .hz-icon-trash:before {
505 | content: "\e605";
506 | }
507 |
--------------------------------------------------------------------------------
/lib/components/Hotzone.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
![]()
7 |
21 |
22 |
23 |
24 |
98 |
99 |
102 |
--------------------------------------------------------------------------------
/lib/components/Zone.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
14 | - {{ index + 1 }}
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
100 |
--------------------------------------------------------------------------------
/lib/directives/addItem.js:
--------------------------------------------------------------------------------
1 | import _ from '../utils'
2 |
3 | export default {
4 | bind: function (el, binding, vnode) {
5 | const MIN_LIMIT = _.MIN_LIMIT
6 |
7 | el.addEventListener('mousedown', handleMouseDown)
8 |
9 | function handleMouseDown (e) {
10 | e && e.preventDefault()
11 |
12 | let itemInfo = {
13 | top: _.getDistanceY(e, el),
14 | left: _.getDistanceX(e, el),
15 | width: 0,
16 | height: 0
17 | }
18 | let container = _.getOffset(el)
19 |
20 | // Only used once at the beginning of init
21 | let setting = {
22 | topPer: _.decimalPoint(itemInfo.top / container.height),
23 | leftPer: _.decimalPoint(itemInfo.left / container.width),
24 | widthPer: 0,
25 | heightPer: 0
26 | }
27 | let preX = _.getPageX(e)
28 | let preY = _.getPageY(e)
29 |
30 | vnode.context.addItem(setting)
31 |
32 | window.addEventListener('mousemove', handleChange)
33 | window.addEventListener('mouseup', handleMouseUp)
34 |
35 | function handleChange (e) {
36 | e && e.preventDefault()
37 |
38 | let moveX = _.getPageX(e) - preX
39 | let moveY = _.getPageY(e) - preY
40 | preX = _.getPageX(e)
41 | preY = _.getPageY(e)
42 |
43 | // Not consider the direction of movement first, consider only the lower right drag point
44 | let minLimit = 0
45 | let styleInfo = _.dealBR(itemInfo, moveX, moveY, minLimit)
46 |
47 | // Boundary value processing
48 | itemInfo = _.dealEdgeValue(itemInfo, styleInfo, container)
49 |
50 | Object.assign(el.lastElementChild.style, {
51 | top: `${itemInfo.top}px`,
52 | left: `${itemInfo.left}px`,
53 | width: `${itemInfo.width}px`,
54 | height: `${itemInfo.height}px`
55 | })
56 | }
57 |
58 | function handleMouseUp () {
59 | let perInfo = {
60 | topPer: _.decimalPoint(itemInfo.top / container.height),
61 | leftPer: _.decimalPoint(itemInfo.left / container.width),
62 | widthPer: _.decimalPoint(itemInfo.width / container.width),
63 | heightPer: _.decimalPoint(itemInfo.height / container.height)
64 | }
65 |
66 | if (vnode.context.isOverRange()) {
67 | vnode.context.overRange()
68 | } else if (container.height < MIN_LIMIT && itemInfo.width > MIN_LIMIT) {
69 | vnode.context.changeItem(Object.assign(perInfo, {
70 | topPer: 0,
71 | heightPer: 1
72 | }))
73 | } else if (container.width < MIN_LIMIT && itemInfo.height > MIN_LIMIT) {
74 | vnode.context.changeItem(Object.assign(perInfo, {
75 | leftper: 0,
76 | widthPer: 1
77 | }))
78 | } else if (itemInfo.width > MIN_LIMIT && itemInfo.height > MIN_LIMIT) {
79 | vnode.context.changeItem(perInfo)
80 | } else {
81 | vnode.context.eraseItem()
82 | }
83 |
84 | window.removeEventListener('mousemove', handleChange)
85 | window.removeEventListener('mouseup', handleMouseUp)
86 | }
87 | }
88 |
89 | el.$destroy = () => el.removeEventListener('mousedown', handleMouseDown)
90 | },
91 | unbind: function (el) {
92 | el.$destroy()
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/directives/changeSize.js:
--------------------------------------------------------------------------------
1 | import _ from '../utils'
2 |
3 | export default {
4 | bind: function (el, binding, vnode) {
5 | el.addEventListener('mousedown', handleMouseDown)
6 |
7 | function handleMouseDown (e) {
8 | let pointer = e.target.dataset.pointer
9 |
10 | if (!pointer) {
11 | return
12 | }
13 |
14 | e && e.stopPropagation()
15 |
16 | let zone = el.parentNode
17 | let setting = vnode.context.setting
18 | let container = _.getOffset(zone.parentNode)
19 | let itemInfo = {
20 | width: _.getOffset(zone).width || 0,
21 | height: _.getOffset(zone).height || 0,
22 | top: setting.topPer * container.height || 0,
23 | left: setting.leftPer * container.width || 0
24 | }
25 | let preX = _.getPageX(e)
26 | let preY = _.getPageY(e)
27 | let flag
28 |
29 | // Hide the info displayed by hover
30 | vnode.context.handlehideZone(true)
31 |
32 | window.addEventListener('mousemove', handleChange)
33 | window.addEventListener('mouseup', handleMouseUp)
34 |
35 | function handleChange (e) {
36 | e && e.preventDefault()
37 | flag = true
38 |
39 | let moveX = _.getPageX(e) - preX
40 | let moveY = _.getPageY(e) - preY
41 |
42 | preX = _.getPageX(e)
43 | preY = _.getPageY(e)
44 |
45 | // Handling the situation when different dragging points are selected
46 | let styleInfo = _[pointer](itemInfo, moveX, moveY)
47 |
48 | // Boundary value processing
49 | itemInfo = _.dealEdgeValue(itemInfo, styleInfo, container)
50 |
51 | Object.assign(zone.style, {
52 | top: `${itemInfo.top}px`,
53 | left: `${itemInfo.left}px`,
54 | width: `${itemInfo.width}px`,
55 | height: `${itemInfo.height}px`
56 | })
57 | }
58 |
59 | function handleMouseUp () {
60 | if (flag) {
61 | flag = false
62 | let perInfo = {
63 | topPer: _.decimalPoint(itemInfo.top / container.height),
64 | leftPer: _.decimalPoint(itemInfo.left / container.width),
65 | widthPer: _.decimalPoint(itemInfo.width / container.width),
66 | heightPer: _.decimalPoint(itemInfo.height / container.height)
67 | }
68 | vnode.context.changeInfo(perInfo)
69 |
70 | // 兼容数据无变更情况下导致 computed 不更新,数据仍为 px 时 resize 出现的问题
71 | Object.assign(zone.style, {
72 | top: `${itemInfo.top}px`,
73 | left: `${itemInfo.left}px`,
74 | width: `${itemInfo.width}px`,
75 | height: `${itemInfo.height}px`
76 | })
77 | }
78 | // Show the info
79 | vnode.context.handlehideZone(false)
80 |
81 | window.removeEventListener('mousemove', handleChange)
82 | window.removeEventListener('mouseup', handleMouseUp)
83 | }
84 | }
85 |
86 | el.$destroy = () => el.removeEventListener('mousedown', handleMouseDown)
87 | },
88 | unbind: function (el) {
89 | el.$destroy()
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/lib/directives/dragItem.js:
--------------------------------------------------------------------------------
1 | import _ from '../utils'
2 |
3 | export default {
4 | bind: function (el, binding, vnode) {
5 | el.addEventListener('mousedown', handleMouseDown)
6 |
7 | function handleMouseDown (e) {
8 | e && e.stopPropagation()
9 |
10 | let container = _.getOffset(el.parentNode)
11 | let preX = _.getPageX(e)
12 | let preY = _.getPageY(e)
13 | let topPer
14 | let leftPer
15 | let flag
16 |
17 | window.addEventListener('mousemove', handleChange)
18 | window.addEventListener('mouseup', handleMouseUp)
19 |
20 | function handleChange (e) {
21 | e && e.preventDefault()
22 | flag = true
23 |
24 | // Hide the info displayed by hover
25 | vnode.context.handlehideZone(true)
26 |
27 | let setting = vnode.context.setting
28 | let moveX = _.getPageX(e) - preX
29 | let moveY = _.getPageY(e) - preY
30 |
31 | setting.topPer = setting.topPer || 0
32 | setting.leftPer = setting.leftPer || 0
33 | topPer = _.decimalPoint(moveY / container.height + setting.topPer)
34 | leftPer = _.decimalPoint(moveX / container.width + setting.leftPer)
35 |
36 | // Hotzone moving boundary processing
37 | if (topPer < 0) {
38 | topPer = 0
39 | moveY = -container.height * setting.topPer
40 | }
41 |
42 | if (leftPer < 0) {
43 | leftPer = 0
44 | moveX = -container.width * setting.leftPer
45 | }
46 |
47 | if (topPer + setting.heightPer > 1) {
48 | topPer = 1 - setting.heightPer
49 | moveY = container.height * (topPer - setting.topPer)
50 | }
51 |
52 | if (leftPer + setting.widthPer > 1) {
53 | leftPer = 1 - setting.widthPer
54 | moveX = container.width * (leftPer - setting.leftPer)
55 | }
56 |
57 | el.style.transform = `translate(${moveX}px, ${moveY}px)`
58 | }
59 |
60 | function handleMouseUp () {
61 | if (flag) {
62 | flag = false
63 | el.style.transform = 'translate(0, 0)'
64 | vnode.context.changeInfo({
65 | topPer,
66 | leftPer
67 | })
68 | }
69 |
70 | // Show the info
71 | vnode.context.handlehideZone(false)
72 |
73 | window.removeEventListener('mousemove', handleChange)
74 | window.removeEventListener('mouseup', handleMouseUp)
75 | }
76 | }
77 |
78 | el.$destroy = () => el.removeEventListener('mousedown', handleMouseDown)
79 | },
80 | unbind: function (el) {
81 | el.$destroy()
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import hotzone from './components/Hotzone.vue'
2 |
3 | hotzone.install = (Vue) => {
4 | Vue.component(hotzone.name, hotzone)
5 | }
6 |
7 | export default hotzone
8 |
--------------------------------------------------------------------------------
/lib/utils/index.js:
--------------------------------------------------------------------------------
1 | let _ = {
2 | MIN_LIMIT: 48, // Min size of zone
3 | DECIMAL_PLACES: 4 // Hotzone positioning decimal point limit number of digits
4 | }
5 |
6 | /**
7 | * Get a power result of 10 for the power of the constant
8 | * @return {Number}
9 | */
10 | _.getMultiple = (decimalPlaces = _.DECIMAL_PLACES) => {
11 | return Math.pow(10, decimalPlaces)
12 | }
13 |
14 | /**
15 | * Limit decimal places
16 | * @param {Number} num
17 | * @return {Number}
18 | */
19 | _.decimalPoint = (val = 0) => {
20 | return Math.round(val * _.getMultiple()) / _.getMultiple() || 0
21 | }
22 |
23 | /**
24 | * Get element width and height
25 | * @param {Object} elem
26 | * @return {Object}
27 | */
28 | _.getOffset = (elem = {}) => ({
29 | width: elem.clientWidth || 0,
30 | height: elem.clientHeight || 0
31 | })
32 |
33 | /**
34 | * Get pageX
35 | * @param {Object} e
36 | * @return {Number}
37 | */
38 | _.getPageX = (e) => ('pageX' in e) ? e.pageX : e.touches[0].pageX
39 |
40 | /**
41 | * Get pageY
42 | * @param {Object} e
43 | * @return {Number}
44 | */
45 | _.getPageY = (e) => ('pageY' in e) ? e.pageY : e.touches[0].pageY
46 |
47 | /**
48 | * Gets the abscissa value of the mouse click relative to the target node
49 | * @param {Object} e
50 | * @param {Object} container
51 | * @return {Number}
52 | */
53 | _.getDistanceX = (e, container) =>
54 | _.getPageX(e) - (container.getBoundingClientRect().left + window.pageXOffset)
55 |
56 | /**
57 | * Gets the ordinate value of the mouse click relative to the target node
58 | * @param {Object} e
59 | * @param {Object} container
60 | * @return {Number}
61 | */
62 | _.getDistanceY = (e, container) =>
63 | _.getPageY(e) - (container.getBoundingClientRect().top + window.pageYOffset)
64 |
65 | /**
66 | * Treatment of boundary conditions when changing the size of the hotzone
67 | * @param {Object} itemInfo
68 | * @param {Object} styleInfo
69 | * @param {Object} container
70 | */
71 | _.dealEdgeValue = (itemInfo, styleInfo, container) => {
72 | if (styleInfo.hasOwnProperty('left') && styleInfo.left < 0) {
73 | styleInfo.left = 0
74 | styleInfo.width = itemInfo.width + itemInfo.left
75 | }
76 |
77 | if (styleInfo.hasOwnProperty('top') && styleInfo.top < 0) {
78 | styleInfo.top = 0
79 | styleInfo.height = itemInfo.height + itemInfo.top
80 | }
81 |
82 | if (!styleInfo.hasOwnProperty('left') && styleInfo.hasOwnProperty('width')) {
83 | if (itemInfo.left + styleInfo.width > container.width) {
84 | styleInfo.width = container.width - itemInfo.left
85 | }
86 | }
87 |
88 | if (!styleInfo.hasOwnProperty('top') && styleInfo.hasOwnProperty('height')) {
89 | if (itemInfo.top + styleInfo.height > container.height) {
90 | styleInfo.height = container.height - itemInfo.top
91 | }
92 | }
93 |
94 | return Object.assign(itemInfo, styleInfo)
95 | }
96 |
97 | /**
98 | * Handle different drag points, capital letters mean: T-top,L-left,C-center,R-right,B-bottom
99 | * @param {Object} itemInfo
100 | * @param {Number} moveX
101 | * @param {Number} moveY
102 | * @return {Object}
103 | */
104 | _.dealTL = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
105 | let styleInfo = {}
106 | let width = itemInfo.width - moveX
107 | let height = itemInfo.height - moveY
108 |
109 | if (width >= Math.min(minLimit, itemInfo.width)) {
110 | Object.assign(styleInfo, {
111 | width,
112 | left: itemInfo.left + moveX
113 | })
114 | }
115 |
116 | if (height >= Math.min(minLimit, itemInfo.height)) {
117 | Object.assign(styleInfo, {
118 | height,
119 | top: itemInfo.top + moveY
120 | })
121 | }
122 |
123 | return styleInfo
124 | }
125 |
126 | _.dealTC = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
127 | let styleInfo = {}
128 | let height = itemInfo.height - moveY
129 |
130 | if (height >= Math.min(minLimit, itemInfo.height)) {
131 | styleInfo = {
132 | height,
133 | top: itemInfo.top + moveY
134 | }
135 | }
136 |
137 | return styleInfo
138 | }
139 |
140 | _.dealTR = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
141 | let styleInfo = {}
142 | let width = itemInfo.width + moveX
143 | let height = itemInfo.height - moveY
144 |
145 | if (width >= Math.min(minLimit, itemInfo.width)) {
146 | Object.assign(styleInfo, {
147 | width
148 | })
149 | }
150 |
151 | if (height >= Math.min(minLimit, itemInfo.height)) {
152 | Object.assign(styleInfo, {
153 | height,
154 | top: itemInfo.top + moveY
155 | })
156 | }
157 |
158 | return styleInfo
159 | }
160 |
161 | _.dealCL = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
162 | let styleInfo = {}
163 | let width = itemInfo.width - moveX
164 |
165 | if (width >= Math.min(minLimit, itemInfo.width)) {
166 | Object.assign(styleInfo, {
167 | width,
168 | left: itemInfo.left + moveX
169 | })
170 | }
171 |
172 | return styleInfo
173 | }
174 |
175 | _.dealCR = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
176 | let styleInfo = {}
177 | let width = itemInfo.width + moveX
178 |
179 | if (width >= Math.min(minLimit, itemInfo.width)) {
180 | Object.assign(styleInfo, {
181 | width
182 | })
183 | }
184 |
185 | return styleInfo
186 | }
187 |
188 | _.dealBL = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
189 | let styleInfo = {}
190 | let width = itemInfo.width - moveX
191 | let height = itemInfo.height + moveY
192 |
193 | if (width >= Math.min(minLimit, itemInfo.width)) {
194 | Object.assign(styleInfo, {
195 | width,
196 | left: itemInfo.left + moveX
197 | })
198 | }
199 |
200 | if (height >= Math.min(minLimit, itemInfo.height)) {
201 | Object.assign(styleInfo, {
202 | height
203 | })
204 | }
205 |
206 | return styleInfo
207 | }
208 |
209 | _.dealBC = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
210 | let styleInfo = {}
211 | let height = itemInfo.height + moveY
212 |
213 | if (height >= Math.min(minLimit, itemInfo.height)) {
214 | Object.assign(styleInfo, {
215 | height
216 | })
217 | }
218 |
219 | return styleInfo
220 | }
221 |
222 | _.dealBR = (itemInfo, moveX, moveY, minLimit = _.MIN_LIMIT) => {
223 | let styleInfo = {}
224 | let width = itemInfo.width + moveX
225 | let height = itemInfo.height + moveY
226 |
227 | if (width >= Math.min(minLimit, itemInfo.width)) {
228 | Object.assign(styleInfo, {
229 | width
230 | })
231 | }
232 |
233 | if (height >= Math.min(minLimit, itemInfo.height)) {
234 | Object.assign(styleInfo, {
235 | height
236 | })
237 | }
238 |
239 | return styleInfo
240 | }
241 |
242 | export default _
243 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-hotzone",
3 | "version": "1.1.0",
4 | "private": false,
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "serve": "vue-cli-service serve --open",
8 | "build": "vue-cli-service build",
9 | "test:unit": "vue-cli-service test:unit",
10 | "lint": "vue-cli-service lint"
11 | },
12 | "devDependencies": {
13 | "@vue/cli-plugin-babel": "4.5.13",
14 | "@vue/cli-plugin-eslint": "4.5.13",
15 | "@vue/cli-plugin-unit-jest": "4.5.13",
16 | "@vue/cli-service": "4.5.13",
17 | "@vue/test-utils": "1.2.2",
18 | "babel-eslint": "10.1.0",
19 | "codecov": "3.8.3",
20 | "core-js": "3.17.3",
21 | "eslint": "7.32.0",
22 | "eslint-plugin-vue": "7.18.0",
23 | "vue": "2.6.14",
24 | "vue-template-compiler": "2.6.14"
25 | },
26 | "dependencies": {}
27 | }
28 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OrangeXC/vue-hotzone/dbf4dc3e238fbe3d6e2abb218885c78d6584c423/public/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | vue-hotzone
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
VUE-HOTZONE
4 |
5 |
Try drag red area or drag create a new area
6 |
👇
7 |
14 |
21 |
Try click your areas
22 |
👇
23 |
24 |
![cover]()
25 |
37 |
38 |
39 |
40 |
41 |
83 |
84 |
133 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 |
4 | Vue.config.productionTip = false
5 |
6 | new Vue({
7 | render: h => h(App)
8 | }).$mount('#app')
9 |
--------------------------------------------------------------------------------
/tests/unit/components/hotzone.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import Hotzone from '../../../lib/components/Hotzone'
3 |
4 | describe('component: Hotzone', () => {
5 | const mocks = {
6 | image: 'test.jpg',
7 | zonesInit: [{
8 | topPer: 0.5,
9 | leftPer: 0.5,
10 | widthPer: 0.5,
11 | heightPer: 0.5
12 | }, {
13 | topPer: 0.3,
14 | leftPer: 0.3,
15 | widthPer: 0.3,
16 | heightPer: 0.3
17 | }],
18 | zone: {
19 | topPer: 0.15,
20 | leftPer: 0.25,
21 | widthPer: 0.35,
22 | heightPer: 0.45
23 | }
24 | }
25 |
26 | test('props', () => {
27 | const options = {
28 | propsData: {
29 | image: mocks.image,
30 | zonesInit: mocks.zonesInit,
31 | max: 5
32 | }
33 | }
34 |
35 | const wrapper = mount(Hotzone, options)
36 |
37 | expect(wrapper.props().image).toBe(mocks.image)
38 |
39 | const image = wrapper.find('.hz-u-img')
40 |
41 | expect(image.attributes().src).toBe(mocks.image)
42 |
43 | expect(wrapper.props().zonesInit).toEqual(mocks.zonesInit)
44 | expect(wrapper.vm.zones).toEqual(mocks.zonesInit)
45 |
46 | expect(wrapper.props().max).toEqual(options.propsData.max)
47 | })
48 |
49 | test('methods: changeInfo', () => {
50 | const changeItem = jest.fn()
51 |
52 | const wrapper = mount(Hotzone, {
53 | propsData: {
54 | image: mocks.image
55 | },
56 | methods: {
57 | changeItem
58 | }
59 | })
60 |
61 | const res = {
62 | info: {},
63 | index: 0
64 | }
65 |
66 | wrapper.vm.changeInfo(res)
67 |
68 | expect(changeItem).toBeCalledWith(res.info, res.index)
69 | })
70 |
71 | test('methods: addItem', () => {
72 | const hasChange = jest.fn()
73 |
74 | const wrapper = mount(Hotzone, {
75 | propsData: {
76 | image: mocks.image
77 | },
78 | methods: {
79 | hasChange
80 | }
81 | })
82 |
83 | wrapper.vm.addItem(mocks.zone)
84 |
85 | expect(hasChange).toBeCalled()
86 | expect(wrapper.vm.zones).toEqual([mocks.zone])
87 | expect(wrapper.emitted('add')[0][0]).toEqual(mocks.zone)
88 | })
89 |
90 | test('methods: eraseItem', () => {
91 | const removeItem = jest.fn()
92 |
93 | const wrapper = mount(Hotzone, {
94 | propsData: {
95 | image: mocks.image,
96 | zonesInit: mocks.zonesInit
97 | },
98 | methods: {
99 | removeItem
100 | }
101 | })
102 |
103 | wrapper.vm.eraseItem()
104 |
105 | expect(removeItem).toHaveBeenCalledWith(1)
106 | expect(wrapper.emitted('erase')[0][0]).toBe(1)
107 |
108 | wrapper.vm.eraseItem(0)
109 |
110 | expect(removeItem).toHaveBeenCalledWith(0)
111 | expect(wrapper.emitted('erase')[1][0]).toBe(0)
112 | })
113 |
114 | test('methods: isOverRange', () => {
115 | const wrapper = mount(Hotzone, {
116 | propsData: {
117 | image: mocks.image,
118 | zonesInit: mocks.zonesInit,
119 | max: 1
120 | }
121 | })
122 |
123 | expect(wrapper.vm.isOverRange()).toBeTruthy()
124 |
125 | wrapper.setProps({
126 | max: 2
127 | })
128 |
129 | expect(wrapper.vm.isOverRange()).toBeFalsy()
130 | })
131 |
132 | test('methods: overRange', () => {
133 | const removeItem = jest.fn()
134 |
135 | const wrapper = mount(Hotzone, {
136 | propsData: {
137 | image: mocks.image,
138 | zonesInit: mocks.zonesInit
139 | },
140 | methods: {
141 | removeItem
142 | }
143 | })
144 |
145 | wrapper.vm.overRange()
146 |
147 | expect(removeItem).toBeCalledWith(1)
148 | expect(wrapper.emitted('overRange')[0][0]).toBe(1)
149 | })
150 |
151 | test('methods: removeItem', () => {
152 | const hasChange = jest.fn()
153 |
154 | const wrapper = mount(Hotzone, {
155 | propsData: {
156 | image: mocks.image,
157 | zonesInit: mocks.zonesInit
158 | },
159 | methods: {
160 | hasChange
161 | }
162 | })
163 |
164 | wrapper.vm.removeItem(0)
165 |
166 | expect(wrapper.vm.zones).toEqual([mocks.zonesInit[1]])
167 | expect(hasChange).toBeCalled()
168 | expect(wrapper.emitted('remove')[0][0]).toBe(0)
169 |
170 | wrapper.vm.removeItem()
171 |
172 | expect(wrapper.vm.zones).toEqual([])
173 | expect(hasChange).toBeCalled()
174 | expect(wrapper.emitted('remove')[1][0]).toBe(0)
175 | })
176 |
177 | test('methods: changeItem', () => {
178 | const hasChange = jest.fn()
179 |
180 | const wrapper = mount(Hotzone, {
181 | propsData: {
182 | image: mocks.image,
183 | zonesInit: mocks.zonesInit
184 | },
185 | methods: {
186 | hasChange
187 | }
188 | })
189 |
190 | wrapper.vm.changeItem(mocks.zone, 0)
191 |
192 | expect(wrapper.vm.zones).toEqual([mocks.zone, mocks.zonesInit[1]])
193 | expect(hasChange).toBeCalled()
194 |
195 | wrapper.vm.changeItem(mocks.zone)
196 |
197 | expect(wrapper.vm.zones).toEqual([mocks.zone, mocks.zone])
198 | expect(hasChange).toBeCalled()
199 | })
200 |
201 | test('methods: hasChange', () => {
202 | const wrapper = mount(Hotzone, {
203 | propsData: {
204 | image: mocks.image,
205 | zonesInit: mocks.zonesInit
206 | }
207 | })
208 |
209 | wrapper.vm.hasChange()
210 |
211 | expect(wrapper.emitted('change')[0][0]).toEqual(mocks.zonesInit)
212 | })
213 | })
214 |
--------------------------------------------------------------------------------
/tests/unit/components/zone.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import Zone from '../../../lib/components/Zone'
3 |
4 | describe('component: Zone', () => {
5 | const mocks = {
6 | setting: {
7 | topPer: 0.15,
8 | leftPer: 0.25,
9 | widthPer: 0.35,
10 | heightPer: 0.45
11 | }
12 | }
13 |
14 | test('template', () => {
15 | const wrapper = mount(Zone, {
16 | propsData: {
17 | setting: mocks.setting,
18 | index: 0
19 | }
20 | })
21 |
22 | const ul = wrapper.find('.hz-m-box')
23 |
24 | expect(ul.classes()).toContain('hz-m-hoverbox')
25 |
26 | const indexWrap = wrapper.find('.hz-u-index')
27 |
28 | expect(indexWrap.attributes().title).toBe('热区1')
29 | expect(indexWrap.text()).toBe('1')
30 |
31 | const closeWrap = wrapper.find('.hz-u-close')
32 |
33 | expect(closeWrap.isVisible()).toBeTruthy()
34 | expect(closeWrap.attributes().title).toBe('删除该热区')
35 | expect(closeWrap.classes()).toContain('hz-icon')
36 | expect(closeWrap.classes()).toContain('hz-icon-trash')
37 |
38 | const pointers = ['dealTL', 'dealTC', 'dealTR', 'dealCL', 'dealCR', 'dealBL', 'dealBC', 'dealBR']
39 |
40 | pointers.forEach((item, index) => {
41 | const square = wrapper.findAll('.hz-u-square').at(index)
42 |
43 | expect(square.attributes()['data-pointer']).toBe(item)
44 | expect(square.classes()).toContain(`hz-u-square-${item.slice(4).toLowerCase()}`)
45 | })
46 | })
47 |
48 | test('mounted', () => {
49 | const setZoneInfo = jest.fn()
50 |
51 | mount(Zone, {
52 | propsData: {
53 | setting: mocks.setting
54 | },
55 | methods: {
56 | setZoneInfo
57 | }
58 | })
59 |
60 | expect(setZoneInfo).toBeCalledWith(mocks.setting)
61 | })
62 |
63 | test('methods: setZoneInfo', () => {
64 | const wrapper = mount(Zone, {
65 | propsData: {
66 | setting: mocks.setting
67 | }
68 | })
69 |
70 | expect(wrapper.vm.zoneTop).toBe('15%')
71 | expect(wrapper.vm.zoneLeft).toBe('25%')
72 | expect(wrapper.vm.zoneWidth).toBe('35%')
73 | expect(wrapper.vm.zoneHeight).toBe('45%')
74 | expect(wrapper.vm.tooSmall).toBeFalsy()
75 |
76 | wrapper.vm.setZoneInfo({
77 | topPer: 0.05,
78 | leftPer: 0.05,
79 | widthPer: 0.005,
80 | heightPer: 0.0005
81 | })
82 |
83 | expect(wrapper.vm.zoneTop).toBe('5%')
84 | expect(wrapper.vm.zoneLeft).toBe('5%')
85 | expect(wrapper.vm.zoneWidth).toBe('0.5%')
86 | expect(wrapper.vm.zoneHeight).toBe('0.05%')
87 | expect(wrapper.vm.tooSmall).toBeTruthy()
88 | })
89 |
90 | test('methods: handlehideZone', () => {
91 | const wrapper = mount(Zone, {
92 | propsData: {
93 | setting: mocks.setting
94 | }
95 | })
96 |
97 | expect(wrapper.vm.hideZone).toBeFalsy()
98 |
99 | wrapper.vm.handlehideZone()
100 |
101 | expect(wrapper.vm.hideZone).toBeTruthy()
102 |
103 | wrapper.vm.handlehideZone(true)
104 |
105 | expect(wrapper.vm.hideZone).toBeTruthy()
106 |
107 | wrapper.vm.handlehideZone(false)
108 |
109 | expect(wrapper.vm.hideZone).toBeFalsy()
110 | })
111 |
112 | test('methods: changeInfo', () => {
113 | const wrapper = mount(Zone, {
114 | propsData: {
115 | setting: mocks.setting,
116 | index: 3
117 | }
118 | })
119 |
120 | wrapper.vm.changeInfo(mocks.setting)
121 |
122 | expect(wrapper.emitted('changeInfo')[0][0]).toEqual({
123 | info: mocks.setting,
124 | index: 3
125 | })
126 |
127 | wrapper.setProps({ index: 0 })
128 | wrapper.vm.changeInfo()
129 |
130 | expect(wrapper.emitted('changeInfo')[1][0]).toEqual({
131 | info: {},
132 | index: 0
133 | })
134 | })
135 |
136 | test('methods: delItem', () => {
137 | const wrapper = mount(Zone, {
138 | propsData: {
139 | setting: mocks.setting,
140 | index: 0
141 | }
142 | })
143 |
144 | const closeWrap = wrapper.find('.hz-u-close')
145 |
146 | closeWrap.trigger('click')
147 |
148 | expect(wrapper.emitted('delItem')[0][0]).toBe(0)
149 | })
150 |
151 | test('methods: getZoneStyle', () => {
152 | const wrapper = mount(Zone, {
153 | propsData: {
154 | setting: mocks.setting
155 | }
156 | })
157 |
158 | expect(wrapper.vm.getZoneStyle()).toBe('0%')
159 | expect(wrapper.vm.getZoneStyle(0.36)).toBe('36%')
160 | })
161 | })
162 |
--------------------------------------------------------------------------------
/tests/unit/utils.spec.js:
--------------------------------------------------------------------------------
1 | import _ from '../../lib/utils'
2 |
3 | describe('utils', () => {
4 | test('getMultiple', () => {
5 | expect(_.getMultiple()).toBe(10000)
6 | expect(_.getMultiple(2)).toBe(100)
7 | })
8 |
9 | test('decimalPoint', () => {
10 | expect(_.decimalPoint()).toBe(0)
11 | expect(_.decimalPoint(0.5)).toBe(0.5)
12 | expect(_.decimalPoint(0.123456)).toBe(0.1235)
13 | })
14 |
15 | test('getOffset', () => {
16 | expect(_.getOffset()).toEqual({
17 | width: 0,
18 | height: 0
19 | })
20 |
21 | const elem = {
22 | clientWidth: 20,
23 | clientHeight: 50
24 | }
25 |
26 | expect(_.getOffset(elem)).toEqual({
27 | width: 20,
28 | height: 50
29 | })
30 | })
31 |
32 | test('getPageX', () => {
33 | let elem = {
34 | pageX: 26,
35 | touches: [{
36 | pageX: 18
37 | }]
38 | }
39 |
40 | expect(_.getPageX(elem)).toBe(26)
41 |
42 | delete elem.pageX
43 |
44 | expect(_.getPageX(elem)).toBe(18)
45 | })
46 |
47 | test('getPageY', () => {
48 | let elem = {
49 | pageY: 26,
50 | touches: [{
51 | pageY: 18
52 | }]
53 | }
54 |
55 | expect(_.getPageY(elem)).toBe(26)
56 |
57 | delete elem.pageY
58 |
59 | expect(_.getPageY(elem)).toBe(18)
60 | })
61 |
62 | test('getDistanceX', () => {
63 | const elem = {
64 | pageX: 100
65 | }
66 |
67 | const container = {
68 | getBoundingClientRect: () => ({
69 | left: 20
70 | })
71 | }
72 |
73 | window.pageXOffset = 5
74 |
75 | expect(_.getDistanceX(elem, container)).toBe(75)
76 | })
77 |
78 | test('getDistanceY', () => {
79 | const elem = {
80 | pageY: 100
81 | }
82 |
83 | const container = {
84 | getBoundingClientRect: () => ({
85 | top: 15
86 | })
87 | }
88 |
89 | window.pageYOffset = 5
90 |
91 | expect(_.getDistanceY(elem, container)).toBe(80)
92 | })
93 |
94 | test('dealEdgeValue', () => {
95 | let itemInfo = {
96 | width: 10,
97 | left: 20
98 | }
99 |
100 | let styleInfo = {
101 | left: -1
102 | }
103 |
104 | expect(_.dealEdgeValue(itemInfo, styleInfo, {})).toEqual({
105 | left: 0,
106 | width: 30
107 | })
108 |
109 | itemInfo = {
110 | top: 3,
111 | height: 6
112 | }
113 |
114 | styleInfo = {
115 | top: -2
116 | }
117 |
118 | expect(_.dealEdgeValue(itemInfo, styleInfo, {})).toEqual({
119 | top: 0,
120 | height: 9
121 | })
122 |
123 | itemInfo = {
124 | left: 2
125 | }
126 |
127 | styleInfo = {
128 | width: 23
129 | }
130 |
131 | let container = {
132 | width: 8
133 | }
134 |
135 | expect(_.dealEdgeValue(itemInfo, styleInfo, container)).toEqual({
136 | left: 2,
137 | width: 6
138 | })
139 |
140 | container.width = 1000
141 |
142 | expect(_.dealEdgeValue(itemInfo, styleInfo, container)).toEqual({
143 | left: 2,
144 | width: 6
145 | })
146 |
147 | itemInfo = {
148 | top: 10
149 | }
150 |
151 | styleInfo = {
152 | height: 21
153 | }
154 |
155 | container = {
156 | height: 13
157 | }
158 |
159 | expect(_.dealEdgeValue(itemInfo, styleInfo, container)).toEqual({
160 | top: 10,
161 | height: 3
162 | })
163 |
164 | container.height = 1000
165 |
166 | expect(_.dealEdgeValue(itemInfo, styleInfo, container)).toEqual({
167 | top: 10,
168 | height: 3
169 | })
170 | })
171 |
172 | test('dealTL', () => {
173 | const itemInfo = {
174 | width: 100,
175 | height: 100,
176 | left: 3,
177 | top: 4
178 | }
179 |
180 | expect(_.dealTL(itemInfo, 2, 2)).toEqual({
181 | left: 5,
182 | top: 6,
183 | height: 98,
184 | width: 98
185 | })
186 |
187 | expect(_.dealTL(itemInfo, 100, 2)).toEqual({
188 | top: 6,
189 | height: 98
190 | })
191 |
192 | expect(_.dealTL(itemInfo, 2, 100)).toEqual({
193 | left: 5,
194 | width: 98
195 | })
196 | })
197 |
198 | test('dealTC', () => {
199 | const itemInfo = {
200 | width: 100,
201 | height: 100,
202 | left: 3,
203 | top: 4
204 | }
205 |
206 | expect(_.dealTC(itemInfo, 2, 2)).toEqual({
207 | top: 6,
208 | height: 98
209 | })
210 |
211 | expect(_.dealTC(itemInfo, 2, 100)).toEqual({})
212 | })
213 |
214 | test('dealTR', () => {
215 | const itemInfo = {
216 | width: 100,
217 | height: 100,
218 | left: 3,
219 | top: 4
220 | }
221 |
222 | expect(_.dealTR(itemInfo, 2, 2)).toEqual({
223 | top: 6,
224 | height: 98,
225 | width: 102
226 | })
227 |
228 | expect(_.dealTR(itemInfo, -100, 2)).toEqual({
229 | top: 6,
230 | height: 98
231 | })
232 |
233 | expect(_.dealTR(itemInfo, 2, 100)).toEqual({
234 | width: 102
235 | })
236 | })
237 |
238 | test('dealCL', () => {
239 | const itemInfo = {
240 | width: 100,
241 | height: 100,
242 | left: 3,
243 | top: 4
244 | }
245 |
246 | expect(_.dealCL(itemInfo, 2, 2)).toEqual({
247 | left: 5,
248 | width: 98
249 | })
250 |
251 | expect(_.dealCL(itemInfo, 100, 2)).toEqual({})
252 | })
253 |
254 | test('dealCR', () => {
255 | const itemInfo = {
256 | width: 100,
257 | height: 100,
258 | left: 3,
259 | top: 4
260 | }
261 |
262 | expect(_.dealCR(itemInfo, 2, 2)).toEqual({
263 | width: 102
264 | })
265 |
266 | expect(_.dealCR(itemInfo, -100, 2)).toEqual({})
267 | })
268 |
269 | test('dealBL', () => {
270 | const itemInfo = {
271 | width: 100,
272 | height: 100,
273 | left: 3,
274 | top: 4
275 | }
276 |
277 | expect(_.dealBL(itemInfo, 2, 2)).toEqual({
278 | height: 102,
279 | left: 5,
280 | width: 98
281 | })
282 |
283 | expect(_.dealBL(itemInfo, 100, 2)).toEqual({
284 | height: 102
285 | })
286 |
287 | expect(_.dealBL(itemInfo, 2, -100)).toEqual({
288 | left: 5,
289 | width: 98
290 | })
291 | })
292 |
293 | test('dealBC', () => {
294 | const itemInfo = {
295 | width: 100,
296 | height: 100,
297 | left: 3,
298 | top: 4
299 | }
300 |
301 | expect(_.dealBC(itemInfo, 2, 2)).toEqual({
302 | height: 102
303 | })
304 |
305 | expect(_.dealBC(itemInfo, 2, -100)).toEqual({})
306 | })
307 |
308 | test('dealBR', () => {
309 | const itemInfo = {
310 | width: 100,
311 | height: 100,
312 | left: 3,
313 | top: 4
314 | }
315 |
316 | expect(_.dealBR(itemInfo, 2, 2)).toEqual({
317 | width: 102,
318 | height: 102
319 | })
320 |
321 | expect(_.dealBR(itemInfo, -100, 2)).toEqual({
322 | height: 102
323 | })
324 |
325 | expect(_.dealBR(itemInfo, 2, -100)).toEqual({
326 | width: 102
327 | })
328 | })
329 | })
330 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | publicPath: process.env.NODE_ENV === 'production' ? './' : ''
4 | }
5 |
--------------------------------------------------------------------------------