/* Copyright (c) 2004-2006, The Dojo Foundation All Rights Reserved. Licensed under the Academic Free License version 2.1 or above OR the modified BSD license. For more information on Dojo licensing, see: http://dojotoolkit.org/community/licensing.shtml */ dojo.provide("dojo.event.common"); dojo.require("dojo.lang.array"); dojo.require("dojo.lang.extras"); dojo.require("dojo.lang.func"); // TODO: connection filter functions // these are functions that accept a method invocation (like around // advice) and return a boolean based on it. That value determines // whether or not the connection proceeds. It could "feel" like around // advice for those who know what it is (calling proceed() or not), // but I think presenting it as a "filter" and/or calling it with the // function args and not the MethodInvocation might make it more // palletable to "normal" users than around-advice currently is // TODO: execution scope mangling // YUI's event facility by default executes listeners in the context // of the source object. This is very odd, but should probably be // supported as an option (both for the source and for the dest). It // can be thought of as a connection-specific hitch(). // TODO: more resiliency for 4+ arguments to connect() dojo.event = new function(){ this._canTimeout = dojo.lang.isFunction(dj_global["setTimeout"])||dojo.lang.isAlien(dj_global["setTimeout"]); // FIXME: where should we put this method (not here!)? function interpolateArgs(args, searchForNames){ var dl = dojo.lang; var ao = { srcObj: dj_global, srcFunc: null, adviceObj: dj_global, adviceFunc: null, aroundObj: null, aroundFunc: null, adviceType: (args.length>2) ? args[0] : "after", precedence: "last", once: false, delay: null, rate: 0, adviceMsg: false }; switch(args.length){ case 0: return; case 1: return; case 2: ao.srcFunc = args[0]; ao.adviceFunc = args[1]; break; case 3: if((dl.isObject(args[0]))&&(dl.isString(args[1]))&&(dl.isString(args[2]))){ ao.adviceType = "after"; ao.srcObj = args[0]; ao.srcFunc = args[1]; ao.adviceFunc = args[2]; }else if((dl.isString(args[1]))&&(dl.isString(args[2]))){ ao.srcFunc = args[1]; ao.adviceFunc = args[2]; }else if((dl.isObject(args[0]))&&(dl.isString(args[1]))&&(dl.isFunction(args[2]))){ ao.adviceType = "after"; ao.srcObj = args[0]; ao.srcFunc = args[1]; var tmpName = dl.nameAnonFunc(args[2], ao.adviceObj, searchForNames); ao.adviceFunc = tmpName; }else if((dl.isFunction(args[0]))&&(dl.isObject(args[1]))&&(dl.isString(args[2]))){ ao.adviceType = "after"; ao.srcObj = dj_global; var tmpName = dl.nameAnonFunc(args[0], ao.srcObj, searchForNames); ao.srcFunc = tmpName; ao.adviceObj = args[1]; ao.adviceFunc = args[2]; } break; case 4: if((dl.isObject(args[0]))&&(dl.isObject(args[2]))){ // we can assume that we've got an old-style "connect" from // the sigslot school of event attachment. We therefore // assume after-advice. ao.adviceType = "after"; ao.srcObj = args[0]; ao.srcFunc = args[1]; ao.adviceObj = args[2]; ao.adviceFunc = args[3]; }else if((dl.isString(args[0]))&&(dl.isString(args[1]))&&(dl.isObject(args[2]))){ ao.adviceType = args[0]; ao.srcObj = dj_global; ao.srcFunc = args[1]; ao.adviceObj = args[2]; ao.adviceFunc = args[3]; }else if((dl.isString(args[0]))&&(dl.isFunction(args[1]))&&(dl.isObject(args[2]))){ ao.adviceType = args[0]; ao.srcObj = dj_global; var tmpName = dl.nameAnonFunc(args[1], dj_global, searchForNames); ao.srcFunc = tmpName; ao.adviceObj = args[2]; ao.adviceFunc = args[3]; }else if((dl.isString(args[0]))&&(dl.isObject(args[1]))&&(dl.isString(args[2]))&&(dl.isFunction(args[3]))){ ao.srcObj = args[1]; ao.srcFunc = args[2]; var tmpName = dl.nameAnonFunc(args[3], dj_global, searchForNames); ao.adviceObj = dj_global; ao.adviceFunc = tmpName; }else if(dl.isObject(args[1])){ ao.srcObj = args[1]; ao.srcFunc = args[2]; ao.adviceObj = dj_global; ao.adviceFunc = args[3]; }else if(dl.isObject(args[2])){ ao.srcObj = dj_global; ao.srcFunc = args[1]; ao.adviceObj = args[2]; ao.adviceFunc = args[3]; }else{ ao.srcObj = ao.adviceObj = ao.aroundObj = dj_global; ao.srcFunc = args[1]; ao.adviceFunc = args[2]; ao.aroundFunc = args[3]; } break; case 6: ao.srcObj = args[1]; ao.srcFunc = args[2]; ao.adviceObj = args[3] ao.adviceFunc = args[4]; ao.aroundFunc = args[5]; ao.aroundObj = dj_global; break; default: ao.srcObj = args[1]; ao.srcFunc = args[2]; ao.adviceObj = args[3] ao.adviceFunc = args[4]; ao.aroundObj = args[5]; ao.aroundFunc = args[6]; ao.once = args[7]; ao.delay = args[8]; ao.rate = args[9]; ao.adviceMsg = args[10]; break; } if(dl.isFunction(ao.aroundFunc)){ var tmpName = dl.nameAnonFunc(ao.aroundFunc, ao.aroundObj, searchForNames); ao.aroundFunc = tmpName; } if(dl.isFunction(ao.srcFunc)){ ao.srcFunc = dl.getNameInObj(ao.srcObj, ao.srcFunc); } if(dl.isFunction(ao.adviceFunc)){ ao.adviceFunc = dl.getNameInObj(ao.adviceObj, ao.adviceFunc); } if((ao.aroundObj)&&(dl.isFunction(ao.aroundFunc))){ ao.aroundFunc = dl.getNameInObj(ao.aroundObj, ao.aroundFunc); } if(!ao.srcObj){ dojo.raise("bad srcObj for srcFunc: "+ao.srcFunc); } if(!ao.adviceObj){ dojo.raise("bad adviceObj for adviceFunc: "+ao.adviceFunc); } if(!ao.adviceFunc){ dojo.debug("bad adviceFunc for srcFunc: "+ao.srcFunc); dojo.debugShallow(ao); } return ao; } this.connect = function(/*...*/){ // summary: // dojo.event.connect is the glue that holds most Dojo-based // applications together. Most combinations of arguments are // supported, with the connect() method attempting to disambiguate // the implied types of positional parameters. The following will // all work: // dojo.event.connect("globalFunctionName1", "globalFunctionName2"); // dojo.event.connect(functionReference1, functionReference2); // dojo.event.connect("globalFunctionName1", functionReference2); // dojo.event.connect(functionReference1, "globalFunctionName2"); // dojo.event.connect(scope1, "functionName1", "globalFunctionName2"); // dojo.event.connect("globalFunctionName1", scope2, "functionName2"); // dojo.event.connect(scope1, "functionName1", scope2, "functionName2"); // dojo.event.connect("after", scope1, "functionName1", scope2, "functionName2"); // dojo.event.connect("before", scope1, "functionName1", scope2, "functionName2"); // dojo.event.connect("around", scope1, "functionName1", // scope2, "functionName2", // aroundFunctionReference); // dojo.event.connect("around", scope1, "functionName1", // scope2, "functionName2", // scope3, "aroundFunctionName"); // dojo.event.connect("before-around", scope1, "functionName1", // scope2, "functionName2", // aroundFunctionReference); // dojo.event.connect("after-around", scope1, "functionName1", // scope2, "functionName2", // aroundFunctionReference); // dojo.event.connect("after-around", scope1, "functionName1", // scope2, "functionName2", // scope3, "aroundFunctionName"); // dojo.event.connect("around", scope1, "functionName1", // scope2, "functionName2", // scope3, "aroundFunctionName", true, 30); // dojo.event.connect("around", scope1, "functionName1", // scope2, "functionName2", // scope3, "aroundFunctionName", null, null, 10); // adviceType: // Optional. String. One of "before", "after", "around", // "before-around", or "after-around". FIXME // srcObj: // the scope in which to locate/execute the named srcFunc. Along // with srcFunc, this creates a way to dereference the function to // call. So if the function in question is "foo.bar", the // srcObj/srcFunc pair would be foo and "bar", where "bar" is a // string and foo is an object reference. // srcFunc: // the name of the function to connect to. When it is executed, // the listener being registered with this call will be called. // The adviceType defines the call order between the source and // the target functions. // adviceObj: // the scope in which to locate/execute the named adviceFunc. // adviceFunc: // the name of the function being conected to srcObj.srcFunc // aroundObj: // the scope in which to locate/execute the named aroundFunc. // aroundFunc: // the name of, or a reference to, the function that will be used // to mediate the advice call. Around advice requires a special // unary function that will be passed a "MethodInvocation" object. // These objects have several important properties, namely: // - args // a mutable array of arguments to be passed into the // wrapped function // - proceed // a function that "continues" the invocation. The result // of this function is the return of the wrapped function. // You can then manipulate this return before passing it // back out (or take further action based on it). // once: // boolean that determines whether or not this connect() will // create a new connection if an identical connect() has already // been made. Defaults to "false". // delay: // an optional delay (in ms), as an integer, for dispatch of a // listener after the source has been fired. // rate: // an optional rate throttling parameter (integer, in ms). When // specified, this particular connection will not fire more than // once in the interval specified by the rate // adviceMsg: // boolean. Should the listener have all the parameters passed in // as a single argument? /* ao.adviceType = args[0]; ao.srcObj = args[1]; ao.srcFunc = args[2]; ao.adviceObj = args[3] ao.adviceFunc = args[4]; ao.aroundObj = args[5]; ao.aroundFunc = args[6]; ao.once = args[7]; ao.delay = args[8]; ao.rate = args[9]; ao.adviceMsg = args[10]; */ if(arguments.length == 1){ var ao = arguments[0]; }else{ var ao = interpolateArgs(arguments, true); } if(dojo.lang.isString(ao.srcFunc) && (ao.srcFunc.toLowerCase() == "onkey") ){ if(dojo.render.html.ie){ ao.srcFunc = "onkeydown"; this.connect(ao); } ao.srcFunc = "onkeypress"; } if(dojo.lang.isArray(ao.srcObj) && ao.srcObj!=""){ var tmpAO = {}; for(var x in ao){ tmpAO[x] = ao[x]; } var mjps = []; dojo.lang.forEach(ao.srcObj, function(src){ if((dojo.render.html.capable)&&(dojo.lang.isString(src))){ src = dojo.byId(src); // dojo.debug(src); } tmpAO.srcObj = src; // dojo.debug(tmpAO.srcObj, tmpAO.srcFunc); // dojo.debug(tmpAO.adviceObj, tmpAO.adviceFunc); mjps.push(dojo.event.connect.call(dojo.event, tmpAO)); }); return mjps; } // FIXME: just doing a "getForMethod()" seems to be enough to put this into infinite recursion!! var mjp = dojo.event.MethodJoinPoint.getForMethod(ao.srcObj, ao.srcFunc); if(ao.adviceFunc){ var mjp2 = dojo.event.MethodJoinPoint.getForMethod(ao.adviceObj, ao.adviceFunc); } mjp.kwAddAdvice(ao); // advanced users might want to fsck w/ the join point manually return mjp; // a MethodJoinPoint object } this.log = function(/*object or funcName*/ a1, /*funcName*/ a2){ // summary: // a function that will wrap and log all calls to the specified // a1.a2() function. If only a1 is passed, it'll be used as a // function or function name on the global context. Logging will // be sent to dojo.debug // a1: // if a2 is passed, this should be an object. If not, it can be a // function or function name. // a2: // a function name var kwArgs; if((arguments.length == 1)&&(typeof a1 == "object")){ kwArgs = a1; }else{ kwArgs = { srcObj: a1, srcFunc: a2 }; } kwArgs.adviceFunc = function(){ var argsStr = []; for(var x=0; x= this.jp_.around.length){ return this.jp_.object[this.jp_.methodname].apply(this.jp_.object, this.args); // return this.jp_.run_before_after(this.object, this.args); }else{ var ti = this.jp_.around[this.around_index]; var mobj = ti[0]||dj_global; var meth = ti[1]; return mobj[meth].call(mobj, this); } } dojo.event.MethodJoinPoint = function(/*Object*/obj, /*String*/funcName){ this.object = obj||dj_global; this.methodname = funcName; this.methodfunc = this.object[funcName]; this.squelch = false; // this.before = []; // this.after = []; // this.around = []; } dojo.event.MethodJoinPoint.getForMethod = function(/*Object*/obj, /*String*/funcName){ // summary: // "static" class function for returning a MethodJoinPoint from a // scoped function. If one doesn't exist, one is created. // obj: // the scope to search for the function in // funcName: // the name of the function to return a MethodJoinPoint for if(!obj){ obj = dj_global; } if(!obj[funcName]){ // supply a do-nothing method implementation obj[funcName] = function(){}; if(!obj[funcName]){ // e.g. cannot add to inbuilt objects in IE6 dojo.raise("Cannot set do-nothing method on that object "+funcName); } }else if((!dojo.lang.isFunction(obj[funcName]))&&(!dojo.lang.isAlien(obj[funcName]))){ // FIXME: should we throw an exception here instead? return null; } // we hide our joinpoint instance in obj[funcName + '$joinpoint'] var jpname = funcName + "$joinpoint"; var jpfuncname = funcName + "$joinpoint$method"; var joinpoint = obj[jpname]; if(!joinpoint){ var isNode = false; if(dojo.event["browser"]){ if( (obj["attachEvent"])|| (obj["nodeType"])|| (obj["addEventListener"]) ){ isNode = true; dojo.event.browser.addClobberNodeAttrs(obj, [jpname, jpfuncname, funcName]); } } var origArity = obj[funcName].length; obj[jpfuncname] = obj[funcName]; // joinpoint = obj[jpname] = new dojo.event.MethodJoinPoint(obj, funcName); joinpoint = obj[jpname] = new dojo.event.MethodJoinPoint(obj, jpfuncname); obj[funcName] = function(){ var args = []; if((isNode)&&(!arguments.length)){ var evt = null; try{ if(obj.ownerDocument){ evt = obj.ownerDocument.parentWindow.event; }else if(obj.documentElement){ evt = obj.documentElement.ownerDocument.parentWindow.event; }else if(obj.event){ //obj is a window evt = obj.event; }else{ evt = window.event; } }catch(e){ evt = window.event; } if(evt){ args.push(dojo.event.browser.fixEvent(evt, this)); } }else{ for(var x=0; x0)){ // pass a cloned array, if this event disconnects this event forEach on this.before wont work dojo.lang.forEach(this.before.concat(new Array()), unRollSquelch); } var result; try{ if((this["around"])&&(this.around.length>0)){ var mi = new dojo.event.MethodInvocation(this, obj, args); result = mi.proceed(); }else if(this.methodfunc){ result = this.object[this.methodname].apply(this.object, args); } }catch(e){ if(!this.squelch){ dojo.debug(e,"when calling",this.methodname,"on",this.object,"with arguments",args); dojo.raise(e); } } if((this["after"])&&(this.after.length>0)){ // see comment on this.before above dojo.lang.forEach(this.after.concat(new Array()), unRollSquelch); } return (this.methodfunc) ? result : null; }, getArr: function(/*String*/kind){ // summary: return a list of listeners of the past "kind" // kind: // can be one of: "before", "after", "around", "before-around", or // "after-around" var type = "after"; // FIXME: we should be able to do this through props or Array.in() if((typeof kind == "string")&&(kind.indexOf("before")!=-1)){ type = "before"; }else if(kind=="around"){ type = "around"; } if(!this[type]){ this[type] = []; } return this[type]; // Array }, kwAddAdvice: function(/*Object*/args){ // summary: // adds advice to the joinpoint with arguments in a map // args: // An object that can have the following properties: // - adviceType // - adviceObj // - adviceFunc // - aroundObj // - aroundFunc // - once // - delay // - rate // - adviceMsg this.addAdvice( args["adviceObj"], args["adviceFunc"], args["aroundObj"], args["aroundFunc"], args["adviceType"], args["precedence"], args["once"], args["delay"], args["rate"], args["adviceMsg"]); }, addAdvice: function( thisAdviceObj, thisAdvice, thisAroundObj, thisAround, adviceType, precedence, once, delay, rate, asMessage){ // summary: // add advice to this joinpoint using positional parameters // thisAdviceObj: // the scope in which to locate/execute the named adviceFunc. // thisAdviceFunc: // the name of the function being conected // thisAroundObj: // the scope in which to locate/execute the named aroundFunc. // thisAroundFunc: // the name of the function that will be used to mediate the // advice call. // adviceType: // Optional. String. One of "before", "after", "around", // "before-around", or "after-around". FIXME // once: // boolean that determines whether or not this advice will create // a new connection if an identical advice set has already been // provided. Defaults to "false". // delay: // an optional delay (in ms), as an integer, for dispatch of a // listener after the source has been fired. // rate: // an optional rate throttling parameter (integer, in ms). When // specified, this particular connection will not fire more than // once in the interval specified by the rate // adviceMsg: // boolean. Should the listener have all the parameters passed in // as a single argument? var arr = this.getArr(adviceType); if(!arr){ dojo.raise("bad this: " + this); } var ao = [thisAdviceObj, thisAdvice, thisAroundObj, thisAround, delay, rate, asMessage]; if(once){ if(this.hasAdvice(thisAdviceObj, thisAdvice, adviceType, arr) >= 0){ return; } } if(precedence == "first"){ arr.unshift(ao); }else{ arr.push(ao); } }, hasAdvice: function(thisAdviceObj, thisAdvice, adviceType, arr){ // summary: // returns the array index of the first existing connection // betweened the passed advice and this joinpoint. Will be -1 if // none exists. // thisAdviceObj: // the scope in which to locate/execute the named adviceFunc. // thisAdviceFunc: // the name of the function being conected // adviceType: // Optional. String. One of "before", "after", "around", // "before-around", or "after-around". FIXME // arr: // Optional. The list of advices to search. Will be found via // adviceType if not passed if(!arr){ arr = this.getArr(adviceType); } var ind = -1; for(var x=0; x