5 See <http://mochikit.com/> for documentation, downloads, license, etc.
7 (c) 2006 Jonathan Gardner, Beau Hartshorne, Bob Ippolito. All rights Reserved.
11 if (typeof(dojo) != 'undefined') {
12 dojo.provide('MochiKit.Signal');
13 dojo.require('MochiKit.Base');
14 dojo.require('MochiKit.DOM');
15 dojo.require('MochiKit.Style');
17 if (typeof(JSAN) != 'undefined') {
18 JSAN.use('MochiKit.Base', []);
19 JSAN.use('MochiKit.DOM', []);
20 JSAN.use('MochiKit.Style', []);
24 if (typeof(MochiKit.Base) == 'undefined') {
28 throw 'MochiKit.Signal depends on MochiKit.Base!';
32 if (typeof(MochiKit.DOM) == 'undefined') {
36 throw 'MochiKit.Signal depends on MochiKit.DOM!';
40 if (typeof(MochiKit.Style) == 'undefined') {
44 throw 'MochiKit.Signal depends on MochiKit.Style!';
47 if (typeof(MochiKit.Signal) == 'undefined') {
51 MochiKit.Signal.NAME = 'MochiKit.Signal';
52 MochiKit.Signal.VERSION = '1.4';
54 MochiKit.Signal._observers = [];
56 /** @id MochiKit.Signal.Event */
57 MochiKit.Signal.Event = function (src, e) {
58 this._event = e || window.event;
62 MochiKit.Base.update(MochiKit.Signal.Event.prototype, {
64 __repr__: function () {
65 var repr = MochiKit.Base.repr;
66 var str = '{event(): ' + repr(this.event()) +
67 ', src(): ' + repr(this.src()) +
68 ', type(): ' + repr(this.type()) +
69 ', target(): ' + repr(this.target()) +
70 ', modifier(): ' + '{alt: ' + repr(this.modifier().alt) +
71 ', ctrl: ' + repr(this.modifier().ctrl) +
72 ', meta: ' + repr(this.modifier().meta) +
73 ', shift: ' + repr(this.modifier().shift) +
74 ', any: ' + repr(this.modifier().any) + '}';
76 if (this.type() && this.type().indexOf('key') === 0) {
77 str += ', key(): {code: ' + repr(this.key().code) +
78 ', string: ' + repr(this.key().string) + '}';
82 this.type().indexOf('mouse') === 0 ||
83 this.type().indexOf('click') != -1 ||
84 this.type() == 'contextmenu')) {
86 str += ', mouse(): {page: ' + repr(this.mouse().page) +
87 ', client: ' + repr(this.mouse().client);
89 if (this.type() != 'mousemove') {
90 str += ', button: {left: ' + repr(this.mouse().button.left) +
91 ', middle: ' + repr(this.mouse().button.middle) +
92 ', right: ' + repr(this.mouse().button.right) + '}}';
97 if (this.type() == 'mouseover' || this.type() == 'mouseout') {
98 str += ', relatedTarget(): ' + repr(this.relatedTarget());
104 /** @id MochiKit.Signal.Event.prototype.toString */
105 toString: function () {
106 return this.__repr__();
109 /** @id MochiKit.Signal.Event.prototype.src */
114 /** @id MochiKit.Signal.Event.prototype.event */
119 /** @id MochiKit.Signal.Event.prototype.type */
121 return this._event.type || undefined;
124 /** @id MochiKit.Signal.Event.prototype.target */
125 target: function () {
126 return this._event.target || this._event.srcElement;
129 _relatedTarget: null,
130 /** @id MochiKit.Signal.Event.prototype.relatedTarget */
131 relatedTarget: function () {
132 if (this._relatedTarget !== null) {
133 return this._relatedTarget;
137 if (this.type() == 'mouseover') {
138 elem = (this._event.relatedTarget ||
139 this._event.fromElement);
140 } else if (this.type() == 'mouseout') {
141 elem = (this._event.relatedTarget ||
142 this._event.toElement);
145 this._relatedTarget = elem;
153 /** @id MochiKit.Signal.Event.prototype.modifier */
154 modifier: function () {
155 if (this._modifier !== null) {
156 return this._modifier;
159 m.alt = this._event.altKey;
160 m.ctrl = this._event.ctrlKey;
161 m.meta = this._event.metaKey || false; // IE and Opera punt here
162 m.shift = this._event.shiftKey;
163 m.any = m.alt || m.ctrl || m.shift || m.meta;
169 /** @id MochiKit.Signal.Event.prototype.key */
171 if (this._key !== null) {
175 if (this.type() && this.type().indexOf('key') === 0) {
179 If you're looking for a special key, look for it in keydown or
180 keyup, but never keypress. If you're looking for a Unicode
181 chracter, look for it with keypress, but never keyup or
186 FF key event behavior:
187 key event charCode keyCode
201 IE key event behavior:
202 (IE doesn't fire keypress events for special keys.)
217 Safari key event behavior:
218 (Safari sets charCode and keyCode to something crazy for
220 key event charCode keyCode
236 /* look for special keys here */
237 if (this.type() == 'keydown' || this.type() == 'keyup') {
238 k.code = this._event.keyCode;
239 k.string = (MochiKit.Signal._specialKeys[k.code] ||
244 /* look for characters here */
245 } else if (this.type() == 'keypress') {
249 Special key behavior:
251 IE: does not fire keypress events for special keys
252 FF: sets charCode to 0, and sets the correct keyCode
253 Safari: sets keyCode and charCode to something stupid
260 if (typeof(this._event.charCode) != 'undefined' &&
261 this._event.charCode !== 0 &&
262 !MochiKit.Signal._specialMacKeys[this._event.charCode]) {
263 k.code = this._event.charCode;
264 k.string = String.fromCharCode(k.code);
265 } else if (this._event.keyCode &&
266 typeof(this._event.charCode) == 'undefined') { // IE
267 k.code = this._event.keyCode;
268 k.string = String.fromCharCode(k.code);
279 /** @id MochiKit.Signal.Event.prototype.mouse */
281 if (this._mouse !== null) {
289 this.type().indexOf('mouse') === 0 ||
290 this.type().indexOf('click') != -1 ||
291 this.type() == 'contextmenu')) {
293 m.client = new MochiKit.Style.Coordinates(0, 0);
294 if (e.clientX || e.clientY) {
295 m.client.x = (!e.clientX || e.clientX < 0) ? 0 : e.clientX;
296 m.client.y = (!e.clientY || e.clientY < 0) ? 0 : e.clientY;
299 m.page = new MochiKit.Style.Coordinates(0, 0);
300 if (e.pageX || e.pageY) {
301 m.page.x = (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
302 m.page.y = (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
306 The IE shortcut can be off by two. We fix it. See:
307 http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/getboundingclientrect.asp
309 This is similar to the method used in
310 MochiKit.Style.getElementPosition().
313 var de = MochiKit.DOM._document.documentElement;
314 var b = MochiKit.DOM._document.body;
316 m.page.x = e.clientX +
317 (de.scrollLeft || b.scrollLeft) -
318 (de.clientLeft || 0);
320 m.page.y = e.clientY +
321 (de.scrollTop || b.scrollTop) -
325 if (this.type() != 'mousemove') {
327 m.button.left = false;
328 m.button.right = false;
329 m.button.middle = false;
331 /* we could check e.button, but which is more consistent */
333 m.button.left = (e.which == 1);
334 m.button.middle = (e.which == 2);
335 m.button.right = (e.which == 3);
339 Mac browsers and right click:
341 - Safari doesn't fire any click events on a right
343 http://bugzilla.opendarwin.org/show_bug.cgi?id=6595
345 - Firefox fires the event, and sets ctrlKey = true
347 - Opera fires the event, and sets metaKey = true
349 oncontextmenu is fired on right clicks between
350 browsers and across platforms.
355 m.button.left = !!(e.button & 1);
356 m.button.right = !!(e.button & 2);
357 m.button.middle = !!(e.button & 4);
366 /** @id MochiKit.Signal.Event.prototype.stop */
368 this.stopPropagation();
369 this.preventDefault();
372 /** @id MochiKit.Signal.Event.prototype.stopPropagation */
373 stopPropagation: function () {
374 if (this._event.stopPropagation) {
375 this._event.stopPropagation();
377 this._event.cancelBubble = true;
381 /** @id MochiKit.Signal.Event.prototype.preventDefault */
382 preventDefault: function () {
383 if (this._event.preventDefault) {
384 this._event.preventDefault();
385 } else if (this._confirmUnload === null) {
386 this._event.returnValue = false;
390 _confirmUnload: null,
392 /** @id MochiKit.Signal.Event.prototype.confirmUnload */
393 confirmUnload: function (msg) {
394 if (this.type() == 'beforeunload') {
395 this._confirmUnload = msg;
396 this._event.returnValue = msg;
401 /* Safari sets keyCode to these special values onkeypress. */
402 MochiKit.Signal._specialMacKeys = {
404 63289: 'KEY_NUM_PAD_CLEAR',
405 63276: 'KEY_PAGE_UP',
406 63277: 'KEY_PAGE_DOWN',
409 63234: 'KEY_ARROW_LEFT',
410 63232: 'KEY_ARROW_UP',
411 63235: 'KEY_ARROW_RIGHT',
412 63233: 'KEY_ARROW_DOWN',
417 /* for KEY_F1 - KEY_F12 */
419 var _specialMacKeys = MochiKit.Signal._specialMacKeys;
420 for (i = 63236; i <= 63242; i++) {
422 _specialMacKeys[i] = 'KEY_F' + (i - 63236 + 1);
426 /* Standard keyboard key codes. */
427 MochiKit.Signal._specialKeys = {
430 12: 'KEY_NUM_PAD_CLEAR', // weird, for Safari and Mac FF only
443 37: 'KEY_ARROW_LEFT',
445 39: 'KEY_ARROW_RIGHT',
446 40: 'KEY_ARROW_DOWN',
447 44: 'KEY_PRINT_SCREEN',
450 59: 'KEY_SEMICOLON', // weird, for Safari and IE only
451 91: 'KEY_WINDOWS_LEFT',
452 92: 'KEY_WINDOWS_RIGHT',
454 106: 'KEY_NUM_PAD_ASTERISK',
455 107: 'KEY_NUM_PAD_PLUS_SIGN',
456 109: 'KEY_NUM_PAD_HYPHEN-MINUS',
457 110: 'KEY_NUM_PAD_FULL_STOP',
458 111: 'KEY_NUM_PAD_SOLIDUS',
460 145: 'KEY_SCROLL_LOCK',
461 186: 'KEY_SEMICOLON',
462 187: 'KEY_EQUALS_SIGN',
464 189: 'KEY_HYPHEN-MINUS',
465 190: 'KEY_FULL_STOP',
467 192: 'KEY_GRAVE_ACCENT',
468 219: 'KEY_LEFT_SQUARE_BRACKET',
469 220: 'KEY_REVERSE_SOLIDUS',
470 221: 'KEY_RIGHT_SQUARE_BRACKET',
471 222: 'KEY_APOSTROPHE'
472 // undefined: 'KEY_UNKNOWN'
476 /* for KEY_0 - KEY_9 */
477 var _specialKeys = MochiKit.Signal._specialKeys;
478 for (var i = 48; i <= 57; i++) {
479 _specialKeys[i] = 'KEY_' + (i - 48);
482 /* for KEY_A - KEY_Z */
483 for (i = 65; i <= 90; i++) {
484 _specialKeys[i] = 'KEY_' + String.fromCharCode(i);
487 /* for KEY_NUM_PAD_0 - KEY_NUM_PAD_9 */
488 for (i = 96; i <= 105; i++) {
489 _specialKeys[i] = 'KEY_NUM_PAD_' + (i - 96);
492 /* for KEY_F1 - KEY_F12 */
493 for (i = 112; i <= 123; i++) {
495 _specialKeys[i] = 'KEY_F' + (i - 112 + 1);
499 MochiKit.Base.update(MochiKit.Signal, {
501 __repr__: function () {
502 return '[' + this.NAME + ' ' + this.VERSION + ']';
505 toString: function () {
506 return this.__repr__();
509 _unloadCache: function () {
510 var self = MochiKit.Signal;
511 var observers = self._observers;
513 for (var i = 0; i < observers.length; i++) {
514 self._disconnect(observers[i]);
517 delete self._observers;
520 window.onload = undefined;
526 window.onunload = undefined;
532 _listener: function (src, func, obj, isDOM) {
533 var self = MochiKit.Signal;
536 return MochiKit.Base.bind(func, obj);
539 if (typeof(func) == "string") {
540 return function (nativeEvent) {
541 obj[func].apply(obj, [new E(src, nativeEvent)]);
544 return function (nativeEvent) {
545 func.apply(obj, [new E(src, nativeEvent)]);
550 _browserAlreadyHasMouseEnterAndLeave: function () {
551 return /MSIE/.test(navigator.userAgent);
554 _mouseEnterListener: function (src, sig, func, obj) {
555 var E = MochiKit.Signal.Event;
556 return function (nativeEvent) {
557 var e = new E(src, nativeEvent);
559 e.relatedTarget().nodeName;
561 /* probably hit a permission denied error; possibly one of
562 * firefox's screwy anonymous DIVs inside an input element.
563 * Allow this event to propogate up.
568 if (MochiKit.DOM.isChildNode(e.relatedTarget(), src)) {
569 /* We've moved between our node and a child. Ignore. */
572 e.type = function () { return sig; };
573 if (typeof(func) == "string") {
574 return obj[func].apply(obj, [e]);
576 return func.apply(obj, [e]);
581 _getDestPair: function (objOrFunc, funcOrStr) {
584 if (typeof(funcOrStr) != 'undefined') {
587 if (typeof(funcOrStr) == 'string') {
588 if (typeof(objOrFunc[funcOrStr]) != "function") {
589 throw new Error("'funcOrStr' must be a function on 'objOrFunc'");
591 } else if (typeof(funcOrStr) != 'function') {
592 throw new Error("'funcOrStr' must be a function or string");
594 } else if (typeof(objOrFunc) != "function") {
595 throw new Error("'objOrFunc' must be a function if 'funcOrStr' is not given");
603 /** @id MochiKit.Signal.connect */
604 connect: function (src, sig, objOrFunc/* optional */, funcOrStr) {
605 src = MochiKit.DOM.getElement(src);
606 var self = MochiKit.Signal;
608 if (typeof(sig) != 'string') {
609 throw new Error("'sig' must be a string");
612 var destPair = self._getDestPair(objOrFunc, funcOrStr);
613 var obj = destPair[0];
614 var func = destPair[1];
615 if (typeof(obj) == 'undefined' || obj === null) {
619 var isDOM = !!(src.addEventListener || src.attachEvent);
620 if (isDOM && (sig === "onmouseenter" || sig === "onmouseleave")
621 && !self._browserAlreadyHasMouseEnterAndLeave()) {
622 var listener = self._mouseEnterListener(src, sig.substr(2), func, obj);
623 if (sig === "onmouseenter") {
629 var listener = self._listener(src, func, obj, isDOM);
632 if (src.addEventListener) {
633 src.addEventListener(sig.substr(2), listener, false);
634 } else if (src.attachEvent) {
635 src.attachEvent(sig, listener); // useCapture unsupported
638 var ident = [src, sig, listener, isDOM, objOrFunc, funcOrStr, true];
639 self._observers.push(ident);
642 if (!isDOM && typeof(src.__connect__) == 'function') {
643 var args = MochiKit.Base.extend([ident], arguments, 1);
644 src.__connect__.apply(src, args);
651 _disconnect: function (ident) {
652 // already disconnected
653 if (!ident[6]) { return; }
656 if (!ident[3]) { return; }
659 var listener = ident[2];
660 if (src.removeEventListener) {
661 src.removeEventListener(sig.substr(2), listener, false);
662 } else if (src.detachEvent) {
663 src.detachEvent(sig, listener); // useCapture unsupported
665 throw new Error("'src' must be a DOM element");
669 /** @id MochiKit.Signal.disconnect */
670 disconnect: function (ident) {
671 var self = MochiKit.Signal;
672 var observers = self._observers;
673 var m = MochiKit.Base;
674 if (arguments.length > 1) {
676 var src = MochiKit.DOM.getElement(arguments[0]);
677 var sig = arguments[1];
678 var obj = arguments[2];
679 var func = arguments[3];
680 for (var i = observers.length - 1; i >= 0; i--) {
681 var o = observers[i];
682 if (o[0] === src && o[1] === sig && o[4] === obj && o[5] === func) {
685 observers.splice(i, 1);
693 var idx = m.findIdentical(observers, ident);
695 self._disconnect(ident);
697 observers.splice(idx, 1);
707 /** @id MochiKit.Signal.disconnectAllTo */
708 disconnectAllTo: function (objOrFunc, /* optional */funcOrStr) {
709 var self = MochiKit.Signal;
710 var observers = self._observers;
711 var disconnect = self._disconnect;
712 var locked = self._lock;
713 var dirty = self._dirty;
714 if (typeof(funcOrStr) === 'undefined') {
717 for (var i = observers.length - 1; i >= 0; i--) {
718 var ident = observers[i];
719 if (ident[4] === objOrFunc &&
720 (funcOrStr === null || ident[5] === funcOrStr)) {
725 observers.splice(i, 1);
732 /** @id MochiKit.Signal.disconnectAll */
733 disconnectAll: function (src/* optional */, sig) {
734 src = MochiKit.DOM.getElement(src);
735 var m = MochiKit.Base;
736 var signals = m.flattenArguments(m.extend(null, arguments, 1));
737 var self = MochiKit.Signal;
738 var disconnect = self._disconnect;
739 var observers = self._observers;
741 var locked = self._lock;
742 var dirty = self._dirty;
743 if (signals.length === 0) {
745 for (i = observers.length - 1; i >= 0; i--) {
746 ident = observers[i];
747 if (ident[0] === src) {
750 observers.splice(i, 1);
758 for (i = 0; i < signals.length; i++) {
759 sigs[signals[i]] = true;
761 for (i = observers.length - 1; i >= 0; i--) {
762 ident = observers[i];
763 if (ident[0] === src && ident[1] in sigs) {
766 observers.splice(i, 1);
776 /** @id MochiKit.Signal.signal */
777 signal: function (src, sig) {
778 var self = MochiKit.Signal;
779 var observers = self._observers;
780 src = MochiKit.DOM.getElement(src);
781 var args = MochiKit.Base.extend(null, arguments, 2);
784 for (var i = 0; i < observers.length; i++) {
785 var ident = observers[i];
786 if (ident[0] === src && ident[1] === sig) {
788 ident[2].apply(src, args);
797 for (var i = observers.length - 1; i >= 0; i--) {
798 if (!observers[i][6]) {
799 observers.splice(i, 1);
803 if (errors.length == 1) {
805 } else if (errors.length > 1) {
806 var e = new Error("Multiple errors thrown in handling 'sig', see errors property");
814 MochiKit.Signal.EXPORT_OK = [];
816 MochiKit.Signal.EXPORT = [
824 MochiKit.Signal.__new__ = function (win) {
825 var m = MochiKit.Base;
826 this._document = document;
832 this.connect(window, 'onunload', this._unloadCache);
834 // pass: might not be a browser
838 ':common': this.EXPORT,
839 ':all': m.concat(this.EXPORT, this.EXPORT_OK)
842 m.nameFunctions(this);
845 MochiKit.Signal.__new__(this);
848 // XXX: Internet Explorer blows
850 if (MochiKit.__export__) {
851 connect = MochiKit.Signal.connect;
852 disconnect = MochiKit.Signal.disconnect;
853 disconnectAll = MochiKit.Signal.disconnectAll;
854 signal = MochiKit.Signal.signal;
857 MochiKit.Base._exportSymbols(this, MochiKit.Signal);