/**
* @namespace core
**/
var core = core || {};
/**
* @memberof core
* @namespace core.util
**/
core.util = core.util || {}
/**
* @memberof core.util
* @namespace core.util.DRS
* @description All DRS methods are available under this namespace.
*
* @version 0.1.1
* @author Simplex Studio, LTD &
* @author original source: http://codepen.io/zz85/post/resizing-moving-snapping-windows-with-js-css, https://github.com/zz85
*
* @licence The MIT License (MIT)
* @Copyright Copyright © 2015 Simplex Studio, LTD
* @Copyright Copyright © 2015 https://github.com/zz85
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the “Software”), to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions
* of the Software.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
core.util.DRS = function(){
/**
* @memberof core.util.DRS
* @class
* @classdesc Adds Drag Resize & Snap to any element.
*
*
* @param pane {HTMLElement} - The element to add drag, resize, snap.
* @param handle {HTMLElement | object | string | Array } - A single handle or Array of handles.
* HTMLElement :Any HTMLElement to use and a drag handle. Any elements
* with class "drag-area" will be added automatically.
* String: Such as "top" see getHandleCoords() for more presets.
* Object: An object representing the bounds of the handle.
* @param options {object} - An object specifying available options.
* @param options.snapEdge {number} - (5) qty pixels pane edge needs to be beyond window to snap.
* @param options.snapFull {number} - (100) qty pixels pane edge needs to be beyond window to snap full screen.
* @param options.resizeOuterM {number} - (5) qty pixels outside pane bounds is dragable.
* @param options.resizeInnerM {number} - (8) qty pixels inside pane bounds is dragable.
*
* @example
Basic usage:
* var pane = document.getElementById('pane')
* var drs = core.util.DRS.makeDRS(pane)
*
* @example Advanced usage:
* // Get an Create an element to drag, resize, and snap
* var pane = document.getElementById('pane')
*
* // (optional) Create an HTMLElement to use as a drag handle.
* // NOTE: Any child element of pane with class 'drag-area' will be added automatically.
* var handleOne = document.getElementById('some-id')
*
* // (optional) create a drag handle by absolutely positioned css rules (bounds).
* // Supply 4 sides or three sides and width or height to make four bounds.
* var handleTwo = {
* left:0, // style.left
* bottom:28, // style.bottom
* top:0, // style.tip
* width:33, // style.width
* right:null, // style.right
* height:null, // style.height
* invert: false, // (false) true: Will invert the bounding box (areas not covered are dragable)
* hide: false, // (false) true: Will not append and HTMLelement for the area defined
*}
* // Specify options
* var options = {
* snapEdge: 5, // (5) qty pixels pane needs to be moved beyond window to snap
* snapFull: 100, // (100) qty pixels pane needs to be moved beyond window to snap full screen
* resizeOuterM: 5, // (5) qty pixels outside pane bounds is dragable
* resizeInnerM: 8, // (8) qty pixels inside pane bounds is dragable
* }
*
* // Apply the drag resize snap to your element
* var drs = core.util.DRS.makeDRS(pane, [handleOne , handleTwo], options)
*
* @returns DRS {object}
*
*/
var makeDRS = function(pane, handle, options){
"use strict";
options = options || {}
// Minimum resizable area
var minWidth = 60;
var minHeight = 40;
// Thresholds
var SNAP_MARGINS = -(options.snapEdge || 5);
var SNAP_FS_MARGINS = -(options.snapFull || 100);
var RESIZE_MARGIN_INNER = options.resizeOuterM || 5;
var RESIZE_MARGIN_OUTER = options.resizeOuterM || 8;
// End of what's configurable.
var onRightEdge, onBottomEdge, onLeftEdge, onTopEdge;
var rightScreenEdge, bottomScreenEdge, topScreenEdge, leftScreenEdge;
var e, b, x, y, inLR, inTB, preSnap;
var clicked = null;
var redraw = false;
var _usePercent = false // Sets the init state of pane bounds type "%" of screen or fixed "px"
// make sure pane has some required styles
pane.style.boxSizing = "border-box" // make sure that bounds take border into account
// create ghostpane
var ghostpane = document.createElement('div')
ghostpane.id = "DRSghost"
ghostpane.style.opacity = "0"
document.body.appendChild(ghostpane)
// Setup drag handles
var handles = handle instanceof Array ? handle : [handle]
var onHTMLhandle // Holds result of event listeners for HTML handles
function createHandleObjects() {
b = b || pane.getBoundingClientRect();
// Grab handles from dom if none are supplied
if (!hasHTMLElement(handles))
handles = handles.concat(
[].slice.call(
pane.getElementsByClassName('drag-area')))
// precess handles
for (var i in handles){
// html
if (handles[i] instanceof HTMLElement) {
bindEvents(handles[i], 'mousedown touchstart', handleDown)
bindEvents(handles[i], 'mouseup touchend', handleUp)
handles[i] = {ele: handles[i], type: 'html'}
// bounds object
}else if (handles[i] instanceof Object) {
handles[i] = {type:'custom', coords:handles[i]}
drawDragHandle(handles[i])
// preset strings
}else{
handles[i] = {type:handles[i]}
drawDragHandle(handles[i])
}
}
}
window.addEventListener('load', createHandleObjects())
function handleDown(){onHTMLhandle = true}
function handleUp(){onHTMLhandle = false}
// core functions
function setBounds(element, x, y, w, h) {
if (x === undefined) {
b = b || pane.getBoundingClientRect();
x = b.left
y = b.top
w = b.width
h = b.height
}
var wh = convertUnits(w, h)
element.style.left = x + 'px';
element.style.top = y + 'px';
element.style.width = wh[0];
element.style.height = wh[1];
}
function getBounds(){
var winW = window.innerWidth
var winH = window.innerHeight
var bounds = []
if (b.top < SNAP_FS_MARGINS || b.left < SNAP_FS_MARGINS ||
b.right > winW - SNAP_FS_MARGINS || b.bottom > winH - SNAP_FS_MARGINS) {
bounds = [0, 0, winW, winH]
} else if (leftScreenEdge) {
bounds = [0, 0, winW / 2, winH]
} else if (rightScreenEdge) {
bounds = [winW / 2, 0, winW / 2, winH]
} else if (topScreenEdge) {
bounds = [0, 0, winW, winH / 2]
} else if (bottomScreenEdge) {
bounds = [0, winH / 2, winW, winH / 2]
}
return bounds
}
function convertUnits(w, h){
if(!_usePercent)
return [w + 'px', h + 'px']
var pH, pW
// use docWidth to take scroll bars into account!
var docWidth = document.documentElement.clientWidth || document.body.clientWidth;
pH = h / window.innerHeight * 100
pW = w / docWidth * 100
var r = [pW + '\%', pH + '\%']
return r
}
/**
* Toggle pane size as percent of window or fixed size
* @param state {boolean} true/false
*/
function togglePercent(state){
_usePercent = state !== undefined? state : !_usePercent
setBounds(pane)
}
/**
* Toggle full screen mode
*/
function snapFullScreen(){
preSnap = {width: b.width, height: b.height, top:b.top, left:b.left};
preSnap.to = [pane, 0, 0, window.innerWidth, window.innerHeight]
setBounds.apply(this, preSnap.to);
}
/**
* Restore pre-snap size and position
*/
function restorePreSnap() {
if(!preSnap) return
var p = preSnap
setBounds(pane, p.left, p.top, p.width, p.height);
}
function hintHide() {
setBounds(ghostpane, b.left, b.top, b.width, b.height);
ghostpane.style.opacity = 0;
}
// Mouse events
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
// Touch events
document.addEventListener('touchstart', onTouchDown);
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', onTouchEnd);
function onTouchDown(e) {
onDown(e.touches[0]);
//e.preventDefault();
}
function onTouchMove(e) {
onMove(e.touches[0]);
if (clicked && (clicked.isMoving || clicked.isResizing)) {
e.preventDefault()
e.stopPropagation()
}
}
function onTouchEnd(e) {
if (e.touches.length ==0) onUp(e.changedTouches[0]);
}
function onMouseDown(e) {
onDown(e);
}
function onDown(e) {
calc(e);
var isResizing = onRightEdge || onBottomEdge || onTopEdge || onLeftEdge;
clicked = {
x: x,
y: y,
cx: e.clientX,
cy: e.clientY,
w: b.width,
h: b.height,
isResizing: isResizing,
isMoving: !isResizing && canMove(),
onTopEdge: onTopEdge,
onLeftEdge: onLeftEdge,
onRightEdge: onRightEdge,
onBottomEdge: onBottomEdge
};
}
function canMove() {
for (var i in handles) {
var h = drawDragHandle(handles[i])
var c = h.coords
var r = {}
var yb = b.height - y
var xr = b.width - x
// determine bounds of click area coordinates
if (c.bottom !== null && c.bottom !== undefined)
r.bottom = yb < c.height || (!c.height && y > c.top) && yb > c.bottom && inLR
if (c.top !== null && c.top !== undefined)
r.top = y < c.height || (!c.height && yb > c.bottom) && y > c.top && inLR
if (c.right !== null && c.right !== undefined)
r.right = xr < c.width || (!c.width && x > c.left ) && xr > c.right && inTB
if (c.left !== null && c.left !== undefined)
r.left = x < c.width || (!c.width && xr > c.right ) && x > c.left && inTB
var result = ( (r.bottom || r.top ) && r.left && r.right ) ||
( (r.left || r.right) && r.bottom && r.top )
if (c.invert && !result || onHTMLhandle) return true
else if( result || onHTMLhandle ) return true
}
return false
}
function drawDragHandle(h){
var c = getHandleCoords(h)
if (h.type == 'html') return h
if (h.coords.hide) return h
if (!h.drawn) {
var e = document.createElement('div')
e.className = 'drag-area'
e.style.position = "absolute"
e.style.pointerEvents = "all"
e.style.overflow = "hidden"
pane.appendChild(e)
h.drawn = true
h.ele = e
}
if( c.bottom !== null ) h.ele.style.bottom = c.bottom + "px"
if( c.right !== null ) h.ele.style.right = c.right + "px"
if( c.top !== null ) h.ele.style.top = c.top + "px"
if( c.left !== null ) h.ele.style.left = c.left + "px"
if( c.height !== null ) h.ele.style.height = c.height + "px"
if( c.width !== null ) h.ele.style.width = c.width + "px"
return h
}
function getHandleCoords(h){
var DO = 30
var types = {
top: {top:0, bottom:null, left:0, right:0, width:null, height:DO, invert:false},
bottom: {top:null, bottom:0, left:0, right:0, width:null, height:DO, invert:false},
left: {top:0, bottom:0, left:0, right:null, width:DO, height:null, invert:false},
right: {top:0, bottom:0, left:null, right:0, width:DO, height:null, invert:false},
full: {top:0, bottom:0, left:0, right:0, width:null, height:null, invert:false},
20: {top:20, bottom:20, left:20, right:20, width:null, height:null, invert:false},
none: {top:null, bottom:null, left:null, right:null, width:null, height:null, invert:false}
}
if (h.type instanceof Array)
h.coords = h.type
else if(h.type == 'html')
h.coords = h.ele.getBoundingClientRect()
else if (!h.type)
h.type = 'full'
if (!h.coords){
h.coords = types[h.type.split(' ')[0]]
h.coords.invert = h.type.indexOf('invert')+1
h.coords.hide = h.type.indexOf('hide')+1
}
return h.coords
}
function calc(e) {
b = pane.getBoundingClientRect();
x = e.clientX - b.left;
y = e.clientY - b.top;
// define inner and outer margins
var dMi = RESIZE_MARGIN_INNER
var dMo = -RESIZE_MARGIN_OUTER
var rMi = b.width - RESIZE_MARGIN_INNER
var rMo = b.width + RESIZE_MARGIN_OUTER
var bMi = b.height - RESIZE_MARGIN_INNER
var bMo = b.height + RESIZE_MARGIN_OUTER
inLR = x > dMo && x < rMo
inTB = y > dMo && y < bMo
onTopEdge = y <= dMi && y > dMo && inLR;
onLeftEdge = x <= dMi && x > dMo && inTB;
onBottomEdge = y >= bMi && y < bMo && inLR;
onRightEdge = x >= rMi && x < rMo && inTB;
rightScreenEdge = b.right > window.innerWidth - SNAP_MARGINS;
bottomScreenEdge = b.bottom > window.innerHeight - SNAP_MARGINS;
topScreenEdge = b.top < SNAP_MARGINS;
leftScreenEdge = b.left < SNAP_MARGINS;
}
function onMove(ee) {
calc(ee);
e = ee;
redraw = true;
}
function animate() {
requestAnimationFrame(animate);
if (!redraw) return;
redraw = false;
// handle resizeing
if (clicked && clicked.isResizing) {
if (clicked.onRightEdge)
pane.style.width = Math.max(x, minWidth) + "px";
if (clicked.onBottomEdge)
pane.style.height = Math.max(y, minHeight) + "px";
if (clicked.onLeftEdge) {
var currentWidth = Math.max(clicked.cx - e.clientX + clicked.w, minWidth);
if (currentWidth > minWidth) {
pane.style.width = currentWidth + "px";
pane.style.left = e.clientX + "px";
}
}
if (clicked.onTopEdge) {
var currentHeight = Math.max(clicked.cy - e.clientY + clicked.h, minHeight);
if (currentHeight > minHeight) {
pane.style.height = currentHeight + "px";
pane.style.top = e.clientY + "px";
}
}
hintHide();
return;
}
if (clicked && clicked.isMoving) {
var bounds = getBounds()
// Set bounds of ghost pane
if (bounds.length) {
bounds.unshift(ghostpane)
setBounds.apply(this, bounds);
ghostpane.style.opacity = 0.2;
} else {
hintHide();
}
// Unsnap with drag, set bounds to preSnap bounds
if (preSnap) {
preSnap.draged = true
setBounds(pane,
e.clientX - preSnap.width / 2,
e.clientY - Math.min(clicked.y, preSnap.height),
preSnap.width,
preSnap.height
);
return;
}
// moving
pane.style.top = (e.clientY - clicked.y) + 'px';
pane.style.left = (e.clientX - clicked.x) + 'px';
return;
}
// This code executes when mouse moves without clicking
// style cursor
var db = document.documentElement || document.body
if (onRightEdge && onBottomEdge || onLeftEdge && onTopEdge) {
db.style.cursor = 'nwse-resize';
} else if (onRightEdge && onTopEdge || onBottomEdge && onLeftEdge) {
db.style.cursor = 'nesw-resize';
} else if (onRightEdge || onLeftEdge) {
db.style.cursor = 'ew-resize';
} else if (onBottomEdge || onTopEdge) {
db.style.cursor = 'ns-resize';
} else if (canMove()) {
db.style.cursor = 'move';
} else {
db.style.cursor = 'default';
}
}
animate();
function onUp(e) {
calc(e);
if (clicked && clicked.isMoving) {
// Check for Snap
var bounds = getBounds()
// Snap to bounds
if (bounds.length) {
// new snap: save preSnap size & bounds then set bounds
preSnap = {width: b.width, height: b.height};
bounds.unshift(pane)
preSnap.to = bounds
setBounds.apply(this, preSnap.to);
// Clicked: reduce size and destroy the preSnap state
} else if (preSnap && !preSnap.draged) {
var o = RESIZE_MARGIN_INNER
preSnap.to[1] += o
preSnap.to[2] += o
preSnap.to[3] -= o * 2
preSnap.to[4] -= o * 2
setBounds.apply(this, preSnap.to);
preSnap = null;
// Was dragged: just destroy the preSnap state
} else {
preSnap = null;
}
hintHide();
} else if(clicked && clicked.isResizing){
// set bounds after resize to make sure they are % as required
setBounds(pane)
}
clicked = null;
}
// Tripple click to center (comes in handy!)
var clicks = [0, 0, 0]
pane.addEventListener('click', trippleClick)
function trippleClick(e) {
clicks[2] = clicks[0] && clicks[1] && !clicks[2] ? e.timeStamp : clicks[2]
clicks[1] = clicks[0] && !clicks[1] ? e.timeStamp : clicks[1]
clicks[0] = !clicks[0] ? e.timeStamp : clicks[0]
var dif = clicks[2] - clicks[0]
if (clicks[2] && dif < 400)
center()
if (!clicks[2] && !clicks[3])
setTimeout(function () {
clicks = [0, 0, 0]
}, 500)
e.preventDefault()
e.stopPropagation()
return false
}
/**
* Center the pane in the browser window
*/
function center() {
var w = window
var pw = w.innerWidth * .75
var ph = w.innerHeight * .75
setBounds(pane, (w.innerWidth / 2) - (pw / 2), (w.innerHeight / 2) - (ph / 2), pw, ph);
}
// utility functions
function hasHTMLElement(a) {
for (var i in a) if (i instanceof HTMLElement) return true
}
function bindEvents(ele, events, callback) {
events = events.split(' ')
for (var e in events) ele.addEventListener(events[e], callback)
}
return {
togglePercent: togglePercent,
snapFullScreen: snapFullScreen,
restorePreSnap:restorePreSnap,
center: center
}
}
return {makeDRS:makeDRS}
}()