diff --git a/ui (gen4)/features/core.js b/ui (gen4)/features/core.js index c969f067..35470139 100755 --- a/ui (gen4)/features/core.js +++ b/ui (gen4)/features/core.js @@ -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: '' }) + 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: | , + + // 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... + + 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 = diff --git a/ui (gen4)/features/examples.js b/ui (gen4)/features/examples.js index 8ddf4949..0f373b77 100755 --- a/ui (gen4)/features/examples.js +++ b/ui (gen4)/features/examples.js @@ -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)) diff --git a/ui (gen4)/features/keyboard.js b/ui (gen4)/features/keyboard.js index 1a822448..bd900862 100755 --- a/ui (gen4)/features/keyboard.js +++ b/ui (gen4)/features/keyboard.js @@ -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: