Here's the latest. Beau Hartshorne has done a lot of work bringing it up to standard.
Here are some of the changes: 1. Fixed bug in IE. 2. Handles errors differently. It will aggregate them then raise a single error with the original errors intact rather than log them. 3. Should conform to MochiKit standards a lot better. 4. Turns off bubbling of events. They can be "uncancelled" if you really want to. I am thinking of either modifying IE's event object OR creating a proxy object that behaves according to DOM standard. That way, when you get an event, you don't have to guess which interface to use. Thoughts? -- Jonathan Gardner [EMAIL PROTECTED]
Index: doc/rst/MochiKit/index.rst =================================================================== --- doc/rst/MochiKit/index.rst (revision 490) +++ doc/rst/MochiKit/index.rst (working copy) @@ -16,6 +16,7 @@ - :mochiref:`MochiKit.Logging` - we're all tired of ``alert()`` - :mochiref:`MochiKit.LoggingPane` - interactive :mochiref:`MochiKit.Logging` pane +- :mochiref:`MochiKit.Signal` - event handling - :mochiref:`MochiKit.Visual` - visual effects Index: doc/rst/MochiKit/Signal.rst =================================================================== --- doc/rst/MochiKit/Signal.rst (revision 0) +++ doc/rst/MochiKit/Signal.rst (revision 0) @@ -0,0 +1,232 @@ +.. title:: MochiKit.Signal - Simple universal event handling + +Name +==== + +MochiKit.Signal - Simple universal event handling + + +Synopsis +======== + +:: + + // Allow your objects to signal 'flash' and 'bang'. + var myObject = {}; + register_signals(myObject, ['flash', 'bang']); + + // Connect myobject's 'flash' signal to otherObject's 'gotFlash' method. + // This means that otherObject,gotFlash() will be called when 'flash' + // signalled. + connect(myObject, 'flash', otherObject, 'gotFlash'); + + // Connect myobject's 'bang' signal to otherObject and gotBang. This + // means that gotBang.apply(otherObject) will be called when 'bang' + // signalled. + connect(myObject, 'bang', otherObject, gotBang); + + // Connect myObject's 'flash' signal to myFunc. This means that + // myFunc.apply(myObject) will be called when 'flash' signalled. + connect(myObject, 'flash', myFunc); + + // You may disconnect with disconnect() and the same parameters. + + // Signal can take parameters. These will be passed along to the connected + // functions. + + // This calls otherObject['gotFlash'].apply(otherObject) and + // myFunc.apply(myObject) + signal(myObject, 'flash'); + + // This calls gotBang.apply(otherObject, 'BANG!') + signal(myObject, 'bang', 'BANG!'); + + // DOM events are also signals. Connect freely! The functions will be + // called with the event as a parameter. Automatically prevents bubbling. + + // calls myClicked.apply($('myID'), event) + connect('myID', 'onclick', myClicked); + + // calls wasClicked.apply(myObject, event) + connect($('myID'), 'onclick', myObject, wasClicked); + + // calls myObject.wasClicked(event) + connect($('myID'), 'onclick', myObject, 'wasClicked'); + + +Description +=========== + +Event handling was never so easy! + +This module takes care of all the hard work---figuring out which event module +to use, trying to retrieve the event object, and handling your own internal +events, as well as cleanup when the page is unloaded to handle IE's nasty +memory leakage. + +Here are the rules for the signal and slot system. + +1. Don't use the browser event handling. That means, no 'onclick="blah"', no +elem.attachEvent(...), and certainly no elem.addEventListener(...). If you +really need to do this, you should not use the MochiKit Signal library for +that particular event. + +2. Objects other than DOM objects (window, document, or any HTMLElement) must +have its signals declared via 'register_signals()' before they can be used. + +3. For DOM objects (window, document, or any HTMLElement), the signals already +exist and are named 'onclick', 'onkeyup', etc... just like they are named +already. The short versions used in DOM2 ('click', 'keyup', etc...) are not +valid. + +4. The following are acceptable for slots. + + - A function. + - An object and a function. + - An object and a string. + +5. You may connect or disconnect slots to signals freely using the 'connect()' +and 'disconnect()' methods. The same parameters to 'disconnect' will only +remove a previous connection made with the same parameters to 'connect'. Also, +connecting multiple times only leaves one connection in place. + +6. Slots that are connected to a signal are called when that signal is +signalled. + + - If the slot was a single function, then it is called with 'this' set to + the object originating the signal with whatever parameters it was + signalled with. + + - If the slot was an object and a function, then it is called with 'this' + set to the object, and with whatever parameters it was signalled with. + + - If the slot was an object and a string, then "object[string]" is called + with the parameters to the signal. + +If an exception is caused by execution of one of the slots, that exception is +trapped. It will be logged as a fatal message if the Loggin module is loaded. +Otherwise, at the end of the signal, it will raise a special error with all +the errors intact. + +The results of a slot call are ignored, never to be seen again. + +7. Signals are triggered with the 'signal(src, 'signal', ...)' function. +Whatever additional parameters are passed to this are passed onto the +connected slots. (DOM events that have listening slots will trigger this +call.) + +8. Signals triggered by DOM events are called with the event as a parameter. +They are also cancelled so that they won't bubble up automatically. If you +want them to bubble up, you have two options: + + - Signal the event explicitly. This is kind of hard to do right because of + browser compatibility issues. + + - Uncancel the event. + +This event system is largely based on Qt's signal/slot system. You should read +more on how that is handled and also how it is used in model/view programming +at http://docs.trolltech.com/. + +Dependencies +============ + +- :mochiref:`MochiKit.Base` +- :mochiref:`MochiKit.Logging` +- :mochiref:`MochiKit.DOM` + +Future Directions +================= + +Here are some things I should probably do better in the next version. + + 1. Make events compatible. This can be done by massaging the actual + event so that it looks like a proper DOM event. + + 2. Provide two events for each browser event: One for bubbling, and + the other for capturing. + + 3. Do not explicitly cancel the event, but make that easier for users + by making the event behave like a DOM2 event. + + 4. Allow the simple creation of events that are reasonably authentic. + + +Overview +======== + + +API Reference +============= + +Functions +--------- + +:mochidef:`connect(src, signal, dest, func)`: + + Connects a signal to a slot. + + 'src' is the object that has the signal. You may pass in a string, in + which case, it is interpreted as an id for an HTML Element. + + 'signal' is a string that represents a signal name. If 'src' is an HTML + Element, Window, or the Document, then it can be one of the 'on-XYZ' + events. Note that you must include the 'on' prefix, and it must be all + lower-case. If 'src' is another kind of object, the signal must be + previously registered with 'register_signals()'. + + 'dest' and 'func' describe the slot, or the action to take when the signal + is triggered. + + - If 'dest' is an object and 'func' is a string, then 'dest[func](...)' will + be called when the signal is signalled. + + - If 'dest' is an object and 'func' is a function, then 'func.apply(dest, + ...)' will be called when the signal is signalled. + + - If 'func' is undefined and 'dest' is a function, then 'func.apply(src, + ...)' will be called when the signal is signalled. + + No other combinations are allowed and should raise and exception. + + You may call 'connect()' multiple times with the same connection + paramters. However, only a single connection will be made. + +:mochidef:`disconnect(src, signal, dest, func)`: + + When 'disconnect()' is called, it will disconnect whatever connection was + made given the same parameters to 'connect()'. Note that if you want to + pass a closure to 'connect()', you'll have to remember it if you want to + later 'disconnect()' it. + +:mochidef:`signal(src, signal, ...)`: + + This will signal a signal, passing whatever additional parameters on to + the connected slots. 'src' and 'signal' are the same as for 'connect()'. + +:mochidef:`register_signals(src, signals)`: + + This will register signals for the object 'src'. (Note that a string here + is not allowed--you don't need to register signals for DOM objects.) + 'signals' is an array of strings. + + You may register the same signals multiple times; subsequent register + calls with the same signal names will have no effect, and the existing + connections, if any, will not be lost. + + +Authors +======= + +- Jonathan Gardner <[EMAIL PROTECTED]> + + +Copyright +========= + +Copyright 2005 Jonathan Gardner <[EMAIL PROTECTED]>. This program +is dual-licensed free software; you can redistribute it and/or modify it under +the terms of the `MIT License`_ or the `Academic Free License v2.1`_. + +.. _`MIT License`: http://www.opensource.org/licenses/mit-license.php +.. _`Academic Free License v2.1`: http://www.opensource.org/licenses/afl-2.1.php Index: MochiKit/MochiKit.js =================================================================== --- MochiKit/MochiKit.js (revision 490) +++ MochiKit/MochiKit.js (working copy) @@ -36,7 +36,8 @@ "DOM", "LoggingPane", "Color", - "Visual" + "Visual", + "Signal" ]; if (typeof(JSAN) != 'undefined' || typeof(dojo) != 'undefined') { @@ -56,6 +57,7 @@ JSAN.use("MochiKit.LoggingPane", []); JSAN.use("MochiKit.Color", []); JSAN.use("MochiKit.Visual", []); + JSAN.use("MochiKit.Signal", []); } (function () { var extend = MochiKit.Base.extend; Index: MochiKit/Signal.js =================================================================== --- MochiKit/Signal.js (revision 0) +++ MochiKit/Signal.js (revision 0) @@ -0,0 +1,291 @@ +/*** + +MochiKit.Signal 1.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Jonathan Gardner. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Signal'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); +} +if (typeof(JSAN) != 'undefined') { + JSAN.use('MochiKit.Base', []); + JSAN.use('MochiKit.DOM', []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Signal depends on MochiKit.Base!'; +} + +try { + if (typeof(MochiKit.DOM) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Signal depends on MochiKit.DOM!'; +} + +if (typeof(MochiKit.Signal) == 'undefined') { + MochiKit.Signal = {}; +} + +MochiKit.Signal.NAME = 'MochiKit.Signal'; +MochiKit.Signal.VERSION = '1.2'; + +MochiKit.Signal._observers = []; + +MochiKit.Base.update(MochiKit.Signal, { + __repr__: function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; + }, + toString: function () { + return this.__repr__(); + }, + + _get_slot: function(slot, func) { + if (typeof(func) == 'string' || typeof(func) == 'function') { + slot = [slot, func]; + } else if (!func && typeof(slot) == 'function') { + slot = [slot]; + } else { + throw new Error('Invalid slot parameters'); + } + + return slot; + }, + + _unloadCache: function() { + for (var i = 0; i < MochiKit.Signal._observers.length; i++) { + var src = MochiKit.Signal._observers[i][0]; + var sig = MochiKit.Signal._observers[i][1]; + var listener = MochiKit.Signal._observers[i][2]; + + if (src.addEventListener) { + src.removeEventListener(sig.substr(2), listener, false); + } else if (src.attachEvent) { + src.detachEvent(sig, listener); + } else { + src[sig] = undefined; + } + } + MochiKit.Signal._observers = []; + }, + + connect: function(src, sig, slot, /* optional */func) { + if (typeof(src) == 'string') { + src = MochiKit.DOM.getElement(src); + } + + if (typeof(sig) != 'string') { + throw new Error("'sig' must be a string"); + } + + slot = MochiKit.Signal._get_slot(slot, func); + + // Find the signal, attach the slot. + if (src.addEventListener || src.attachEvent || src[signal]) { // DOM object + // Create the __listeners object. This will help us remember which + // events we are watching. + if (!src.__listeners) { + src.__listeners = {}; + } + + // Add the signal connector if it hasn't been done already. + if (!src.__listeners[sig]) { + var listener = function(ev) { + ev = ev || event; + + // Cancel bubbling + ev.cancelBubble = true; + if (ev.stopPropagation) { + ev.stopPropagation(); + } + + MochiKit.Signal.signal(src, sig, ev); + + return true; + }; + MochiKit.Signal._observers.push([src, sig, listener]); + + if (src.addEventListener) { + src.addEventListener(sig.substr(2), listener, false); + } else if (src.attachEvent) { + src.attachEvent(sig, listener); + } else { + src[sig] = listener; + } + + src.__listeners[sig] = listener; + } + + if (!src.__signals) { + src.__signals = {}; + } + if (!src.__signals[sig]) { + src.__signals[sig] = []; + } + } else { + if (!src.__signals || !src.__signals[sig]) { + throw new Error("No such signal '" + sig + "' registered."); + } + } + + // Actually add the slot... if it isn't there already. + if (MochiKit.Base.filter( + function(s) { + return MochiKit.Base.operator.ceq(s, slot); + }, + src.__signals[sig]) == 0) { + + src.__signals[sig].push(slot); + } + }, + + disconnect: function(src, sig, slot, /* optional */func) { + if (typeof(src) == 'string') { + src = MochiKit.DOM.getElement(src); + } + + if (typeof(sig) != 'string') { + throw new Error("'signal' must be a string"); + } + + slot = MochiKit.Signal._get_slot(slot, func); + + if (src.__signals && src.__signals[sig]) { + src.__signals[sig] = MochiKit.Base.filter( + function(s) { + return MochiKit.Base.operator.cne(s, slot); + }, + src.__signals[sig]); + } + + if (src.addEventListener || src.attachEvent || src[signal]) { + // DOM object + + // Stop listening if there are no connected slots. + if (src.__listeners && src.__listeners[sig] && + src.__signals[sig].length == 0) { + + var listener = src.__listeners[sig]; + + if (src.addEventListener) { + src.removeEventListener(sig.substr(2), listener, false); + } else if (src.attachEvent) { + src.detachEvent(sig, listener); + } else { + src[sig] = undefined; + } + + MochiKit.Signal._observers = MochiKit.Base.filter( + function(o) { + return MochiKit.Base.operator.cne(o, + [src. sig, listener]); + }, + MochiKit.Signal._observers + ); + + src.__listeners[sig] = undefined; + + } + } + + }, + + signal: function(src, sig) { + if (typeof src == 'string') { + src = MochiKit.DOM.getElement(src); + } + + if (typeof(sig) != 'string') { + throw new Error("'signal' must be a string"); + } + + if (!src.__signals || !src.__signals[sig]) { + if (src.addEventListener || src.attachEvent || src[sig]) { + // Ignored. + return; + } else { + throw new Error("No such signal '" + sig + "'"); + } + } + var slots = src.__signals[sig]; + + var args = MochiKit.Base.extend(null, arguments, 2); + + var slot; + var errors = []; + for (var i = 0; i < slots.length; i++) { + slot = slots[i]; + try { + if (slot.length == 1) { + slot[0].apply(src, args); + } else { + if (typeof slot[1] == 'string') { + slot[0][slot[1]].apply(slot[0], args) + } else { + slot[1].apply(slot[0], args); + } + } + } catch (e) { + // TODO: Should we use the Logging module? Hartshorne says no. + // I need to ask Bob. + errors.push(e); + } + } + if (errors.length) { + var e = new Error("There were errors in handling signal 'sig'."); + e.errors = errors; + throw e; + } + }, + + register_signals: function(src, signals) { + if (!src.__signals) { + src.__signals = {}; + } + for (var i = 0; i < signals.length; i++) { + if (!src.__signals[signals[i]]) { + src.__signals[signals[i]] = []; + } + } + } +}); + +MochiKit.Signal.connect(window, 'onunload', MochiKit.Signal._unloadCache); + +MochiKit.Signal.EXPORT_OK = []; + +MochiKit.Signal.EXPORT = [ + 'connect', + 'disconnect', + 'signal', + 'register_signals', +]; + +MochiKit.Signal.__new__ = function (win) { + var m = MochiKit.Base; + this._document = document; + this._window = win; + + this.EXPORT_TAGS = { + ':common': this.EXPORT, + ':all': m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + +}; + +MochiKit.Signal.__new__(this); + +MochiKit.Base._exportSymbols(this, MochiKit.Signal);