diff --git a/index.html b/index.html index 81e0794..448753f 100755 --- a/index.html +++ b/index.html @@ -62,6 +62,10 @@ + + + + diff --git a/lib/_template.js b/lib/_template.js new file mode 100755 index 0000000..72b424f --- /dev/null +++ b/lib/_template.js @@ -0,0 +1,17 @@ + +/********************************************************************** +* +* +* +**********************************************************************/ + +//var DEBUG = DEBUG != null ? DEBUG : true + + +/*********************************************************************/ + + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ diff --git a/lib/jli.js b/lib/jli.js index e4fc012..ed427e4 100755 --- a/lib/jli.js +++ b/lib/jli.js @@ -145,6 +145,7 @@ function createCSSClassToggler(elem, class_list, callback_a, callback_b){ +/* // show a jQuary opject in viewer overlay... // XXX need to set .scrollTop(0) when showing different UI... // ...and not set it when the UI is the same @@ -177,6 +178,7 @@ function showInOverlay(obj){ function overlayMessage(text){ return showInOverlay($('
' +text+ '
')) } +*/ @@ -244,6 +246,7 @@ var getElementShift = makeCSSVendorAttrGetter( } }) + var DEFAULT_TRANSITION_DURATION = 200 var getElementTransitionDuration = makeCSSVendorAttrGetter( @@ -464,641 +467,6 @@ function setTransitionDuration(elem, ms){ -/************************************************* keyboard handler **/ - -// NOTE: don't understand why am I the one who has to write this... -var SPECIAL_KEYS = { - // Special Keys... - 9: 'Tab', 33: 'PgUp', 45: 'Ins', - 13: 'Enter', 34: 'PgDown', 46: 'Del', - 16: 'Shift', 35: 'End', 80: 'Backspace', - 17: 'Ctrl', 36: 'Home', 91: 'Win', - 18: 'Alt', 37: 'Right', 93: 'Menu', - 20: 'Caps Lock',38: 'Up', - 27: 'Esc', 39: 'Left', - 32: 'Space', 40: 'Down', - - // Function Keys... - 112: 'F1', 116: 'F5', 120: 'F9', - 113: 'F2', 117: 'F6', 121: 'F10', - 114: 'F3', 118: 'F7', 122: 'F11', - 115: 'F4', 119: 'F8', 123: 'F12', -} - -// XXX some keys look really wrong... -function toKeyName(code){ - // check for special keys... - var k = SPECIAL_KEYS[code] - if(k != null){ - return k - } - // chars... - k = String.fromCharCode(code) - if(k != ''){ - return k.toLowerCase() - } - return null -} - - -// if set to false the event handlers will always return false... -var KEYBOARD_HANDLER_PROPAGATE = true - -/* Basic key format: - * - * : , - * - * : { - * 'default': , - * // a modifier can be any single modifier, like shift or a - * // combination of modifers like 'ctrl+shift', given in order - * // of priority. - * // supported modifiers are (in order of priority): - * // - ctrl - * // - alt - * // - shift - * : [...] - * }, - * - * : [ - * // this can be any type of handler except for an alias... - * , - * - * ], - * - * // alias... - * : , - * - * XXX might need to add meta information to generate sensible help... - */ -function makeKeyboardHandler(keybindings, unhandled){ - if(unhandled == null){ - //unhandled = function(){return false} - unhandled = function(){return KEYBOARD_HANDLER_PROPAGATE} - } - return function(evt){ - var did_handling = false - var res = null - for(var mode in keybindings){ - if($(mode).length > 0){ - var bindings = keybindings[mode] - - var key = evt.keyCode - if(bindings.ignore == '*' - || bindings.ignore != null && bindings.ignore.indexOf(key) != -1){ - // return true - res = res == null ? true : res - did_handling = true - continue - } - // XXX ugly... - var modifers = evt.ctrlKey ? 'ctrl' : '' - modifers += evt.altKey ? (modifers != '' ? '+alt' : 'alt') : '' - modifers += evt.shiftKey ? (modifers != '' ? '+shift' : 'shift') : '' - - var handler = bindings[key] - - // alias... - while (typeof(handler) == typeof(123)) { - handler = bindings[handler] - } - // no handler... - if(handler == null){ - continue - } - // Array, lisp style with docs... - // XXX for some odd reason in chrome typeof([]) == typeof({})!!! - if(typeof(handler) == typeof([]) && handler.constructor.name == 'Array'){ - // we do not care about docs here, so just get the handler... - handler = handler[0] - } - // complex handler... - if(typeof(handler) == typeof({})){ - var callback = handler[modifers] - if(callback == null){ - callback = handler['default'] - } - if(callback != null){ - res = callback() - did_handling = true - continue - } - } else { - // simple callback... - res = handler() - did_handling = true - continue - } - } - } - if(!did_handling){ - // key is unhandled by any modes... - return unhandled(key) - } else { - // XXX should we handle multiple hits??? - return KEYBOARD_HANDLER_PROPAGATE&&res?true:false - } - } -} - - - -// click threshold in pixels, if the distance between start and end is -// less than this, the whole event is considered a click and not a -// drag/swipe... -var CLICK_THRESHOLD = 10 - -// if the amount of time to wait beween start and end is greater than this -// the event is considered a long click. -// NOTE: this will not auto-fire the event, the user MUST release first. -var LONG_CLICK_THRESHOLD = 400 - -// the maximum amount of time between clicks to count them together. -// NOTE: if multi-clicks are disabled this has no effect. -// NOTE: this is reset by the timeout explicitly set in the handler... -// NOTE: this is the timeout between two consecutive clicks and not the total. -// NOTE: if multiple clicks are enabled this will introduce a lag after -// each click (while we wait for the next), so keep this as small -// as possible. -var MULTI_CLICK_TIMEOUT = 200 - -// the amount of time between finger releases. -// NOTE: when this is passed all the fingers released before are ignored. -var MULTITOUCH_RELEASE_THRESHOLD = 100 - -// XXX add a resonable cancel scheme... -// ... something similar to touch threshold but bigger... -// XXX handle multiple touches... -// - timeout on lift to count fingers... -// XXX setup basic styles for the contained element... -// XXX revise... -// XXX test on other devices... -// XXX BUG: on landing a second finger while scrolling the things goes -// haywhire... -// ...check if this is gone... -// XXX add something like a scrollTo that would understand elements as -// well as explicit positions. -// XXX split this into a seporate lib... -function makeScrollHandler(root, config){ - root = $(root) - - // local data... - var ignoring = false - // XXX this and scroller.state are redundent... - var scrolling = false - var touch = false - var touches = 0 - var max_dx = 0 - var max_dy = 0 - - var cancelThreshold, scrolled - // initial state... - , start_x, start_y, start_t - // previous state... - , prev_x, prev_y, prev_t - // current state... - , x, y, t - // state delta... - , dx, dy, dt - - , shift - , scale - //, bounds - - function startMoveHandler(evt){ - var options = scroller.options - // ignore... - if(options.ignoreElements - && $(evt.target).closest(options.ignoreElements).length > 0 - || scroller.state == 'paused'){ - ignoring = true - return - } else { - ignoring = false - } - if(event.touches != null){ - touch = true - } - cancelThreshold = options.scrollCancelThreshold - touches = touch ? event.touches.length : 1 - // if we are already touching then just skip on this... - // XXX test this... - if(touches > 1){ - return false - } - prev_t = event.timeStamp || Date.now(); - start_t = prev_t - /* - if(options.autoCancelEvents){ - bounds = { - left: options.eventBounds, - right: root.width() - options.eventBounds, - top: options.eventBounds, - bottom: root.height() - options.eventBounds - } - } - */ - //togglePageDragging('on') - scrolled = $(root.children()[0]) - setTransitionDuration(scrolled, 0) - // XXX these two are redundant... - scrolling = true - scroller.state = 'scrolling' - options.enabelStartEvent && root.trigger('userScrollStart') - // XXX do we need to pass something to this? - options.preCallback && options.preCallback() - - shift = getElementShift(scrolled) - scale = getElementScale(scrolled) - // get the user coords... - prev_x = touch ? event.touches[0].pageX : evt.clientX - start_x = prev_x - prev_y = touch ? event.touches[0].pageY : evt.clientY - start_y = prev_y - - return false - } - - // XXX try and make this adaptive to stay ahead of the lags... - // NOTE: this does not support limiting the scroll, might be done in - // the future though. - // The way to go about this is to track scrolled size in the - // callback... - function moveHandler(evt){ - if(ignoring){ - return - } - var options = scroller.options - evt.preventDefault() - t = event.timeStamp || Date.now(); - // get the user coords... - x = touch ? event.touches[0].pageX : evt.clientX - y = touch ? event.touches[0].pageY : evt.clientY - touches = touch ? event.touches.length : 1 - - /* - // XXX needs testing... - // XXX do we need to account for scrollDisabled here??? - // check scroll bounds... - if(bounds != null){ - if(options.hScroll && (x <= bounds.left || x >= bounds.right) - || options.vScroll && (y <= bounds.top || y >= bounds.bottom)){ - // XXX cancel the touch event and trigger the end handler... - return endMoveHandler(evt) - } - } - */ - - // do the actual scroll... - if(!options.scrollDisabled && scrolling){ - if(options.hScroll){ - shift.left += x - prev_x - } - if(options.vScroll){ - shift.top += y - prev_y - } - setElementTransform(scrolled, shift, scale) - - // XXX these should be done every time the event is caught or - // just while scrolling? - dx = x - prev_x - dy = y - prev_y - max_dx += Math.abs(dx) - max_dy += Math.abs(dy) - dt = t - prev_t - prev_x = x - prev_y = y - prev_t = t - - options.enableUserScrollEvent && root.trigger('userScroll') - } - return false - } - - function endMoveHandler(evt){ - t = event.timeStamp || Date.now(); - touches = touch ? event.touches.length : 0 - if(ignoring){ - if(touches == 0){ - ignoring = false - } - return - } - var options = scroller.options - - // XXX get real transition duration... - scroller.resetTransitions() - - x = touch ? event.changedTouches[0].pageX : evt.clientX - y = touch ? event.changedTouches[0].pageY : evt.clientY - // check if we are canceling... - if(cancelThreshold - && Math.abs(start_x-x) < cancelThreshold - && Math.abs(start_y-y) < cancelThreshold - && (max_dx > cancelThreshold - || max_dy > cancelThreshold)){ - scroller.state = 'canceling' - } - - var data = { - orig_event: evt, - scroller: scroller, - speed: { - x: dx/dt, - y: dy/dt - }, - distance: { - x: start_x-x, - y: start_y-y - }, - duration: t-start_t, - // current touches... - touches: touches, - clicks: null, - } - // XXX stop only if no fingers are touching or let the callback decide... - //togglePageDragging('off') - // XXX update this with the new data model!!! (see below) - options.enableEndEvent && root.trigger('userScrollEnd', data) - if(options.postCallback - // XXX revise this.... - && options.postCallback(data) === false - || touches == 0){ - // cleanup and stop... - touch = false - scrolling = false - scroller.state = 'waiting' - scrolled = null - //bounds = null - max_dx = 0 - max_dy = 0 - } - - return false - } - - - var scroller = { - options: { - // if one of these is false, it will restrict scrolling in - // that direction. hScroll for horizontal and vScroll for - // vertical. - // NOTE: to disable scroll completely use scrollDisabled, see - // below for details. - hScroll: true, - vScroll: true, - - // this will disable scroll. - // NOTE: this is the same as setting both vScroll and hScroll - // to false, but can be set and reset without affecting - // the actual settings individually... - // NOTE: this takes priority over hScroll/vScroll. - scrollDisabled: false, - - // sets the default transition settings while not scrolling... - transitionDuration: 200, - transitionEasing: 'ease', - - // items to be ignored by the scroller... - // this is a jQuery compatible selector. - ignoreElements: '.noScroll', - // this is the side of the rectangle in px, if the user moves - // out of it, and then returns back, the action will get cancelled. - // i.e. the callback will get called with the "cancelling" state. - scrollCancelThreshold: 100, - - // these control weather in-scroll events will get triggered. - // NOTE: these may impact performance, especially the scroll - // event, thus they need to enabled explicitly. - enabelStartEvent: false, - enableUserScrollEvent: false, - enableEndEvent: false, - - /* - // XXX padding within the target element moving out of which - // will cancell the action... - // XXX needs testing... - autoCancelEvents: false, - eventBounds: 5, - */ - - // callback to be called when the user first touches the screen... - preCallback: null, - // callback to be called when the user lifts a finger/mouse. - // NOTE: this may happen before the scroll is done, for instance - // when one of several fingers participating in the action - // gets lifted. - // NOTE: if this returns false explicitly, this will stop scrolling. - postCallback: postScrollCallback, - - // These are used by the default callback... - // if true then doubleClick and multiClick events will get - // triggered. - // NOTE: this will introduce a lag needed to wait for next - // clicks in a group. - // NOTE: when this is false, shortClick is triggered for every - // single click separately. - enableMultiClicks: false, - // NOTE: if these are null, respective values from the env will - // be used. - clickThreshold: null, - longClickThreshold: null, - multiClickTimeout: null, - multitouchTimeout: null, - }, - // NOTE: this is updated live but not used by the system in any way... - state: 'stopped', - root: root, - - start: function(){ - if(this.state == 'paused'){ - this.state = 'waiting' - } else { - this.state = 'waiting' - // XXX STUB: this makes starting the scroll a bit sluggish, - // find a faster way... - //togglePageDragging('on') - - // NOTE: if we bind both touch and mouse events, on touch devices they - // might start interfering with each other... - if('ontouchmove' in window){ - root - .on('touchstart', startMoveHandler) - .on('touchmove', moveHandler) - .on('touchend', endMoveHandler) - .on('touchcancel', endMoveHandler) - } else { - root - .on('mousedown', startMoveHandler) - .on('mousemove', moveHandler) - .on('mouseup', endMoveHandler) - } - } - return this - }, - // XXX test... - pause: function(){ - this.state = 'paused' - return this - }, - stop: function(){ - if('ontouchmove' in window){ - root - .off('touchstart', startMoveHandler) - .off('touchmove', moveHandler) - .off('touchend', endMoveHandler) - } else { - root - .off('mousedown', startMoveHandler) - .off('mousemove', moveHandler) - .off('mouseup', endMoveHandler) - } - this.state = 'stopped' - return this - }, - resetTransitions: function(){ - var scrolled = this.root.children().first() - setTransitionDuration(scrolled, this.options.transitionDuration) - setTransitionEasing(scrolled, this.options.transitionEasing) - } - } - - // merge the config with the defaults... - if(config != null){ - $.extend(scroller.options, config) - } - - return scroller -} - -// default callback... -// This will provide support for the folowing events on the scroll root -// element: -// - scrollCancelled -// -// - shortClick -// - doubleClick -// - multiClick -// this will store the number of clicks in data.clicks -// - longClick -// -// - swipeLeft -// - swipeRight -// - swipeUp -// - swipeDown -// -// - screenReleased -// -// NOTE: data.touches passed to the event is the number of touches -// released within the multitouchTimeout. -// this differs from what postScrollCallback actually gets in the -// same field when it recieves the object. -// XXX add generic snap -// XXX add generic innertial scroll -// XXX test multiple touches... -function postScrollCallback(data){ - var scroller = data.scroller - var options = scroller.options - var root = scroller.root - var clickThreshold = options.clickThreshold || CLICK_THRESHOLD - var longClickThreshold = options.longClickThreshold || LONG_CLICK_THRESHOLD - var multitouchTimeout = options.multitouchTimeout || MULTITOUCH_RELEASE_THRESHOLD - var enableMultiClicks = options.enableMultiClicks - var multiClickTimeout = options.multiClickTimeout || MULTI_CLICK_TIMEOUT - - var now = Date.now(); - - // cancel event... - if(scroller.state == 'canceling'){ - return root.trigger('scrollCancelled', data) - } - - // handle multiple touches... - if(data.touches > 0){ - var then = scroller._last_touch_release - if(then == null || now - then < multitouchTimeout){ - if(scroller._touches == null){ - scroller._touches = 1 - } else { - scroller._touches += 1 - } - } else { - scroller._touches = null - } - // wait for the next touch release... - scroller._last_touch_release = now - return - - // calculate how many touches did participate... - } else { - data.touches = scroller._touches ? scroller._touches + 1 : 1 - scroller._last_touch_release = null - scroller._touches = null - } - - // clicks, double-clicks, multi-clicks and long-clicks... - if(Math.max( - Math.abs(data.distance.x), - Math.abs(data.distance.y)) < clickThreshold){ - if(data.duration > longClickThreshold){ - return root.trigger('longClick', data) - } - if(!enableMultiClicks){ - return root.trigger('shortClick', data) - - } else { - // count the clicks so far... - if(scroller._clicks == null){ - scroller._clicks = 1 - } else { - scroller._clicks += 1 - } - - // kill any previous waits... - if(scroller._click_timeout_id != null){ - clearTimeout(scroller._click_timeout_id) - } - - // wait for the next click... - scroller._click_timeout_id = setTimeout(function(){ - var clicks = scroller._clicks - data.clicks = clicks - if(clicks == 1){ - root.trigger('shortClick', data) - } else if(clicks == 2){ - root.trigger('doubleClick', data) - } else { - root.trigger('multiClick', data) - } - scroller._clicks = null - scroller._click_timeout_id = null - }, multiClickTimeout) - - return - } - } - - // swipes... - // XXX might be a good idea to chain these with swipe and screenReleased - if(Math.abs(data.distance.x) > Math.abs(data.distance.y)){ - if(data.distance.x <= -clickThreshold && root.data('events').swipeLeft){ - return root.trigger('swipeLeft', data) - } else if(data.distance.x >= clickThreshold && root.data('events').swipeRight){ - return root.trigger('swipeRight', data) - } - } else { - if(data.distance.y <= -clickThreshold && root.data('events').swipeUp){ - return root.trigger('swipeUp', data) - } else if(data.distance.y >= clickThreshold && root.data('events').swipeDown){ - return root.trigger('swipeDown', data) - } - } - - // this is triggered of no swipes were handled... - return root.trigger('screenReleased', data) -} - - /************************************************ jQuery extensions **/ @@ -1145,57 +513,5 @@ var cancelAnimationFrame = (window.cancelRequestAnimationFrame - -/********************************************************** logger ***/ - -function Logger(){ - _log = null - return { - setup: function(){ - if(_log == null){ - _log = $('
') - .css({ - position: 'fixed', - background: 'silver', - opacity: 0.5, - width: 200, - height: '80%', - top: 10, - left: 10, - 'z-index': 90000, - overflow: 'hidden', - padding: 10, - }) - .text('log') - .appendTo($('body')) - } else { - _log.appendTo($('body')) - } - return this - }, - remove: function(){ - _log.detach() - return this - }, - log: function(text){ - _log.html(_log.html() + '
' + text + '') - _log.scrollTop(_log.prop('scrollHeight')) - return this - }, - clear: function(){ - _log.html('') - return this - }, - get: function(){ - return _log - }, - set: function(elem){ - _log = elem - } - }.setup() -} - - - /********************************************************************** * vim:set ts=4 sw=4 : */ diff --git a/lib/keyboard.js b/lib/keyboard.js new file mode 100755 index 0000000..cb45deb --- /dev/null +++ b/lib/keyboard.js @@ -0,0 +1,151 @@ +/********************************************************************** +* +* +* +**********************************************************************/ + +//var DEBUG = DEBUG != null ? DEBUG : true + + + +/*********************************************************************/ + +// NOTE: don't understand why am I the one who has to write this... +var SPECIAL_KEYS = { + // Special Keys... + 9: 'Tab', 33: 'PgUp', 45: 'Ins', + 13: 'Enter', 34: 'PgDown', 46: 'Del', + 16: 'Shift', 35: 'End', 80: 'Backspace', + 17: 'Ctrl', 36: 'Home', 91: 'Win', + 18: 'Alt', 37: 'Right', 93: 'Menu', + 20: 'Caps Lock',38: 'Up', + 27: 'Esc', 39: 'Left', + 32: 'Space', 40: 'Down', + + // Function Keys... + 112: 'F1', 116: 'F5', 120: 'F9', + 113: 'F2', 117: 'F6', 121: 'F10', + 114: 'F3', 118: 'F7', 122: 'F11', + 115: 'F4', 119: 'F8', 123: 'F12', +} + +// XXX some keys look really wrong... +function toKeyName(code){ + // check for special keys... + var k = SPECIAL_KEYS[code] + if(k != null){ + return k + } + // chars... + k = String.fromCharCode(code) + if(k != ''){ + return k.toLowerCase() + } + return null +} + + +// if set to false the event handlers will always return false... +var KEYBOARD_HANDLER_PROPAGATE = true + +/* Basic key format: + * + * : , + * + * : { + * 'default': , + * // a modifier can be any single modifier, like shift or a + * // combination of modifers like 'ctrl+shift', given in order + * // of priority. + * // supported modifiers are (in order of priority): + * // - ctrl + * // - alt + * // - shift + * : [...] + * }, + * + * : [ + * // this can be any type of handler except for an alias... + * , + * + * ], + * + * // alias... + * : , + * + * XXX might need to add meta information to generate sensible help... + */ +function makeKeyboardHandler(keybindings, unhandled){ + if(unhandled == null){ + //unhandled = function(){return false} + unhandled = function(){return KEYBOARD_HANDLER_PROPAGATE} + } + return function(evt){ + var did_handling = false + var res = null + for(var mode in keybindings){ + if($(mode).length > 0){ + var bindings = keybindings[mode] + + var key = evt.keyCode + if(bindings.ignore == '*' + || bindings.ignore != null && bindings.ignore.indexOf(key) != -1){ + // return true + res = res == null ? true : res + did_handling = true + continue + } + // XXX ugly... + var modifers = evt.ctrlKey ? 'ctrl' : '' + modifers += evt.altKey ? (modifers != '' ? '+alt' : 'alt') : '' + modifers += evt.shiftKey ? (modifers != '' ? '+shift' : 'shift') : '' + + var handler = bindings[key] + + // alias... + while (typeof(handler) == typeof(123)) { + handler = bindings[handler] + } + // no handler... + if(handler == null){ + continue + } + // Array, lisp style with docs... + // XXX for some odd reason in chrome typeof([]) == typeof({})!!! + if(typeof(handler) == typeof([]) && handler.constructor.name == 'Array'){ + // we do not care about docs here, so just get the handler... + handler = handler[0] + } + // complex handler... + if(typeof(handler) == typeof({})){ + var callback = handler[modifers] + if(callback == null){ + callback = handler['default'] + } + if(callback != null){ + res = callback() + did_handling = true + continue + } + } else { + // simple callback... + res = handler() + did_handling = true + continue + } + } + } + if(!did_handling){ + // key is unhandled by any modes... + return unhandled(key) + } else { + // XXX should we handle multiple hits??? + return KEYBOARD_HANDLER_PROPAGATE&&res?true:false + } + } +} + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ diff --git a/lib/log.js b/lib/log.js new file mode 100755 index 0000000..0cf3996 --- /dev/null +++ b/lib/log.js @@ -0,0 +1,63 @@ +/********************************************************************** +* +* +* +**********************************************************************/ + +//var DEBUG = DEBUG != null ? DEBUG : true + + + +/********************************************************** logger ***/ + +function Logger(){ + _log = null + return { + setup: function(){ + if(_log == null){ + _log = $('
') + .css({ + position: 'fixed', + background: 'silver', + opacity: 0.5, + width: 200, + height: '80%', + top: 10, + left: 10, + 'z-index': 90000, + overflow: 'hidden', + padding: 10, + }) + .text('log') + .appendTo($('body')) + } else { + _log.appendTo($('body')) + } + return this + }, + remove: function(){ + _log.detach() + return this + }, + log: function(text){ + _log.html(_log.html() + '
' + text + '') + _log.scrollTop(_log.prop('scrollHeight')) + return this + }, + clear: function(){ + _log.html('') + return this + }, + get: function(){ + return _log + }, + set: function(elem){ + _log = elem + } + }.setup() +} + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ diff --git a/lib/scroller.js b/lib/scroller.js new file mode 100755 index 0000000..1ad2d11 --- /dev/null +++ b/lib/scroller.js @@ -0,0 +1,512 @@ +/********************************************************************** +* +* +* +**********************************************************************/ + +//var DEBUG = DEBUG != null ? DEBUG : true + +// click threshold in pixels, if the distance between start and end is +// less than this, the whole event is considered a click and not a +// drag/swipe... +var CLICK_THRESHOLD = 10 + +// if the amount of time to wait beween start and end is greater than this +// the event is considered a long click. +// NOTE: this will not auto-fire the event, the user MUST release first. +var LONG_CLICK_THRESHOLD = 400 + +// the maximum amount of time between clicks to count them together. +// NOTE: if multi-clicks are disabled this has no effect. +// NOTE: this is reset by the timeout explicitly set in the handler... +// NOTE: this is the timeout between two consecutive clicks and not the total. +// NOTE: if multiple clicks are enabled this will introduce a lag after +// each click (while we wait for the next), so keep this as small +// as possible. +var MULTI_CLICK_TIMEOUT = 200 + +// the amount of time between finger releases. +// NOTE: when this is passed all the fingers released before are ignored. +var MULTITOUCH_RELEASE_THRESHOLD = 100 + + + +/*********************************************************************/ + +// XXX add a resonable cancel scheme... +// ... something similar to touch threshold but bigger... +// XXX handle multiple touches... +// - timeout on lift to count fingers... +// XXX setup basic styles for the contained element... +// XXX revise... +// XXX test on other devices... +// XXX BUG: on landing a second finger while scrolling the things goes +// haywhire... +// ...check if this is gone... +// XXX add something like a scrollTo that would understand elements as +// well as explicit positions. +// XXX split this into a seporate lib... +function makeScrollHandler(root, config){ + root = $(root) + + // local data... + var ignoring = false + // XXX this and scroller.state are redundent... + var scrolling = false + var touch = false + var touches = 0 + var max_dx = 0 + var max_dy = 0 + + var cancelThreshold, scrolled + // initial state... + , start_x, start_y, start_t + // previous state... + , prev_x, prev_y, prev_t + // current state... + , x, y, t + // state delta... + , dx, dy, dt + + , shift + , scale + //, bounds + + function startMoveHandler(evt){ + var options = scroller.options + // ignore... + if(options.ignoreElements + && $(evt.target).closest(options.ignoreElements).length > 0 + || scroller.state == 'paused'){ + ignoring = true + return + } else { + ignoring = false + } + if(event.touches != null){ + touch = true + } + cancelThreshold = options.scrollCancelThreshold + touches = touch ? event.touches.length : 1 + // if we are already touching then just skip on this... + // XXX test this... + if(touches > 1){ + return false + } + prev_t = event.timeStamp || Date.now(); + start_t = prev_t + /* + if(options.autoCancelEvents){ + bounds = { + left: options.eventBounds, + right: root.width() - options.eventBounds, + top: options.eventBounds, + bottom: root.height() - options.eventBounds + } + } + */ + //togglePageDragging('on') + scrolled = $(root.children()[0]) + setTransitionDuration(scrolled, 0) + // XXX these two are redundant... + scrolling = true + scroller.state = 'scrolling' + options.enabelStartEvent && root.trigger('userScrollStart') + // XXX do we need to pass something to this? + options.preCallback && options.preCallback() + + shift = getElementShift(scrolled) + scale = getElementScale(scrolled) + // get the user coords... + prev_x = touch ? event.touches[0].pageX : evt.clientX + start_x = prev_x + prev_y = touch ? event.touches[0].pageY : evt.clientY + start_y = prev_y + + return false + } + + // XXX try and make this adaptive to stay ahead of the lags... + // NOTE: this does not support limiting the scroll, might be done in + // the future though. + // The way to go about this is to track scrolled size in the + // callback... + function moveHandler(evt){ + if(ignoring){ + return + } + var options = scroller.options + evt.preventDefault() + t = event.timeStamp || Date.now(); + // get the user coords... + x = touch ? event.touches[0].pageX : evt.clientX + y = touch ? event.touches[0].pageY : evt.clientY + touches = touch ? event.touches.length : 1 + + /* + // XXX needs testing... + // XXX do we need to account for scrollDisabled here??? + // check scroll bounds... + if(bounds != null){ + if(options.hScroll && (x <= bounds.left || x >= bounds.right) + || options.vScroll && (y <= bounds.top || y >= bounds.bottom)){ + // XXX cancel the touch event and trigger the end handler... + return endMoveHandler(evt) + } + } + */ + + // do the actual scroll... + if(!options.scrollDisabled && scrolling){ + if(options.hScroll){ + shift.left += x - prev_x + } + if(options.vScroll){ + shift.top += y - prev_y + } + setElementTransform(scrolled, shift, scale) + + // XXX these should be done every time the event is caught or + // just while scrolling? + dx = x - prev_x + dy = y - prev_y + max_dx += Math.abs(dx) + max_dy += Math.abs(dy) + dt = t - prev_t + prev_x = x + prev_y = y + prev_t = t + + options.enableUserScrollEvent && root.trigger('userScroll') + } + return false + } + + function endMoveHandler(evt){ + t = event.timeStamp || Date.now(); + touches = touch ? event.touches.length : 0 + if(ignoring){ + if(touches == 0){ + ignoring = false + } + return + } + var options = scroller.options + + // XXX get real transition duration... + scroller.resetTransitions() + + x = touch ? event.changedTouches[0].pageX : evt.clientX + y = touch ? event.changedTouches[0].pageY : evt.clientY + // check if we are canceling... + if(cancelThreshold + && Math.abs(start_x-x) < cancelThreshold + && Math.abs(start_y-y) < cancelThreshold + && (max_dx > cancelThreshold + || max_dy > cancelThreshold)){ + scroller.state = 'canceling' + } + + var data = { + orig_event: evt, + scroller: scroller, + speed: { + x: dx/dt, + y: dy/dt + }, + distance: { + x: start_x-x, + y: start_y-y + }, + duration: t-start_t, + // current touches... + touches: touches, + clicks: null, + } + // XXX stop only if no fingers are touching or let the callback decide... + //togglePageDragging('off') + // XXX update this with the new data model!!! (see below) + options.enableEndEvent && root.trigger('userScrollEnd', data) + if(options.postCallback + // XXX revise this.... + && options.postCallback(data) === false + || touches == 0){ + // cleanup and stop... + touch = false + scrolling = false + scroller.state = 'waiting' + scrolled = null + //bounds = null + max_dx = 0 + max_dy = 0 + } + + return false + } + + + var scroller = { + options: { + // if one of these is false, it will restrict scrolling in + // that direction. hScroll for horizontal and vScroll for + // vertical. + // NOTE: to disable scroll completely use scrollDisabled, see + // below for details. + hScroll: true, + vScroll: true, + + // this will disable scroll. + // NOTE: this is the same as setting both vScroll and hScroll + // to false, but can be set and reset without affecting + // the actual settings individually... + // NOTE: this takes priority over hScroll/vScroll. + scrollDisabled: false, + + // sets the default transition settings while not scrolling... + transitionDuration: 200, + transitionEasing: 'ease', + + // items to be ignored by the scroller... + // this is a jQuery compatible selector. + ignoreElements: '.noScroll', + // this is the side of the rectangle in px, if the user moves + // out of it, and then returns back, the action will get cancelled. + // i.e. the callback will get called with the "cancelling" state. + scrollCancelThreshold: 100, + + // these control weather in-scroll events will get triggered. + // NOTE: these may impact performance, especially the scroll + // event, thus they need to enabled explicitly. + enabelStartEvent: false, + enableUserScrollEvent: false, + enableEndEvent: false, + + /* + // XXX padding within the target element moving out of which + // will cancell the action... + // XXX needs testing... + autoCancelEvents: false, + eventBounds: 5, + */ + + // callback to be called when the user first touches the screen... + preCallback: null, + // callback to be called when the user lifts a finger/mouse. + // NOTE: this may happen before the scroll is done, for instance + // when one of several fingers participating in the action + // gets lifted. + // NOTE: if this returns false explicitly, this will stop scrolling. + postCallback: postScrollCallback, + + // These are used by the default callback... + // if true then doubleClick and multiClick events will get + // triggered. + // NOTE: this will introduce a lag needed to wait for next + // clicks in a group. + // NOTE: when this is false, shortClick is triggered for every + // single click separately. + enableMultiClicks: false, + // NOTE: if these are null, respective values from the env will + // be used. + clickThreshold: null, + longClickThreshold: null, + multiClickTimeout: null, + multitouchTimeout: null, + }, + // NOTE: this is updated live but not used by the system in any way... + state: 'stopped', + root: root, + + start: function(){ + if(this.state == 'paused'){ + this.state = 'waiting' + } else { + this.state = 'waiting' + // XXX STUB: this makes starting the scroll a bit sluggish, + // find a faster way... + //togglePageDragging('on') + + // NOTE: if we bind both touch and mouse events, on touch devices they + // might start interfering with each other... + if('ontouchmove' in window){ + root + .on('touchstart', startMoveHandler) + .on('touchmove', moveHandler) + .on('touchend', endMoveHandler) + .on('touchcancel', endMoveHandler) + } else { + root + .on('mousedown', startMoveHandler) + .on('mousemove', moveHandler) + .on('mouseup', endMoveHandler) + } + } + return this + }, + // XXX test... + pause: function(){ + this.state = 'paused' + return this + }, + stop: function(){ + if('ontouchmove' in window){ + root + .off('touchstart', startMoveHandler) + .off('touchmove', moveHandler) + .off('touchend', endMoveHandler) + } else { + root + .off('mousedown', startMoveHandler) + .off('mousemove', moveHandler) + .off('mouseup', endMoveHandler) + } + this.state = 'stopped' + return this + }, + resetTransitions: function(){ + var scrolled = this.root.children().first() + setTransitionDuration(scrolled, this.options.transitionDuration) + setTransitionEasing(scrolled, this.options.transitionEasing) + } + } + + // merge the config with the defaults... + if(config != null){ + $.extend(scroller.options, config) + } + + return scroller +} + +// default callback... +// This will provide support for the folowing events on the scroll root +// element: +// - scrollCancelled +// +// - shortClick +// - doubleClick +// - multiClick +// this will store the number of clicks in data.clicks +// - longClick +// +// - swipeLeft +// - swipeRight +// - swipeUp +// - swipeDown +// +// - screenReleased +// +// NOTE: data.touches passed to the event is the number of touches +// released within the multitouchTimeout. +// this differs from what postScrollCallback actually gets in the +// same field when it recieves the object. +// XXX add generic snap +// XXX add generic innertial scroll +// XXX test multiple touches... +function postScrollCallback(data){ + var scroller = data.scroller + var options = scroller.options + var root = scroller.root + var clickThreshold = options.clickThreshold || CLICK_THRESHOLD + var longClickThreshold = options.longClickThreshold || LONG_CLICK_THRESHOLD + var multitouchTimeout = options.multitouchTimeout || MULTITOUCH_RELEASE_THRESHOLD + var enableMultiClicks = options.enableMultiClicks + var multiClickTimeout = options.multiClickTimeout || MULTI_CLICK_TIMEOUT + + var now = Date.now(); + + // cancel event... + if(scroller.state == 'canceling'){ + return root.trigger('scrollCancelled', data) + } + + // handle multiple touches... + if(data.touches > 0){ + var then = scroller._last_touch_release + if(then == null || now - then < multitouchTimeout){ + if(scroller._touches == null){ + scroller._touches = 1 + } else { + scroller._touches += 1 + } + } else { + scroller._touches = null + } + // wait for the next touch release... + scroller._last_touch_release = now + return + + // calculate how many touches did participate... + } else { + data.touches = scroller._touches ? scroller._touches + 1 : 1 + scroller._last_touch_release = null + scroller._touches = null + } + + // clicks, double-clicks, multi-clicks and long-clicks... + if(Math.max( + Math.abs(data.distance.x), + Math.abs(data.distance.y)) < clickThreshold){ + if(data.duration > longClickThreshold){ + return root.trigger('longClick', data) + } + if(!enableMultiClicks){ + return root.trigger('shortClick', data) + + } else { + // count the clicks so far... + if(scroller._clicks == null){ + scroller._clicks = 1 + } else { + scroller._clicks += 1 + } + + // kill any previous waits... + if(scroller._click_timeout_id != null){ + clearTimeout(scroller._click_timeout_id) + } + + // wait for the next click... + scroller._click_timeout_id = setTimeout(function(){ + var clicks = scroller._clicks + data.clicks = clicks + if(clicks == 1){ + root.trigger('shortClick', data) + } else if(clicks == 2){ + root.trigger('doubleClick', data) + } else { + root.trigger('multiClick', data) + } + scroller._clicks = null + scroller._click_timeout_id = null + }, multiClickTimeout) + + return + } + } + + // swipes... + // XXX might be a good idea to chain these with swipe and screenReleased + if(Math.abs(data.distance.x) > Math.abs(data.distance.y)){ + if(data.distance.x <= -clickThreshold && root.data('events').swipeLeft){ + return root.trigger('swipeLeft', data) + } else if(data.distance.x >= clickThreshold && root.data('events').swipeRight){ + return root.trigger('swipeRight', data) + } + } else { + if(data.distance.y <= -clickThreshold && root.data('events').swipeUp){ + return root.trigger('swipeUp', data) + } else if(data.distance.y >= clickThreshold && root.data('events').swipeDown){ + return root.trigger('swipeDown', data) + } + } + + // this is triggered of no swipes were handled... + return root.trigger('screenReleased', data) +} + + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */