4 * Copyright 2016 RIFT.IO Inc
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
20 * Created by onvelocity on 8/9/15.
24 import getZoomFactor
from './zoomFactor'
25 import '../styles/ResizableManager.scss'
27 const sideComplement
= {
34 function zoneName(sides
) {
35 return sides
.sort((a
, b
) => {
42 if (a
.side
=== 'top' || a
.side
=== 'bottom') {
49 const resizeDragZones
= {
50 top (position
, x
, y
, resizeDragZoneWidth
) {
51 const adj
= y
> 0 && y
< document
.body
.clientHeight
;
52 const pos
= position
.top
- (adj
? resizeDragZoneWidth
/ 2 : 0);
53 if (Math
.abs(y
- pos
) < resizeDragZoneWidth
) {
57 bottom (position
, x
, y
, resizeDragZoneWidth
) {
58 const adj
= y
> 0 && y
< document
.body
.clientHeight
;
59 const pos
= position
.bottom
- (adj
? resizeDragZoneWidth
/ 2 : 0);
60 if (Math
.abs(y
- pos
) < resizeDragZoneWidth
) {
64 right (position
, x
, y
, resizeDragZoneWidth
) {
65 const adj
= x
> 0 && x
< document
.body
.clientWidth
;
66 const pos
= position
.right
- (adj
? resizeDragZoneWidth
/ 2 : 0);
67 if (Math
.abs(x
- pos
) < resizeDragZoneWidth
) {
71 left (position
, x
, y
, resizeDragZoneWidth
) {
72 const adj
= x
> 0 && x
< document
.body
.clientWidth
;
73 const pos
= position
.left
- (adj
? resizeDragZoneWidth
/ 2 : 0);
74 if (Math
.abs(x
- pos
) < resizeDragZoneWidth
) {
78 outside (position
, x
, y
, resizeDragZoneWidth
) {
79 if (x
< (position
.left
- resizeDragZoneWidth
) || x
> (position
.right
+ resizeDragZoneWidth
) || y
< (position
.top
- resizeDragZoneWidth
) || y
> (position
.bottom
+ resizeDragZoneWidth
)) {
85 const resizeDragLimitForSide
= {
86 top (position
, x
, y
, resizeDragZoneWidth
, resizing
) {
87 // y must be inside the position coordinates
88 const adj
= y
> 0 && y
< document
.body
.clientHeight
;
89 const top
= position
.top
- (adj
? resizeDragZoneWidth
/ 2 : 0);
90 const bottom
= position
.bottom
+ (adj
? resizeDragZoneWidth
/ 2 : 0);
91 return y
> top
&& y
< bottom
;
93 bottom (position
, x
, y
, resizeDragZoneWidth
) {
94 return this.top(position
, x
, y
, resizeDragZoneWidth
);
96 right (position
, x
, y
, resizeDragZoneWidth
) {
97 const adj
= x
> 0 && x
< document
.body
.clientWidth
;
98 const left
= position
.left
- (adj
? resizeDragZoneWidth
/ 2 : 0);
99 const right
= position
.right
+ (adj
? resizeDragZoneWidth
/ 2 : 0);
100 return x
> left
&& x
< right
;
102 left (position
, x
, y
, resizeDragZoneWidth
) {
103 return this.right(position
, x
, y
, resizeDragZoneWidth
);
105 limit_top (position
, x
, y
, resizeDragZoneWidth
, resizing
) {
106 // must be outside the position coordinates
109 right
: position
.right
,
110 bottom
: position
.bottom
,
113 return this.top(limit
, x
, y
, resizeDragZoneWidth
, resizing
);
115 limit_bottom (position
, x
, y
, resizeDragZoneWidth
, resizing
) {
118 right
: position
.right
,
122 return this.bottom(limit
, x
, y
, resizeDragZoneWidth
, resizing
);
124 limit_right (position
, x
, y
, resizeDragZoneWidth
, resizing
) {
128 bottom
: position
.bottom
,
131 return this.right(position
, x
, y
, resizeDragZoneWidth
, resizing
);
133 limit_left (position
, x
, y
, resizeDragZoneWidth
, resizing
) {
136 right
: position
.right
,
137 bottom
: position
.bottom
,
140 return this.left(position
, x
, y
, resizeDragZoneWidth
, resizing
);
144 function createEvent(e
, eventName
= 'resize') {
145 const lastX
= this.resizing
.lastX
|| 0;
146 const lastY
= this.resizing
.lastY
|| 0;
147 const zoomFactor
= getZoomFactor();
148 this.resizing
.lastX
= (e
.clientX
/ zoomFactor
);
149 this.resizing
.lastY
= (e
.clientY
/ zoomFactor
);
154 x
: e
.clientX
/ zoomFactor
,
155 y
: e
.clientY
/ zoomFactor
,
156 side
: this.resizing
.side
,
157 start
: {x
: this.resizing
.startX
, y
: this.resizing
.startY
},
159 x
: lastX
- (e
.clientX
/ zoomFactor
),
160 y
: lastY
- (e
.clientY
/ zoomFactor
)
162 target
: this.resizing
.target
,
166 if (window
.CustomEvent
.prototype.initCustomEvent
) {
168 var evt
= document
.createEvent('CustomEvent');
169 evt
.initCustomEvent(eventName
, data
.bubbles
, data
.cancelable
, data
.detail
);
172 return new CustomEvent(eventName
, data
);
175 const defaultHandleOffset
= [0, 0, 0, 0]; // top, right, bottom, left
177 class ResizableManager
{
179 constructor(target
= document
, dragZones
= resizeDragZones
) {
180 this.target
= target
;
181 this.resizing
= null;
182 this.lastResizable
= null;
183 this.activeResizable
= null;
184 this.resizeDragZones
= dragZones
;
185 this.defaultDragZoneWidth
= 5;
186 this.isPaused
= false;
187 this.addAllEventListeners();
191 this.isPaused
= true;
192 this.removeAllEventListeners();
197 this.addAllEventListeners();
198 this.isPaused
= false;
202 static isResizing() {
203 return document
.body
.classList
.contains('resizing');
206 addAllEventListeners() {
207 this.removeAllEventListeners();
208 this.mouseout
= this.mouseout
.bind(this);
209 this.mousedown
= this.mousedown
.bind(this);
210 this.dispatchResizeStop
= this.dispatchResizeStop
.bind(this);
211 this.dispatchResize
= this.dispatchResize
.bind(this);
212 this.updateResizeCursor
= this.updateResizeCursor
.bind(this);
213 this.target
.addEventListener('mousemove', this.updateResizeCursor
, true);
214 this.target
.addEventListener('mousedown', this.mousedown
, true);
215 this.target
.addEventListener('mousemove', this.dispatchResize
, true);
216 this.target
.addEventListener('mouseup', this.dispatchResizeStop
, true);
217 this.target
.addEventListener('mouseout', this.mouseout
, true);
220 removeAllEventListeners() {
221 this.target
.removeEventListener('mousemove', this.updateResizeCursor
, true);
222 this.target
.removeEventListener('mousedown', this.mousedown
, true);
223 this.target
.removeEventListener('mousemove', this.dispatchResize
, true);
224 this.target
.removeEventListener('mouseup', this.dispatchResizeStop
, true);
225 this.target
.removeEventListener('mouseout', this.mouseout
, true);
228 makeResizableActive(d
) {
229 this.activeResizable
= d
;
232 clearActiveResizable() {
233 this.activeResizable
= null;
237 if (this.resizing
&& (e
.clientX
> window
.innerWidth
|| e
.clientX
< 0 || e
.clientY
< 0 || e
.clientY
> window
.innerHeight
)) {
238 this.dispatchResizeStop(e
);
243 if (!this.resizing
&& this.activeResizable
) {
244 this.resizing
= this.activeResizable
;
245 const zoomFactor
= getZoomFactor();
246 this.resizing
.startX
= e
.clientX
/ zoomFactor
;
247 this.resizing
.startY
= e
.clientY
/ zoomFactor
;
249 this.dispatchResizeStart(e
);
254 if (this.resizing
&& !this.resizeLimitReached
) {
256 const resizeEvent
= createEvent
.call(this, e
, 'resize');
257 this.resizing
.target
.dispatchEvent(resizeEvent
)
261 dispatchResizeStart(e
) {
263 document
.body
.classList
.add('resizing');
265 const resizeEvent
= createEvent
.call(this, e
, 'resize-start');
266 this.resizing
.target
.dispatchEvent(resizeEvent
)
270 dispatchResizeStop(e
) {
272 document
.body
.classList
.remove('resizing');
274 const resizeEvent
= createEvent
.call(this, e
, 'resize-stop');
275 this.resizing
.target
.dispatchEvent(resizeEvent
);
276 this.lastResizable
= this.resizing
;
278 this.lastResizable
= this.activeResizable
;
280 this.clearActiveResizable();
281 this.resizing
= null;
285 return this.activeResizable
&& this.resizing
;
288 updateResizeCursor(e
) {
289 if (e
.defaultPrevented
) {
290 if (this.activeResizable
) {
291 document
.body
.style
.cursor
= '';
292 this.clearActiveResizable();
293 this.dispatchResizeStop(e
);
297 const zoomFactor
= getZoomFactor();
298 const x
= e
.clientX
/ zoomFactor
;
299 const y
= e
.clientY
/ zoomFactor
;
300 const resizables
= Array
.from(document
.querySelectorAll('[resizable], [data-resizable]'));
301 if (this.isResizing()) {
302 const others
= resizables
.filter(d
=> d
!== this.activeResizable
.target
);
303 this.resizeLimitReached
= this.checkResizeLimitReached(x
, y
, others
);
306 if (this.lastResizable
) {
307 resizables
.unshift(this.lastResizable
.target
)
309 let resizable
= null;
310 for (resizable
of resizables
) {
311 const result
= this.isCoordinatesOnDragZone(x
, y
, resizable
);
312 if (result
.side
=== 'inside' || result
.side
=== 'outside') {
313 this.clearActiveResizable();
314 // todo IE cursor flickers - now sure why might need to reduce the amount of classList updates
315 document
.body
.classList
.remove('-is-show-resize-cursor-col-resize');
316 document
.body
.classList
.remove('-is-show-resize-cursor-row-resize');
317 document
.body
.classList
.remove('-is-show-resize-cursor-nesw-resize');
318 document
.body
.classList
.remove('-is-show-resize-cursor-nwse-resize');
319 document
.body
.style
.cursor
= '';
322 if (result
.side
=== 'right' || result
.side
=== 'left') {
323 if (!document
.body
.classList
.contains('-is-show-resize-cursor-col-resize')) {
324 document
.body
.classList
.add('-is-show-resize-cursor-col-resize');
325 document
.body
.style
.cursor
= 'col-resize';
327 } else if (result
.side
=== 'top' || result
.side
=== 'bottom') {
328 if (!document
.body
.classList
.contains('-is-show-resize-cursor-row-resize')) {
329 document
.body
.classList
.add('-is-show-resize-cursor-row-resize');
330 document
.body
.style
.cursor
= 'row-resize';
332 } else if (result
.side
=== 'top-right' || result
.side
=== 'bottom-left') {
333 if (!document
.body
.classList
.contains('-is-show-resize-cursor-nesw-resize')) {
334 document
.body
.classList
.add('-is-show-resize-cursor-nesw-resize');
335 document
.body
.style
.cursor
= 'nesw-resize';
337 } else if (result
.side
=== 'top-left' || result
.side
=== 'bottom-right') {
338 if (!document
.body
.classList
.contains('-is-show-resize-cursor-nwse-resize')) {
339 document
.body
.classList
.add('-is-show-resize-cursor-nwse-resize');
340 document
.body
.style
.cursor
= 'nwse-resize';
343 this.makeResizableActive(result
);
348 checkResizeLimitReached(x
, y
, resizables
) {
349 let hasReachedLimit
= false;
350 if (this.isResizing()) {
351 const sides
= this.activeResizable
.side
.split('-');
352 resizables
.filter(resizable
=> {
353 const resizableSide
= resizable
.dataset
.resizable
;
354 return sides
.filter(side
=> resizableSide
.indexOf(sideComplement
[side
]) > -1).length
;
355 }).forEach(resizable
=> {
356 const position
= resizable
.getBoundingClientRect();
357 const zoneWidth
= resizable
.dataset
.resizableDragZoneWidth
|| this.defaultDragZoneWidth
;
358 const resizableSide
= resizable
.dataset
.resizable
;
359 const isLimit
= /^limit/.test(resizableSide
);
360 sides
.forEach(side
=> {
361 const key
= (isLimit
? 'limit_' : '') + side
;
362 if (resizeDragLimitForSide
[key
]) {
363 if (resizeDragLimitForSide
[key
](position
, x
, y
, zoneWidth
, this.resizing
)) {
364 hasReachedLimit
= true;
370 return hasReachedLimit
;
373 isCoordinatesOnDragZone(x
, y
, element
) {
378 const check
= this.resizeDragZones
;
379 const sides
= element
.dataset
.resizable
|| 'top,right,bottom,left';
380 const zoneWidth
= element
.dataset
.resizableDragZoneWidth
|| this.defaultDragZoneWidth
;
381 const zoneOffsets
= this.parseHandleOffsets(element
.dataset
.resizableHandleOffset
);
382 const rect
= element
.getBoundingClientRect();
384 top
: rect
.top
+ zoneOffsets
[0],
385 right
: rect
.right
+ zoneOffsets
[1],
386 bottom
: rect
.bottom
+ zoneOffsets
[2],
387 left
: rect
.left
+ zoneOffsets
[3]
390 if (check
.outside
&& check
.outside(position
, x
, y
, zoneWidth
)) {
391 result
.side
= 'outside';
394 sides
.split(/\W+|\s*,\s*/).forEach(side
=> {
396 const r
= check
[side
](position
, x
, y
, zoneWidth
);
403 result
.side
= zoneName(temp
);
409 parseHandleOffsets(str
) {
410 const val
= String(str
).trim().split(' ');
411 if (val
.length
=== 0) {
412 return defaultHandleOffset
.splice();
414 return defaultHandleOffset
.map((defaultOffset
, i
) => {
415 let offset
= parseFloat(val
[i
]);
417 offset
= parseFloat(val
[i
- 2]);
419 return defaultHandleOffset
[i
];
428 export default ResizableManager
;