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());
72 this.type().indexOf('key') === 0 ||
73 this.type().indexOf('mouse') === 0 ||
74 this.type().indexOf('click') != -1 ||
75 this.type() == 'contextmenu') {
76 str += ', modifier(): ' + '{alt: ' + repr(this.modifier().alt) +
77 ', ctrl: ' + repr(this.modifier().ctrl) +
78 ', meta: ' + repr(this.modifier().meta) +
79 ', shift: ' + repr(this.modifier().shift) +
80 ', any: ' + repr(this.modifier().any) + '}';
83 if (this.type() && this.type().indexOf('key') === 0) {
84 str += ', key(): {code: ' + repr(this.key().code) +
85 ', string: ' + repr(this.key().string) + '}';
89 this.type().indexOf('mouse') === 0 ||
90 this.type().indexOf('click') != -1 ||
91 this.type() == 'contextmenu')) {
93 str += ', mouse(): {page: ' + repr(this.mouse().page) +
94 ', client: ' + repr(this.mouse().client);
96 if (this.type() != 'mousemove') {
97 str += ', button: {left: ' + repr(this.mouse().button.left) +
98 ', middle: ' + repr(this.mouse().button.middle) +
99 ', right: ' + repr(this.mouse().button.right) + '}}';
104 if (this.type() == 'mouseover' || this.type() == 'mouseout') {
105 str += ', relatedTarget(): ' + repr(this.relatedTarget());
111 /** @id MochiKit.Signal.Event.prototype.toString */
112 toString: function () {
113 return this.__repr__();
116 /** @id MochiKit.Signal.Event.prototype.src */
121 /** @id MochiKit.Signal.Event.prototype.event */
126 /** @id MochiKit.Signal.Event.prototype.type */
128 return this._event.type || undefined;
131 /** @id MochiKit.Signal.Event.prototype.target */
132 target: function () {
133 return this._event.target || this._event.srcElement;
136 _relatedTarget: null,
137 /** @id MochiKit.Signal.Event.prototype.relatedTarget */
138 relatedTarget: function () {
139 if (this._relatedTarget !== null) {
140 return this._relatedTarget;
144 if (this.type() == 'mouseover') {
145 elem = (this._event.relatedTarget ||
146 this._event.fromElement);
147 } else if (this.type() == 'mouseout') {
148 elem = (this._event.relatedTarget ||
149 this._event.toElement);
152 this._relatedTarget = elem;
160 /** @id MochiKit.Signal.Event.prototype.modifier */
161 modifier: function () {
162 if (this._modifier !== null) {
163 return this._modifier;
166 m.alt = this._event.altKey;
167 m.ctrl = this._event.ctrlKey;
168 m.meta = this._event.metaKey || false; // IE and Opera punt here
169 m.shift = this._event.shiftKey;
170 m.any = m.alt || m.ctrl || m.shift || m.meta;
176 /** @id MochiKit.Signal.Event.prototype.key */
178 if (this._key !== null) {
182 if (this.type() && this.type().indexOf('key') === 0) {
186 If you're looking for a special key, look for it in keydown or
187 keyup, but never keypress. If you're looking for a Unicode
188 chracter, look for it with keypress, but never keyup or
193 FF key event behavior:
194 key event charCode keyCode
208 IE key event behavior:
209 (IE doesn't fire keypress events for special keys.)
224 Safari key event behavior:
225 (Safari sets charCode and keyCode to something crazy for
227 key event charCode keyCode
243 /* look for special keys here */
244 if (this.type() == 'keydown' || this.type() == 'keyup') {
245 k.code = this._event.keyCode;
246 k.string = (MochiKit.Signal._specialKeys[k.code] ||
251 /* look for characters here */
252 } else if (this.type() == 'keypress') {
256 Special key behavior:
258 IE: does not fire keypress events for special keys
259 FF: sets charCode to 0, and sets the correct keyCode
260 Safari: sets keyCode and charCode to something stupid
267 if (typeof(this._event.charCode) != 'undefined' &&
268 this._event.charCode !== 0 &&
269 !MochiKit.Signal._specialMacKeys[this._event.charCode]) {
270 k.code = this._event.charCode;
271 k.string = String.fromCharCode(k.code);
272 } else if (this._event.keyCode &&
273 typeof(this._event.charCode) == 'undefined') { // IE
274 k.code = this._event.keyCode;
275 k.string = String.fromCharCode(k.code);
286 /** @id MochiKit.Signal.Event.prototype.mouse */
288 if (this._mouse !== null) {
296 this.type().indexOf('mouse') === 0 ||
297 this.type().indexOf('click') != -1 ||
298 this.type() == 'contextmenu')) {
300 m.client = new MochiKit.Style.Coordinates(0, 0);
301 if (e.clientX || e.clientY) {
302 m.client.x = (!e.clientX || e.clientX < 0) ? 0 : e.clientX;
303 m.client.y = (!e.clientY || e.clientY < 0) ? 0 : e.clientY;
306 m.page = new MochiKit.Style.Coordinates(0, 0);
307 if (e.pageX || e.pageY) {
308 m.page.x = (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
309 m.page.y = (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
313 The IE shortcut can be off by two. We fix it. See:
314 http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/getboundingclientrect.asp
316 This is similar to the method used in
317 MochiKit.Style.getElementPosition().
320 var de = MochiKit.DOM._document.documentElement;
321 var b = MochiKit.DOM._document.body;
323 m.page.x = e.clientX +
324 (de.scrollLeft || b.scrollLeft) -
325 (de.clientLeft || 0);
327 m.page.y = e.clientY +
328 (de.scrollTop || b.scrollTop) -
332 if (this.type() != 'mousemove') {
334 m.button.left = false;
335 m.button.right = false;
336 m.button.middle = false;
338 /* we could check e.button, but which is more consistent */
340 m.button.left = (e.which == 1);
341 m.button.middle = (e.which == 2);
342 m.button.right = (e.which == 3);
346 Mac browsers and right click:
348 - Safari doesn't fire any click events on a right
350 http://bugs.webkit.org/show_bug.cgi?id=6595
352 - Firefox fires the event, and sets ctrlKey = true
354 - Opera fires the event, and sets metaKey = true
356 oncontextmenu is fired on right clicks between
357 browsers and across platforms.
362 m.button.left = !!(e.button & 1);
363 m.button.right = !!(e.button & 2);
364 m.button.middle = !!(e.button & 4);
373 /** @id MochiKit.Signal.Event.prototype.stop */
375 this.stopPropagation();
376 this.preventDefault();
379 /** @id MochiKit.Signal.Event.prototype.stopPropagation */
380 stopPropagation: function () {
381 if (this._event.stopPropagation) {
382 this._event.stopPropagation();
384 this._event.cancelBubble = true;
388 /** @id MochiKit.Signal.Event.prototype.preventDefault */
389 preventDefault: function () {
390 if (this._event.preventDefault) {
391 this._event.preventDefault();
392 } else if (this._confirmUnload === null) {
393 this._event.returnValue = false;
397 _confirmUnload: null,
399 /** @id MochiKit.Signal.Event.prototype.confirmUnload */
400 confirmUnload: function (msg) {
401 if (this.type() == 'beforeunload') {
402 this._confirmUnload = msg;
403 this._event.returnValue = msg;
408 /* Safari sets keyCode to these special values onkeypress. */
409 MochiKit.Signal._specialMacKeys = {
411 63289: 'KEY_NUM_PAD_CLEAR',
412 63276: 'KEY_PAGE_UP',
413 63277: 'KEY_PAGE_DOWN',
416 63234: 'KEY_ARROW_LEFT',
417 63232: 'KEY_ARROW_UP',
418 63235: 'KEY_ARROW_RIGHT',
419 63233: 'KEY_ARROW_DOWN',
424 /* for KEY_F1 - KEY_F12 */
426 var _specialMacKeys = MochiKit.Signal._specialMacKeys;
427 for (i = 63236; i <= 63242; i++) {
429 _specialMacKeys[i] = 'KEY_F' + (i - 63236 + 1);
433 /* Standard keyboard key codes. */
434 MochiKit.Signal._specialKeys = {
437 12: 'KEY_NUM_PAD_CLEAR', // weird, for Safari and Mac FF only
450 37: 'KEY_ARROW_LEFT',
452 39: 'KEY_ARROW_RIGHT',
453 40: 'KEY_ARROW_DOWN',
454 44: 'KEY_PRINT_SCREEN',
457 59: 'KEY_SEMICOLON', // weird, for Safari and IE only
458 91: 'KEY_WINDOWS_LEFT',
459 92: 'KEY_WINDOWS_RIGHT',
461 106: 'KEY_NUM_PAD_ASTERISK',
462 107: 'KEY_NUM_PAD_PLUS_SIGN',
463 109: 'KEY_NUM_PAD_HYPHEN-MINUS',
464 110: 'KEY_NUM_PAD_FULL_STOP',
465 111: 'KEY_NUM_PAD_SOLIDUS',
467 145: 'KEY_SCROLL_LOCK',
468 186: 'KEY_SEMICOLON',
469 187: 'KEY_EQUALS_SIGN',
471 189: 'KEY_HYPHEN-MINUS',
472 190: 'KEY_FULL_STOP',
474 192: 'KEY_GRAVE_ACCENT',
475 219: 'KEY_LEFT_SQUARE_BRACKET',
476 220: 'KEY_REVERSE_SOLIDUS',
477 221: 'KEY_RIGHT_SQUARE_BRACKET',
478 222: 'KEY_APOSTROPHE'
479 // undefined: 'KEY_UNKNOWN'
483 /* for KEY_0 - KEY_9 */
484 var _specialKeys = MochiKit.Signal._specialKeys;
485 for (var i = 48; i <= 57; i++) {
486 _specialKeys[i] = 'KEY_' + (i - 48);
489 /* for KEY_A - KEY_Z */
490 for (i = 65; i <= 90; i++) {
491 _specialKeys[i] = 'KEY_' + String.fromCharCode(i);
494 /* for KEY_NUM_PAD_0 - KEY_NUM_PAD_9 */
495 for (i = 96; i <= 105; i++) {
496 _specialKeys[i] = 'KEY_NUM_PAD_' + (i - 96);
499 /* for KEY_F1 - KEY_F12 */
500 for (i = 112; i <= 123; i++) {
502 _specialKeys[i] = 'KEY_F' + (i - 112 + 1);
506 MochiKit.Base.update(MochiKit.Signal, {
508 __repr__: function () {
509 return '[' + this.NAME + ' ' + this.VERSION + ']';
512 toString: function () {
513 return this.__repr__();
516 _unloadCache: function () {
517 var self = MochiKit.Signal;
518 var observers = self._observers;
520 for (var i = 0; i < observers.length; i++) {
521 if (observers[i][1] !== 'onload' && observers[i][1] !== 'onunload') {
522 self._disconnect(observers[i]);
527 _listener: function (src, sig, func, obj, isDOM) {
528 var self = MochiKit.Signal;
531 return MochiKit.Base.bind(func, obj);
534 if (typeof(func) == "string") {
535 if (sig === 'onload' || sig === 'onunload') {
536 return function (nativeEvent) {
537 obj[func].apply(obj, [new E(src, nativeEvent)]);
538 MochiKit.Signal.disconnect(src, sig, obj, func);
541 return function (nativeEvent) {
542 obj[func].apply(obj, [new E(src, nativeEvent)]);
546 if (sig === 'onload' || sig === 'onunload') {
547 return function (nativeEvent) {
548 func.apply(obj, [new E(src, nativeEvent)]);
549 MochiKit.Signal.disconnect(src, sig, func);
552 return function (nativeEvent) {
553 func.apply(obj, [new E(src, nativeEvent)]);
559 _browserAlreadyHasMouseEnterAndLeave: function () {
560 return /MSIE/.test(navigator.userAgent);
563 _mouseEnterListener: function (src, sig, func, obj) {
564 var E = MochiKit.Signal.Event;
565 return function (nativeEvent) {
566 var e = new E(src, nativeEvent);
568 e.relatedTarget().nodeName;
570 /* probably hit a permission denied error; possibly one of
571 * firefox's screwy anonymous DIVs inside an input element.
572 * Allow this event to propogate up.
577 if (MochiKit.DOM.isChildNode(e.relatedTarget(), src)) {
578 /* We've moved between our node and a child. Ignore. */
581 e.type = function () { return sig; };
582 if (typeof(func) == "string") {
583 return obj[func].apply(obj, [e]);
585 return func.apply(obj, [e]);
590 _getDestPair: function (objOrFunc, funcOrStr) {
593 if (typeof(funcOrStr) != 'undefined') {
596 if (typeof(funcOrStr) == 'string') {
597 if (typeof(objOrFunc[funcOrStr]) != "function") {
598 throw new Error("'funcOrStr' must be a function on 'objOrFunc'");
600 } else if (typeof(funcOrStr) != 'function') {
601 throw new Error("'funcOrStr' must be a function or string");
603 } else if (typeof(objOrFunc) != "function") {
604 throw new Error("'objOrFunc' must be a function if 'funcOrStr' is not given");
612 /** @id MochiKit.Signal.connect */
613 connect: function (src, sig, objOrFunc/* optional */, funcOrStr) {
614 src = MochiKit.DOM.getElement(src);
615 var self = MochiKit.Signal;
617 if (typeof(sig) != 'string') {
618 throw new Error("'sig' must be a string");
621 var destPair = self._getDestPair(objOrFunc, funcOrStr);
622 var obj = destPair[0];
623 var func = destPair[1];
624 if (typeof(obj) == 'undefined' || obj === null) {
628 var isDOM = !!(src.addEventListener || src.attachEvent);
629 if (isDOM && (sig === "onmouseenter" || sig === "onmouseleave")
630 && !self._browserAlreadyHasMouseEnterAndLeave()) {
631 var listener = self._mouseEnterListener(src, sig.substr(2), func, obj);
632 if (sig === "onmouseenter") {
638 var listener = self._listener(src, sig, func, obj, isDOM);
641 if (src.addEventListener) {
642 src.addEventListener(sig.substr(2), listener, false);
643 } else if (src.attachEvent) {
644 src.attachEvent(sig, listener); // useCapture unsupported
647 var ident = [src, sig, listener, isDOM, objOrFunc, funcOrStr, true];
648 self._observers.push(ident);
651 if (!isDOM && typeof(src.__connect__) == 'function') {
652 var args = MochiKit.Base.extend([ident], arguments, 1);
653 src.__connect__.apply(src, args);
660 _disconnect: function (ident) {
661 // already disconnected
662 if (!ident[6]) { return; }
665 if (!ident[3]) { return; }
668 var listener = ident[2];
670 if (src.removeEventListener) {
671 src.removeEventListener(sig.substr(2), listener, false);
672 } else if (src.detachEvent) {
673 src.detachEvent(sig, listener); // useCapture unsupported
675 throw new Error("'src' must be a DOM element");
679 /** @id MochiKit.Signal.disconnect */
680 disconnect: function (ident) {
681 var self = MochiKit.Signal;
682 var observers = self._observers;
683 var m = MochiKit.Base;
684 if (arguments.length > 1) {
686 var src = MochiKit.DOM.getElement(arguments[0]);
687 var sig = arguments[1];
688 var obj = arguments[2];
689 var func = arguments[3];
690 for (var i = observers.length - 1; i >= 0; i--) {
691 var o = observers[i];
692 if (o[0] === src && o[1] === sig && o[4] === obj && o[5] === func) {
695 observers.splice(i, 1);
703 var idx = m.findIdentical(observers, ident);
705 self._disconnect(ident);
707 observers.splice(idx, 1);
717 /** @id MochiKit.Signal.disconnectAllTo */
718 disconnectAllTo: function (objOrFunc, /* optional */funcOrStr) {
719 var self = MochiKit.Signal;
720 var observers = self._observers;
721 var disconnect = self._disconnect;
722 var locked = self._lock;
723 var dirty = self._dirty;
724 if (typeof(funcOrStr) === 'undefined') {
727 for (var i = observers.length - 1; i >= 0; i--) {
728 var ident = observers[i];
729 if (ident[4] === objOrFunc &&
730 (funcOrStr === null || ident[5] === funcOrStr)) {
735 observers.splice(i, 1);
742 /** @id MochiKit.Signal.disconnectAll */
743 disconnectAll: function (src/* optional */, sig) {
744 src = MochiKit.DOM.getElement(src);
745 var m = MochiKit.Base;
746 var signals = m.flattenArguments(m.extend(null, arguments, 1));
747 var self = MochiKit.Signal;
748 var disconnect = self._disconnect;
749 var observers = self._observers;
751 var locked = self._lock;
752 var dirty = self._dirty;
753 if (signals.length === 0) {
755 for (i = observers.length - 1; i >= 0; i--) {
756 ident = observers[i];
757 if (ident[0] === src) {
760 observers.splice(i, 1);
768 for (i = 0; i < signals.length; i++) {
769 sigs[signals[i]] = true;
771 for (i = observers.length - 1; i >= 0; i--) {
772 ident = observers[i];
773 if (ident[0] === src && ident[1] in sigs) {
776 observers.splice(i, 1);
786 /** @id MochiKit.Signal.signal */
787 signal: function (src, sig) {
788 var self = MochiKit.Signal;
789 var observers = self._observers;
790 src = MochiKit.DOM.getElement(src);
791 var args = MochiKit.Base.extend(null, arguments, 2);
794 for (var i = 0; i < observers.length; i++) {
795 var ident = observers[i];
796 if (ident[0] === src && ident[1] === sig) {
798 ident[2].apply(src, args);
807 for (var i = observers.length - 1; i >= 0; i--) {
808 if (!observers[i][6]) {
809 observers.splice(i, 1);
813 if (errors.length == 1) {
815 } else if (errors.length > 1) {
816 var e = new Error("Multiple errors thrown in handling 'sig', see errors property");
824 MochiKit.Signal.EXPORT_OK = [];
826 MochiKit.Signal.EXPORT = [
834 MochiKit.Signal.__new__ = function (win) {
835 var m = MochiKit.Base;
836 this._document = document;
842 this.connect(window, 'onunload', this._unloadCache);
844 // pass: might not be a browser
848 ':common': this.EXPORT,
849 ':all': m.concat(this.EXPORT, this.EXPORT_OK)
852 m.nameFunctions(this);
855 MochiKit.Signal.__new__(this);
858 // XXX: Internet Explorer blows
860 if (MochiKit.__export__) {
861 connect = MochiKit.Signal.connect;
862 disconnect = MochiKit.Signal.disconnect;
863 disconnectAll = MochiKit.Signal.disconnectAll;
864 signal = MochiKit.Signal.signal;
867 MochiKit.Base._exportSymbols(this, MochiKit.Signal);