/**
* @function addDrag
* @summary
* Makes a Movie Clip draggable
* @description
* Required Parameters:
*
* options.draggable = Movie Clip to be dragged
*
* Notes:
* 1. If the draggable movie clip does not already have it's hitArea set, it will be added
* automatically here.
* 2. The draggable onclick handler function is set here to evt.preventDefault(), so the slate
* click-out is not triggered when the object is dragged. You can still add other onclick handlers
* if there is other custom actions you need when the object is released (click event fired).
*
* Optional Parameters:
*
* * options.dragUpdate = function which is called at each drag update with an object containing
* the Movie Clip's x, y and raw event {x: Number, y: Number, event: object}
* * options.dragEnd = function which is called at the end of drag gesture with an object containing
* the Movie Clip's x, y and raw event {x: Number, y: Number, event: object}
* * options.container = Movie Clip defining bounds that draggable can be moved. If container is
* not specified, then the parent movie clip of the draggable is used
* * options.draggable.bounds = bounds object defining edges of draggable that is bounded
*
* The bounds object is a hotspot based on fractional units of the object's width & height.
* Each bound direction (left, right, top, bottom) represents the fraction of the width or height
* that the boundary is from the center.
*
* To constrain the draggable object within the container, so the center (0,0) of the draggable object
* cannot move outside the container:
* ```
* draggable.bounds = {
* left: 0,
* right: 0,
* top: 0,
* bottom: 0
* }
* ```
* To constrain the draggable object within the container, so the outside edges of the draggable object cannot
* move outside the container:
* ```
* draggable.bounds = {
* left: -1,
* right: -1,
* top: -1,
* bottom: -1
* }
* ```
* To constrain the draggable object within the container, so the inside edges of the draggable object cannot
* move outside the container:
* ```
* draggable.bounds = {
* left: 1,
* right: 1,
* top: 1,
* bottom: 1
* }
* ```
* Returns: ```{removeEventHandlers<function>}```
*
* Internal functions are also returned for unit testing.
*
* @param {Object} options
* @param {createjs.MovieClip} options.draggable
* @param {createjs.MovieClip=} options.container
* @param {function({x: Number, y: Number, event: object})=} options.dragUpdate
* @param {function({x: Number, y: Number, event: object})=} options.dragEnd
* @param {boolean=} options.forceDragRelease (defaults false) if true, then draggable is released when the mouse is
* no longer over the boundaries of the object
* @params {boolean=} options.doLogging (defaults false)
*/
function addDrag (options) {
var draggable = options.draggable
if (!(draggable instanceof window.createjs.MovieClip)) {
console.log('Error setting up addDrag function: options.draggable must be a MovieClip')
return
}
draggable.bounds = draggable.bounds ||
{
left: 0,
top: 0,
right: 0,
bottom: 0
}
var container = options.container || draggable.parent
/*
* Set the hitArea of the draggable movie clip
*/
var draggableWidth = draggable.nominalBounds.width * draggable.scaleX
var draggableHeight = draggable.nominalBounds.height * draggable.scaleY
var containerWidth = container.nominalBounds.width * container.scaleX
var containerHeight = container.nominalBounds.height * container.scaleY
if (!(draggable.hitArea instanceof window.createjs.Shape)) {
draggable.hitArea = new window.createjs.Shape()
draggable.hitArea
.graphics
.beginFill('#000')
.drawRect(-draggableWidth / 2, -draggableHeight / 2, draggableWidth, draggableHeight)
}
/*
* Prevent the default slate click-out
*/
draggable.on('click', function (evt) { evt.preventDefault() })
var dragUpdate
if (typeof options.dragUpdate === 'function') {
if (window.$b.slateAutoCloseTimer) {
dragUpdate = function (updateOptions) {
window.$b.slateAutoCloseTimer.reset()
options.dragUpdate(updateOptions)
}
} else {
dragUpdate = options.dragUpdate
}
}
var dragEnd
if (typeof options.dragEnd === 'function') {
dragEnd = options.dragEnd
}
/*
* The values of evt.delta X & Y are cumulative since the drag gesture begin,
* so the change for the current update is the difference from the lastEvt.
*
* Occasionally, the panend event is not dispatched, so a timer is used to reset the
* lastEvt as a precaution.
*/
var lastEvt = null
var waitTime = options.waitTime || 1000
var timerId = null
var canvas = window.$b.stage.canvas
var refWidth = window.$b.adParameters.refWidth
var refHeight = window.$b.adParameters.refHeight
var getDragBounds = getDragBoundsFunction({ draggable: draggable, container: container })
var isDesktopSafariEdge = !window.$b.isMobile() && (window.$b.isSafari() || window.$b.isEdge())
var isWindows = /Windows/i.test(window.navigator.userAgent)
var isWindowsFirefox = isWindows && window.$b.isFirefox()
var isMobile = window.$b.isMobile()
var forceRelease = !!options.forceDragRelease
var doLogging = !!options.doLogging
var parent = draggable.parent
var stage = window.$b.stage
/*
* The event CanvasSizeChanged is called at the end of blink/animate-cc-resize-canvas.
* Since dx & dy are calculated by a difference of evt and lastEvt, we must null out lastEvt
* after the draggable is released so the object does not jump.
*/
if (window.$b) {
window.$b.on('CanvasSizeChanged', function (evt) {
getDragBounds = getDragBoundsFunction({ draggable: draggable, container: container })
lastEvt = null
})
window.$b.on('AdPaused', function (evt) {
lastEvt = null
})
}
/*
* On JWPlayer Mobile, the AdPaused event is not fired. Detect if an ad is paused by
* testing if the value of previousTime is not updated while the App is paused in the background.
*/
if (isMobile) {
var dt = 250
var previousTime = Date.now()
var currentTime
window.setInterval(function () {
currentTime = Date.now()
if ((currentTime - previousTime) > 2 * dt) {
window.$b.resetHandler()
lastEvt = null
if (doLogging) {
var pauseTime = ((currentTime - previousTime) / 1000).toFixed(2)
console.log('Mobile JavaScript engine paused for ' + pauseTime + 's')
}
}
previousTime = currentTime
}, dt)
}
var draggableBounds
var pointInBounds = function (point) {
draggableBounds = {
top: draggable.y - draggableHeight / 2,
bottom: draggable.y + draggableHeight / 2,
left: draggable.x - draggableWidth / 2,
right: draggable.x + draggableWidth / 2
}
return (
point &&
(point.x >= draggableBounds.left) &&
(point.x <= draggableBounds.right) &&
(point.y >= draggableBounds.top) &&
(point.y <= draggableBounds.bottom)
)
}
var mouseInBounds = function () {
var point = parent.globalToLocal(stage.mouseX, stage.mouseY)
var status = pointInBounds(point)
if (doLogging && !status) {
console.log('mouse out of bounds: ', point.x, point.y)
console.log('draggableBounds:', draggableBounds)
}
return status
}
var containerBounds = {
top: container.y - containerHeight / 2,
bottom: container.y + containerHeight / 2,
left: container.x - containerWidth / 2,
right: container.x + containerWidth / 2
}
var pointInContainer = function (point) {
return (
point &&
(point.x >= containerBounds.left) &&
(point.x <= containerBounds.right) &&
(point.y >= containerBounds.top) &&
(point.y <= containerBounds.bottom)
)
}
var mouseInContainer = function () {
var point = container.parent.globalToLocal(stage.mouseX, stage.mouseY)
var status = pointInContainer(point)
if (doLogging && !status) {
console.log('mouse out of container: ', point.x, point.y)
}
return status
}
if (doLogging) {
console.log('containerBounds:', containerBounds)
}
var mouseStatus = true
var updateHandler = draggable.addEventListener('pan', function (evt) {
if (timerId) {
clearTimeout(timerId)
}
/*
* Safari and Edge to do fire stage.onmouseleave nor document.onmouseleave when the edge of the
* iframe goes beyond the window. To force draggable objects to be released for these browsers,
* the handler is forcibly reset if a pan event is not received after 0.5 seconds, which
* will occur when the user drags out of the iframe. Also for Windows Firefox.
*/
timerId = setTimeout(function (evt) {
lastEvt = null
;(isWindowsFirefox || isDesktopSafariEdge) && window.$b.resetHandler()
}, waitTime)
if (lastEvt) {
var dx = (evt.deltaX - lastEvt.deltaX) * refWidth / canvas.clientWidth
var dy = (evt.deltaY - lastEvt.deltaY) * refHeight / canvas.clientHeight
if (doLogging) {
console.log('dx, dy: ', dx, dy)
}
/*
* NaN's rarely occur, but if they do, they throw the draggable off the screen
*/
if (isNaN(dx) || isNaN(dy)) {
lastEvt = evt
return
}
var point = {
x: draggable.x + dx,
y: draggable.y + dy
}
mouseStatus = boundDragPoint({ draggable: draggable, container: container, point: point, getDragBounds: getDragBounds })
if (!mouseStatus) {
window.$b.resetHandler()
lastEvt = null
return
}
draggable.x = point.x
draggable.y = point.y
if (dragUpdate) {
dragUpdate({ x: draggable.x, y: draggable.y, event: evt })
}
if (forceRelease && !mouseInBounds(point)) {
if (doLogging) {
console.log('mouse not over draggable')
}
window.$b.resetHandler()
lastEvt = null
return
}
}
lastEvt = evt
})
draggable.addEventListener('panstart', updateHandler)
var endHandler = draggable.addEventListener('panend', function (evt) {
lastEvt = null
if (dragEnd) dragEnd({ x: draggable.x, y: draggable.y, event: evt })
})
function removeEventHandlers () {
draggable.removeEventListener('pan', updateHandler)
draggable.removeEventListener('panstart', updateHandler)
draggable.removeEventListener('panend', endHandler)
}
function restoreEventHandlers () {
draggable.addEventListener('pan', updateHandler)
draggable.addEventListener('panstart', updateHandler)
draggable.addEventListener('panend', endHandler)
}
function getDragBoundsFunction (options) {
var draggable = options.draggable
var container = options.container
var parent = draggable.parent
/*
* Precompute values that are constant. Only the scale can change.
*/
var containerWidth = container.nominalBounds.width
var containerHeight = container.nominalBounds.height
var left0 = 0
var left1 = -draggable.nominalBounds.width * draggable.bounds.left * 0.5
var right0 = containerWidth
var right1 = draggable.nominalBounds.width * draggable.bounds.right * 0.5
var top0 = 0
var top1 = -draggable.nominalBounds.height * draggable.bounds.top * 0.5
var bottom0 = containerHeight
var bottom1 = draggable.nominalBounds.height * draggable.bounds.bottom * 0.5
window.devicePixelRatio = window.devicePixelRatio || 1
/*
* The coordinates of the draggable is in it's parents coordinate system.
* If the container is not the draggable's parent, then the coordinates
* must be transformed.
*/
if (container !== parent) {
var upperLeft = container.localToLocal(left0, top0, parent)
var lowerRight = container.localToLocal(right0, bottom0, parent)
left0 = upperLeft.x - containerWidth / 2
top0 = upperLeft.y - containerHeight / 2
right0 = lowerRight.x - containerWidth / 2
bottom0 = lowerRight.y - containerHeight / 2
}
return function () {
return {
left: left0 + left1 * draggable.scaleX,
right: right0 + right1 * draggable.scaleX,
top: top0 + top1 * draggable.scaleY,
bottom: bottom0 + bottom1 * draggable.scaleY
}
}
}
/**
* Bound the point that the draggable object will be moved to.
* Returns false if the cursor is both out of the container and outside the draggable, so the
* calling function can release the draggable.
*
* @param options
* @returns {boolean}
*/
function boundDragPoint (options) {
var draggable = options.draggable
var container = options.container
var point = options.point
var getDragBounds = options.getDragBounds
var bounds = getDragBounds(draggable, container)
point.x = Math.max(point.x, bounds.left)
point.x = Math.min(point.x, bounds.right)
point.y = Math.max(point.y, bounds.top)
point.y = Math.min(point.y, bounds.bottom)
/*
* Only check if the mouse is out of bounds if the draggable object is on the edge of the container.
* If it is, then release the draggable.
*/
var status = true
if (
point.x === bounds.left ||
point.x === bounds.right ||
point.y === bounds.top ||
point.y === bounds.bottom
) {
/*
* CreateJS loses track of the mouse position after an app switch on mobile devices,
* so mouseInContainer and mouseInBounds do not work correctly afterwards.
* Just release the draggable on mobile if the object is on the edge of the container.
*/
if (isMobile) {
status = false
} else if (!mouseInContainer() && !mouseInBounds()) {
if (doLogging) {
console.log('mouseStatus = false')
}
status = false
}
}
return status
}
return {
getDragBoundsFunction: getDragBoundsFunction,
boundDragPoint: boundDragPoint,
updateHandler: updateHandler,
endHandler: endHandler,
removeEventHandlers: removeEventHandlers,
restoreEventHandlers: restoreEventHandlers
}
}
window.$b = window.$b || {}
window.$b.snippets = window.$b.snippets || {}
var snippets = window.$b.snippets
snippets.addDrag = addDrag
if (window.module) {
window.module.exports = addDrag
}