diff --git a/lib/jli.js b/lib/jli.js index 8da08f3..3d0ba64 100755 --- a/lib/jli.js +++ b/lib/jli.js @@ -9,6 +9,16 @@ //var DEBUG = DEBUG != null ? DEBUG : true +var POOL_SIZE = 64 + +var DEFAULT_TRANSITION_DURATION = 200 + +// XXX this affects only the innertial part, not setCurrentPage... +var USE_TRANSITIONS_FOR_ANIMATION = false + +var USE_TRANSFORM = true +var USE_3D_TRANSFORM = true + /*********************************************************************/ @@ -33,20 +43,25 @@ // - : 0 for 'off' and 1 for 'on' (see below) // - 'on' : switch mode on -- add class // - 'off' : switch mode off -- remove class +// - '!' : reload current state, same as toggler(toggler('?')) // - '?' : return current state ('on'|'off') // // In forms 2 and 3, if class_list is a list of strings, the can be: // - : explicitly set the state to index in class_list // - : explicitly set a class from the list +// - '!' : reload current state, same as toggler(toggler('?')) // - '?' : return current state ('on'|'off') -// +// +// // In the third form the is a jquery-compatible object. // // In all forms this will return the current state string or null if the // action argument given is invalid. // +// NOTE: action '?' is handled internally and not passed to the callbacks. // NOTE: there is a special action 'next', passing it will have the same -// effect as not passing any action. +// effect as not passing any action -- we will change to the next +// state. // NOTE: if it is needed to apply this to an explicit target but with // no explicit action, just pass 'next' as the second argument. // NOTE: a special class name 'none' means no class is set, if it is present @@ -58,14 +73,27 @@ // a way around this is to pass an empty function as callback_b // NOTE: leading dots in class names in class_list are optional. // this is due to several times I've repeated the same mistake of -// forgetting to write the classes without leading dots, this now -// will normalize the class list... +// forgetting to write the classes without leading dots, the class +// list is not normalized... +// NOTE: the toggler can be passed a non-jquery object, but then only an +// explicit state is supported as the second argument, the reason +// being that we can not determain the current state without a propper +// .hasClass(..) test... // // // This also takes one or two callbacks. If only one is given then it is // called after (post) the change is made. If two are given then the first // is called before the change and the second after the change. -// The callbacks are passed the current action. +// +// The callbacks are passed two arguments: +// - : the state we are going in +// - : the target element or the element passed to the +// toggler +// +// +// The callback function will have 'this' set to the same value as the +// toggler itself, e.g. if the toggler is called as a method, the +// callback's 'this' will reference it's parent object. // // NOTE: the pre-callback will get the "intent" action, i.e. the state the // we are changing into but the changes are not yet made. @@ -105,6 +133,7 @@ function createCSSClassToggler(elem, class_list, callback_a, callback_b){ var e = a var action = b == 'next' ? null : b } + var args = args2array(arguments).slice(2) e = $(e) // option number... if(typeof(action) == typeof(1)){ @@ -119,7 +148,7 @@ function createCSSClassToggler(elem, class_list, callback_a, callback_b){ } } // we need to get the current state... - if(action == null || action == '?'){ + if(action == null || action == '?' || action == '!'){ // get current state... var cur = 'none' for(var i=0; i < class_list.length; i++){ @@ -133,6 +162,11 @@ function createCSSClassToggler(elem, class_list, callback_a, callback_b){ return bool_action ? (cur == 'none' ? 'off' : 'on') : cur } + // force reload of current state... + if(action == '!'){ + action = bool_action ? (cur == 'none' ? 'off' : 'on') : cur + } + // invalid action... } else if((bool_action && ['on', 'off'].indexOf(action) == -1) || (!bool_action && class_list.indexOf(action) == -1)){ @@ -154,10 +188,14 @@ function createCSSClassToggler(elem, class_list, callback_a, callback_b){ } } + // NOTE: the callbacks are passed the same this as the calling + // function, this will enable them to act as metods correctly // pre callback... if(callback_pre != null){ - if(callback_pre(action) === false){ - return + if(callback_pre.apply(this, [action, e].concat(args)) === false){ + // XXX should we return action here??? + //return + return func('?') } } // update the element... @@ -167,7 +205,7 @@ function createCSSClassToggler(elem, class_list, callback_a, callback_b){ } // post callback... if(callback_post != null){ - callback_post(action) + callback_post.apply(this, [action, e].concat(args)) } return action @@ -284,6 +322,7 @@ var getElementScale = makeCSSVendorAttrGetter( return parseFloat((/(scale|matrix)\(([^),]*)\)/).exec(data)[2]) }) + var getElementShift = makeCSSVendorAttrGetter( 'transform', {left: 0, top: 0}, @@ -296,21 +335,17 @@ var getElementShift = makeCSSVendorAttrGetter( }) -var DEFAULT_TRANSITION_DURATION = 200 - var getElementTransitionDuration = makeCSSVendorAttrGetter( 'transitionDuration', DEFAULT_TRANSITION_DURATION, parseInt) - -var USE_3D_TRANSFORM = true - // NOTE: at this point this works only on the X axis... function setElementTransform(elem, offset, scale, duration){ elem = $(elem) - var t3d = USE_3D_TRANSFORM ? 'translateZ(0px)' : '' + //var t3d = USE_3D_TRANSFORM ? 'translateZ(0px)' : '' + var t3d = USE_3D_TRANSFORM ? 'translate3d(0,0,0)' : '' if(offset == null){ offset = getElementShift(elem) @@ -366,9 +401,6 @@ function setElementTransform(elem, offset, scale, duration){ } -// XXX this affects only the innertial part, not setCurrentPage... -var USE_TRANSITIONS_FOR_ANIMATION = false - // XXX make this a drop-in replacement for setElementTransform... // XXX cleanup, still flacky... function animateElementTo(elem, to, duration, easing, speed, use_transitions){ @@ -430,12 +462,12 @@ function animateElementTo(elem, to, duration, easing, speed, use_transitions){ } // do an intermediate step... - // XXX do propper easing... + // XXX do proper easing... // XXX sometimes results in jumping around... // ...result of jumping over the to position... if(speed != null){ - // XXX the folowing two blocks are the same... + // XXX the following two blocks are the same... // XXX looks a bit too complex, revise... if(Math.abs(dist.top) >= 1){ dy = ((t - start) * speed.y) @@ -466,7 +498,7 @@ function animateElementTo(elem, to, duration, easing, speed, use_transitions){ } } - // XXX this is a staright forward linear function... + // XXX this is a straight forward linear function... } else { var r = (t - start) / duration cur.top = Math.round(from.top + (dist.top * r)) @@ -481,6 +513,7 @@ function animateElementTo(elem, to, duration, easing, speed, use_transitions){ } } + function stopAnimation(elem){ if(elem.next_frame){ cancelAnimationFrame(elem.next_frame) @@ -493,11 +526,26 @@ function setElementScale(elem, scale){ return setElementTransform(elem, null, scale) } + +function setElementOrigin(elem, x, y, z){ + x = x == null ? '50%' : x + y = y == null ? '50%' : y + z = z == null ? '0' : z + var value = x +' '+ y +' '+ z + + return $(elem).css({ + 'transform-origin': value, + '-ms-transform-origin': value, + '-webkit-transform-origin': value, + }) +} + + function setTransitionEasing(elem, ease){ if(typeof(ms) == typeof(0)){ ms = ms + 'ms' } - return elem.css({ + return $(elem).css({ 'transition-timing-function': ease, '-moz-transition-timing-function': ease, '-o-transition-timing-function': ease, @@ -506,6 +554,7 @@ function setTransitionEasing(elem, ease){ }) } + function setTransitionDuration(elem, ms){ if(typeof(ms) == typeof(0)){ ms = ms + 'ms' @@ -521,7 +570,6 @@ function setTransitionDuration(elem, ms){ - /************************************************ jQuery extensions **/ jQuery.fn.reverseChildren = function(){ @@ -540,13 +588,406 @@ jQuery.fn.sortChildren = function(func){ +/************************************************** Deferred utils ***/ + +// Deferred worker pool... +// +// makeDeferredPool([size][, paused]) -> pool +// +// +// This will create and return a pooled queue of deferred workers. +// +// Public interface: +// +// .enqueue(obj, func, args) -> deferred +// Add a worker to queue. +// If the pool is not filled and not paused, this will run the +// worker right away. +// If the pool is full the worker is added to queue (FIFO) and +// ran in its turn. +// +// .pause() -> pool +// Pause the queue. +// NOTE: this also has a second form: .pause(func), see below. +// +// .resume() -> pool +// Restart the queue. +// +// .dropQueue() -> pool +// Drop the queued workers. +// NOTE: this will not stop the already running workers. +// +// .isRunning() -> bool +// Test if any workers are running in the pool. +// NOTE: this will return false ONLY when the pool is empty. +// +// .isPaused() -> bool +// Test if pool is in a paused state. +// NOTE: some workers may sill be finishing up so if you want +// to test whether any workers are still running use +// .isRunning() +// +// +// Event handler/callback registration: +// +// .on(evt, func) -> pool +// Register a handler (func) for an event (evt). +// +// .off(evt[, func]) -> pool +// Remove a handler (func) form and event (evt). +// NOTE: if func is omitted, remove all handlers from the given +// event... +// +// .progress(func) -> pool +// Register a progress handler. +// The handler is called after each worker is done and will get +// passed: +// - workers done count +// - workers total count +// Short hand for: +// .on('progress', func) -> pool +// NOTE: the total number of workers can change as new workers +// are added or the queue is cleared... +// +// .fail(func) -> pool +// Register a worker fail handler. +// The handler is called when a worker goes into the fail state. +// This will get passed: +// - workers done count +// - workers total count +// Short hand for: +// .on('fail', func) -> pool +// NOTE: this will not stop the execution of other handlers. +// +// .pause(func) -> pool +// Register a pause handler. +// This handler is called after the last worker finishes when +// the queue is paused. +// Short hand for: +// .on('progress', func) -> pool +// +// .resume(func) -> pool +// Short hand for: +// .on('resume', func) -> pool +// +// .depleted(func) -> pool +// Register a depleted pool handler. +// The handler will get called when the queue and pool are empty +// (depleted) and the last worker is done. +// Short hand for: +// .on('deplete', func) -> pool +// +// XXX should this be an object or a factory??? +function makeDeferredPool(size, paused){ + size = size == null ? POOL_SIZE : size + size = size < 0 ? 1 + : size > 512 ? 512 + : size + paused = paused == null ? false : paused + + + var Pool = { + pool: [], + queue: [], + size: size, + + // XXX do we need to hide or expose them and use their API??? + _event_handlers: { + deplete: $.Callbacks(), + progress: $.Callbacks(), + pause: $.Callbacks(), + resume: $.Callbacks(), + fail: $.Callbacks() + }, + + _paused: paused, + } + + // Run a worker... + // + // This will: + // - create and add a worker to the pool, which will: + // - run an element from the queue + // - remove self from pool + // - if the pool is not full, create another worker (call + // ._run(..)) else exit + // - call ._fill() to replenish the pool + Pool._run = function(deferred, func, args){ + var that = this + var pool = this.pool + var pool_size = this.size + var queue = this.queue + var run = this._run + + // run an element from the queue... + var worker = func.apply(null, args) + .always(function(){ + // prepare to remove self from pool... + var i = pool.indexOf(worker) + + Pool._event_handlers.progress.fire(pool.length - pool.len(), pool.length + queue.length) + + // remove self from queue... + delete pool[i] + + // shrink the pool if it's overfilled... + // i.e. do not pop another worker and let the "thread" die. + if(pool.len() > pool_size){ + // remove self... + return + } + // pause the queue -- do not do anything else... + if(that._paused == true){ + // if pool is empty fire the pause event... + if(pool.len() == 0){ + Pool._event_handlers.pause.fire() + } + return + } + + // get the next queued worker... + var next = queue.splice(0, 1)[0] + + // run the next worker if it exists... + if(next != null){ + run.apply(that, next) + + // empty queue AND empty pool mean we are done... + } else if(pool.len() == 0){ + var l = pool.length + // NOTE: potential race condition -- something can be + // pushed to pool just before it's "compacted"... + pool.length = 0 + + that._event_handlers.deplete.fire(l) + } + + // keep the pool full... + that._fill() + }) + .fail(function(){ + Pool._event_handlers.fail.fire(pool.length - pool.len(), pool.length + queue.length) + deferred.reject.apply(deferred, arguments) + }) + .progress(function(){ + deferred.notify.apply(deferred, arguments) + }) + .done(function(){ + deferred.resolve.apply(deferred, arguments) + }) + + this.pool.push(worker) + + return worker + } + + // Fill the pool... + // + Pool._fill = function(){ + var that = this + var pool_size = this.size + var run = this._run + var l = this.pool.len() + + if(this._paused != true + && l < pool_size + && this.queue.length > 0){ + this.queue.splice(0, pool_size - l) + .forEach(function(e){ + run.apply(that, e) + }) + } + + return this + } + + + // Public methods... + + // Add a worker to queue... + // + Pool.enqueue = function(func){ + var deferred = $.Deferred() + + // add worker to queue... + this.queue.push([deferred, func, args2array(arguments).slice(1)]) + + // start work if we have not already... + this._fill() + + //return this + return deferred + } + + // Drop the queued workers... + // + // NOTE: this will not stop the running workers... + // XXX should this return the pool or the dropped queue??? + Pool.dropQueue = function(){ + this.queue.splice(0, this.queue.length) + return this + } + + // NOTE: this will not directly cause .isRunning() to return false + // as this will not directly spot all workers, it will just + // pause the queue and the workers that have already started + // will keep running until they are done, and only when the + // pool is empty will the .isRunning() return false. + // + // XXX test... + Pool.pause = function(func){ + if(func == null){ + this._paused = true + } else { + this.on('pause', func) + } + return this + } + + // XXX test... + Pool.resume = function(func){ + if(func == null){ + this._paused = false + this._event_handlers['resume'].forEach(function(f){ f() }) + this._fill() + } else { + this.on('resume', func) + } + return this + } + + Pool.isPaused = function(){ + return this._paused + } + Pool.isRunning = function(){ + return this.pool.len() > 0 + } + + + // Generic event handlers... + Pool.on = function(evt, handler){ + this._event_handlers[evt].add(handler) + return this + } + // NOTE: if this is not given a handler, it will clear all handlers + // from the given event... + Pool.off = function(evt, handler){ + if(handler != null){ + this._event_handlers[evt].remove(handler) + } else { + this._event_handlers[evt].empty() + } + return this + } + + // Register a queue depleted handler... + // + // This occurs when a populated queue is depleted and the last worker + // is done. + // + // NOTE: this is similar to jQuery.Deferred().done(..) but differs in + // that the pool can fill up and get depleted more than once, + // thus, the handlers may get called more than once per pool + // life... + // NOTE: it is recommended to fill the queue faster than the workers + // finish, as this may get called after last worker is done and + // the next is queued... + Pool.depleted = function(func){ + return this.on('deplete', func) + } + + // Register queue progress handler... + // + // This occurs after each worker is done. + // + // handler will be passed: + // - the pool object + // - workers done + // - total workers (done + queued) + Pool.progress = function(func){ + return this.on('progress', func) + } + + // Register worker fail handler... + // + Pool.fail = function(func){ + return this.on('fail', func) + } + + + return Pool +} + + + /**************************************************** JS utilities ***/ + +// Get screen dpi... +// +// This will calculate the value and save it to screen.dpi +// +// if force is true this will re-calculate the value. +// +// NOTE: this needs the body loaded to work... +// NOTE: this may depend on page zoom... +// NOTE: yes, this is a hack, but since we have no other reliable way to +// do this... +function getDPI(force){ + if(screen.dpi == null || force){ + var e = $('
') + .css({ + position: 'absolute', + width: '1in', + left: '-100%', + top: '-100%' + }) + .appendTo($('body')) + var res = e.width() + e.remove() + screen.dpi = res + return res + } else { + return screen.dpi + } +} +// XXX is this correct??? +$(getDPI) + + +// return 1, -1, or 0 depending on sign of x +function sign(x){ + return (x > 0) - (x < 0) +} + + String.prototype.capitalize = function(){ return this[0].toUpperCase() + this.slice(1) } +// XXX not sure if this has to be a utility or a method... +Object.get = function(obj, name, dfl){ + var val = obj[name] + if(val === undefined && dfl != null){ + return dfl + } + return val +} + + +// like .length but for sparse arrays will return the element count... +Array.prototype.len = function(){ + return this.filter(function(){ return true }).length +} + + +// convert JS arguments to Array... +function args2array(args){ + return Array.apply(null, args) +} + + var getAnimationFrame = (window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame @@ -556,6 +997,7 @@ var getAnimationFrame = (window.requestAnimationFrame setTimeout(callback, 1000/60) }) + var cancelAnimationFrame = (window.cancelRequestAnimationFrame || window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame @@ -565,6 +1007,78 @@ var cancelAnimationFrame = (window.cancelRequestAnimationFrame || clearTimeout) +Date.prototype.toShortDate = function(){ + var y = this.getFullYear() + var M = this.getMonth()+1 + M = M < 10 ? '0'+M : M + var D = this.getDate() + D = D < 10 ? '0'+D : D + var H = this.getHours() + H = H < 10 ? '0'+H : H + var m = this.getMinutes() + m = m < 10 ? '0'+m : m + var s = this.getSeconds() + s = s < 10 ? '0'+s : s + + return ''+y+'-'+M+'-'+D+' '+H+':'+m+':'+s +} +Date.prototype.getTimeStamp = function(no_seconds){ + var y = this.getFullYear() + var M = this.getMonth()+1 + M = M < 10 ? '0'+M : M + var D = this.getDate() + D = D < 10 ? '0'+D : D + var H = this.getHours() + H = H < 10 ? '0'+H : H + var m = this.getMinutes() + m = m < 10 ? '0'+m : m + var s = this.getSeconds() + s = s < 10 ? '0'+s : s + + return ''+y+M+D+H+m+s +} +Date.prototype.setTimeStamp = function(ts){ + ts = ts.replace(/[^0-9]*/g, '') + this.setFullYear(ts.slice(0, 4)) + this.setMonth(ts.slice(4, 6)*1-1) + this.setDate(ts.slice(6, 8)) + this.setHours(ts.slice(8, 10)) + this.setMinutes(ts.slice(10, 12)) + this.setSeconds(ts.slice(12, 14)) + return this +} +Date.timeStamp = function(){ + return (new Date()).getTimeStamp() +} +Date.fromTimeStamp = function(ts){ + return (new Date()).setTimeStamp(ts) +} + + +function logCalls(func, logger){ + var that = this + var _func = function(){ + logger(func, arguments) + return func.apply(that, arguments) + } + _func.name = func.name + return _func +} + + +function assyncCall(func){ + var that = this + var _func = function(){ + var res = $.Deferred() + setTimeout(function(){ + res.resolve(func.apply(that, arguments)) + }, 0) + return res + } + _func.name = func.name + return _func +} + /**********************************************************************