UNPKG

resizable

Version:

Resizable behaviour for elements

530 lines (445 loc) 11.3 kB
import Draggable from 'draggy'; import emit from 'emmy/emit'; import on from 'emmy/on'; import off from 'emmy/off'; import css from 'mucss/css'; import paddings from 'mucss/padding'; import borders from 'mucss/border'; import margins from 'mucss/margin'; import offsets from 'mucss/offset'; var doc = document, root = doc.documentElement; /** * Make an element resizable. * * Note that we don’t need a container option * as arbitrary container is emulatable via fake resizable. * * @constructor */ function Resizable(el, options) { var self = this; if (!(self instanceof Resizable)) { return new Resizable(el, options); } self.element = el; Object.assign(self, options); //if element isn’t draggable yet - force it to be draggable, without movements if (self.draggable === true) { self.draggable = new Draggable(self.element, { within: self.within, css3: self.css3 }); } else if (self.draggable) { self.draggable = new Draggable(self.element, self.draggable); self.draggable.css3 = self.css3; } else { self.draggable = new Draggable(self.element, { handle: null }); } self.createHandles(); //bind event, if any if (self.resize) { self.on('resize', self.resize); } } var proto = Resizable.prototype; /** Use css3 for draggable, if any */ proto.css3 = true; /** Make itself draggable to the row */ proto.draggable = false; // events proto.on = function (event, callback) { on(this, event, callback) } proto.off = function (event, callback) { off(this, event, callback) } /** Create handles according to options */ proto.createHandles = function () { var self = this; //init handles var handles; //parse value if (Array.isArray(self.handles)) { handles = {}; for (var i = self.handles.length; i--;) { handles[self.handles[i]] = null; } } else if (typeof self.handles === 'string') { handles = {}; var arr = self.handles.match(/([swne]+)/g); for (var i = arr.length; i--;) { handles[arr[i]] = null; } } else if (typeof self.handles === 'object' && self.handles.constructor === Object) { handles = self.handles; } //default set of handles depends on position. else { var position = getComputedStyle(self.element).position; var display = getComputedStyle(self.element).display; //if display is inline-like - provide only three handles //it is position: static or display: inline if (/inline/.test(display) || /static/.test(position)) { handles = { s: null, se: null, e: null }; //ensure position is not static css(self.element, 'position', 'relative'); } //else - all handles else { handles = { s: null, se: null, e: null, ne: null, n: null, nw: null, w: null, sw: null }; } } //create proper number of handles var handle; for (var direction in handles) { handles[direction] = self.createHandle(handles[direction], direction); } //save handles elements self.handles = handles; } /** Create handle for the direction */ proto.createHandle = function (handle, direction) { var self = this; var el = self.element; //make handle element if (!handle) { handle = document.createElement('div'); handle.classList.add('resizable-handle'); } //insert handle to the element self.element.appendChild(handle); //save direction handle.direction = direction; //detect self.within //FIXME: may be painful if resizable is created on detached element var within = self.within === 'parent' ? self.element.parentNode : self.within; //make handle draggable var draggy = new Draggable(handle, { within: within, //can’t use abs pos, as we engage it in styling // css3: false, threshold: self.threshold, axis: /^[ns]$/.test(direction) ? 'y' : /^[we]$/.test(direction) ? 'x' : 'both' }); draggy.on('dragstart', function (e) { self.m = margins(el); self.b = borders(el); self.p = paddings(el); //update draggalbe params self.draggable.update(e); //save initial dragging offsets var s = getComputedStyle(el); self.offsets = self.draggable.getCoords(); //recalc border-box if (getComputedStyle(el).boxSizing === 'border-box') { self.p.top = 0; self.p.bottom = 0; self.p.left = 0; self.p.right = 0; self.b.top = 0; self.b.bottom = 0; self.b.left = 0; self.b.right = 0; } //save initial size self.initSize = [el.offsetWidth - self.b.left - self.b.right - self.p.left - self.p.right, el.offsetHeight - self.b.top - self.b.bottom - self.p.top - self.p.bottom]; //save initial full size self.initSizeFull = [ el.offsetWidth, el.offsetHeight ]; //movement prev coords self.prevCoords = [0, 0]; //shift-caused offset self.shiftOffset = [0, 0]; //central initial coords self.center = [self.offsets[0] + self.initSize[0] / 2, self.offsets[1] + self.initSize[1] / 2]; //calc limits (max height/width from left/right) if (self.within) { var po = offsets(within); var o = offsets(el); self.maxSize = [ o.left - po.left + self.initSize[0], o.top - po.top + self.initSize[1], po.right - o.right + self.initSize[0], po.bottom - o.bottom + self.initSize[1] ]; } else { self.maxSize = [9999, 9999, 9999, 9999]; } //preset mouse cursor css(root, { 'cursor': direction + '-resize' }); //clear cursors for (var h in self.handles) { css(self.handles[h], 'cursor', null); } //trigger callbacks emit(self, 'resizestart'); emit(el, 'resizestart'); }); draggy.on('drag', function () { var coords = draggy.getCoords(); var prevSize = [ el.offsetWidth, el.offsetHeight ]; //change width/height properly if (draggy.shiftKey) { switch (direction) { case 'se': case 's': case 'e': break; case 'nw': coords[0] = -coords[0]; coords[1] = -coords[1]; break; case 'n': coords[1] = -coords[1]; break; case 'w': coords[0] = -coords[0]; break; case 'ne': coords[1] = -coords[1]; break; case 'sw': coords[0] = -coords[0]; break; }; //set placement is relative to initial center line css(el, { width: Math.min( self.initSize[0] + coords[0] * 2, self.maxSize[2] + coords[0], self.maxSize[0] + coords[0] ), height: Math.min( self.initSize[1] + coords[1] * 2, self.maxSize[3] + coords[1], self.maxSize[1] + coords[1] ) }); var difX = prevSize[0] - el.offsetWidth; var difY = prevSize[1] - el.offsetHeight; //update draggable limits self.draggable.updateLimits(); if (difX) { self.draggable.move(self.center[0] - self.initSize[0] / 2 - coords[0]); } if (difY) { self.draggable.move(null, self.center[1] - self.initSize[1] / 2 - coords[1]); } } else { switch (direction) { case 'se': css(el, { width: Math.min( self.initSize[0] + coords[0], self.maxSize[2] ), height: Math.min( self.initSize[1] + coords[1], self.maxSize[3] ) }); case 's': css(el, { height: Math.min( self.initSize[1] + coords[1], self.maxSize[3] ) }); case 'e': css(el, { width: Math.min( self.initSize[0] + coords[0], self.maxSize[2] ) }); case 'se': case 's': case 'e': self.draggable.updateLimits(); self.draggable.move( self.center[0] - self.initSize[0] / 2, self.center[1] - self.initSize[1] / 2 ); break; case 'nw': css(el, { width: clamp(self.initSize[0] - coords[0], 0, self.maxSize[0]), height: clamp(self.initSize[1] - coords[1], 0, self.maxSize[1]) }); case 'n': css(el, { height: clamp(self.initSize[1] - coords[1], 0, self.maxSize[1]) }); case 'w': css(el, { width: clamp(self.initSize[0] - coords[0], 0, self.maxSize[0]) }); case 'nw': case 'n': case 'w': self.draggable.updateLimits(); //subtract t/l on changed size var deltaX = self.initSizeFull[0] - el.offsetWidth; var deltaY = self.initSizeFull[1] - el.offsetHeight; self.draggable.move(self.offsets[0] + deltaX, self.offsets[1] + deltaY); break; case 'ne': css(el, { width: clamp(self.initSize[0] + coords[0], 0, self.maxSize[2]), height: clamp(self.initSize[1] - coords[1], 0, self.maxSize[1]) }); self.draggable.updateLimits(); //subtract t/l on changed size var deltaY = self.initSizeFull[1] - el.offsetHeight; self.draggable.move(null, self.offsets[1] + deltaY); break; case 'sw': css(el, { width: clamp(self.initSize[0] - coords[0], 0, self.maxSize[0]), height: clamp(self.initSize[1] + coords[1], 0, self.maxSize[3]) }); self.draggable.updateLimits(); //subtract t/l on changed size var deltaX = self.initSizeFull[0] - el.offsetWidth; self.draggable.move(self.offsets[0] + deltaX); break; }; } //trigger callbacks emit(self, 'resize'); emit(el, 'resize'); draggy.setCoords(0, 0); }); draggy.on('dragend', function () { //clear cursor & pointer-events css(root, { 'cursor': null }); //get back cursors for (var h in self.handles) { css(self.handles[h], 'cursor', self.handles[h].direction + '-resize'); } //trigger callbacks emit(self, 'resizeend'); emit(el, 'resizeend'); }); //append styles css(handle, handleStyles[direction]); css(handle, 'cursor', direction + '-resize'); //append proper class handle.classList.add('resizable-handle-' + direction); return handle; }; /** deconstructor - removes any memory bindings */ proto.destroy = function () { //remove all handles for (var hName in this.handles) { this.element.removeChild(this.handles[hName]); Draggable.cache.get(this.handles[hName]).destroy(); } //remove references this.element = null; }; var w = 10; /** Threshold size */ proto.threshold = w; /** Styles for handles */ var handleStyles = { "e": { "left": "auto", "right": "-5px", "position": "absolute", "width": "10px", "top": "0px", "bottom": "0px" }, "w": { "right": "auto", "left": "-5px", "position": "absolute", "width": "10px", "top": "0px", "bottom": "0px" }, "s": { "top": "auto", "bottom": "-5px", "position": "absolute", "height": "10px", "left": "0px", "right": "0px" }, "n": { "bottom": "auto", "top": "-5px", "position": "absolute", "height": "10px", "left": "0px", "right": "0px" }, "nw": { "position": "absolute", "width": "10px", "height": "10px", "z-index": 1, "top": "-5px", "left": "-5px", "bottom": "auto", "right": "auto" }, "ne": { "position": "absolute", "width": "10px", "height": "10px", "z-index": 1, "top": "-5px", "right": "-5px", "bottom": "auto", "left": "auto" }, "sw": { "position": "absolute", "width": "10px", "height": "10px", "z-index": 1, "bottom": "-5px", "left": "-5px", "top": "auto", "right": "auto" }, "se": { "position": "absolute", "width": "10px", "height": "10px", "z-index": 1, "bottom": "-5px", "right": "-5px", "top": "auto", "left": "auto" } } function clamp(value, min, max) { return Math.max(min, Math.min(value, max)); } export default Resizable