/*!
* Module Timers
*/
/**
* @namespace Timers
*/
const MODULE_NAME = 'Timers';
//###[ IMPORTS ]########################################################################################################
import {orDefault, isFunction, assert, hasValue, hasMembers} from './basic.js';
//###[ EXPORTS ]########################################################################################################
/**
* @namespace Timers:schedule
*/
/**
* Setup a timer for one-time execution of a callback, kills old timer if given
* to prevent overlapping timers.
*
* @param {Number} ms - time in milliseconds until execution
* @param {Function} callback - callback function to execute after ms
* @param {?(Object|Number)} [oldTimer=null] - if set, kills the timer before setting up new one
* @throws error if ms is negative or callback is not a function
* @returns {Object} new timer
*
* @memberof Timers:schedule
* @alias schedule
* @see pschedule
* @see countermand
* @example
* const timer = schedule(1000, function(){ alert('time for tea'); });
* const timer = schedule(2000, function(){ alert('traffic jam, tea has to wait'); }, timer);
*/
export function schedule(ms, callback, oldTimer=null){
ms = orDefault(ms, 1, 'int');
assert(ms >= 0, `${MODULE_NAME}:schedule | ms must be positive`);
assert(isFunction(callback), `${MODULE_NAME}:schedule | callback must be a function`);
if( hasValue(oldTimer) ){
countermand(oldTimer);
}
return {id : window.setTimeout(callback, ms), type : 'timeout'};
}
/**
* @namespace Timers:pschedule
*/
/**
* Setup a timer for one-time execution of a callback, kills old timer if given
* to prevent overlapping timers.
* This implementation uses Date.now()/Date.getTime() to improve on timer precision for long
* running timers. The timers of this method can also be used in countermand().
*
* Warning: these timers are more precise than normal timer for _long_ time spans and less precise for short ones,
* if you are dealing with times at least above 30s (or minutes and hours) this the right choice, if you look to
* use precise timers in the second and millisecond range, definitely use schedule/loop instead!
*
* @param {Number} ms - time in milliseconds until execution
* @param {Function} callback - callback function to execute after ms
* @param {?(Object|Number)} [oldTimer=null] - if set, kills the timer before setting up new one
* @throws error if ms is not positive or if callback is not a function
* @returns {Object} timer (does not create new timer object if oldTimer given, but returns old one)
*
* @memberof Timers:pschedule
* @alias pschedule
* @see schedule
* @see countermand
* @example
* const timer = pschedule(1000, function(){ alert('time for tea'); });
* const timer = pschedule(2000, function(){ alert('traffic jam, tea has to wait'); }, timer);
*/
export function pschedule(ms, callback, oldTimer=null){
ms = orDefault(ms, 1, 'int');
assert(ms >= 0, `${MODULE_NAME}:pschedule | ms must be positive`);
assert(isFunction(callback), `${MODULE_NAME}:pschedule | callback must be a function`);
if(
hasValue(oldTimer)
&& hasMembers(oldTimer, ['id', 'type'])
){
countermand(oldTimer);
oldTimer.precise = true;
} else {
oldTimer = {id : -1, type : 'timeout', precise : true};
}
const waitStart = Date.now();
let waitMilliSecs = ms;
const fAdjustWait = function(){
if( waitMilliSecs > 0 ){
waitMilliSecs -= (Date.now() - waitStart);
oldTimer.id = window.setTimeout(fAdjustWait, (waitMilliSecs > 10) ? waitMilliSecs : 10);
} else {
callback();
}
};
oldTimer.id = window.setTimeout(fAdjustWait, waitMilliSecs);
return oldTimer;
}
/**
* @namespace Timers:reschedule
*/
/**
* Alias for schedule() with more natural param-order for rescheduling.
*
* @param {(Object|Number)} timer - the timer to refresh/reset
* @param {Number} ms - time in milliseconds until execution
* @param {Function} callback - callback function to execute after ms
* @throws error if ms is not positive or if callback is not a function
* @returns {Object} timer (may be the original timer, if given timer is precise from pschedule or ploop)
*
* @memberof Timers:reschedule
* @alias reschedule
* @see schedule
* @example
* const timer = reschedule(timer, 3000, function(){ alert('taking even more time'); });
*/
export function reschedule(timer, ms, callback){
ms = orDefault(ms, 1, 'int');
assert(ms >= 0, `${MODULE_NAME}:reschedule | ms must be positive`);
assert(isFunction(callback), `${MODULE_NAME}:reschedule | callback must be a function`);
if( hasValue(timer) && hasValue(timer.precise) && !!timer.precise ){
return pschedule(ms, callback, timer);
} else {
return schedule(ms, callback, timer);
}
}
/**
* @namespace Timers:loop
*/
/**
* Setup a loop for repeated execution of a callback, kills old loop if wished
* to prevent overlapping loops.
*
* @param {Number} ms - time in milliseconds until execution
* @param {Function} callback - callback function to execute after ms
* @param {?(Object|Number)} [oldLoop=null] - if set, kills the loop before setting up new one
* @throws error if ms is not positive or if callback is not a function
* @returns {Object} new loop
*
* @memberof Timers:loop
* @alias loop
* @see ploop
* @see countermand
* @example
* const loop = loop(250, function(){ document.body.classList.add('brightred'); });
* const loop = loop(100, function(){ document.body.classList.add('brightgreen'); }, loop);
*/
export function loop(ms, callback, oldLoop=null){
ms = orDefault(ms, 1, 'int');
assert(ms >= 0, `${MODULE_NAME}:loop | ms must be positive`);
assert(isFunction(callback), `${MODULE_NAME}:loop | callback must be a function`);
if( hasValue(oldLoop) ){
countermand(oldLoop, true);
}
return {id : window.setInterval(callback, ms), type : 'interval'};
}
/**
* @namespace Timers:ploop
*/
/**
* Setup a loop for repeated execution of a callback, kills old loop if wished
* to prevent overlapping loops.
* This implementation uses Date.now()/Date.getTime() to improve on timer precision for long running loops.
*
* Warning: these timers are more precise than normal timer for _long_ time spans and less precise for short ones,
* if you are dealing with times at least above 30s (or minutes and hours) this the right choice, if you look to
* use precise timers in the second and millisecond range, definitely use schedule/loop instead!
*
* The loops of this method can also be used in countermand().
* This method does not actually use intervals internally but timeouts,
* so don't wonder if you can't find the ids in JS.
*
* @param {Number} ms - time in milliseconds until execution
* @param {Function} callback - callback function to execute after ms
* @param {?(Object|Number)} [oldLoop=null] - if set, kills the loop before setting up new one
* @throws error if ms is not positive or if callback is not a function
* @returns {Object} loop (if you give an old loop into the function the same reference will be returned)
*
* @memberof Timers:ploop
* @alias ploop
* @see loop
* @see countermand
* @example
* const loop = ploop(250, function(){ document.body.classList.add('brightred'); });
* const loop = ploop(100, function(){ document.body.classList.add('brightgreen'); }, loop);
*/
export function ploop(ms, callback, oldLoop=null){
ms = orDefault(ms, 1, 'int');
assert(ms >= 0, `${MODULE_NAME}:ploop | ms must be positive`);
assert(isFunction(callback), `${MODULE_NAME}:ploop | callback must be a function`);
if(
hasValue(oldLoop)
&& hasMembers(oldLoop, ['id', 'type'])
){
countermand(oldLoop, true);
oldLoop.precise = true;
} else {
oldLoop = {id : -1, type : 'interval', precise : true};
}
let
waitStart = Date.now(),
waitMilliSecs = ms
;
const fAdjustWait = function(){
if( waitMilliSecs > 0 ){
waitMilliSecs -= (Date.now() - waitStart);
oldLoop.id = window.setTimeout(fAdjustWait, (waitMilliSecs > 10) ? waitMilliSecs : 10);
} else {
callback();
waitStart = Date.now();
waitMilliSecs = ms;
oldLoop.id = window.setTimeout(fAdjustWait, waitMilliSecs);
}
};
oldLoop.id = window.setTimeout(fAdjustWait, waitMilliSecs);
return oldLoop;
}
/**
* @namespace Timers:countermand
*/
/**
* Cancel a timer or loop immediately.
*
* @param {(Object|Number)} timer - the timer or loop to end
* @param {?Boolean} [isInterval=false] - defines if a timer or a loop is to be stopped, set in case timer is a GUID
*
* @memberof Timers:countermand
* @alias countermand
* @see schedule
* @see pschedule
* @see loop
* @see ploop
* @example
* countermand(timer);
* countermand(loop);
*/
export function countermand(timer, isInterval=false){
isInterval = orDefault(isInterval, false, 'bool');
if( hasValue(timer) ){
if( hasMembers(timer, ['id', 'type']) ){
if( timer.type === 'interval' ){
window.clearInterval(timer.id);
} else {
window.clearTimeout(timer.id);
}
} else {
if( !isInterval ){
window.clearTimeout(timer);
} else {
window.clearInterval(timer);
}
}
}
}
/**
* @namespace Timers:requestAnimationFrame
*/
/**
* This is a simple streamlined, vendor-cascading version of window.requestAnimationFrame with a timeout fallback in
* case the functionality is missing from the browser.
*
* @param {Function} callback - the code to execute once the browser has assigned an execution slot for it
* @throws error if callback is not a function
* @return {Number} either the id of the requestAnimationFrame or the internal timeout, both are cancellable via cancelAnimationFrame
*
* @memberof Timers:requestAnimationFrame
* @alias requestAnimationFrame
* @see raf
* @see cancelAnimationFrame
* @see caf
* @example
* const requestId = requestAnimationFrame(function(){ window.body.style.opacity = 0; });
*/
export function requestAnimationFrame(callback){
assert(isFunction(callback), `${MODULE_NAME}:requestAnimationFrame | callback is no function`);
const raf = window.requestAnimationFrame
?? window.webkitRequestAnimationFrame
?? window.mozRequestAnimationFrame
?? window.msRequestAnimationFrame
?? function(callback){ return schedule(16, callback); }
;
return raf(callback);
}
/**
* @namespace Timers:cancelAnimationFrame
*/
/**
* This is a simple streamlined, vendor-cascading version of window.cancelAnimationFrame.
*
* @param {Number} id - either the id of the requestAnimationFrame or its timeout fallback
*
* @memberof Timers:cancelAnimationFrame
* @alias cancelAnimationFrame
* @see requestAnimationFrame
* @see raf
* @see caf
* @example
* cancelAnimationFrame(requestAnimationFrame(function(){ window.body.style.opacity = 0; }));
*/
export function cancelAnimationFrame(id){
const raf = window.requestAnimationFrame
?? window.webkitRequestAnimationFrame
?? window.mozRequestAnimationFrame
?? window.msRequestAnimationFrame
;
let caf = window.cancelAnimationFrame
?? window.mozCancelAnimationFrame
;
if( !hasValue(raf) ){
caf = countermand;
}
return caf(id);
}
/**
* @namespace Timers:waitForRepaint
*/
/**
* This function has the purpose to offer a safe execution slot for code depending on an up-to-date rendering state of
* the DOM after a change to styles for example. Let's say you add a class to an element and right in the next line
* you'll want to read a layout attribute like width or height from it. This might fail, because there is no guarantee
* the browser actually already applied the new styles to be read from the DOM.
*
* To wait safely for the new DOM state this method works with two stacked requestAnimationFrame calls.
*
* Since requestAnimationFrame always happens _before_ a repaint, two stacked calls ensure, that there has to be a
* repaint between them.
*
* @param {Function} callback - the code to execute once the browser performed a repaint
* @throws error if callback is not a function
* @return {Object} dictionary of ids for the inner and outer request ids, outer gets assigned right away, while inner gets assigned after first callback => {outer : 1, inner : 2}
*
* @memberof Timers:waitForRepaint
* @alias waitForRepaint
* @see requestAnimationFrame
* @see raf
* @example
* element.classList.add('special-stuff');
* waitForRepaint(function(){ alert(`the new dimensions after class change are: ${element.offsetWidth}x${element.offsetHeight}`); });
*/
export function waitForRepaint(callback){
assert(isFunction(callback), `${MODULE_NAME}:waitForRepaint | callback is no function`);
const ids = {};
ids.outer = requestAnimationFrame(function(){
ids.inner = requestAnimationFrame(callback);
});
return ids;
}