--- /dev/null
+
+/*
+ *
+ * Copyright 2016 RIFT.IO Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 8/9/15.
+ */
+'use strict';
+
+import getZoomFactor from './zoomFactor'
+import '../styles/ResizableManager.scss'
+
+const sideComplement = {
+ left: 'right',
+ right: 'left',
+ top: 'bottom',
+ bottom: 'top'
+};
+
+function zoneName(sides) {
+ return sides.sort((a, b) => {
+ if (!a) {
+ return 1;
+ }
+ if (!b) {
+ return 1;
+ }
+ if (a.side === 'top' || a.side === 'bottom') {
+ return -1;
+ }
+ return 1;
+ }).join('-');
+}
+
+const resizeDragZones = {
+ top (position, x, y, resizeDragZoneWidth) {
+ const adj = y > 0 && y < document.body.clientHeight;
+ const pos = position.top - (adj ? resizeDragZoneWidth / 2 : 0);
+ if (Math.abs(y - pos) < resizeDragZoneWidth) {
+ return 'top';
+ }
+ },
+ bottom (position, x, y, resizeDragZoneWidth) {
+ const adj = y > 0 && y < document.body.clientHeight;
+ const pos = position.bottom - (adj ? resizeDragZoneWidth / 2 : 0);
+ if (Math.abs(y - pos) < resizeDragZoneWidth) {
+ return 'bottom';
+ }
+ },
+ right (position, x, y, resizeDragZoneWidth) {
+ const adj = x > 0 && x < document.body.clientWidth;
+ const pos = position.right - (adj ? resizeDragZoneWidth / 2 : 0);
+ if (Math.abs(x - pos) < resizeDragZoneWidth) {
+ return 'right';
+ }
+ },
+ left (position, x, y, resizeDragZoneWidth) {
+ const adj = x > 0 && x < document.body.clientWidth;
+ const pos = position.left - (adj ? resizeDragZoneWidth / 2 : 0);
+ if (Math.abs(x - pos) < resizeDragZoneWidth) {
+ return 'left';
+ }
+ },
+ outside (position, x, y, resizeDragZoneWidth) {
+ if (x < (position.left - resizeDragZoneWidth) || x > (position.right + resizeDragZoneWidth) || y < (position.top - resizeDragZoneWidth) || y > (position.bottom + resizeDragZoneWidth)) {
+ return 'outside';
+ }
+ }
+};
+
+const resizeDragLimitForSide = {
+ top (position, x, y, resizeDragZoneWidth, resizing) {
+ // y must be inside the position coordinates
+ const adj = y > 0 && y < document.body.clientHeight;
+ const top = position.top - (adj ? resizeDragZoneWidth / 2 : 0);
+ const bottom = position.bottom + (adj ? resizeDragZoneWidth / 2 : 0);
+ return y > top && y < bottom;
+ },
+ bottom (position, x, y, resizeDragZoneWidth) {
+ return this.top(position, x, y, resizeDragZoneWidth);
+ },
+ right (position, x, y, resizeDragZoneWidth) {
+ const adj = x > 0 && x < document.body.clientWidth;
+ const left = position.left - (adj ? resizeDragZoneWidth / 2 : 0);
+ const right = position.right + (adj ? resizeDragZoneWidth / 2 : 0);
+ return x > left && x < right;
+ },
+ left (position, x, y, resizeDragZoneWidth) {
+ return this.right(position, x, y, resizeDragZoneWidth);
+ },
+ limit_top (position, x, y, resizeDragZoneWidth, resizing) {
+ // must be outside the position coordinates
+ const limit = {
+ top: 0,
+ right: position.right,
+ bottom: position.bottom,
+ left: position.left
+ };
+ return this.top(limit, x, y, resizeDragZoneWidth, resizing);
+ },
+ limit_bottom (position, x, y, resizeDragZoneWidth, resizing) {
+ const limit = {
+ top: position.top,
+ right: position.right,
+ bottom:0,
+ left: position.left
+ };
+ return this.bottom(limit, x, y, resizeDragZoneWidth, resizing);
+ },
+ limit_right (position, x, y, resizeDragZoneWidth, resizing) {
+ const limit = {
+ top: position.top,
+ right: 0,
+ bottom: position.bottom,
+ left: position.left
+ };
+ return this.right(position, x, y, resizeDragZoneWidth, resizing);
+ },
+ limit_left (position, x, y, resizeDragZoneWidth, resizing) {
+ const limit = {
+ top: position.top,
+ right: position.right,
+ bottom: position.bottom,
+ left: 0
+ };
+ return this.left(position, x, y, resizeDragZoneWidth, resizing);
+ }
+};
+
+function createEvent(e, eventName = 'resize') {
+ const lastX = this.resizing.lastX || 0;
+ const lastY = this.resizing.lastY || 0;
+ const zoomFactor = getZoomFactor();
+ this.resizing.lastX = (e.clientX / zoomFactor);
+ this.resizing.lastY = (e.clientY / zoomFactor);
+ const data = {
+ bubbles: true,
+ cancelable: true,
+ detail: {
+ x: e.clientX / zoomFactor,
+ y: e.clientY / zoomFactor,
+ side: this.resizing.side,
+ start: {x: this.resizing.startX, y: this.resizing.startY},
+ moved: {
+ x: lastX - (e.clientX / zoomFactor),
+ y: lastY - (e.clientY / zoomFactor)
+ },
+ target: this.resizing.target,
+ originalEvent: e
+ }
+ };
+ if (window.CustomEvent.prototype.initCustomEvent) {
+ // support IE
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(eventName, data.bubbles, data.cancelable, data.detail);
+ return evt;
+ }
+ return new CustomEvent(eventName, data);
+}
+
+const defaultHandleOffset = [0, 0, 0, 0]; // top, right, bottom, left
+
+class ResizableManager {
+
+ constructor(target = document, dragZones = resizeDragZones) {
+ this.target = target;
+ this.resizing = null;
+ this.lastResizable = null;
+ this.activeResizable = null;
+ this.resizeDragZones = dragZones;
+ this.defaultDragZoneWidth = 5;
+ this.isPaused = false;
+ this.addAllEventListeners();
+ }
+
+ pause() {
+ this.isPaused = true;
+ this.removeAllEventListeners();
+ }
+
+ resume() {
+ if (this.isPaused) {
+ this.addAllEventListeners();
+ this.isPaused = false;
+ }
+ }
+
+ static isResizing() {
+ return document.body.classList.contains('resizing');
+ }
+
+ addAllEventListeners() {
+ this.removeAllEventListeners();
+ this.mouseout = this.mouseout.bind(this);
+ this.mousedown = this.mousedown.bind(this);
+ this.dispatchResizeStop = this.dispatchResizeStop.bind(this);
+ this.dispatchResize = this.dispatchResize.bind(this);
+ this.updateResizeCursor = this.updateResizeCursor.bind(this);
+ this.target.addEventListener('mousemove', this.updateResizeCursor, true);
+ this.target.addEventListener('mousedown', this.mousedown, true);
+ this.target.addEventListener('mousemove', this.dispatchResize, true);
+ this.target.addEventListener('mouseup', this.dispatchResizeStop, true);
+ this.target.addEventListener('mouseout', this.mouseout, true);
+ }
+
+ removeAllEventListeners() {
+ this.target.removeEventListener('mousemove', this.updateResizeCursor, true);
+ this.target.removeEventListener('mousedown', this.mousedown, true);
+ this.target.removeEventListener('mousemove', this.dispatchResize, true);
+ this.target.removeEventListener('mouseup', this.dispatchResizeStop, true);
+ this.target.removeEventListener('mouseout', this.mouseout, true);
+ }
+
+ makeResizableActive(d) {
+ this.activeResizable = d;
+ }
+
+ clearActiveResizable() {
+ this.activeResizable = null;
+ }
+
+ mouseout(e) {
+ if (this.resizing && (e.clientX > window.innerWidth || e.clientX < 0 || e.clientY < 0 || e.clientY > window.innerHeight)) {
+ this.dispatchResizeStop(e);
+ }
+ }
+
+ mousedown(e) {
+ if (!this.resizing && this.activeResizable) {
+ this.resizing = this.activeResizable;
+ const zoomFactor = getZoomFactor();
+ this.resizing.startX = e.clientX / zoomFactor;
+ this.resizing.startY = e.clientY / zoomFactor;
+ e.preventDefault();
+ this.dispatchResizeStart(e);
+ }
+ }
+
+ dispatchResize(e) {
+ if (this.resizing && !this.resizeLimitReached) {
+ e.preventDefault();
+ const resizeEvent = createEvent.call(this, e, 'resize');
+ this.resizing.target.dispatchEvent(resizeEvent)
+ }
+ }
+
+ dispatchResizeStart(e) {
+ if (this.resizing) {
+ document.body.classList.add('resizing');
+ e.preventDefault();
+ const resizeEvent = createEvent.call(this, e, 'resize-start');
+ this.resizing.target.dispatchEvent(resizeEvent)
+ }
+ }
+
+ dispatchResizeStop(e) {
+ if (this.resizing) {
+ document.body.classList.remove('resizing');
+ e.preventDefault();
+ const resizeEvent = createEvent.call(this, e, 'resize-stop');
+ this.resizing.target.dispatchEvent(resizeEvent);
+ this.lastResizable = this.resizing;
+ } else {
+ this.lastResizable = this.activeResizable;
+ }
+ this.clearActiveResizable();
+ this.resizing = null;
+ }
+
+ isResizing() {
+ return this.activeResizable && this.resizing;
+ }
+
+ updateResizeCursor(e) {
+ if (e.defaultPrevented) {
+ if (this.activeResizable) {
+ document.body.style.cursor = '';
+ this.clearActiveResizable();
+ this.dispatchResizeStop(e);
+ }
+ return;
+ }
+ const zoomFactor = getZoomFactor();
+ const x = e.clientX / zoomFactor;
+ const y = e.clientY / zoomFactor;
+ const resizables = Array.from(document.querySelectorAll('[resizable], [data-resizable]'));
+ if (this.isResizing()) {
+ const others = resizables.filter(d => d !== this.activeResizable.target);
+ this.resizeLimitReached = this.checkResizeLimitReached(x, y, others);
+ return;
+ }
+ if (this.lastResizable) {
+ resizables.unshift(this.lastResizable.target)
+ }
+ let resizable = null;
+ for (resizable of resizables) {
+ const result = this.isCoordinatesOnDragZone(x, y, resizable);
+ if (result.side === 'inside' || result.side === 'outside') {
+ this.clearActiveResizable();
+ // todo IE cursor flickers - now sure why might need to reduce the amount of classList updates
+ document.body.classList.remove('-is-show-resize-cursor-col-resize');
+ document.body.classList.remove('-is-show-resize-cursor-row-resize');
+ document.body.classList.remove('-is-show-resize-cursor-nesw-resize');
+ document.body.classList.remove('-is-show-resize-cursor-nwse-resize');
+ document.body.style.cursor = '';
+ continue
+ }
+ if (result.side === 'right' || result.side === 'left') {
+ if (!document.body.classList.contains('-is-show-resize-cursor-col-resize')) {
+ document.body.classList.add('-is-show-resize-cursor-col-resize');
+ document.body.style.cursor = 'col-resize';
+ }
+ } else if (result.side === 'top' || result.side === 'bottom') {
+ if (!document.body.classList.contains('-is-show-resize-cursor-row-resize')) {
+ document.body.classList.add('-is-show-resize-cursor-row-resize');
+ document.body.style.cursor = 'row-resize';
+ }
+ } else if (result.side === 'top-right' || result.side === 'bottom-left') {
+ if (!document.body.classList.contains('-is-show-resize-cursor-nesw-resize')) {
+ document.body.classList.add('-is-show-resize-cursor-nesw-resize');
+ document.body.style.cursor = 'nesw-resize';
+ }
+ } else if (result.side === 'top-left' || result.side === 'bottom-right') {
+ if (!document.body.classList.contains('-is-show-resize-cursor-nwse-resize')) {
+ document.body.classList.add('-is-show-resize-cursor-nwse-resize');
+ document.body.style.cursor = 'nwse-resize';
+ }
+ }
+ this.makeResizableActive(result);
+ break
+ }
+ }
+
+ checkResizeLimitReached(x, y, resizables) {
+ let hasReachedLimit = false;
+ if (this.isResizing()) {
+ const sides = this.activeResizable.side.split('-');
+ resizables.filter(resizable => {
+ const resizableSide = resizable.dataset.resizable;
+ return sides.filter(side => resizableSide.indexOf(sideComplement[side]) > -1).length;
+ }).forEach(resizable => {
+ const position = resizable.getBoundingClientRect();
+ const zoneWidth = resizable.dataset.resizableDragZoneWidth || this.defaultDragZoneWidth;
+ const resizableSide = resizable.dataset.resizable;
+ const isLimit = /^limit/.test(resizableSide);
+ sides.forEach(side => {
+ const key = (isLimit ? 'limit_' : '') + side;
+ if (resizeDragLimitForSide[key]) {
+ if (resizeDragLimitForSide[key](position, x, y, zoneWidth, this.resizing)) {
+ hasReachedLimit = true;
+ }
+ }
+ });
+ });
+ }
+ return hasReachedLimit;
+ }
+
+ isCoordinatesOnDragZone(x, y, element) {
+ const result = {
+ side: 'inside',
+ target: element
+ };
+ const check = this.resizeDragZones;
+ const sides = element.dataset.resizable || 'top,right,bottom,left';
+ const zoneWidth = element.dataset.resizableDragZoneWidth || this.defaultDragZoneWidth;
+ const zoneOffsets = this.parseHandleOffsets(element.dataset.resizableHandleOffset);
+ const rect = element.getBoundingClientRect();
+ const position = {
+ top: rect.top + zoneOffsets[0],
+ right: rect.right + zoneOffsets[1],
+ bottom: rect.bottom + zoneOffsets[2],
+ left: rect.left + zoneOffsets[3]
+ };
+ const temp = [];
+ if (check.outside && check.outside(position, x, y, zoneWidth)) {
+ result.side = 'outside';
+ return result;
+ }
+ sides.split(/\W+|\s*,\s*/).forEach(side => {
+ if (check[side]) {
+ const r = check[side](position, x, y, zoneWidth);
+ if (r) {
+ temp.push(r);
+ }
+ }
+ });
+ if (temp.length) {
+ result.side = zoneName(temp);
+ return result;
+ }
+ return result;
+ }
+
+ parseHandleOffsets(str) {
+ const val = String(str).trim().split(' ');
+ if (val.length === 0) {
+ return defaultHandleOffset.splice();
+ }
+ return defaultHandleOffset.map((defaultOffset, i) => {
+ let offset = parseFloat(val[i]);
+ if (isNaN(offset)) {
+ offset = parseFloat(val[i - 2]);
+ if (isNaN(offset)) {
+ return defaultHandleOffset[i];
+ }
+ }
+ return offset;
+ });
+ }
+
+}
+
+export default ResizableManager;