diff --git a/ui (gen4)/features/core.js b/ui (gen4)/features/core.js index 35470139..833e76b1 100755 --- a/ui (gen4)/features/core.js +++ b/ui (gen4)/features/core.js @@ -847,30 +847,71 @@ module.Cache = ImageGridFeatures.Feature({ //--------------------------------------------------------------------- // Timers... +// Create a debounced action... +// +// options format: +// { +// timeout: number, +// returns: 'cached' | 'dropped', +// callback: function(retriggered, args), +// } +// 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, - }) +function(options, func){ + // parse args... + func = options instanceof Function ? options : func + options = options instanceof Function ? {} : options + + if(typeof(options) == typeof(123)){ + options.timeout = options } + + // closure state... + var res = undefined + var debounced = false + var retriggered = 0 + + var f = function(...args){ + if(!debounced){ + res = func instanceof Function ? + func.call(this, ...args) + // alias... + : this.parseStringAction.callAction(this, func, ...args) + res = options.returns != 'cahced' ? res : undefined + + // start the timer... + debounced = setTimeout( + function(){ + // callback... + options.callback instanceof Function + && options.callback.call(this, retriggered, args) + + // cleanup... + retriggered = 0 + res = undefined + debounced = false + }.bind(this), + options.timeout + || this.config['debounce-action-timeout'] + || 200) + + } else { + retriggered++ + return res + } + } + 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: '' }) + return f } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + var TimersActions = actions.Actions({ config: { // @@ -1100,120 +1141,6 @@ var TimersActions = actions.Actions({ // Action debounce... // - debounceActionCall: ['- System/', - doc`Debounce the action call... - - .debounceActionCall(call) - -> result - - Call format: - { - // action name of function to be called... - action: | , - - // arguments to be passed to action/function... - args: , - - // tag to identify the call... - // - // Defaults to action name, optional for actions and - // required for functions... - tag: | null, - - // timeout to drop calls within (optional). - // - // defaults to .config['debounce-action-timeout'] then - // to 200. - timeout: | 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: , - } - - 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... @@ -1227,6 +1154,19 @@ var TimersActions = actions.Actions({ .debounce(tag, func, ...) .debounce(timeout, tag, func, ...) + Generic debounce: + .debounce(options, action, ...) + .debounce(options, func, ...) + + options format: + { + tag: , + timeout: , + returns: 'cached' | 'dropped', + callback: , + } + + Protocol: - call - start timeout timer @@ -1244,26 +1184,58 @@ var TimersActions = actions.Actions({ `, 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() + if(!(args[0] instanceof Function + || typeof(args[0]) == typeof(123) + || typeof(args[0]) == typeof('str'))){ + var options = args.shift() + var tag = options.tag || args[0].name || args[0] - return this.debounceActionCall({ - action: action, - args: args, - tag: tag, - timeout: timeout, - // XXX - returns: 'dropped', - retrigger: true, + } else { + var options = { + 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() + } + + // sanity check: 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 action = args.shift() + var attr = '__debounce_'+ tag + + options = Object.assign(Object.create(options), { + callback: function(retriggered, args){ + // cleanup... + delete this[attr] + + // call the original callback... + options.__proto__.callback + && options.__proto__.callback.call(that, ...args) + + if(options.retrigger + && retriggered > 0 + // this prevents an extra action after "sitting" + // on the keyboard and triggering key repeat... + && retriggered < (options.timeout || 200) / 200){ + var func = this[attr] = this[attr] || debounce(options, action) + func.call(this, ...args) + } + }, }) + + var func = this[attr] = this[attr] || debounce(options, action) + + return func.call(this, ...args) }], }) diff --git a/ui (gen4)/features/examples.js b/ui (gen4)/features/examples.js index 0f373b77..8bacab36 100755 --- a/ui (gen4)/features/examples.js +++ b/ui (gen4)/features/examples.js @@ -60,6 +60,9 @@ var ExampleActions = actions.Actions({ return args })], + testDebounce: ['Test/', + core.debounce(1000, 'exampleAction: ... -- docs...')], + // a normal method... exampleMethod: function(){ console.log('example method:', [].slice.call(arguments)) diff --git a/ui (gen4)/features/keyboard.js b/ui (gen4)/features/keyboard.js index bd900862..e3037ac0 100755 --- a/ui (gen4)/features/keyboard.js +++ b/ui (gen4)/features/keyboard.js @@ -555,9 +555,12 @@ var KeyboardActions = actions.Actions({ }, this) return data.debounce ? // debounce... - this.debounce( - data.debounce, - 'tag:'+data.action, + this.debounce({ + timeout: data.debounce, + tag: 'tag:'+data.action, + retrigger: true, + returns: 'dropped', + }, meth.bind(context), ...data.arguments) // direct call... : meth.call(context, ...data.arguments) },