From 3ff736d6b26f2fb154772ae7f6b281df391571c6 Mon Sep 17 00:00:00 2001 From: Joey Schulze Date: Sat, 29 Aug 2015 18:15:31 +0000 Subject: [PATCH] jquery-udraggable plugin and dependency http://grantm.github.com/jquery-udraggable/ --- src/jquery.event.ue.js | 769 +++++++++++++++++++++++++++++++++++++++ src/jquery.udraggable.js | 341 +++++++++++++++++ 2 files changed, 1110 insertions(+) create mode 100644 src/jquery.event.ue.js create mode 100644 src/jquery.udraggable.js diff --git a/src/jquery.event.ue.js b/src/jquery.event.ue.js new file mode 100644 index 0000000..efda624 --- /dev/null +++ b/src/jquery.event.ue.js @@ -0,0 +1,769 @@ +/* + * Jquery plugin for unified mouse and touch events + * + * Copyright (c) 2013 Michael S. Mikowski + * (mike[dot]mikowski[at]gmail[dotcom]) + * + * Dual licensed under the MIT or GPL Version 2 + * http://jquery.org/license + * + * Versions + * 0.3.0 - Initial jQuery plugin site release + * - Replaced scrollwheel zoom with drag motion. + * This resolved a conflict with scrollable areas. + * 0.3.1 - Change for jQuery plugins site + * 0.3.2 - Updated to jQuery 1.9.1. + * Confirmed 1.7.0-1.9.1 compatibility. + * 0.4.2 - Updated documentation + * 0.4.3 - Removed fatal execption possibility if originalEvent + * is not defined on event object + * +*/ + +/*jslint browser : true, continue : true, + devel : true, indent : 2, maxerr : 50, + newcap : true, plusplus : true, regexp : true, + sloppy : true, vars : true, white : true +*/ +/*global jQuery, sl */ + +(function ( $ ) { + //---------------- BEGIN MODULE SCOPE VARIABLES -------------- + var + $Special = $.event.special, // shortcut for special event + motionMapMap = {}, // map of pointer motions by cursor + isMoveBound = false, // flag if move handlers bound + pxPinchZoom = -1, // distance between pinch-zoom points + optionKey = 'ue_bound', // data key for storing options + doDisableMouse = false, // flag to discard mouse input + defaultOptMap = { // Default option hash + bound_ns_map : {}, // namspace hash e.g. bound_ns_map.utap.fred + wheel_ratio : 15, // multiplier for mousewheel delta + px_radius : 3, // 'distance' dragged before dragstart + ignore_class : ':input', // 'not' suppress matching elements + tap_time : 200, // millisecond max time to consider tap + held_tap_time : 300 // millisecond min time to consider taphold + }, + callbackList = [], // global callback stack + zoomMouseNum = 1, // multiplier for mouse zoom + zoomTouchNum = 4, // multiplier for touch zoom + + boundList, Ue, + motionDragId, motionHeldId, motionDzoomId, + motion1ZoomId, motion2ZoomId, + + checkMatchVal, removeListVal, pushUniqVal, makeListPlus, + fnHeld, fnMotionStart, fnMotionMove, + fnMotionEnd, onMouse, onTouch, + onMousewheel + ; + //----------------- END MODULE SCOPE VARIABLES --------------- + + //------------------- BEGIN UTILITY METHODS ------------------ + // Begin utiltity /makeListPlus/ + // Returns an array with much desired methods: + // * remove_val(value) : remove element that matches + // the provided value. Returns number of elements + // removed. + // * match_val(value) : shows if a value exists + // * push_uniq(value) : pushes a value onto the stack + // iff it does not already exist there + // Note: the reason I need this is to compare objects to + // objects (perhaps jQuery has something similar?) + checkMatchVal = function ( data ) { + var match_count = 0, idx; + for ( idx = this.length; idx; 0 ) { + if ( this[--idx] === data ) { match_count++; } + } + return match_count; + }; + removeListVal = function ( data ) { + var removed_count = 0, idx; + for ( idx = this.length; idx; 0 ) { + if ( this[--idx] === data ) { + this.splice(idx, 1); + removed_count++; + idx++; + } + } + return removed_count; + }; + pushUniqVal = function ( data ) { + if ( checkMatchVal.call(this, data ) ) { return false; } + this.push( data ); + return true; + }; + // primary utility + makeListPlus = function ( input_list ) { + if ( input_list && $.isArray(input_list) ) { + if ( input_list.remove_val ) { + console.warn( 'The array appears to already have listPlus capabilities' ); + return input_list; + } + } + else { + input_list = []; + } + input_list.remove_val = removeListVal; + input_list.match_val = checkMatchVal; + input_list.push_uniq = pushUniqVal; + + return input_list; + }; + // End utility /makeListPlus/ + //-------------------- END UTILITY METHODS ------------------- + + //--------------- BEGIN JQUERY SPECIAL EVENTS ---------------- + // Unique array for bound objects + boundList = makeListPlus(); + + // Begin define special event handlers + Ue = { + setup : function( data, a_names, fn_bind ) { + var + this_el = this, + $to_bind = $(this_el), + seen_map = {}, + option_map, idx, namespace_key, ue_namespace_code, namespace_list + ; + + // if previous related event bound do not rebind, but do add to + // type of event bound to this element, if not already noted + if ( $.data( this, optionKey ) ) { return; } + + option_map = {}; + $.extend( true, option_map, defaultOptMap ); + $.data( this_el, optionKey, option_map ); + + namespace_list = makeListPlus(a_names.slice(0)); + if ( ! namespace_list.length + || namespace_list[0] === "" + ) { namespace_list = ["000"]; } + + NSPACE_00: + for ( idx = 0; idx < namespace_list.length; idx++ ) { + namespace_key = namespace_list[idx]; + + if ( ! namespace_key ) { continue NSPACE_00; } + if ( seen_map.hasOwnProperty(namespace_key) ) { continue NSPACE_00; } + + seen_map[namespace_key] = true; + + ue_namespace_code = '.__ue' + namespace_key; + + $to_bind.bind( 'mousedown' + ue_namespace_code, onMouse ); + $to_bind.bind( 'touchstart' + ue_namespace_code, onTouch ); + $to_bind.bind( 'mousewheel' + ue_namespace_code, onMousewheel ); + } + + boundList.push_uniq( this_el ); // record as bound element + + if ( ! isMoveBound ) { + // console.log('first element bound - adding global binds'); + $(document).bind( 'mousemove.__ue', onMouse ); + $(document).bind( 'touchmove.__ue', onTouch ); + $(document).bind( 'mouseup.__ue' , onMouse ); + $(document).bind( 'touchend.__ue' , onTouch ); + $(document).bind( 'touchcancel.__ue', onTouch ); + isMoveBound = true; + } + }, + + // arg_map.type = string - name of event to bind + // arg_map.data = poly - whatever (optional) data was passed when binding + // arg_map.namespace = string - A sorted, dot-delimited list of namespaces + // specified when binding the event + // arg_map.handler = fn - the event handler the developer wishes to be bound + // to the event. This function should be called whenever the event + // is triggered + // arg_map.guid = number - unique ID for event handler, provided by jQuery + // arg_map.selector = string - selector used by 'delegate' or 'live' jQuery + // methods. Only available when these methods are used. + // + // this - the element to which the event handler is being bound + // this always executes immediate after setup (if first binding) + add : function ( arg_map ) { + var + this_el = this, + option_map = $.data( this_el, optionKey ), + namespace_str = arg_map.namespace, + event_type = arg_map.type, + bound_ns_map, namespace_list, idx, namespace_key + ; + if ( ! option_map ) { return; } + + bound_ns_map = option_map.bound_ns_map; + + if ( ! bound_ns_map[event_type] ) { + // this indicates a non-namespaced entry + bound_ns_map[event_type] = {}; + } + + if ( ! namespace_str ) { return; } + + namespace_list = namespace_str.split('.'); + + for ( idx = 0; idx < namespace_list.length; idx++ ) { + namespace_key = namespace_list[idx]; + bound_ns_map[event_type][namespace_key] = true; + } + }, + + remove : function ( arg_map ) { + var + elem_bound = this, + option_map = $.data( elem_bound, optionKey ), + bound_ns_map = option_map.bound_ns_map, + event_type = arg_map.type, + namespace_str = arg_map.namespace, + namespace_list, idx, namespace_key + ; + + if ( ! bound_ns_map[event_type] ) { return; } + + // No namespace(s) provided: + // Remove complete record for custom event type (e.g. utap) + if ( ! namespace_str ) { + delete bound_ns_map[event_type]; + return; + } + + // Namespace(s) provided: + // Remove namespace flags from each custom event typei (e.g. utap) + // record. If all claimed namespaces are removed, remove + // complete record. + namespace_list = namespace_str.split('.'); + + for ( idx = 0; idx < namespace_list.length; idx++ ) { + namespace_key = namespace_list[idx]; + if (bound_ns_map[event_type][namespace_key]) { + delete bound_ns_map[event_type][namespace_key]; + } + } + + if ( $.isEmptyObject( bound_ns_map[event_type] ) ) { + delete bound_ns_map[event_type]; + } + }, + + teardown : function( a_names ) { + var + elem_bound = this, + $bound = $(elem_bound), + option_map = $.data( elem_bound, optionKey ), + bound_ns_map = option_map.bound_ns_map, + idx, namespace_key, ue_namespace_code, namespace_list + ; + + // do not tear down if related handlers are still bound + if ( ! $.isEmptyObject( bound_ns_map ) ) { return; } + + namespace_list = makeListPlus(a_names); + namespace_list.push_uniq('000'); + + NSPACE_01: + for ( idx = 0; idx < namespace_list.length; idx++ ) { + namespace_key = namespace_list[idx]; + + if ( ! namespace_key ) { continue NSPACE_01; } + + ue_namespace_code = '.__ue' + namespace_key; + $bound.unbind( 'mousedown' + ue_namespace_code ); + $bound.unbind( 'touchstart' + ue_namespace_code ); + $bound.unbind( 'mousewheel' + ue_namespace_code ); + } + + $.removeData( elem_bound, optionKey ); + + // Unbind document events only after last element element is removed + boundList.remove_val(this); + if ( boundList.length === 0 ) { + // console.log('last bound element removed - removing global binds'); + $(document).unbind( 'mousemove.__ue'); + $(document).unbind( 'touchmove.__ue'); + $(document).unbind( 'mouseup.__ue'); + $(document).unbind( 'touchend.__ue'); + $(document).unbind( 'touchcancel.__ue'); + isMoveBound = false; + } + } + }; + // End define special event handlers + //--------------- BEGIN JQUERY SPECIAL EVENTS ---------------- + + //------------------ BEGIN MOTION CONTROLS ------------------- + // Begin motion control /fnHeld/ + fnHeld = function ( arg_map ) { + var + timestamp = +new Date(), + motion_id = arg_map.motion_id, + motion_map = arg_map.motion_map, + bound_ns_map = arg_map.bound_ns_map, + event_ue + ; + + delete motion_map.idto_tapheld; + + if ( ! motion_map.do_allow_tap ) { return; } + + motion_map.px_end_x = motion_map.px_start_x; + motion_map.px_end_y = motion_map.px_start_y; + motion_map.ms_timestop = timestamp; + motion_map.ms_elapsed = timestamp - motion_map.ms_timestart; + + if ( bound_ns_map.uheld ) { + event_ue = $.Event('uheld'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + } + + // remove tracking, as we want no futher action on this motion + if ( bound_ns_map.uheldstart ) { + event_ue = $.Event('uheldstart'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + motionHeldId = motion_id; + } + else { + delete motionMapMap[motion_id]; + } + }; + // End motion control /fnHeld/ + + + // Begin motion control /fnMotionStart/ + fnMotionStart = function ( arg_map ) { + var + motion_id = arg_map.motion_id, + event_src = arg_map.event_src, + request_dzoom = arg_map.request_dzoom, + + option_map = $.data( arg_map.elem, optionKey ), + bound_ns_map = option_map.bound_ns_map, + $target = $(event_src.target ), + do_zoomstart = false, + motion_map, cb_map, do_allow_tap, event_ue + ; + + // this should never happen, but it does + if ( motionMapMap[ motion_id ] ) { return; } + + if ( request_dzoom && ! bound_ns_map.uzoomstart ) { return; } + + // :input selector includes text areas + if ( $target.is( option_map.ignore_class ) ) { return; } + + do_allow_tap = bound_ns_map.utap + || bound_ns_map.uheld || bound_ns_map.uheldstart + ? true : false; + + cb_map = callbackList.pop(); + + while ( cb_map ) { + if ( $target.is( cb_map.selector_str ) + || $( arg_map.elem ).is( cb_map.selector_str ) + ) { + if ( cb_map.callback_match ) { + cb_map.callback_match( arg_map ); + } + } + else { + if ( cb_map.callback_nomatch ) { + cb_map.callback_nomatch( arg_map ); + } + } + cb_map = callbackList.pop(); + } + + motion_map = { + do_allow_tap : do_allow_tap, + elem_bound : arg_map.elem, + elem_target : event_src.target, + ms_elapsed : 0, + ms_timestart : event_src.timeStamp, + ms_timestop : undefined, + option_map : option_map, + orig_target : event_src.target, + px_current_x : event_src.clientX, + px_current_y : event_src.clientY, + px_end_x : undefined, + px_end_y : undefined, + px_start_x : event_src.clientX, + px_start_y : event_src.clientY, + timeStamp : event_src.timeStamp + }; + + motionMapMap[ motion_id ] = motion_map; + + if ( bound_ns_map.uzoomstart ) { + if ( request_dzoom ) { + motionDzoomId = motion_id; + } + else if ( ! motion1ZoomId ) { + motion1ZoomId = motion_id; + } + else if ( ! motion2ZoomId ) { + motion2ZoomId = motion_id; + event_ue = $.Event('uzoomstart'); + do_zoomstart = true; + } + + if ( do_zoomstart ) { + event_ue = $.Event( 'uzoomstart' ); + motion_map.px_delta_zoom = 0; + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + return; + } + } + + if ( bound_ns_map.uheld || bound_ns_map.uheldstart ) { + motion_map.idto_tapheld = setTimeout( + function() { + fnHeld({ + motion_id : motion_id, + motion_map : motion_map, + bound_ns_map : bound_ns_map + }); + }, + option_map.held_tap_time + ); + } + }; + // End motion control /fnMotionStart/ + + // Begin motion control /fnMotionMove/ + fnMotionMove = function ( arg_map ) { + var + motion_id = arg_map.motion_id, + event_src = arg_map.event_src, + do_zoommove = false, + motion_map, option_map, bound_ns_map, + event_ue, px_pinch_zoom, px_delta_zoom, + mzoom1_map, mzoom2_map + ; + + if ( ! motionMapMap[motion_id] ) { return; } + + motion_map = motionMapMap[motion_id]; + option_map = motion_map.option_map; + bound_ns_map = option_map.bound_ns_map; + + motion_map.timeStamp = event_src.timeStamp; + motion_map.elem_target = event_src.target; + motion_map.ms_elapsed = event_src.timeStamp - motion_map.ms_timestart; + + motion_map.px_delta_x = event_src.clientX - motion_map.px_current_x; + motion_map.px_delta_y = event_src.clientY - motion_map.px_current_y; + + motion_map.px_current_x = event_src.clientX; + motion_map.px_current_y = event_src.clientY; + + // native event object override + motion_map.timeStamp = event_src.timeStamp; + + // disallow tap if outside of zone or time elapsed + // we use this for other events, so we do it every time + if ( motion_map.do_allow_tap ) { + if ( Math.abs(motion_map.px_delta_x) > option_map.px_radius + || Math.abs(motion_map.pd_delta_y) > option_map.px_radius + || motion_map.ms_elapsed > option_map.tap_time + ) { motion_map.do_allow_tap = false; } + } + + if ( motion1ZoomId && motion2ZoomId + && ( motion_id === motion1ZoomId + || motion_id === motion2ZoomId + )) { + motionMapMap[motion_id] = motion_map; + mzoom1_map = motionMapMap[motion1ZoomId]; + mzoom2_map = motionMapMap[motion2ZoomId]; + + px_pinch_zoom = Math.floor( + Math.sqrt( + Math.pow((mzoom1_map.px_current_x - mzoom2_map.px_current_x),2) + + Math.pow((mzoom1_map.px_current_y - mzoom2_map.px_current_y),2) + ) +0.5 + ); + + if ( pxPinchZoom === -1 ) { px_delta_zoom = 0; } + else { px_delta_zoom = ( px_pinch_zoom - pxPinchZoom ) * zoomTouchNum;} + + // save value for next iteration delta comparison + pxPinchZoom = px_pinch_zoom; + do_zoommove = true; + } + else if ( motionDzoomId === motion_id ) { + if ( bound_ns_map.uzoommove ) { + px_delta_zoom = motion_map.px_delta_y * zoomMouseNum; + do_zoommove = true; + } + } + + if ( do_zoommove ){ + event_ue = $.Event('uzoommove'); + motion_map.px_delta_zoom = px_delta_zoom; + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + return; + } + + if ( motionHeldId === motion_id ) { + if ( bound_ns_map.uheldmove ) { + event_ue = $.Event('uheldmove'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + event_src.preventDefault(); + } + } + else if ( motionDragId === motion_id ) { + if ( bound_ns_map.udragmove ) { + event_ue = $.Event('udragmove'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + event_src.preventDefault(); + } + } + + if ( ! motionDragId + && ! motionHeldId + && bound_ns_map.udragstart + && motion_map.do_allow_tap === false + ) { + motionDragId = motion_id; + event_ue = $.Event('udragstart'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + event_src.preventDefault(); + + if ( motion_map.idto_tapheld ) { + clearTimeout(motion_map.idto_tapheld); + delete motion_map.idto_tapheld; + } + } + }; + // End motion control /fnMotionMove/ + + // Begin motion control /fnMotionEnd/ + fnMotionEnd = function ( arg_map ) { + var + motion_id = arg_map.motion_id, + event_src = arg_map.event_src, + do_zoomend = false, + motion_map, option_map, bound_ns_map, event_ue + ; + + doDisableMouse = false; + + if ( ! motionMapMap[motion_id] ) { return; } + + motion_map = motionMapMap[motion_id]; + option_map = motion_map.option_map; + bound_ns_map = option_map.bound_ns_map; + + motion_map.elem_target = event_src.target; + motion_map.ms_elapsed = event_src.timeStamp - motion_map.ms_timestart; + motion_map.ms_timestop = event_src.timeStamp; + + if ( motion_map.px_current_x ) { + motion_map.px_delta_x = event_src.clientX - motion_map.px_current_x; + motion_map.px_delta_y = event_src.clientY - motion_map.px_current_y; + } + + motion_map.px_current_x = event_src.clientX; + motion_map.px_current_y = event_src.clientY; + + motion_map.px_end_x = event_src.clientX; + motion_map.px_end_y = event_src.clientY; + + // native event object override + motion_map.timeStamp = event_src.timeStamp + ; + + // clear-out any long-hold tap timer + if ( motion_map.idto_tapheld ) { + clearTimeout(motion_map.idto_tapheld); + delete motion_map.idto_tapheld; + } + + // trigger utap + if ( bound_ns_map.utap + && motion_map.ms_elapsed <= option_map.tap_time + && motion_map.do_allow_tap + ) { + event_ue = $.Event('utap'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + } + + // trigger udragend + if ( motion_id === motionDragId ) { + if ( bound_ns_map.udragend ) { + event_ue = $.Event('udragend'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + event_src.preventDefault(); + } + motionDragId = undefined; + } + + // trigger heldend + if ( motion_id === motionHeldId ) { + if ( bound_ns_map.uheldend ) { + event_ue = $.Event('uheldend'); + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + } + motionHeldId = undefined; + } + + // trigger uzoomend + if ( motion_id === motionDzoomId ) { + do_zoomend = true; + motionDzoomId = undefined; + } + + // cleanup zoom info + else if ( motion_id === motion1ZoomId ) { + if ( motion2ZoomId ) { + motion1ZoomId = motion2ZoomId; + motion2ZoomId = undefined; + do_zoomend = true; + } + else { motion1ZoomId = undefined; } + pxPinchZoom = -1; + } + if ( motion_id === motion2ZoomId ) { + motion2ZoomId = undefined; + pxPinchZoom = -1; + do_zoomend = true; + } + + if ( do_zoomend && bound_ns_map.uzoomend ) { + event_ue = $.Event('uzoomend'); + motion_map.px_delta_zoom = 0; + $.extend( event_ue, motion_map ); + $(motion_map.elem_bound).trigger(event_ue); + } + // remove pointer from consideration + delete motionMapMap[motion_id]; + }; + // End motion control /fnMotionEnd/ + //------------------ END MOTION CONTROLS ------------------- + + //------------------- BEGIN EVENT HANDLERS ------------------- + // Begin event handler /onTouch/ for all touch events. + // We use the 'type' attribute to dispatch to motion control + onTouch = function ( event ) { + var + this_el = this, + timestamp = +new Date(), + o_event = event.originalEvent, + touch_list = o_event ? o_event.changedTouches || [] : [], + touch_count = touch_list.length, + idx, touch_event, motion_id, handler_fn + ; + + doDisableMouse = true; + + event.timeStamp = timestamp; + + switch ( event.type ) { + case 'touchstart' : + handler_fn = fnMotionStart; + event.preventDefault(); + break; + case 'touchmove' : + handler_fn = fnMotionMove; + break; + case 'touchend' : + case 'touchcancel' : handler_fn = fnMotionEnd; break; + default : handler_fn = null; + } + + if ( ! handler_fn ) { return; } + + for ( idx = 0; idx < touch_count; idx++ ) { + touch_event = touch_list[idx]; + + motion_id = 'touch' + String(touch_event.identifier); + + event.clientX = touch_event.clientX; + event.clientY = touch_event.clientY; + handler_fn({ + elem : this_el, + motion_id : motion_id, + event_src : event + }); + } + }; + // End event handler /onTouch/ + + + // Begin event handler /onMouse/ for all mouse events + // We use the 'type' attribute to dispatch to motion control + onMouse = function ( event ) { + var + this_el = this, + motion_id = 'mouse' + String(event.button), + request_dzoom = false, + handler_fn + ; + + if ( doDisableMouse ) { + event.stopImmediatePropagation(); + return; + } + + if ( event.shiftKey ) { request_dzoom = true; } + + // skip left or middle clicks + if ( event.type !== 'mousemove' ) { + if ( event.button !== 0 ) { return true; } + } + + switch ( event.type ) { + case 'mousedown' : + handler_fn = fnMotionStart; + event.preventDefault(); + break; + case 'mouseup' : + handler_fn = fnMotionEnd; + break; + case 'mousemove' : + handler_fn = fnMotionMove; + break; + default: + handler_fn = null; + } + + if ( ! handler_fn ) { return; } + + handler_fn({ + elem : this_el, + event_src : event, + request_dzoom : request_dzoom, + motion_id : motion_id + }); + }; + // End event handler /onMouse/ + //-------------------- END EVENT HANDLERS -------------------- + + + // Export special events through jQuery API + $Special.ue + = $Special.utap = $Special.uheld + = $Special.uzoomstart = $Special.uzoommove = $Special.uzoomend + = $Special.udragstart = $Special.udragmove = $Special.udragend + = $Special.uheldstart = $Special.uheldmove = $Special.uheldend + = Ue + ; + $.ueSetGlobalCb = function ( selector_str, callback_match, callback_nomatch ) { + callbackList.push( { + selector_str : selector_str || '', + callback_match : callback_match || null, + callback_nomatch : callback_nomatch || null + }); + }; + +}(jQuery)); diff --git a/src/jquery.udraggable.js b/src/jquery.udraggable.js new file mode 100644 index 0000000..ef304af --- /dev/null +++ b/src/jquery.udraggable.js @@ -0,0 +1,341 @@ +/* + * jQuery udraggable plugin v0.3.0 + * Copyright (c) 2013-2014 Grant McLean (grant@mclean.net.nz) + * + * Homepage: https://github.com/grantm/jquery-udraggable + * + * Dual licensed under the MIT and GPL (v2.0 or later) licenses: + * http://opensource.org/licenses/MIT + * http://opensource.org/licenses/GPL-2.0 + * + * This library requires Michael S. Mikowski's unified mouse and touch + * event plugin: https://github.com/mmikowski/jquery.event.ue + * + */ + +(function($) { + "use strict"; + + var floor = Math.floor; + var min = Math.min; + var max = Math.max; + + window.requestAnimationFrame = window.requestAnimationFrame || function(work) { + return setTimeout(work, 10); + }; + + window.cancelAnimationFrame = window.cancelAnimationFrame || function(id) { + return clearTimeout(id); + }; + + + // Constructor function + + var UDraggable = function (el, options) { + var that = this; + this.el = el; + this.$el = $(el); + this.options = $.extend({}, $.fn.udraggable.defaults, options); + this.positionElement = this.options.positionElement || this.positionElement; + this.getStartPosition = this.options.getStartPosition || this.getStartPosition; + this.updatePositionFrameHandler = function() { + delete that.queuedUpdate; + var pos = that.ui.position; + that.positionElement(that.$el, that.started, pos.left, pos.top); + if (that.options.dragUpdate) { + that.options.dragUpdate.apply(that.el, [that.ui]); + } + }; + this.queuePositionUpdate = function() { + if (!that.queuedUpdate) { + that.queuedUpdate = window.requestAnimationFrame(that.updatePositionFrameHandler); + } + }; + this.init(); + }; + + UDraggable.prototype = { + + constructor: UDraggable, + + init: function() { + var that = this; + this.disabled = false; + this.started = false; + this.normalisePosition(); + var $target = this.options.handle ? + this.$el.find( this.options.handle ) : + this.$el; + if (this.options.longPress) { + $target + .on('uheldstart.udraggable', function(e) { that.start(e); }) + .on('uheldmove.udraggable', function(e) { that.move(e); }) + .on('uheldend.udraggable', function(e) { that.end(e); }); + } + else { + $target + .on('udragstart.udraggable', function(e) { that.start(e); }) + .on('udragmove.udraggable', function(e) { that.move(e); }) + .on('udragend.udraggable', function(e) { that.end(e); }); + } + }, + + destroy: function() { + var $target = this.options.handle ? + this.$el.find( this.options.handle ) : + this.$el; + $target.off('.udraggable'); + this.$el.removeData('udraggable'); + }, + + disable: function() { + this.disabled = true; + }, + + enable: function() { + this.disabled = false; + }, + + option: function() { + var name; + if (arguments.length === 0) { + return this.options; + } + if (arguments.length === 2) { + this.options[ arguments[0] ] = arguments[1]; + return; + } + if (arguments.length === 1) { + if (typeof arguments[0] === 'string') { + return this.options[ arguments[0] ]; + } + if (typeof arguments[0] === 'object') { + for(name in arguments[0]) { + if (arguments[0].hasOwnProperty(name)) { + this.options[name] = arguments[0][name]; + } + } + } + } + if (this.options.containment) { + this._initContainment(); + } + }, + + normalisePosition: function() { + var pos = this.$el.position(); + this.$el.css({ + position: 'absolute', + top: pos.top, + left: pos.left, + right: 'auto', + bottom: 'auto' + }); + }, + + start: function(e) { + if (this.disabled) { + return; + } + var start = this.getStartPosition(this.$el); + this._initContainment(); + this.ui = { + helper: this.$el, + offset: { top: start.y, left: start.x}, + originalPosition: { top: start.y, left: start.x}, + position: { top: start.y, left: start.x}, + }; + if (this.options.longPress) { + this._start(e); + } + return this._stopPropagation(e); + }, + + move: function(e) { + if (this.disabled || (!this.started && !this._start(e))) { + return; + } + var delta_x = e.px_current_x - e.px_start_x; + var delta_y = e.px_current_y - e.px_start_y; + var axis = this.options.axis; + if (axis && axis === "x") { + delta_y = 0; + } + if (axis && axis === "y") { + delta_x = 0; + } + var cur = { + left: this.ui.originalPosition.left, + top: this.ui.originalPosition.top + }; + if (!axis || (axis === "x")) { + cur.left += delta_x; + } + if (!axis || (axis === "y")) { + cur.top += delta_y; + } + this._applyGrid(cur); + this._applyContainment(cur); + var pos = this.ui.position; + if ((cur.top !== pos.top) || (cur.left !== pos.left)) { + this.ui.position.left = cur.left; + this.ui.position.top = cur.top; + this.ui.offset.left = cur.left; + this.ui.offset.top = cur.top; + if (this.options.drag) { + this.options.drag.apply(this.el, [e, this.ui]); + } + this.queuePositionUpdate(); + } + return this._stopPropagation(e); + }, + + end: function(e) { + if (this.started || this._start(e)) { + this.$el.removeClass("udraggable-dragging"); + this.started = false; + if (this.queuedUpdate) { + window.cancelAnimationFrame(this.queuedUpdate); + } + this.updatePositionFrameHandler(); + if (this.options.stop) { + this.options.stop.apply(this.el, [e, this.ui]); + } + } + return this._stopPropagation(e); + }, + + // helper methods + + _stopPropagation: function(e) { + e.stopPropagation(); + e.preventDefault(); + return false; + }, + + _start: function(e) { + if (!this._mouseDistanceMet(e) || !this._mouseDelayMet(e)) { + return; + } + this.started = true; + this.queuePositionUpdate(); + if (this.options.start) { + this.options.start.apply(this.el, [e, this.ui]); + } + this.$el.addClass("udraggable-dragging"); + return true; + }, + + _mouseDistanceMet: function(e) { + return max( + Math.abs(e.px_start_x - e.px_current_x), + Math.abs(e.px_start_y - e.px_current_y) + ) >= this.options.distance; + }, + + _mouseDelayMet: function(e) { + return e.ms_elapsed > this.options.delay; + }, + + _initContainment: function() { + var o = this.options; + var $c, ce; + + if (!o.containment) { + this.containment = null; + return; + } + + if (o.containment.constructor === Array) { + this.containment = o.containment; + return; + } + + if (o.containment === "parent") { + o.containment = this.$el.offsetParent(); + } + + $c = $( o.containment ); + ce = $c[ 0 ]; + if (!ce) { + return; + } + + this.containment = [ + 0, + 0, + $c.innerWidth() - this.$el.outerWidth(), + $c.innerHeight() - this.$el.outerHeight(), + ]; + }, + + _applyGrid: function(cur) { + if (this.options.grid) { + var gx = this.options.grid[0]; + var gy = this.options.grid[1]; + cur.left = floor( (cur.left + gx / 2) / gx ) * gx; + cur.top = floor( (cur.top + gy / 2) / gy ) * gy; + } + }, + + _applyContainment: function(cur) { + var cont = this.containment; + if (cont) { + cur.left = min( max(cur.left, cont[0]), cont[2] ); + cur.top = min( max(cur.top, cont[1]), cont[3] ); + } + }, + + getStartPosition: function($el) { + return { + x: parseInt($el.css('left'), 10) || 0, + y: parseInt($el.css('top'), 10) || 0 + }; + }, + + positionElement: function($el, dragging, left, top) { + $el.css({ left: left, top: top }); + } + + }; + + + // jQuery plugin function + + $.fn.udraggable = function(option) { + var args = Array.prototype.slice.call(arguments, 1); + var results = []; + this.each(function () { + var $this = $(this); + var data = $this.data('udraggable'); + if (!data) { + data = new UDraggable(this, option); + $this.data('udraggable', data); + } + if (typeof option === 'string') { // option is a method - call it + if(typeof data[option] !== 'function') { + throw "jquery.udraggable has no '" + option + "' method"; + } + var result = data[option].apply(data, args); + if (result !== undefined) { + results.push( result ); + } + } + }); + return results.length > 0 ? results[0] : this; + }; + + $.fn.udraggable.defaults = { + axis: null, + delay: 0, + distance: 0, + longPress: false, + // callbacks + drag: null, + start: null, + stop: null + }; + + +})(jQuery); + -- 2.20.1