first commit
[ratchet] / docs / js / fingerblast.js
1 // FINGERBLAST.js
2 // --------------
3 // Adapted from phantom limb by brian cartensen
4
5 function FingerBlast(element) {
6   this.element = typeof element == 'string' ? document.querySelector(element) : element;
7   this.listen();
8 }
9
10 FingerBlast.prototype = {
11   x: NaN,
12   y: NaN,
13
14   startDistance: NaN,
15   startAngle:    NaN,
16
17   mouseIsDown: false,
18
19   listen: function () {
20
21     var activate = this.activate.bind(this);
22     var deactivate = this.deactivate.bind(this);
23
24     function contains (element, ancestor) {
25       var descendants, index, descendant;
26       if ("compareDocumentPosition" in ancestor) {
27         return !!(ancestor.compareDocumentPosition(element) & 16);
28       } else if ("contains" in ancestor) {
29         return ancestor != element && ancestor.contains(element);
30       } else {
31         for (descendants = ancestor.getElementsByTagName("*"), index = 0; descendant = descendants[index++];) {
32           if (descendant == element) return true;
33         }
34         return false;
35       }
36     }
37
38     this.element.addEventListener('mouseover', function (e) {
39       var target = e.relatedTarget;
40       if (target != this && !contains(target, this)) activate();
41     });
42
43     this.element.addEventListener("mouseout", function (e) {
44       var target = e.relatedTarget;
45       if (target != this && !contains(target, this)) deactivate(e);
46     });
47   },
48
49   activate: function () {
50     if (this.active) return;
51     this.element.addEventListener('mousedown', (this.touchStart = this.touchStart.bind(this)), true);
52     this.element.addEventListener('mousemove', (this.touchMove  = this.touchMove.bind(this)),  true);
53     this.element.addEventListener('mouseup',   (this.touchEnd   = this.touchEnd.bind(this)),   true);
54     this.element.addEventListener('click',     (this.click      = this.click.bind(this)),      true);
55     this.active = true;
56   },
57
58   deactivate: function (e) {
59     this.active = false;
60     if (this.mouseIsDown) this.touchEnd(e);
61     this.element.removeEventListener('mousedown', this.touchStart, true);
62     this.element.removeEventListener('mousemove', this.touchMove,  true);
63     this.element.removeEventListener('mouseup',   this.touchEnd,   true);
64     this.element.removeEventListener('click',     this.click,      true);
65   },
66
67   click: function (e) {
68     if (e.synthetic) return;
69     e.preventDefault();
70     e.stopPropagation();
71   },
72
73   touchStart: function (e) {
74     if (e.synthetic || /input|textarea/.test(e.target.tagName.toLowerCase())) return;
75
76     this.mouseIsDown = true;
77
78     e.preventDefault();
79     e.stopPropagation();
80
81     this.fireTouchEvents('touchstart', e);
82   },
83
84   touchMove: function (e) {
85     if (e.synthetic) return;
86
87     e.preventDefault();
88     e.stopPropagation();
89
90     this.move(e.clientX, e.clientY);
91
92     if (this.mouseIsDown) this.fireTouchEvents('touchmove', e);
93   },
94
95   touchEnd: function (e) {
96     if (e.synthetic) return;
97
98     this.mouseIsDown = false;
99
100     e.preventDefault();
101     e.stopPropagation();
102
103     this.fireTouchEvents('touchend', e);
104
105     if (!this.target) return;
106
107     // Mobile Safari moves all the mouse events to fire after the touchend event.
108     this.target.dispatchEvent(this.createMouseEvent('mouseover', e));
109     this.target.dispatchEvent(this.createMouseEvent('mousemove', e));
110     this.target.dispatchEvent(this.createMouseEvent('mousedown', e));
111   },
112
113   fireTouchEvents: function (eventName, originalEvent) {
114     var events   = [];
115     var gestures = [];
116
117     if (!this.target) return;
118
119     // Convert "ontouch*" properties and attributes to listeners.
120     var onEventName = 'on' + eventName;
121
122     if (onEventName in this.target) {
123       console.warn('Converting `' + onEventName + '` property to event listener.', this.target);
124       this.target.addEventListener(eventName, this.target[onEventName], false);
125       delete this.target[onEventName];
126     }
127
128     if (this.target.hasAttribute(onEventName)) {
129       console.warn('Converting `' + onEventName + '` attribute to event listener.', this.target);
130       var handler = new GLOBAL.Function('event', this.target.getAttribute(onEventName));
131       this.target.addEventListener(eventName, handler, false);
132       this.target.removeAttribute(onEventName);
133     }
134
135     // Set up a new event with the coordinates of the finger.
136     var touch = this.createMouseEvent(eventName, originalEvent);
137
138     events.push(touch);
139
140     // Figure out scale and rotation.
141     if (events.length > 1) {
142       var x = events[0].pageX - events[1].pageX;
143       var y = events[0].pageY - events[1].pageY;
144
145       var distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
146       var angle = Math.atan2(x, y) * (180 / Math.PI);
147
148       var gestureName = 'gesturechange';
149
150       if (eventName === 'touchstart') {
151         gestureName = 'gesturestart';
152         this.startDistance = distance;
153         this.startAngle = angle;
154       }
155
156       if (eventName === 'touchend') gestureName = 'gestureend';
157
158       events.forEach(function(event) {
159         var gesture = this.createMouseEvent.call(event._finger, gestureName, event);
160         gestures.push(gesture);
161       }.bind(this));
162
163       events.concat(gestures).forEach(function(event) {
164         event.scale = distance / this.startDistance;
165         event.rotation = this.startAngle - angle;
166       });
167     }
168
169     // Loop through the events array and fill in each touch array.
170     events.forEach(function(touch) {
171       touch.touches = events.filter(function(e) {
172         return ~e.type.indexOf('touch') && e.type !== 'touchend';
173       });
174
175       touch.changedTouches = events.filter(function(e) {
176         return ~e.type.indexOf('touch') && e._finger.target === touch._finger.target;
177       });
178
179       touch.targetTouches = touch.changedTouches.filter(function(e) {
180         return ~e.type.indexOf('touch') && e.type !== 'touchend';
181       });
182     });
183
184     // Then fire the events.
185     events.concat(gestures).forEach(function(event, i) {
186       event.identifier = i;
187       event._finger.target.dispatchEvent(event);
188     });
189   },
190
191   createMouseEvent: function (eventName, originalEvent) {
192     var e = document.createEvent('MouseEvent');
193
194     e.initMouseEvent(eventName, true, true,
195       originalEvent.view, originalEvent.detail,
196       this.x || originalEvent.screenX, this.y || originalEvent.screenY,
197       this.x || originalEvent.clientX, this.y || originalEvent.clientY,
198       originalEvent.ctrlKey, originalEvent.shiftKey,
199       originalEvent.altKey, originalEvent.metaKey,
200       originalEvent.button, this.target || originalEvent.relatedTarget
201     );
202
203     e.synthetic = true;
204     e._finger   = this;
205
206     return e;
207   },
208
209   move: function (x, y) {
210     if (isNaN(x) || isNaN(y)) {
211       this.target = null;
212     } else {
213       this.x = x;
214       this.y = y;
215
216       if (!this.mouseIsDown) {
217         this.target = document.elementFromPoint(x, y);
218       }
219     }
220   }
221 };