/**
* Copyright Franson Technology AB, Sweden, 2009
* http://gpsgate.com, http://franson.com
*
* author Fredrik Blomqvist
*
* <p>
* Event/Signal handling helpers. <br />
* Built ontop of <a href="http://mochikit.com/doc/html/MochiKit/Signal.html">MochiKit.Signal</a>, thus the usage is similar and all handlers compatible.
* </p>
*
* @module Event
*
* @requires MochiKit.Base, MochiKit.Signal, Franson.DOM
*
* todo: hmm, should this have been named .Signal instead?
*
*/
var Franson = Franson || {};
/**
* namespace
* @class Franson.Event
* @static
*/
Franson.Event = Franson.Event || {};
/**
* Useful when creating wrappers/adapters.<br />
* todo: add support for some kind of parameter control? at least a pass-no-params flag?
* @method forwardEvent
* @param {object} from Source object
* @param {string} srcEventName
* @param {object} to Destination object
* @param {string} [dstEventName=srcEventName]
* @return {EventHandler}
*/
Franson.Event.forwardEvent = function(from, srcEventName, to, dstEventName) // or just 'forward'?
{
dstEventName = dstEventName || srcEventName;
return MochiKit.Signal.connect(from, srcEventName, function(/*arguments*/)
{
var params = MochiKit.Base.extend([to, dstEventName], arguments);
MochiKit.Signal.signal.apply(MochiKit.Signal, params);
});
};
/**
* connect single handler to multiple events.<br />
* Useful?
* @private (for now)
* @method listenToMultiple
* @param {object|DOM|string} src
* @param {string[]} eventNames
* @param {object|function} objOrFunc
* @param {function|string} [funcOrStr]
* @return {EventHandler[]}
*/
Franson.Event.listenToMultiple = function(src, eventNames, objOrFunc, funcOrStr)
{
return MochiKit.Base.map(
function(eventName)
{
return MochiKit.Signal.connect(src, eventName, objOrFunc, funcOrStr);
},
eventNames
);
};
/**
* disconnects automatically when predicate returns true.<br />
* todo: support context obj
* @method listenUntil
* @param {object|DOM|string} source
* @param {string} eventName
* @param {function} predicateHandler callback handler, when returing true disconnects
* @return {Eventhandler} only needed in case termination before the event is necessary, typically ignored (the purpose of this function..).
*/
Franson.Event.listenUntil = function(source, eventName, predicateHandler)
{
var eh = MochiKit.Signal.connect(source, eventName, function(/*arguments*/)
{
var ret = predicateHandler.apply(this, arguments); // todo: should we guard this in try-finally to make sure event is disconnected even if exception is thrown?
if (ret)
MochiKit.Signal.disconnect(eh);
});
return eh; // just to give opportunity to disconnect before event is fired. typically ignored
};
/**
* execute the handler (incl cleanup) just once.<br />
* todo: should support context obj
* @method listenOnce
* @param {object|DOM|string} source
* @param {string} eventName
* @param {function} handler
* @return {Eventhandler} only needed in case termination before the event is necessary, typically ignored.
*/
Franson.Event.listenOnce = function(source, eventName, handler)
{
return Franson.Event.listenUntil(source, eventName, function(/*arguments*/)
{
handler.apply(this, arguments);
return true;
});
};
/**
*
* todo: could create several versions/overloads.<br />
* <code>findEventHandler(source)</code> => all handlers connected.<br />
* <code>findEventHandler(source, eventName)</code> => all handlers listening to eventName.<br />
* <code>findEventHandler(source, handler)</code> => all events the handler is attached via.<br />
* <code>findEventHandler(eventName)</code> => all events hook to eventName regardless of obj.
*
* @method findEventHandler
* @param {object|DOM|string} source
* @param {string} eventName
* @param {object|function} objOrFunc
* @param {function|string} [funcOrStr]
* @return {EventHandler}
*/
Franson.Event.findEventHandler = function(source, eventName, objOrFunc, funcOrStr)
{
var src = MochiKit.DOM.getElement(source);
var observers = MochiKit.Signal._observers;
for (var i = 0; i < observers.length; ++i)
{
var o = observers[i];
if (o.source == src && o.signal == eventName && o.objOrFunc === objOrFunc && o.funcOrStr === funcOrStr)
return o;
}
return null;
};
/**
* extracts all listeners attached to source.
* ok name?
* @method findAllEventHandlers
* @param {object|DOM|string} source
* @return {EventHandler[]}
*/
Franson.Event.findAllEventHandlers = function(source)
{
var src = MochiKit.DOM.getElement(source);
return MochiKit.Base.filter(
function(observer)
{
return observer.source == src;
},
MochiKit.Signal._observers
);
};
/**
* Note: observe that this will <i>not</i> work if called with a closure as parameter (closures are by definition unique on each instantiation)
* @method isConnected
* @param {object|DOM|string} source
* @param {string} eventName
* @param {object|function} objOrFunc
* @param {function|string} [funcOrStr]
* @return {boolean}
*/
Franson.Event.isConnected = function(source, eventName, objOrFunc, funcOrStr)
{
return Franson.Event.findEventHandler(source, eventName, objOrFunc, funcOrStr) !== null;
};
/**
* see <a href="#method_isConnected">isConnected()</a>. same arguments and same condition applies.
* @method connectOnce
* @param {object|DOM|string} source
* @param {string} eventName
* @param {object|function} objOrFunc
* @param {function|string} [funcOrStr]
* @return {EventHandler} null if already connected
*/
Franson.Event.connectOnce = function(source, eventName, objOrFunc, funcOrStr)
{
if (!Franson.Event.isConnected(source, eventName, objOrFunc, funcOrStr))
return MochiKit.Signal.connect(source, eventName, objOrFunc, funcOrStr);
return null;
};
/**
* to be used to hook on events with a unique function (connectOnce compatible) for example.<br />
* todo: could add some more Event methods (and also generalize this)<br />
* function version of <a href="http://mochikit.com/doc/html/MochiKit/Signal.html#fn-preventdefault">Event.preventDefault()</a>
* @method preventDefault
* @param {MochiKit.Signal.Event} e (Not a native event)
*/
Franson.Event.preventDefault = function(e) // = MochiKit.Base.methodcaller('preventDefault');
{
e.preventDefault();
};
/**
* function version of <a href="http://mochikit.com/doc/html/MochiKit/Signal.html#fn-stoppropagation">Event.stopPropagation()</a>
* @method stopPropagation
* @param {MochiKit.Signal.Event} e (Not a native event)
*/
Franson.Event.stopPropagation = function(e)
{
e.stopPropagation();
};
/**
* function version of <a href="http://mochikit.com/doc/html/MochiKit/Signal.html#fn-stop">Event.stop()</a>
* @method stop
* @param {MochiKit.Signal.Event} e (Not a native event)
*/
Franson.Event.stop = function(e)
{
e.stop();
};
/**
* removes all signals connected to the elem tree.<br />
* todo: optionally control type of signal to remove? (supply purge fn and/or event-name(s))
* @method purgeDomNode
* @param {DOMNode|string} elem
*/
Franson.Event.purgeDomNode = function(elem)
{
MochiKit.Iter.forEach(
Franson.Iter.treePreOrderIter( // could use any traversal order (todo: use the new Franson.DOM.preOrderIter etc wrappers)
MochiKit.DOM.getElement(elem),
MochiKit.Base.itemgetter('childNodes')
),
MochiKit.Signal.disconnectAll
);
};
/**
* @private (for now)
* @method listenOnMany
* @param {Array[object|DOM|string]} sources
* @param {string} eventName
* @param {object|function} objOrFunc
* @param {function|string} [funcOrStr]
* @return {EventHandler[]}
*/
Franson.Event.listenOnMany = function(sources, eventName, objOrFunc, funcOrStr)
{
return MochiKit.Base.map(
function(src)
{
return MochiKit.Signal.connect(src, eventName, objOrFunc, funcOrStr);
},
sources
);
};
/**
* test
* todo: same for checkbox buttons
* todo: currently hardwired to use onclick. expose 'onfocus' etc?
* todo: hmm, would be convenient if MochiKit.Signal.disconnect would recognize array-like params and then disconnect each?
* @private (for now)
* @method listenToRadioGroup
* @param {string} groupName
* @param {function(Event)} handler
* @return {EventHandler[]}
*/
Franson.Event.listenToRadioGroup = function(groupName, objOrFunc, funcOrStr)
{
return Franson.Event.listenOnMany(
MochiKit.Base.filter(
function(button) { return button.type == 'radio'; },
document.getElementsByName(groupName)
),
'onclick',
objOrFunc,
funcOrStr
);
};
/**
* Meta-event that triggers when a user clicks <i>outside</i> an object (used in the popup-controls for example)<br />
* note: doesn't take overlapping elems into account, only geometry (and visibility)<br />
* todo: hmm, could be implemented as enableOnOutSideClickEvent(elem) also that fires a MochiKit.Signal.signal(source, 'onoutsideclick', e).
* and make listeners use connect(source, 'onoutsideclick', handler) hookup?
* @method onOutsideClick
* @param {DOM|string} source - todo: support multiple elements?
* @param {function} handler
* @return {EventHandler}
*/
Franson.Event.onOutsideClick = function(source, handler)
{
// todo: perhaps this should be put in a proper bounds-class
function isInside(bounds, pos)
{
return (bounds.x <= pos.x && pos.x <= (bounds.x + bounds.w)) &&
(bounds.y <= pos.y && pos.y <= (bounds.y + bounds.h));
}
return MochiKit.Signal.connect(document, 'onclick', MochiKit.DOM.getElement(source), function(e)
{
if (this.style.display == 'none' || this.style.visibility == 'hidden') // ok? (or use .offsetWidth <= 0?)
{
return;
}
// todo: hmm, or better to just traverse the e.target's parentNode chain and look for source?
var pos = e.mouse().page;
var bounds = Franson.DOM.getElementBounds(this);
if (!isInside(bounds, pos))
{
handler.call(this, e);
}
});
};
/**
* for debugging purposes, to be able to detect handler leaks.
* note that it won't return 0 even at the very first call since
* MochiKit needs a couple of events for internal use (page-unload etc)
* @method getTotalNumberOfListeners
* @return {integer}
*/
Franson.Event.getTotalNumberOfListeners = function()
{
return MochiKit.Signal._observers.length; // undocumented
};
/**
* note: this doesn't (can't) set the .src() property of e.
* @method normalize
* @param {event} e browser-native event (if passed a Mochi.Event it is returned as-is)
* @return {MochiKit.Signal.Event}
*/
Franson.Event.normalize = function(e)
{
return (e instanceof MochiKit.Signal.Event) ? e : new MochiKit.Signal.Event(null, e);
};
/**
* makes sure this connection to source is the only one listening to eventName
* useful? (name?)
* see connectOnce
* @method singleConnect
* @private (for now)
* @return {EventHandler}
*/
Franson.Event.singleConnect = function(source, eventName, objOrFunc, funcOrStr)
{
MochiKit.Signal.disconnectAll(source, eventName);
return MochiKit.Signal.connect(source, eventName, objOrFunc, funcOrStr);
};
/**
* convenience function that accepts array as input also (and multiple arguments).<br />
* see <a href="http://mochikit.com/doc/html/MochiKit/Signal.html#fn-disconnect">MochiKit.Signal.disconnect()</a>
* todo: accept (silently) null args also?
* @private (for now)
* @method disconnect
* @param {EventHandler|EventHandler[]|..} handlers
*/
Franson.Event.disconnect = function(handlers)
{
MochiKit.Iter.forEach(arguments, function(handler)
{
if (MochiKit.Base.isArrayLike(handler))
MochiKit.Iter.forEach(handler, MochiKit.Signal.disconnect);
else
MochiKit.Signal.disconnect(handler);
});
};
/**
* hidden dummy object to create a broadcast system
* see publish, subscribe
* @private
*/
Franson.Event._topics = {};
/**
* broadcasts a signal to all listeners to the topic attached via <a href="#method_subscribe">subscribe()</a> <br />
* ref <a href="http://docs.dojocampus.org/dojo/publish">dojo.publish()</a> (difference is that this allows multiple arguments)
* @method publish
* @param {string} topic
* @param {[object, ...]} [args] optional multiple arguments passed to the listeners
*/
Franson.Event.publish = function(topic, args)
{
MochiKit.Signal.signal.apply(MochiKit.Signal/*or null?*/, MochiKit.Base.extend([Franson.Event._topics, topic], arguments, 1));
};
/**
* same syntax as <a href="http://mochikit.com/doc/html/MochiKit/Signal.html#fn-connect">MochiKit.Signal.connect()</a> but with source (first param) already bound.<br />
* see also <a href="#method_publish">publish()</a><br />
* ref <a href="http://docs.dojocampus.org/dojo/subscribe">dojo.subscribe()</a>
* @method subscribe
* @param {string} topic
* @param {object|function} objOrFunc
* @param {function|string} [funcOrStr]
* @return {EventHandler}
*/
Franson.Event.subscribe = function(topic, objOrFunc, funcOrStr)
{
return MochiKit.Signal.connect(Franson.Event._topics, topic, objOrFunc, funcOrStr);
};
/**
* disconnects a signal attached with <a href="#method_subscribe">subscribe()</a> <br />
* same as disconnect (alias), mostly for symmetry (same as dojo) (or skip?)
* ref <a href="http://docs.dojocampus.org/dojo/unsubscribe">dojo.unsubscribe()</a>
* @method unsubscribe
* @param {EventHandler} handle
*/
Franson.Event.unsubscribe = function(handle)
{
MochiKit.Signal.disconnect(handle);
};