123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- /*
- * JavaScript Hooker
- * http://github.com/cowboy/javascript-hooker
- *
- * Copyright (c) 2012 "Cowboy" Ben Alman
- * Licensed under the MIT license.
- * http://benalman.com/about/license/
- */
- (function(exports) {
- // Get an array from an array-like object with slice.call(arrayLikeObject).
- var slice = [].slice;
- // Get an "[object [[Class]]]" string with toString.call(value).
- var toString = {}.toString;
- // I can't think of a better way to ensure a value is a specific type other
- // than to create instances and use the `instanceof` operator.
- function HookerOverride(v) { this.value = v; }
- function HookerPreempt(v) { this.value = v; }
- function HookerFilter(c, a) { this.context = c; this.args = a; }
- // When a pre- or post-hook returns the result of this function, the value
- // passed will be used in place of the original function's return value. Any
- // post-hook override value will take precedence over a pre-hook override
- // value.
- exports.override = function(value) {
- return new HookerOverride(value);
- };
- // When a pre-hook returns the result of this function, the value passed will
- // be used in place of the original function's return value, and the original
- // function will NOT be executed.
- exports.preempt = function(value) {
- return new HookerPreempt(value);
- };
- // When a pre-hook returns the result of this function, the context and
- // arguments passed will be applied into the original function.
- exports.filter = function(context, args) {
- return new HookerFilter(context, args);
- };
- // Execute callback(s) for properties of the specified object.
- function forMethods(obj, props, callback) {
- var prop;
- if (typeof props === "string") {
- // A single prop string was passed. Create an array.
- props = [props];
- } else if (props == null) {
- // No props were passed, so iterate over all properties, building an
- // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so
- // this has to be done manually.
- props = [];
- for (prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- props.push(prop);
- }
- }
- }
- // Execute callback for every method in the props array.
- var i = props.length;
- while (i--) {
- // If the property isn't a function...
- if (toString.call(obj[props[i]]) !== "[object Function]" ||
- // ...or the callback returns false...
- callback(obj, props[i]) === false) {
- // ...remove it from the props array to be returned.
- props.splice(i, 1);
- }
- }
- // Return an array of method names for which the callback didn't fail.
- return props;
- }
- // Monkey-patch (hook) a method of an object.
- exports.hook = function(obj, props, options) {
- // If the props argument was omitted, shuffle the arguments.
- if (options == null) {
- options = props;
- props = null;
- }
- // If just a function is passed instead of an options hash, use that as a
- // pre-hook function.
- if (typeof options === "function") {
- options = {pre: options};
- }
- // Hook the specified method of the object.
- return forMethods(obj, props, function(obj, prop) {
- // The original (current) method.
- var orig = obj[prop];
- // The new hooked function.
- function hooked() {
- var result, origResult, tmp;
- // Get an array of arguments.
- var args = slice.call(arguments);
- // If passName option is specified, prepend prop to the args array,
- // passing it as the first argument to any specified hook functions.
- if (options.passName) {
- args.unshift(prop);
- }
- // If a pre-hook function was specified, invoke it in the current
- // context with the passed-in arguments, and store its result.
- if (options.pre) {
- result = options.pre.apply(this, args);
- }
- if (result instanceof HookerFilter) {
- // If the pre-hook returned hooker.filter(context, args), invoke the
- // original function with that context and arguments, and store its
- // result.
- origResult = result = orig.apply(result.context, result.args);
- } else if (result instanceof HookerPreempt) {
- // If the pre-hook returned hooker.preempt(value) just use the passed
- // value and don't execute the original function.
- origResult = result = result.value;
- } else {
- // Invoke the original function in the current context with the
- // passed-in arguments, and store its result.
- origResult = orig.apply(this, arguments);
- // If the pre-hook returned hooker.override(value), use the passed
- // value, otherwise use the original function's result.
- result = result instanceof HookerOverride ? result.value : origResult;
- }
- if (options.post) {
- // If a post-hook function was specified, invoke it in the current
- // context, passing in the result of the original function as the
- // first argument, followed by any passed-in arguments.
- tmp = options.post.apply(this, [origResult].concat(args));
- if (tmp instanceof HookerOverride) {
- // If the post-hook returned hooker.override(value), use the passed
- // value, otherwise use the previously computed result.
- result = tmp.value;
- }
- }
- // Unhook if the "once" option was specified.
- if (options.once) {
- exports.unhook(obj, prop);
- }
- // Return the result!
- return result;
- }
- // Re-define the method.
- obj[prop] = hooked;
- // Fail if the function couldn't be hooked.
- if (obj[prop] !== hooked) { return false; }
- // Store a reference to the original method as a property on the new one.
- obj[prop]._orig = orig;
- });
- };
- // Get a reference to the original method from a hooked function.
- exports.orig = function(obj, prop) {
- return obj[prop]._orig;
- };
- // Un-monkey-patch (unhook) a method of an object.
- exports.unhook = function(obj, props) {
- return forMethods(obj, props, function(obj, prop) {
- // Get a reference to the original method, if it exists.
- var orig = exports.orig(obj, prop);
- // If there's no original method, it can't be unhooked, so fail.
- if (!orig) { return false; }
- // Unhook the method.
- obj[prop] = orig;
- });
- };
- }(typeof exports === "object" && exports || this));
|