Rift.IO OSM R1 Initial Submission
[osm/UI.git] / skyquake / plugins / composer / src / src / libraries / ResizableManager.js
1
2 /*
3 *
4 * Copyright 2016 RIFT.IO Inc
5 *
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
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
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.
17 *
18 */
19 /**
20 * Created by onvelocity on 8/9/15.
21 */
22 'use strict';
23
24 import getZoomFactor from './zoomFactor'
25 import '../styles/ResizableManager.scss'
26
27 const sideComplement = {
28 left: 'right',
29 right: 'left',
30 top: 'bottom',
31 bottom: 'top'
32 };
33
34 function zoneName(sides) {
35 return sides.sort((a, b) => {
36 if (!a) {
37 return 1;
38 }
39 if (!b) {
40 return 1;
41 }
42 if (a.side === 'top' || a.side === 'bottom') {
43 return -1;
44 }
45 return 1;
46 }).join('-');
47 }
48
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) {
54 return 'top';
55 }
56 },
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) {
61 return 'bottom';
62 }
63 },
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) {
68 return 'right';
69 }
70 },
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) {
75 return 'left';
76 }
77 },
78 outside (position, x, y, resizeDragZoneWidth) {
79 if (x < (position.left - resizeDragZoneWidth) || x > (position.right + resizeDragZoneWidth) || y < (position.top - resizeDragZoneWidth) || y > (position.bottom + resizeDragZoneWidth)) {
80 return 'outside';
81 }
82 }
83 };
84
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;
92 },
93 bottom (position, x, y, resizeDragZoneWidth) {
94 return this.top(position, x, y, resizeDragZoneWidth);
95 },
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;
101 },
102 left (position, x, y, resizeDragZoneWidth) {
103 return this.right(position, x, y, resizeDragZoneWidth);
104 },
105 limit_top (position, x, y, resizeDragZoneWidth, resizing) {
106 // must be outside the position coordinates
107 const limit = {
108 top: 0,
109 right: position.right,
110 bottom: position.bottom,
111 left: position.left
112 };
113 return this.top(limit, x, y, resizeDragZoneWidth, resizing);
114 },
115 limit_bottom (position, x, y, resizeDragZoneWidth, resizing) {
116 const limit = {
117 top: position.top,
118 right: position.right,
119 bottom:0,
120 left: position.left
121 };
122 return this.bottom(limit, x, y, resizeDragZoneWidth, resizing);
123 },
124 limit_right (position, x, y, resizeDragZoneWidth, resizing) {
125 const limit = {
126 top: position.top,
127 right: 0,
128 bottom: position.bottom,
129 left: position.left
130 };
131 return this.right(position, x, y, resizeDragZoneWidth, resizing);
132 },
133 limit_left (position, x, y, resizeDragZoneWidth, resizing) {
134 const limit = {
135 top: position.top,
136 right: position.right,
137 bottom: position.bottom,
138 left: 0
139 };
140 return this.left(position, x, y, resizeDragZoneWidth, resizing);
141 }
142 };
143
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);
150 const data = {
151 bubbles: true,
152 cancelable: true,
153 detail: {
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},
158 moved: {
159 x: lastX - (e.clientX / zoomFactor),
160 y: lastY - (e.clientY / zoomFactor)
161 },
162 target: this.resizing.target,
163 originalEvent: e
164 }
165 };
166 if (window.CustomEvent.prototype.initCustomEvent) {
167 // support IE
168 var evt = document.createEvent('CustomEvent');
169 evt.initCustomEvent(eventName, data.bubbles, data.cancelable, data.detail);
170 return evt;
171 }
172 return new CustomEvent(eventName, data);
173 }
174
175 const defaultHandleOffset = [0, 0, 0, 0]; // top, right, bottom, left
176
177 class ResizableManager {
178
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();
188 }
189
190 pause() {
191 this.isPaused = true;
192 this.removeAllEventListeners();
193 }
194
195 resume() {
196 if (this.isPaused) {
197 this.addAllEventListeners();
198 this.isPaused = false;
199 }
200 }
201
202 static isResizing() {
203 return document.body.classList.contains('resizing');
204 }
205
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);
218 }
219
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);
226 }
227
228 makeResizableActive(d) {
229 this.activeResizable = d;
230 }
231
232 clearActiveResizable() {
233 this.activeResizable = null;
234 }
235
236 mouseout(e) {
237 if (this.resizing && (e.clientX > window.innerWidth || e.clientX < 0 || e.clientY < 0 || e.clientY > window.innerHeight)) {
238 this.dispatchResizeStop(e);
239 }
240 }
241
242 mousedown(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;
248 e.preventDefault();
249 this.dispatchResizeStart(e);
250 }
251 }
252
253 dispatchResize(e) {
254 if (this.resizing && !this.resizeLimitReached) {
255 e.preventDefault();
256 const resizeEvent = createEvent.call(this, e, 'resize');
257 this.resizing.target.dispatchEvent(resizeEvent)
258 }
259 }
260
261 dispatchResizeStart(e) {
262 if (this.resizing) {
263 document.body.classList.add('resizing');
264 e.preventDefault();
265 const resizeEvent = createEvent.call(this, e, 'resize-start');
266 this.resizing.target.dispatchEvent(resizeEvent)
267 }
268 }
269
270 dispatchResizeStop(e) {
271 if (this.resizing) {
272 document.body.classList.remove('resizing');
273 e.preventDefault();
274 const resizeEvent = createEvent.call(this, e, 'resize-stop');
275 this.resizing.target.dispatchEvent(resizeEvent);
276 this.lastResizable = this.resizing;
277 } else {
278 this.lastResizable = this.activeResizable;
279 }
280 this.clearActiveResizable();
281 this.resizing = null;
282 }
283
284 isResizing() {
285 return this.activeResizable && this.resizing;
286 }
287
288 updateResizeCursor(e) {
289 if (e.defaultPrevented) {
290 if (this.activeResizable) {
291 document.body.style.cursor = '';
292 this.clearActiveResizable();
293 this.dispatchResizeStop(e);
294 }
295 return;
296 }
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);
304 return;
305 }
306 if (this.lastResizable) {
307 resizables.unshift(this.lastResizable.target)
308 }
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 = '';
320 continue
321 }
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';
326 }
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';
331 }
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';
336 }
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';
341 }
342 }
343 this.makeResizableActive(result);
344 break
345 }
346 }
347
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;
365 }
366 }
367 });
368 });
369 }
370 return hasReachedLimit;
371 }
372
373 isCoordinatesOnDragZone(x, y, element) {
374 const result = {
375 side: 'inside',
376 target: element
377 };
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();
383 const position = {
384 top: rect.top + zoneOffsets[0],
385 right: rect.right + zoneOffsets[1],
386 bottom: rect.bottom + zoneOffsets[2],
387 left: rect.left + zoneOffsets[3]
388 };
389 const temp = [];
390 if (check.outside && check.outside(position, x, y, zoneWidth)) {
391 result.side = 'outside';
392 return result;
393 }
394 sides.split(/\W+|\s*,\s*/).forEach(side => {
395 if (check[side]) {
396 const r = check[side](position, x, y, zoneWidth);
397 if (r) {
398 temp.push(r);
399 }
400 }
401 });
402 if (temp.length) {
403 result.side = zoneName(temp);
404 return result;
405 }
406 return result;
407 }
408
409 parseHandleOffsets(str) {
410 const val = String(str).trim().split(' ');
411 if (val.length === 0) {
412 return defaultHandleOffset.splice();
413 }
414 return defaultHandleOffset.map((defaultOffset, i) => {
415 let offset = parseFloat(val[i]);
416 if (isNaN(offset)) {
417 offset = parseFloat(val[i - 2]);
418 if (isNaN(offset)) {
419 return defaultHandleOffset[i];
420 }
421 }
422 return offset;
423 });
424 }
425
426 }
427
428 export default ResizableManager;