/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Utility object for function-related utilities. Note that this is not a Class, and so does
* not need to be instantiated before use.
*
* @module alfresco/util/functionUtils
* @author Martin Doyle
*/
define([
"dojo/_base/array",
"dojo/_base/lang"
],
function(array, lang) {
// Define the repeating periods variables.
var REPEATING_PERIODS = {
"SHORT": {
delay: 100,
funcs: {}
},
"MEDIUM": {
delay: 1000,
funcs: {}
},
"LONG": {
delay: 10000,
funcs: {}
}
};
// The private container for the functionality and properties of the util
var util = {
// See API below
defaultDebounceMs: 250,
// See API below
defaultThrottleMs: 250,
// A holder for the debounce-functionality pointers/variables
//
// EXAMPLE:
// debounceVars: {
// lastExecutions: {
// foo: [{
// filter: "", <-- DEFAULT
// value: [Timeout pointer]
// }, {
// filter: [Object],
// value: [Timeout pointer]
// }]
// },
// timeouts: {
// foo: [{
// filter: "", <-- DEFAULT
// value: [Function]
// }, {
// filter: [Object],
// value: [Function]
// }]
// }
// }
debounceVars: {
lastExecutions: {},
timeouts: {}
},
// A holder for the debounce-functionality pointers/variables (format as debounceVars)
throttleVars: {
lastExecutions: {},
timeouts: {}
},
// See API below
addRepeatingFunction: function alfresco_util_functionUtils__addRepeatingFunction(newFunc, periodType) {
// Put the new function into the appropriate collection
var period = REPEATING_PERIODS[periodType],
currentFuncs = period.funcs,
newFuncKey = Date.now();
if (currentFuncs) {
while (currentFuncs.hasOwnProperty(newFuncKey)) {
newFuncKey = Date.now();
}
currentFuncs[newFuncKey] = newFunc;
}
// Make sure the period's functions are running
if (!period.running) {
this._startRepeatingTimeout(period);
}
// Pass back the removal object
return {
remove: function() {
delete currentFuncs[newFuncKey];
}
};
},
// See API below
debounce: function alfresco_util_functionUtils__debounce(args) {
return this._limit("debounce", args);
},
// See API below
throttle: function alfresco_util_functionUtils__throttle(args) {
return this._limit("throttle", args);
},
// Helper to get the value for a name and filter
_getFilteredValue: function alfresco_util_functionUtils___getFilteredValue(collection, name, filter, defaultValue) {
var items = collection[name] || [],
valueFound = false,
value;
array.some(items, function(item) {
if (item.filter === filter) {
value = item.value;
valueFound = true;
}
return valueFound;
});
return valueFound ? value : defaultValue;
},
// Helper to set the value for a name and filter
_setFilteredValue: function alfresco_util_functionUtils___setFilteredValue(collection, name, filter, value) {
var items = collection[name] || [],
updatedValue = array.some(items, function(item) {
if (item.filter === filter) {
item.value = value;
return true;
}
return false;
});
if (!updatedValue) {
items.push({
filter: filter,
value: value
});
collection[name] = items;
}
},
// Implements the functionality of the debounce and throttle methods
_limit: function alfresco_util_functionUtils___limit(type, args) {
// Setup variables
var pointers = this[type + "Vars"],
lastExecutions = pointers.lastExecutions,
timeouts = pointers.timeouts,
name = args.name,
filter = args.filter || "",
execFirst = (args.execFirst === true),
ignoreFirst = (args.ignoreFirst === true),
timeoutMs = args.timeoutMs || (type === "throttle" ? this.defaultThrottleMs : this.defaultDebounceMs);
// Retrieve the specific info using name and filter
var currentTimeout = this._getFilteredValue(timeouts, name, filter),
lastExecutionTime = this._getFilteredValue(lastExecutions, name, filter, 0),
timeSinceLastExec = Date.now() - lastExecutionTime,
lastExecWithinTimePeriod = timeSinceLastExec < timeoutMs;
// Clear any existing timeout
clearTimeout(currentTimeout);
// Execute or defer
if (type === "debounce") {
// Debounce logic
if (execFirst) { // Should we run at start of debounce
!lastExecWithinTimePeriod && args.func(); // If first run then exec now
this._setFilteredValue(lastExecutions, name, filter, Date.now()); // Whether first run or within run-period, update last-exec time
} else {
// Not in run-at-start mode, so setTimeout
this._setFilteredValue(timeouts, name, filter, setTimeout(lang.hitch(this, function() {
args.func(); // Execute the function
this._setFilteredValue(lastExecutions, name, filter, 0); // Reset last-exec time
}), timeoutMs) // Defer by defined timeout period
);
}
} else {
// Throttle logic
if (lastExecWithinTimePeriod) { // Within a throttle "period"?
// Defer execution
this._setFilteredValue(timeouts, name, filter, setTimeout(lang.hitch(this, function() {
args.func(); // Execute the function
this._setFilteredValue(lastExecutions, name, filter, Date.now()); // Update the last-run time
}), (timeoutMs - timeSinceLastExec)) // Defer until this period ends
);
} else { // Not been run recently
if (ignoreFirst) { // Should we ignore the first execution call?
// Ignore, so defer execution
this._setFilteredValue(timeouts, name, filter, setTimeout(lang.hitch(this, function() {
args.func(); // Execute the function
this._setFilteredValue(lastExecutions, name, filter, Date.now()); // Update the last-run time
}), timeoutMs) // Defer until period ends
);
} else { // Do not ignore first calling
args.func(); // Execute the function
}
this._setFilteredValue(lastExecutions, name, filter, Date.now()); // Update the last execution (request) time
}
}
// Pass back a remove-object for cancelling the queued function
currentTimeout = this._getFilteredValue(timeouts, name, filter);
return {
remove: function() {
clearTimeout(currentTimeout);
}
};
},
// Start calling a repeating function
_startRepeatingTimeout: function alfresco_util_functionUtils___startRepeatingTimeout(period) {
setTimeout(function alfresco_util_functionUtils___timeoutFunc() {
var funcKeys = Object.keys(period.funcs);
if (funcKeys.length) {
array.forEach(funcKeys, function(funcKey) {
/*jshint devel:true*/
try {
period.funcs[funcKey]();
} catch (e) {
console.error("Removing erroring periodic function: " + period.funcs[funcKey]);
delete period.funcs[funcKey];
}
});
setTimeout(alfresco_util_functionUtils___timeoutFunc, period.delay);
} else {
delete period.running;
}
}, period.delay);
period.running = true;
}
};
/**
* The public API for this utility class
*
* @alias module:alfresco/util/functionUtils
*/
return {
/**
* The default timeout for debounce calls
*
* @instance
* @type {number}
* @default 250
*/
defaultDebounceMs: util.defaultDebounceMs,
/**
* The default timeout for throttle calls
*
* @instance
* @type {number}
* @default 250
*/
defaultThrottleMs: util.defaultThrottleMs,
/**
* Register a new repeating function.
*
* @instance
* @function
* @param {function} func The function to be added
* @param {string} period One of "SHORT" (100ms), "MEDIUM" (1000ms) or "LONG" (10000ms), to determine how often the function is called
* @returns {object} An object with a remove method on it, which will de-register this function
*/
addRepeatingFunction: lang.hitch(util, util.addRepeatingFunction),
/**
* <p>Debounce the supplied function.</p>
*
* <p>This means that repeated calls to execute a function will be batched up,
* with only the last submitted function call being executed, and then only after the
* [debounce period]{@link module:alfresco/util/functionUtils#defaultDebounceMs} has elapsed. A common usage for
* this is for debouncing keypress events (in a text box) that will ultimately trigger an XHR request.</p>
*
* @instance
* @function
* @param {Object} args The arguments for this function
* @param {string} args.name Debounce calls under different names will not be limited by each other,
* so this specifies the name for the group of functions to debounce
* @param {Function} args.func The function to be called after the debounce expires
* @param {bool} [args.execFirst=false] By default, the last-provided function will execute after a period of inactivity.
* Setting this to true will execute the first-provided function immediately and
* then discard any others that occur during the timeout period, with each subsequent
* function received within that period extending it by the debounce timeout.
* @param {int} [args.timeoutMs] The length of the debounce, if different from the
* [default]{@link module:alfresco/util/functionUtils#defaultDebounceMs}
* @param {*} [args.filter] An optional parameter which is used in conjunction with the name to give further
* context to the debounce: i.e. will debounce only when name AND filter match. The
* filter can be any value, and will use a strict-equality check to make the comparison.
* @return {Object} An object containing a remove() function which will clear any outstanding timeout
*/
debounce: lang.hitch(util, util.debounce),
/**
* <p>Throttle the supplied function.</p>
*
* <p>This means that repeated calls to execute a function will be controlled
* such that the first call will execute and the last call before the next
* [throttle period]{@link module:alfresco/util/functionUtils#defaultThrottleMs} will execute, but all others will
* be discarded.</p>
*
* <p>Example: If the throttle period were 250ms and the function was called at 0ms, 60ms, 120ms, 280ms and 400ms
* then the first, third and fifth functions would execute at 0ms, 250ms and 500ms respectively, and the second
* and fourth function calls would be discarded.</p>
*
* @instance
* @function
* @param {Object} args The arguments for this function
* @param {string} args.name Throttle calls under different names will not be limited by each other,
* so this specifies the name for the group of functions to throttle
* @param {Function} args.func The function to be called after the throttle expires
* @param {bool} [args.ignoreFirst=false] If set to true, then do not fire the first-received function call,
* so in the above example, only the third and fifth functions would
* be executed.
* @param {int} [args.timeoutMs] The length of the throttle, if different from the
* [default]{@link module:alfresco/util/functionUtils#defaultThrottleMs}
* @param {*} [args.filter] An optional parameter which is used in conjunction with the name to give further
* context to the throttle: i.e. will throttle only when name AND filter match. The
* filter can be any value, and will use a strict-equality check to make the comparison.
* @return {Object} An object containing a remove() function which will clear any outstanding timeout
*/
throttle: lang.hitch(util, util.throttle)
};
});