Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

731 lines
30 KiB

/**
* @class Ext.Function
*
* A collection of useful static methods to deal with function callbacks.
* @singleton
*/
Ext.Function = (function() {
// @define Ext.lang.Function
// @define Ext.Function
// @require Ext
// @require Ext.lang.Array
var lastTime = 0,
animFrameId,
animFrameHandlers = [],
animFrameNoArgs = [],
idSource = 0,
animFrameMap = {},
win = window,
requestAnimFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame ||
win.mozRequestAnimationFrame || win.oRequestAnimationFrame ||
function(callback) {
var currTime = Ext.now(),
timeToCall = Math.max(0, 16 - (currTime - lastTime)),
id = win.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
},
fireHandlers = function() {
var len = animFrameHandlers.length,
id, i, handler;
animFrameId = null;
// Fire all animation frame handlers in one go
for (i = 0; i < len; i++) {
handler = animFrameHandlers[i];
id = handler[3];
// Check if this timer has been canceled; its map entry is going to be removed
if (animFrameMap[id]) {
handler[0].apply(handler[1] || Ext.global, handler[2] || animFrameNoArgs);
delete animFrameMap[id];
}
}
// Clear all fired animation frame handlers, don't forget that new handlers
// could have been created in user handler functions called in the loop above
animFrameHandlers = animFrameHandlers.slice(len);
},
fireElevatedHandlers = function() {
Ext.elevateFunction(fireHandlers);
},
ExtFunction = {
/**
* A very commonly used method throughout the framework. It acts as a wrapper around another method
* which originally accepts 2 arguments for `name` and `value`.
* The wrapped function then allows "flexible" value setting of either:
*
* - `name` and `value` as 2 arguments
* - one single object argument with multiple key - value pairs
*
* For example:
*
* var setValue = Ext.Function.flexSetter(function(name, value) {
* this[name] = value;
* });
*
* // Afterwards
* // Setting a single name - value
* setValue('name1', 'value1');
*
* // Settings multiple name - value pairs
* setValue({
* name1: 'value1',
* name2: 'value2',
* name3: 'value3'
* });
*
* @param {Function} setter The single value setter method.
* @param {String} setter.name The name of the value being set.
* @param {Object} setter.value The value being set.
* @return {Function}
*/
flexSetter: function(setter) {
return function(name, value) {
var k, i;
if (name !== null) {
if (typeof name !== 'string') {
for (k in name) {
if (name.hasOwnProperty(k)) {
setter.call(this, k, name[k]);
}
}
if (Ext.enumerables) {
for (i = Ext.enumerables.length; i--;) {
k = Ext.enumerables[i];
if (name.hasOwnProperty(k)) {
setter.call(this, k, name[k]);
}
}
}
} else {
setter.call(this, name, value);
}
}
return this;
};
},
/**
* Create a new function from the provided `fn`, change `this` to the provided scope,
* optionally overrides arguments for the call. Defaults to the arguments passed by
* the caller.
*
* {@link Ext#bind Ext.bind} is alias for {@link Ext.Function#bind Ext.Function.bind}
*
* **NOTE:** This method is deprecated. Use the standard `bind` method of JavaScript
* `Function` instead:
*
* function foo () {
* ...
* }
*
* var fn = foo.bind(this);
*
* This method is unavailable natively on IE8 and IE/Quirks but Ext JS provides a
* "polyfill" to emulate the important features of the standard `bind` method. In
* particular, the polyfill only provides binding of "this" and optional arguments.
*
* @param {Function} fn The function to delegate.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
* **If omitted, defaults to the default global environment object (usually the browser window).**
* @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller)
* @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding,
* if a number the args are inserted at the specified position.
* @return {Function} The new function.
*/
bind: function(fn, scope, args, appendArgs) {
if (arguments.length === 2) {
return function() {
return fn.apply(scope, arguments);
};
}
var method = fn,
slice = Array.prototype.slice;
return function() {
var callArgs = args || arguments;
if (appendArgs === true) {
callArgs = slice.call(arguments, 0);
callArgs = callArgs.concat(args);
}
else if (typeof appendArgs == 'number') {
callArgs = slice.call(arguments, 0); // copy arguments first
Ext.Array.insert(callArgs, appendArgs, args);
}
return method.apply(scope || Ext.global, callArgs);
};
},
/**
* Captures the given parameters for a later call to `Ext.callback`. This binding is
* most useful for resolving scopes for example to an `Ext.app.ViewController`.
*
* The arguments match that of `Ext.callback` except for the `args` which, if provided
* to this method, are prepended to any arguments supplied by the eventual caller of
* the returned function.
*
* @return {Function} A function that, when called, uses `Ext.callback` to call the
* captured `callback`.
* @since 5.0.0
*/
bindCallback: function (callback, scope, args, delay, caller) {
return function () {
var a = Ext.Array.slice(arguments);
return Ext.callback(callback, scope, args ? args.concat(a) : a, delay, caller);
};
},
/**
* Create a new function from the provided `fn`, the arguments of which are pre-set to `args`.
* New arguments passed to the newly created callback when it's invoked are appended after the pre-set ones.
* This is especially useful when creating callbacks.
*
* For example:
*
* var originalFunction = function(){
* alert(Ext.Array.from(arguments).join(' '));
* };
*
* var callback = Ext.Function.pass(originalFunction, ['Hello', 'World']);
*
* callback(); // alerts 'Hello World'
* callback('by Me'); // alerts 'Hello World by Me'
*
* {@link Ext#pass Ext.pass} is alias for {@link Ext.Function#pass Ext.Function.pass}
*
* @param {Function} fn The original function.
* @param {Array} args The arguments to pass to new callback.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
* @return {Function} The new callback function.
*/
pass: function(fn, args, scope) {
if (!Ext.isArray(args)) {
if (Ext.isIterable(args)) {
args = Ext.Array.clone(args);
} else {
args = args !== undefined ? [args] : [];
}
}
return function() {
var fnArgs = args.slice();
fnArgs.push.apply(fnArgs, arguments);
return fn.apply(scope || this, fnArgs);
};
},
/**
* Create an alias to the provided method property with name `methodName` of `object`.
* Note that the execution scope will still be bound to the provided `object` itself.
*
* @param {Object/Function} object
* @param {String} methodName
* @return {Function} aliasFn
*/
alias: function(object, methodName) {
return function() {
return object[methodName].apply(object, arguments);
};
},
/**
* Create a "clone" of the provided method. The returned method will call the given
* method passing along all arguments and the "this" pointer and return its result.
*
* @param {Function} method
* @return {Function} cloneFn
*/
clone: function(method) {
return function() {
return method.apply(this, arguments);
};
},
/**
* Creates an interceptor function. The passed function is called before the original one. If it returns false,
* the original one is not called. The resulting function returns the results of the original function.
* The passed function is called with the parameters of the original function. Example usage:
*
* var sayHi = function(name){
* alert('Hi, ' + name);
* };
*
* sayHi('Fred'); // alerts "Hi, Fred"
*
* // create a new function that validates input without
* // directly modifying the original function:
* var sayHiToFriend = Ext.Function.createInterceptor(sayHi, function(name){
* return name === 'Brian';
* });
*
* sayHiToFriend('Fred'); // no alert
* sayHiToFriend('Brian'); // alerts "Hi, Brian"
*
* @param {Function} origFn The original function.
* @param {Function} newFn The function to call before the original.
* @param {Object} [scope] The scope (`this` reference) in which the passed function is executed.
* **If omitted, defaults to the scope in which the original function is called or the browser window.**
* @param {Object} [returnValue=null] The value to return if the passed function return `false`.
* @return {Function} The new function.
*/
createInterceptor: function(origFn, newFn, scope, returnValue) {
if (!Ext.isFunction(newFn)) {
return origFn;
} else {
returnValue = Ext.isDefined(returnValue) ? returnValue : null;
return function() {
var me = this,
args = arguments;
newFn.target = me;
newFn.method = origFn;
return (newFn.apply(scope || me || Ext.global, args) !== false) ?
origFn.apply(me || Ext.global, args) : returnValue;
};
}
},
/**
* Creates a delegate (callback) which, when called, executes after a specific delay.
*
* @param {Function} fn The function which will be called on a delay when the returned function is called.
* Optionally, a replacement (or additional) argument list may be specified.
* @param {Number} delay The number of milliseconds to defer execution by whenever called.
* @param {Object} scope (optional) The scope (`this` reference) used by the function at execution time.
* @param {Array} args (optional) Override arguments for the call. (Defaults to the arguments passed by the caller)
* @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding,
* if a number the args are inserted at the specified position.
* @return {Function} A function which, when called, executes the original function after the specified delay.
*/
createDelayed: function(fn, delay, scope, args, appendArgs) {
if (scope || args) {
fn = Ext.Function.bind(fn, scope, args, appendArgs);
}
return function() {
var me = this,
args = Array.prototype.slice.call(arguments);
setTimeout(function() {
if (Ext.elevateFunction) {
Ext.elevateFunction(fn, me, args);
} else {
fn.apply(me, args);
}
}, delay);
};
},
/**
* Calls this function after the number of milliseconds specified, optionally in a specific scope. Example usage:
*
* var sayHi = function(name){
* alert('Hi, ' + name);
* }
*
* // executes immediately:
* sayHi('Fred');
*
* // executes after 2 seconds:
* Ext.Function.defer(sayHi, 2000, this, ['Fred']);
*
* // this syntax is sometimes useful for deferring
* // execution of an anonymous function:
* Ext.Function.defer(function(){
* alert('Anonymous');
* }, 100);
*
* {@link Ext#defer Ext.defer} is alias for {@link Ext.Function#defer Ext.Function.defer}
*
* @param {Function} fn The function to defer.
* @param {Number} millis The number of milliseconds for the `setTimeout` call
* (if less than or equal to 0 the function is executed immediately).
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
* **If omitted, defaults to the browser window.**
* @param {Array} [args] Overrides arguments for the call. Defaults to the arguments passed by the caller.
* @param {Boolean/Number} [appendArgs=false] If `true` args are appended to call args instead of overriding,
* or, if a number, then the args are inserted at the specified position.
* @return {Number} The timeout id that can be used with `clearTimeout`.
*/
defer: function(fn, millis, scope, args, appendArgs) {
fn = Ext.Function.bind(fn, scope, args, appendArgs);
if (millis > 0) {
return setTimeout(function () {
if (Ext.elevateFunction) {
Ext.elevateFunction(fn);
} else {
fn();
}
}, millis);
}
fn();
return 0;
},
/**
* Calls this function repeatedly at a given interval, optionally in a specific scope.
*
* {@link Ext#defer Ext.defer} is alias for {@link Ext.Function#defer Ext.Function.defer}
*
* @param {Function} fn The function to defer.
* @param {Number} millis The number of milliseconds for the `setInterval` call
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
* **If omitted, defaults to the browser window.**
* @param {Array} [args] Overrides arguments for the call. Defaults to the arguments passed by the caller.
* @param {Boolean/Number} [appendArgs=false] If `true` args are appended to call args instead of overriding,
* or, if a number, then the args are inserted at the specified position.
* @return {Number} The interval id that can be used with `clearInterval`.
*/
interval: function(fn, millis, scope, args, appendArgs) {
fn = Ext.Function.bind(fn, scope, args, appendArgs);
return setInterval(function () {
if (Ext.elevateFunction) {
Ext.elevateFunction(fn);
} else {
fn();
}
}, millis);
},
/**
* Create a combined function call sequence of the original function + the passed function.
* The resulting function returns the results of the original function.
* The passed function is called with the parameters of the original function. Example usage:
*
* var sayHi = function(name){
* alert('Hi, ' + name);
* };
*
* sayHi('Fred'); // alerts "Hi, Fred"
*
* var sayGoodbye = Ext.Function.createSequence(sayHi, function(name){
* alert('Bye, ' + name);
* });
*
* sayGoodbye('Fred'); // both alerts show
*
* @param {Function} originalFn The original function.
* @param {Function} newFn The function to sequence.
* @param {Object} [scope] The scope (`this` reference) in which the passed function is executed.
* If omitted, defaults to the scope in which the original function is called or the
* default global environment object (usually the browser window).
* @return {Function} The new function.
*/
createSequence: function(originalFn, newFn, scope) {
if (!newFn) {
return originalFn;
}
else {
return function() {
var result = originalFn.apply(this, arguments);
newFn.apply(scope || this, arguments);
return result;
};
}
},
/**
* Creates a delegate function, optionally with a bound scope which, when called, buffers
* the execution of the passed function for the configured number of milliseconds.
* If called again within that period, the impending invocation will be canceled, and the
* timeout period will begin again.
*
* @param {Function} fn The function to invoke on a buffered timer.
* @param {Number} buffer The number of milliseconds by which to buffer the invocation of the
* function.
* @param {Object} [scope] The scope (`this` reference) in which.
* the passed function is executed. If omitted, defaults to the scope specified by the caller.
* @param {Array} [args] Override arguments for the call. Defaults to the arguments
* passed by the caller.
* @return {Function} A function which invokes the passed function after buffering for the specified time.
*/
createBuffered: function(fn, buffer, scope, args) {
var timerId;
return function() {
var callArgs = args || Array.prototype.slice.call(arguments, 0),
me = scope || this;
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(function(){
if (Ext.elevateFunction) {
Ext.elevateFunction(fn, me, callArgs);
} else {
fn.apply(me, callArgs);
}
}, buffer);
};
},
/**
* Creates a wrapped function that, when invoked, defers execution until the next
* animation frame
* @private
* @param {Function} fn The function to call.
* @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to the window object.
* @param {Array} [args] The argument list to pass to the function.
* @param {Number} [queueStrategy=3] A bit flag that indicates how multiple calls to
* the returned function within the same animation frame should be handled.
*
* - 1: All calls will be queued - FIFO order
* - 2: Only the first call will be queued
* - 3: The last call will replace all previous calls
*
* @return {Function}
*/
createAnimationFrame: function(fn, scope, args, queueStrategy) {
var timerId;
queueStrategy = queueStrategy || 3;
return function() {
var callArgs = args || Array.prototype.slice.call(arguments, 0);
scope = scope || this;
if (queueStrategy === 3 && timerId) {
ExtFunction.cancelAnimationFrame(timerId);
}
if ((queueStrategy & 1) || !timerId) {
timerId = ExtFunction.requestAnimationFrame(function() {
timerId = null;
fn.apply(scope, callArgs);
});
}
};
},
/**
* @private
* Schedules the passed function to be called on the next animation frame.
* @param {Function} fn The function to call.
* @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to the window object.
* @param {Mixed[]} [args] The argument list to pass to the function.
*
* @return {Number} Timer id for the new animation frame to use when canceling it.
*/
requestAnimationFrame: function(fn, scope, args) {
var id = ++idSource, // Ids start at 1
handler = Array.prototype.slice.call(arguments, 0);
handler[3] = id;
animFrameMap[id] = 1; // A flag to indicate that the timer exists
// We might be in fireHandlers at this moment but this new entry will not
// be executed until the next frame
animFrameHandlers.push(handler);
if (!animFrameId) {
animFrameId = requestAnimFrame(Ext.elevateFunction ? fireElevatedHandlers : fireHandlers);
}
return id;
},
cancelAnimationFrame: function(id) {
// Don't remove any handlers from animFrameHandlers array, because
// the might be in use at the moment (when cancelAnimationFrame is called).
// Just remove the handler id from the map so it will not be executed
delete animFrameMap[id];
},
/**
* Creates a throttled version of the passed function which, when called repeatedly and
* rapidly, invokes the passed function only after a certain interval has elapsed since the
* previous invocation.
*
* This is useful for wrapping functions which may be called repeatedly, such as
* a handler of a mouse move event when the processing is expensive.
*
* @param {Function} fn The function to execute at a regular time interval.
* @param {Number} interval The interval in milliseconds on which the passed function is executed.
* @param {Object} [scope] The scope (`this` reference) in which
* the passed function is executed. If omitted, defaults to the scope specified by the caller.
* @return {Function} A function which invokes the passed function at the specified interval.
*/
createThrottled: function(fn, interval, scope) {
var lastCallTime = 0,
elapsed,
lastArgs,
timer,
execute = function() {
if (Ext.elevateFunction) {
Ext.elevateFunction(fn, scope, lastArgs);
} else {
fn.apply(scope, lastArgs);
}
lastCallTime = Ext.now();
timer = null;
};
return function() {
// Use scope of last call unless the creator specified a scope
if (!scope) {
scope = this;
}
elapsed = Ext.now() - lastCallTime;
lastArgs = arguments;
// If this is the first invocation, or the throttle interval has been reached, clear any
// pending invocation, and call the target function now.
if (elapsed >= interval) {
clearTimeout(timer);
execute();
}
// Throttle interval has not yet been reached. Only set the timer to fire if not already set.
else if (!timer) {
timer = Ext.defer(execute, interval - elapsed);
}
};
},
/**
* Wraps the passed function in a barrier function which will call the passed function after the passed number of invocations.
* @param {Number} count The number of invocations which will result in the calling of the passed function.
* @param {Function} fn The function to call after the required number of invocations.
* @param {Object} scope The scope (`this` reference) in which the function will be called.
*/
createBarrier: function(count, fn, scope) {
return function() {
if (!--count) {
fn.apply(scope, arguments);
}
};
},
/**
* Adds behavior to an existing method that is executed before the
* original behavior of the function. For example:
*
* var soup = {
* contents: [],
* add: function(ingredient) {
* this.contents.push(ingredient);
* }
* };
* Ext.Function.interceptBefore(soup, "add", function(ingredient){
* if (!this.contents.length && ingredient !== "water") {
* // Always add water to start with
* this.contents.push("water");
* }
* });
* soup.add("onions");
* soup.add("salt");
* soup.contents; // will contain: water, onions, salt
*
* @param {Object} object The target object
* @param {String} methodName Name of the method to override
* @param {Function} fn Function with the new behavior. It will
* be called with the same arguments as the original method. The
* return value of this function will be the return value of the
* new method.
* @param {Object} [scope] The scope to execute the interceptor function. Defaults to the object.
* @return {Function} The new function just created.
*/
interceptBefore: function(object, methodName, fn, scope) {
var method = object[methodName] || Ext.emptyFn;
return (object[methodName] = function() {
var ret = fn.apply(scope || this, arguments);
method.apply(this, arguments);
return ret;
});
},
/**
* Adds behavior to an existing method that is executed after the
* original behavior of the function. For example:
*
* var soup = {
* contents: [],
* add: function(ingredient) {
* this.contents.push(ingredient);
* }
* };
* Ext.Function.interceptAfter(soup, "add", function(ingredient){
* // Always add a bit of extra salt
* this.contents.push("salt");
* });
* soup.add("water");
* soup.add("onions");
* soup.contents; // will contain: water, salt, onions, salt
*
* @param {Object} object The target object
* @param {String} methodName Name of the method to override
* @param {Function} fn Function with the new behavior. It will
* be called with the same arguments as the original method. The
* return value of this function will be the return value of the
* new method.
* @param {Object} [scope] The scope to execute the interceptor function. Defaults to the object.
* @return {Function} The new function just created.
*/
interceptAfter: function(object, methodName, fn, scope) {
var method = object[methodName] || Ext.emptyFn;
return (object[methodName] = function() {
method.apply(this, arguments);
return fn.apply(scope || this, arguments);
});
},
makeCallback: function (callback, scope) {
//<debug>
if (!scope[callback]) {
if (scope.$className) {
Ext.Error.raise('No method "' + callback + '" on ' + scope.$className);
}
Ext.Error.raise('No method "' + callback + '"');
}
//</debug>
return function () {
return scope[callback].apply(scope, arguments);
};
}
};
/**
* @method
* @member Ext
* @inheritdoc Ext.Function#defer
*/
Ext.defer = ExtFunction.defer;
/**
* @method
* @member Ext
* @inheritdoc Ext.Function#interval
*/
Ext.interval = ExtFunction.interval;
/**
* @method
* @member Ext
* @inheritdoc Ext.Function#pass
*/
Ext.pass = ExtFunction.pass;
/**
* @method
* @member Ext
* @inheritdoc Ext.Function#bind
*/
Ext.bind = ExtFunction.bind;
Ext.deferCallback = ExtFunction.requestAnimationFrame;
return ExtFunction;
})();