moved .debounce(..) to core + some refactoring and tweaking...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2018-09-01 20:19:12 +03:00
parent ac20bb957e
commit 50e523718f
3 changed files with 209 additions and 75 deletions

View File

@ -847,6 +847,30 @@ module.Cache = ImageGridFeatures.Feature({
//---------------------------------------------------------------------
// Timers...
var debounce =
module.debounce =
function(timeout, func){
func = timeout instanceof Function ? timeout : func
var f = function(...args){
return this.debounceActionCall({
action: func,
args: args,
tag: func instanceof Function ?
(func.name || f.name)
: func,
timeout: timeout instanceof Function ? null : timeout,
returns: 'cached',
retrigger: true,
})
}
f.toString = function(){
return `// debounced...\n${doc([func.toString()])}`
}
// NOTE: this will force Action(..) to set the .name to the action name...
Object.defineProperty(f, 'name', { value: '<action-name>' })
return f
}
var TimersActions = actions.Actions({
config: {
//
@ -861,6 +885,11 @@ var TimersActions = actions.Actions({
// ...
// }
'persistent-intervals': null,
// A timeout to wait between calls to actions triggered via
// .debounce(..)
'debounce-action-timeout': 200,
},
// XXX should we store more metadata (ms?) and provide introspection
@ -1068,6 +1097,174 @@ var TimersActions = actions.Actions({
Event(function(){
// XXX
})],
// Action debounce...
//
debounceActionCall: ['- System/',
doc`Debounce the action call...
.debounceActionCall(call)
-> result
Call format:
{
// action name of function to be called...
action: <name> | <function>,
// arguments to be passed to action/function...
args: <array>,
// tag to identify the call...
//
// Defaults to action name, optional for actions and
// required for functions...
tag: <tag> | null,
// timeout to drop calls within (optional).
//
// defaults to .config['debounce-action-timeout'] then
// to 200.
timeout: <number> | null,
// controls how action return value is handled:
//
// Values:
// 'cached' - cache the value and return it for
// every call within the timeout.
// 'dropped' - ignore return values.
//
// NOTE: this is designed to produce uniform results
// without and exceptions.
returns: 'cached' | 'dropped',
// if true re trigger the action after timeout if it was
// called after the initial call but before the timeout
// ended...
retrigger: <bool>,
}
NOTE: this does not affect actions called directly in any way.
`,
function(call){
var action = call.action
var args = call.args || []
var tag = call.tag || call.action
var timeout = call.timeout
|| this.config['debounce-action-timeout']
|| 200
var returns = call.returns || 'cached'
var retrigger = call.retrigger || false
// when debouncing a function a tag is required...
if(tag instanceof Function){
throw new TypeError('debounce: when passing a function a tag is required.')
}
var attr = '__debounce_'+ tag
// repeated call...
if(this[attr]){
if(retrigger){
this[attr +'_retriggered'] = true
}
var res = returns == 'cached' ?
this[attr +'_return']
: undefined
// setup and first call...
} else {
// NOTE: we are ignoring the return value here so as to
// make the first and repeated call uniform...
var context = this
var res = (action instanceof Function ?
action
: action.split('.')
.reduce(function(res, e){
context = res
return res[e]
}, this))
.call(context, ...args)
// cache the return value...
if(returns == 'cached'){
this[attr +'_return'] = res
// drop the return value...
} else {
res = undefined
}
this[attr] = setTimeout(function(){
delete this[attr]
delete this[attr +'_return']
// retrigger...
if(this[attr +'_retriggered']){
delete this[attr +'_retriggered']
tag == action ?
this.debounce(timeout, action, ...args)
: this.debounce(timeout, tag, action, ...args)
}
}.bind(this), timeout)
}
return res
}],
// shorthand...
debounce: ['- System/',
doc`Debounce action call...
Debounce call an action...
.debounce(action, ...)
.debounce(timeout, action, ...)
.debounce(tag, action, ...)
.debounce(timeout, tag, action, ...)
Debounce call a function...
.debounce(tag, func, ...)
.debounce(timeout, tag, func, ...)
Protocol:
- call
- start timeout timer
- trigger target action
- drop
- call (within timeout)
- drop
- re-trigger when timer ends
NOTE: when using a tag, it must not resolve to and action, i.e.
this[tag] must not be callable...
NOTE: this ignores action return value and returns this...
NOTE: this is a shorthand to .debounceActionCall(..)
`,
function(...args){
// parse the args...
var timeout = typeof(args[0]) == typeof(123) ?
args.shift()
: (this.config['debounce-action-timeout'] || 200)
// NOTE: this[tag] must not be callable, otherwise we treat it
// as an action...
var tag = (args[0] instanceof Function
|| this[args[0]] instanceof Function) ?
args[0]
: args.shift()
var action = args.shift()
return this.debounceActionCall({
action: action,
args: args,
tag: tag,
timeout: timeout,
// XXX
returns: 'dropped',
retrigger: true,
})
}],
})
var Timers =

View File

@ -48,6 +48,18 @@ var ExampleActions = actions.Actions({
// XXX
}],
exampleActionDebounced: ['Test/Action (debounced)',
core.doc`This is .exampleAction(..) debounced.
`,
core.debounce('exampleAction')],
exampleDebouncedAction: ['Test/Custom debounced action',
core.debounce(1000, function(...args){
console.log('exampleDebouncedAction: This can\'t be called more often than once per 1 second.')
console.log('exampleDebouncedAction: note that within this second only the original return value is returned.')
console.log(' <', args)
return args
})],
// a normal method...
exampleMethod: function(){
console.log('example method:', [].slice.call(arguments))

View File

@ -487,11 +487,6 @@ var KeyboardActions = actions.Actions({
// The amount of keyboard "quiet" time to wait for when
// .pauseKeyboardRepeat(..) is called...
'keyboard-repeat-pause-check': 100,
// A timeout to wait between calls to actions triggered via
// .debounce(..)
'debounce-action-timeout': 200,
},
get keybindings(){
@ -512,76 +507,6 @@ var KeyboardActions = actions.Actions({
function(){ return that.dom })
return kb },
debounce: ['- Interface/',
core.doc`Debounce action call...
Debounce call an action...
.debounce(action, ...)
.debounce(timeout, action, ...)
.debounce(tag, action, ...)
.debounce(timeout, tag, action, ...)
Debounce call a function...
.debounce(tag, func, ...)
.debounce(timeout, tag, func, ...)
NOTE: when using a tag, it must not resolve to and action, i.e.
this[tag] must not be callable...
NOTE: this ignores action return value and returns this...
`,
function(...args){
// parse the args...
var timeout = typeof(args[0]) == typeof(123) ?
args.shift()
: (this.config['debounce-action-timeout'] || 200)
// NOTE: this[tag] must not be callable, otherwise we treat it
// as an action...
var tag = (args[0] instanceof Function
|| this[args[0]] instanceof Function) ?
args[0]
: args.shift()
var action = args.shift()
// when debouncing a function a tag is required...
if(tag instanceof Function){
throw new TypeError('debounce: when passing a function a tag is required.')
}
var attr = '__debounce_'+ tag
// repeated call...
if(this[attr]){
this[attr +'_retriggered'] = true
// setup and first call...
} else {
// NOTE: we are ignoring the return value here so as to
// make the first and repeated call uniform...
var context = this
;(action instanceof Function ?
action
: action.split('.')
.reduce(function(res, e){
context = res
return res[e]
}, this))
.call(context, ...args)
this[attr] = setTimeout(function(){
delete this[attr]
// retrigger...
if(this[attr +'_retriggered']){
delete this[attr +'_retriggered']
tag == action ?
this.debounce(timeout, action, ...args)
: this.debounce(timeout, tag, action, ...args)
}
}.bind(this), timeout)
}
}],
// Add debounce support to keyboard handling...
//
// Syntax: