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
);