source: main/trunk/model-sites-dev/respooled/collect/popup-video-respooled/js/pep.js@ 29882

Last change on this file since 29882 was 29882, checked in by davidb, 9 years ago

Supporting files for guitar tab and better mouse/touch event integration

File size: 39.3 KB
Line 
1/*!
2 * PEP v0.3.0 | https://github.com/jquery/PEP
3 * Copyright jQuery Foundation and other contributors | http://jquery.org/license
4 */
5(function (global, factory) {
6 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
7 typeof define === 'function' && define.amd ? define(factory) :
8 global.PointerEventsPolyfill = factory()
9}(this, function () { 'use strict';
10
11 /**
12 * This module implements an map of pointer states
13 */
14 var USE_MAP = window.Map && window.Map.prototype.forEach;
15 var POINTERS_FN = function(){ return this.size; };
16 function PointerMap() {
17 if (USE_MAP) {
18 var m = new Map();
19 m.pointers = POINTERS_FN;
20 return m;
21 } else {
22 this.keys = [];
23 this.values = [];
24 }
25 }
26
27 PointerMap.prototype = {
28 set: function(inId, inEvent) {
29 var i = this.keys.indexOf(inId);
30 if (i > -1) {
31 this.values[i] = inEvent;
32 } else {
33 this.keys.push(inId);
34 this.values.push(inEvent);
35 }
36 },
37 has: function(inId) {
38 return this.keys.indexOf(inId) > -1;
39 },
40 'delete': function(inId) {
41 var i = this.keys.indexOf(inId);
42 if (i > -1) {
43 this.keys.splice(i, 1);
44 this.values.splice(i, 1);
45 }
46 },
47 get: function(inId) {
48 var i = this.keys.indexOf(inId);
49 return this.values[i];
50 },
51 clear: function() {
52 this.keys.length = 0;
53 this.values.length = 0;
54 },
55 // return value, key, map
56 forEach: function(callback, thisArg) {
57 this.values.forEach(function(v, i) {
58 callback.call(thisArg, v, this.keys[i], this);
59 }, this);
60 },
61 pointers: function() {
62 return this.keys.length;
63 }
64 };
65
66 var _pointermap = PointerMap;
67
68 var CLONE_PROPS = [
69 // MouseEvent
70 'bubbles',
71 'cancelable',
72 'view',
73 'detail',
74 'screenX',
75 'screenY',
76 'clientX',
77 'clientY',
78 'ctrlKey',
79 'altKey',
80 'shiftKey',
81 'metaKey',
82 'button',
83 'relatedTarget',
84 // DOM Level 3
85 'buttons',
86 // PointerEvent
87 'pointerId',
88 'width',
89 'height',
90 'pressure',
91 'tiltX',
92 'tiltY',
93 'pointerType',
94 'hwTimestamp',
95 'isPrimary',
96 // event instance
97 'type',
98 'target',
99 'currentTarget',
100 'which',
101 'pageX',
102 'pageY',
103 'timeStamp'
104 ];
105
106 var CLONE_DEFAULTS = [
107 // MouseEvent
108 false,
109 false,
110 null,
111 null,
112 0,
113 0,
114 0,
115 0,
116 false,
117 false,
118 false,
119 false,
120 0,
121 null,
122 // DOM Level 3
123 0,
124 // PointerEvent
125 0,
126 0,
127 0,
128 0,
129 0,
130 0,
131 '',
132 0,
133 false,
134 // event instance
135 '',
136 null,
137 null,
138 0,
139 0,
140 0,
141 0
142 ];
143
144 var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined');
145
146 /**
147 * This module is for normalizing events. Mouse and Touch events will be
148 * collected here, and fire PointerEvents that have the same semantics, no
149 * matter the source.
150 * Events fired:
151 * - pointerdown: a pointing is added
152 * - pointerup: a pointer is removed
153 * - pointermove: a pointer is moved
154 * - pointerover: a pointer crosses into an element
155 * - pointerout: a pointer leaves an element
156 * - pointercancel: a pointer will no longer generate events
157 */
158 var dispatcher = {
159 pointermap: new _pointermap(),
160 eventMap: Object.create(null),
161 captureInfo: Object.create(null),
162 // Scope objects for native events.
163 // This exists for ease of testing.
164 eventSources: Object.create(null),
165 eventSourceList: [],
166 /**
167 * Add a new event source that will generate pointer events.
168 *
169 * `inSource` must contain an array of event names named `events`, and
170 * functions with the names specified in the `events` array.
171 * @param {string} name A name for the event source
172 * @param {Object} source A new source of platform events.
173 */
174 registerSource: function(name, source) {
175 var s = source;
176 var newEvents = s.events;
177 if (newEvents) {
178 newEvents.forEach(function(e) {
179 if (s[e]) {
180 this.eventMap[e] = s[e].bind(s);
181 }
182 }, this);
183 this.eventSources[name] = s;
184 this.eventSourceList.push(s);
185 }
186 },
187 register: function(element) {
188 var l = this.eventSourceList.length;
189 for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {
190 // call eventsource register
191 es.register.call(es, element);
192 }
193 },
194 unregister: function(element) {
195 var l = this.eventSourceList.length;
196 for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {
197 // call eventsource register
198 es.unregister.call(es, element);
199 }
200 },
201 contains: /*scope.external.contains || */function(container, contained) {
202 return container.contains(contained);
203 },
204 // EVENTS
205 down: function(inEvent) {
206 inEvent.bubbles = true;
207 this.fireEvent('pointerdown', inEvent);
208 },
209 move: function(inEvent) {
210 inEvent.bubbles = true;
211 this.fireEvent('pointermove', inEvent);
212 },
213 up: function(inEvent) {
214 inEvent.bubbles = true;
215 this.fireEvent('pointerup', inEvent);
216 },
217 enter: function(inEvent) {
218 inEvent.bubbles = false;
219 this.fireEvent('pointerenter', inEvent);
220 },
221 leave: function(inEvent) {
222 inEvent.bubbles = false;
223 this.fireEvent('pointerleave', inEvent);
224 },
225 over: function(inEvent) {
226 inEvent.bubbles = true;
227 this.fireEvent('pointerover', inEvent);
228 },
229 out: function(inEvent) {
230 inEvent.bubbles = true;
231 this.fireEvent('pointerout', inEvent);
232 },
233 cancel: function(inEvent) {
234 inEvent.bubbles = true;
235 this.fireEvent('pointercancel', inEvent);
236 },
237 leaveOut: function(event) {
238 this.out(event);
239 if (!this.contains(event.target, event.relatedTarget)) {
240 this.leave(event);
241 }
242 },
243 enterOver: function(event) {
244 this.over(event);
245 if (!this.contains(event.target, event.relatedTarget)) {
246 this.enter(event);
247 }
248 },
249 // LISTENER LOGIC
250 eventHandler: function(inEvent) {
251 // This is used to prevent multiple dispatch of pointerevents from
252 // platform events. This can happen when two elements in different scopes
253 // are set up to create pointer events, which is relevant to Shadow DOM.
254 if (inEvent._handledByPE) {
255 return;
256 }
257 var type = inEvent.type;
258 var fn = this.eventMap && this.eventMap[type];
259 if (fn) {
260 fn(inEvent);
261 }
262 inEvent._handledByPE = true;
263 },
264 // set up event listeners
265 listen: function(target, events) {
266 events.forEach(function(e) {
267 this.addEvent(target, e);
268 }, this);
269 },
270 // remove event listeners
271 unlisten: function(target, events) {
272 events.forEach(function(e) {
273 this.removeEvent(target, e);
274 }, this);
275 },
276 addEvent: /*scope.external.addEvent || */function(target, eventName) {
277 target.addEventListener(eventName, this.boundHandler);
278 },
279 removeEvent: /*scope.external.removeEvent || */function(target, eventName) {
280 target.removeEventListener(eventName, this.boundHandler);
281 },
282 // EVENT CREATION AND TRACKING
283 /**
284 * Creates a new Event of type `inType`, based on the information in
285 * `inEvent`.
286 *
287 * @param {string} inType A string representing the type of event to create
288 * @param {Event} inEvent A platform event with a target
289 * @return {Event} A PointerEvent of type `inType`
290 */
291 makeEvent: function(inType, inEvent) {
292 // relatedTarget must be null if pointer is captured
293 if (this.captureInfo[inEvent.pointerId]) {
294 inEvent.relatedTarget = null;
295 }
296 var e = new PointerEvent(inType, inEvent);
297 if (inEvent.preventDefault) {
298 e.preventDefault = inEvent.preventDefault;
299 }
300 e._target = e._target || inEvent.target;
301 return e;
302 },
303 // make and dispatch an event in one call
304 fireEvent: function(inType, inEvent) {
305 var e = this.makeEvent(inType, inEvent);
306 return this.dispatchEvent(e);
307 },
308 /**
309 * Returns a snapshot of inEvent, with writable properties.
310 *
311 * @param {Event} inEvent An event that contains properties to copy.
312 * @return {Object} An object containing shallow copies of `inEvent`'s
313 * properties.
314 */
315 cloneEvent: function(inEvent) {
316 var eventCopy = Object.create(null), p;
317 for (var i = 0; i < CLONE_PROPS.length; i++) {
318 p = CLONE_PROPS[i];
319 eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i];
320 // Work around SVGInstanceElement shadow tree
321 // Return the <use> element that is represented by the instance for Safari, Chrome, IE.
322 // This is the behavior implemented by Firefox.
323 if (HAS_SVG_INSTANCE && (p === 'target' || p === 'relatedTarget')) {
324 if (eventCopy[p] instanceof SVGElementInstance) {
325 eventCopy[p] = eventCopy[p].correspondingUseElement;
326 }
327 }
328 }
329 // keep the semantics of preventDefault
330 if (inEvent.preventDefault) {
331 eventCopy.preventDefault = function() {
332 inEvent.preventDefault();
333 };
334 }
335 return eventCopy;
336 },
337 getTarget: function(inEvent) {
338 // if pointer capture is set, route all events for the specified pointerId
339 // to the capture target
340 return this.captureInfo[inEvent.pointerId] || inEvent._target;
341 },
342 setCapture: function(inPointerId, inTarget) {
343 if (this.captureInfo[inPointerId]) {
344 this.releaseCapture(inPointerId);
345 }
346 this.captureInfo[inPointerId] = inTarget;
347 var e = document.createEvent('Event');
348 e.initEvent('gotpointercapture', true, false);
349 e.pointerId = inPointerId;
350 this.implicitRelease = this.releaseCapture.bind(this, inPointerId);
351 document.addEventListener('pointerup', this.implicitRelease);
352 document.addEventListener('pointercancel', this.implicitRelease);
353 e._target = inTarget;
354 this.asyncDispatchEvent(e);
355 },
356 releaseCapture: function(inPointerId) {
357 var t = this.captureInfo[inPointerId];
358 if (t) {
359 var e = document.createEvent('Event');
360 e.initEvent('lostpointercapture', true, false);
361 e.pointerId = inPointerId;
362 this.captureInfo[inPointerId] = undefined;
363 document.removeEventListener('pointerup', this.implicitRelease);
364 document.removeEventListener('pointercancel', this.implicitRelease);
365 e._target = t;
366 this.asyncDispatchEvent(e);
367 }
368 },
369 /**
370 * Dispatches the event to its target.
371 *
372 * @param {Event} inEvent The event to be dispatched.
373 * @return {Boolean} True if an event handler returns true, false otherwise.
374 */
375 dispatchEvent: /*scope.external.dispatchEvent || */function(inEvent) {
376 var t = this.getTarget(inEvent);
377 if (t) {
378 return t.dispatchEvent(inEvent);
379 }
380 },
381 asyncDispatchEvent: function(inEvent) {
382 requestAnimationFrame(this.dispatchEvent.bind(this, inEvent));
383 }
384 };
385 dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher);
386
387 var _dispatcher = dispatcher;
388
389 var targeting = {
390 shadow: function(inEl) {
391 if (inEl) {
392 return inEl.shadowRoot || inEl.webkitShadowRoot;
393 }
394 },
395 canTarget: function(shadow) {
396 return shadow && Boolean(shadow.elementFromPoint);
397 },
398 targetingShadow: function(inEl) {
399 var s = this.shadow(inEl);
400 if (this.canTarget(s)) {
401 return s;
402 }
403 },
404 olderShadow: function(shadow) {
405 var os = shadow.olderShadowRoot;
406 if (!os) {
407 var se = shadow.querySelector('shadow');
408 if (se) {
409 os = se.olderShadowRoot;
410 }
411 }
412 return os;
413 },
414 allShadows: function(element) {
415 var shadows = [], s = this.shadow(element);
416 while(s) {
417 shadows.push(s);
418 s = this.olderShadow(s);
419 }
420 return shadows;
421 },
422 searchRoot: function(inRoot, x, y) {
423 if (inRoot) {
424 var t = inRoot.elementFromPoint(x, y);
425 var st, sr, os;
426 // is element a shadow host?
427 sr = this.targetingShadow(t);
428 while (sr) {
429 // find the the element inside the shadow root
430 st = sr.elementFromPoint(x, y);
431 if (!st) {
432 // check for older shadows
433 sr = this.olderShadow(sr);
434 } else {
435 // shadowed element may contain a shadow root
436 var ssr = this.targetingShadow(st);
437 return this.searchRoot(ssr, x, y) || st;
438 }
439 }
440 // light dom element is the target
441 return t;
442 }
443 },
444 owner: function(element) {
445 var s = element;
446 // walk up until you hit the shadow root or document
447 while (s.parentNode) {
448 s = s.parentNode;
449 }
450 // the owner element is expected to be a Document or ShadowRoot
451 if (s.nodeType != Node.DOCUMENT_NODE && s.nodeType != Node.DOCUMENT_FRAGMENT_NODE) {
452 s = document;
453 }
454 return s;
455 },
456 findTarget: function(inEvent) {
457 var x = inEvent.clientX, y = inEvent.clientY;
458 // if the listener is in the shadow root, it is much faster to start there
459 var s = this.owner(inEvent.target);
460 // if x, y is not in this root, fall back to document search
461 if (!s.elementFromPoint(x, y)) {
462 s = document;
463 }
464 return this.searchRoot(s, x, y);
465 }
466 };
467
468 /**
469 * This module uses Mutation Observers to dynamically adjust which nodes will
470 * generate Pointer Events.
471 *
472 * All nodes that wish to generate Pointer Events must have the attribute
473 * `touch-action` set to `none`.
474 */
475 var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
476 var map = Array.prototype.map.call.bind(Array.prototype.map);
477 var toArray = Array.prototype.slice.call.bind(Array.prototype.slice);
478 var filter = Array.prototype.filter.call.bind(Array.prototype.filter);
479 var MO = window.MutationObserver || window.WebKitMutationObserver;
480 var SELECTOR = '[touch-action]';
481 var OBSERVER_INIT = {
482 subtree: true,
483 childList: true,
484 attributes: true,
485 attributeOldValue: true,
486 attributeFilter: ['touch-action']
487 };
488
489 function Installer(add, remove, changed, binder) {
490 this.addCallback = add.bind(binder);
491 this.removeCallback = remove.bind(binder);
492 this.changedCallback = changed.bind(binder);
493 if (MO) {
494 this.observer = new MO(this.mutationWatcher.bind(this));
495 }
496 }
497
498 Installer.prototype = {
499 watchSubtree: function(target) {
500 // Only watch scopes that can target find, as these are top-level.
501 // Otherwise we can see duplicate additions and removals that add noise.
502 //
503 // TODO(dfreedman): For some instances with ShadowDOMPolyfill, we can see
504 // a removal without an insertion when a node is redistributed among
505 // shadows. Since it all ends up correct in the document, watching only
506 // the document will yield the correct mutations to watch.
507 if (targeting.canTarget(target)) {
508 this.observer.observe(target, OBSERVER_INIT);
509 }
510 },
511 enableOnSubtree: function(target) {
512 this.watchSubtree(target);
513 if (target === document && document.readyState !== 'complete') {
514 this.installOnLoad();
515 } else {
516 this.installNewSubtree(target);
517 }
518 },
519 installNewSubtree: function(target) {
520 forEach(this.findElements(target), this.addElement, this);
521 },
522 findElements: function(target) {
523 if (target.querySelectorAll) {
524 return target.querySelectorAll(SELECTOR);
525 }
526 return [];
527 },
528 removeElement: function(el) {
529 this.removeCallback(el);
530 },
531 addElement: function(el) {
532 this.addCallback(el);
533 },
534 elementChanged: function(el, oldValue) {
535 this.changedCallback(el, oldValue);
536 },
537 concatLists: function(accum, list) {
538 return accum.concat(toArray(list));
539 },
540 // register all touch-action = none nodes on document load
541 installOnLoad: function() {
542 document.addEventListener('readystatechange', function() {
543 if (document.readyState === 'complete') {
544 this.installNewSubtree(document);
545 }
546 }.bind(this));
547 },
548 isElement: function(n) {
549 return n.nodeType === Node.ELEMENT_NODE;
550 },
551 flattenMutationTree: function(inNodes) {
552 // find children with touch-action
553 var tree = map(inNodes, this.findElements, this);
554 // make sure the added nodes are accounted for
555 tree.push(filter(inNodes, this.isElement));
556 // flatten the list
557 return tree.reduce(this.concatLists, []);
558 },
559 mutationWatcher: function(mutations) {
560 mutations.forEach(this.mutationHandler, this);
561 },
562 mutationHandler: function(m) {
563 if (m.type === 'childList') {
564 var added = this.flattenMutationTree(m.addedNodes);
565 added.forEach(this.addElement, this);
566 var removed = this.flattenMutationTree(m.removedNodes);
567 removed.forEach(this.removeElement, this);
568 } else if (m.type === 'attributes') {
569 this.elementChanged(m.target, m.oldValue);
570 }
571 }
572 };
573
574 if (!MO) {
575 Installer.prototype.watchSubtree = function(){
576 console.warn('PointerEventsPolyfill: MutationObservers not found, touch-action will not be dynamically detected');
577 };
578 }
579
580 var installer = Installer;
581
582 /**
583 * This is the constructor for new PointerEvents.
584 *
585 * New Pointer Events must be given a type, and an optional dictionary of
586 * initialization properties.
587 *
588 * Due to certain platform requirements, events returned from the constructor
589 * identify as MouseEvents.
590 *
591 * @constructor
592 * @param {String} inType The type of the event to create.
593 * @param {Object} [inDict] An optional dictionary of initial event properties.
594 * @return {Event} A new PointerEvent of type `inType` and initialized with properties from `inDict`.
595 */
596 var MOUSE_PROPS = [
597 'bubbles',
598 'cancelable',
599 'view',
600 'detail',
601 'screenX',
602 'screenY',
603 'clientX',
604 'clientY',
605 'ctrlKey',
606 'altKey',
607 'shiftKey',
608 'metaKey',
609 'button',
610 'relatedTarget',
611 'pageX',
612 'pageY'
613 ];
614
615 var MOUSE_DEFAULTS = [
616 false,
617 false,
618 null,
619 null,
620 0,
621 0,
622 0,
623 0,
624 false,
625 false,
626 false,
627 false,
628 0,
629 null,
630 0,
631 0
632 ];
633
634 function _PointerEvent__PointerEvent(inType, inDict) {
635 inDict = inDict || Object.create(null);
636
637 var e = document.createEvent('Event');
638 e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false);
639
640 // define inherited MouseEvent properties
641 // skip bubbles and cancelable since they're set above in initEvent()
642 for(var i = 2, p; i < MOUSE_PROPS.length; i++) {
643 p = MOUSE_PROPS[i];
644 e[p] = inDict[p] || MOUSE_DEFAULTS[i];
645 }
646 e.buttons = inDict.buttons || 0;
647
648 // Spec requires that pointers without pressure specified use 0.5 for down
649 // state and 0 for up state.
650 var pressure = 0;
651 if (inDict.pressure) {
652 pressure = inDict.pressure;
653 } else {
654 pressure = e.buttons ? 0.5 : 0;
655 }
656
657 // add x/y properties aliased to clientX/Y
658 e.x = e.clientX;
659 e.y = e.clientY;
660
661 // define the properties of the PointerEvent interface
662 e.pointerId = inDict.pointerId || 0;
663 e.width = inDict.width || 0;
664 e.height = inDict.height || 0;
665 e.pressure = pressure;
666 e.tiltX = inDict.tiltX || 0;
667 e.tiltY = inDict.tiltY || 0;
668 e.pointerType = inDict.pointerType || '';
669 e.hwTimestamp = inDict.hwTimestamp || 0;
670 e.isPrimary = inDict.isPrimary || false;
671 return e;
672 }
673
674 var _PointerEvent = _PointerEvent__PointerEvent;
675
676 function shadowSelector(v) {
677 return 'body /shadow-deep/ ' + selector(v);
678 }
679 function selector(v) {
680 return '[touch-action="' + v + '"]';
681 }
682 function rule(v) {
683 return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + '; touch-action-delay: none; }';
684 }
685 var attrib2css = [
686 'none',
687 'auto',
688 'pan-x',
689 'pan-y',
690 {
691 rule: 'pan-x pan-y',
692 selectors: [
693 'pan-x pan-y',
694 'pan-y pan-x'
695 ]
696 }
697 ];
698 var styles = '';
699 // only install stylesheet if the browser has touch action support
700 var head = document.head;
701 var hasNativePE = window.PointerEvent || window.MSPointerEvent;
702 // only add shadow selectors if shadowdom is supported
703 var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot;
704
705 function applyAttributeStyles() {
706 if (hasNativePE) {
707 attrib2css.forEach(function(r) {
708 if (String(r) === r) {
709 styles += selector(r) + rule(r) + '\n';
710 if (hasShadowRoot) {
711 styles += shadowSelector(r) + rule(r) + '\n';
712 }
713 } else {
714 styles += r.selectors.map(selector) + rule(r.rule) + '\n';
715 if (hasShadowRoot) {
716 styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n';
717 }
718 }
719 });
720
721 var el = document.createElement('style');
722 el.textContent = styles;
723 document.head.appendChild(el);
724 }
725 }
726
727 var mouse__pointermap = _dispatcher.pointermap;
728 // radius around touchend that swallows mouse events
729 var DEDUP_DIST = 25;
730
731 var WHICH_TO_BUTTONS = [0, 1, 4, 2];
732
733 var HAS_BUTTONS = false;
734 try {
735 HAS_BUTTONS = new MouseEvent('test', {buttons: 1}).buttons === 1;
736 } catch (e) {}
737
738 // handler block for native mouse events
739 var mouseEvents = {
740 POINTER_ID: 1,
741 POINTER_TYPE: 'mouse',
742 events: [
743 'mousedown',
744 'mousemove',
745 'mouseup',
746 'mouseover',
747 'mouseout'
748 ],
749 register: function(target) {
750 _dispatcher.listen(target, this.events);
751 },
752 unregister: function(target) {
753 _dispatcher.unlisten(target, this.events);
754 },
755 lastTouches: [],
756 // collide with the global mouse listener
757 isEventSimulatedFromTouch: function(inEvent) {
758 var lts = this.lastTouches;
759 var x = inEvent.clientX, y = inEvent.clientY;
760 for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) {
761 // simulated mouse events will be swallowed near a primary touchend
762 var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
763 if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) {
764 return true;
765 }
766 }
767 },
768 prepareEvent: function(inEvent) {
769 var e = _dispatcher.cloneEvent(inEvent);
770 // forward mouse preventDefault
771 var pd = e.preventDefault;
772 e.preventDefault = function() {
773 inEvent.preventDefault();
774 pd();
775 };
776 e.pointerId = this.POINTER_ID;
777 e.isPrimary = true;
778 e.pointerType = this.POINTER_TYPE;
779 if (!HAS_BUTTONS) {
780 e.buttons = WHICH_TO_BUTTONS[e.which] || 0;
781 }
782 return e;
783 },
784 mousedown: function(inEvent) {
785 if (!this.isEventSimulatedFromTouch(inEvent)) {
786 var p = mouse__pointermap.has(this.POINTER_ID);
787 // TODO(dfreedman) workaround for some elements not sending mouseup
788 // http://crbug/149091
789 if (p) {
790 this.cancel(inEvent);
791 }
792 var e = this.prepareEvent(inEvent);
793 mouse__pointermap.set(this.POINTER_ID, inEvent);
794 _dispatcher.down(e);
795 }
796 },
797 mousemove: function(inEvent) {
798 if (!this.isEventSimulatedFromTouch(inEvent)) {
799 var e = this.prepareEvent(inEvent);
800 _dispatcher.move(e);
801 }
802 },
803 mouseup: function(inEvent) {
804 if (!this.isEventSimulatedFromTouch(inEvent)) {
805 var p = mouse__pointermap.get(this.POINTER_ID);
806 if (p && p.button === inEvent.button) {
807 var e = this.prepareEvent(inEvent);
808 _dispatcher.up(e);
809 this.cleanupMouse();
810 }
811 }
812 },
813 mouseover: function(inEvent) {
814 if (!this.isEventSimulatedFromTouch(inEvent)) {
815 var e = this.prepareEvent(inEvent);
816 _dispatcher.enterOver(e);
817 }
818 },
819 mouseout: function(inEvent) {
820 if (!this.isEventSimulatedFromTouch(inEvent)) {
821 var e = this.prepareEvent(inEvent);
822 _dispatcher.leaveOut(e);
823 }
824 },
825 cancel: function(inEvent) {
826 var e = this.prepareEvent(inEvent);
827 _dispatcher.cancel(e);
828 this.cleanupMouse();
829 },
830 cleanupMouse: function() {
831 mouse__pointermap['delete'](this.POINTER_ID);
832 }
833 };
834
835 var mouse = mouseEvents;
836
837 var captureInfo = _dispatcher.captureInfo;
838 var findTarget = targeting.findTarget.bind(targeting);
839 var allShadows = targeting.allShadows.bind(targeting);
840 var touch__pointermap = _dispatcher.pointermap;
841 var touchMap = Array.prototype.map.call.bind(Array.prototype.map);
842 // This should be long enough to ignore compat mouse events made by touch
843 var DEDUP_TIMEOUT = 2500;
844 var CLICK_COUNT_TIMEOUT = 200;
845 var ATTRIB = 'touch-action';
846 var INSTALLER;
847 // The presence of touch event handlers blocks scrolling, and so we must be careful to
848 // avoid adding handlers unnecessarily. Chrome plans to add a touch-action-delay property
849 // (crbug.com/329559) to address this, and once we have that we can opt-in to a simpler
850 // handler registration mechanism. Rather than try to predict how exactly to opt-in to
851 // that we'll just leave this disabled until there is a build of Chrome to test.
852 var HAS_TOUCH_ACTION_DELAY = false;
853
854 // handler block for native touch events
855 var touchEvents = {
856 events: [
857 'touchstart',
858 'touchmove',
859 'touchend',
860 'touchcancel'
861 ],
862 register: function(target) {
863 if (HAS_TOUCH_ACTION_DELAY) {
864 _dispatcher.listen(target, this.events);
865 } else {
866 INSTALLER.enableOnSubtree(target);
867 }
868 },
869 unregister: function(target) {
870 if (HAS_TOUCH_ACTION_DELAY) {
871 _dispatcher.unlisten(target, this.events);
872 } else {
873 // TODO(dfreedman): is it worth it to disconnect the MO?
874 }
875 },
876 elementAdded: function(el) {
877 var a = el.getAttribute(ATTRIB);
878 var st = this.touchActionToScrollType(a);
879 if (st) {
880 el._scrollType = st;
881 _dispatcher.listen(el, this.events);
882 // set touch-action on shadows as well
883 allShadows(el).forEach(function(s) {
884 s._scrollType = st;
885 _dispatcher.listen(s, this.events);
886 }, this);
887 }
888 },
889 elementRemoved: function(el) {
890 el._scrollType = undefined;
891 _dispatcher.unlisten(el, this.events);
892 // remove touch-action from shadow
893 allShadows(el).forEach(function(s) {
894 s._scrollType = undefined;
895 _dispatcher.unlisten(s, this.events);
896 }, this);
897 },
898 elementChanged: function(el, oldValue) {
899 var a = el.getAttribute(ATTRIB);
900 var st = this.touchActionToScrollType(a);
901 var oldSt = this.touchActionToScrollType(oldValue);
902 // simply update scrollType if listeners are already established
903 if (st && oldSt) {
904 el._scrollType = st;
905 allShadows(el).forEach(function(s) {
906 s._scrollType = st;
907 }, this);
908 } else if (oldSt) {
909 this.elementRemoved(el);
910 } else if (st) {
911 this.elementAdded(el);
912 }
913 },
914 scrollTypes: {
915 EMITTER: 'none',
916 XSCROLLER: 'pan-x',
917 YSCROLLER: 'pan-y',
918 SCROLLER: /^(?:pan-x pan-y)|(?:pan-y pan-x)|auto$/
919 },
920 touchActionToScrollType: function(touchAction) {
921 var t = touchAction;
922 var st = this.scrollTypes;
923 if (t === 'none') {
924 return 'none';
925 } else if (t === st.XSCROLLER) {
926 return 'X';
927 } else if (t === st.YSCROLLER) {
928 return 'Y';
929 } else if (st.SCROLLER.exec(t)) {
930 return 'XY';
931 }
932 },
933 POINTER_TYPE: 'touch',
934 firstTouch: null,
935 isPrimaryTouch: function(inTouch) {
936 return this.firstTouch === inTouch.identifier;
937 },
938 setPrimaryTouch: function(inTouch) {
939 // set primary touch if there no pointers, or the only pointer is the mouse
940 if (touch__pointermap.pointers() === 0 || (touch__pointermap.pointers() === 1 && touch__pointermap.has(1))) {
941 this.firstTouch = inTouch.identifier;
942 this.firstXY = {X: inTouch.clientX, Y: inTouch.clientY};
943 this.scrolling = false;
944 this.cancelResetClickCount();
945 }
946 },
947 removePrimaryPointer: function(inPointer) {
948 if (inPointer.isPrimary) {
949 this.firstTouch = null;
950 this.firstXY = null;
951 this.resetClickCount();
952 }
953 },
954 clickCount: 0,
955 resetId: null,
956 resetClickCount: function() {
957 var fn = function() {
958 this.clickCount = 0;
959 this.resetId = null;
960 }.bind(this);
961 this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT);
962 },
963 cancelResetClickCount: function() {
964 if (this.resetId) {
965 clearTimeout(this.resetId);
966 }
967 },
968 typeToButtons: function(type) {
969 var ret = 0;
970 if (type === 'touchstart' || type === 'touchmove') {
971 ret = 1;
972 }
973 return ret;
974 },
975 touchToPointer: function(inTouch) {
976 var cte = this.currentTouchEvent;
977 var e = _dispatcher.cloneEvent(inTouch);
978 // Spec specifies that pointerId 1 is reserved for Mouse.
979 // Touch identifiers can start at 0.
980 // Add 2 to the touch identifier for compatibility.
981 var id = e.pointerId = inTouch.identifier + 2;
982 e.target = captureInfo[id] || findTarget(e);
983 e.bubbles = true;
984 e.cancelable = true;
985 e.detail = this.clickCount;
986 e.button = 0;
987 e.buttons = this.typeToButtons(cte.type);
988 e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0;
989 e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0;
990 e.pressure = inTouch.webkitForce || inTouch.force || 0.5;
991 e.isPrimary = this.isPrimaryTouch(inTouch);
992 e.pointerType = this.POINTER_TYPE;
993 // forward touch preventDefaults
994 var self = this;
995 e.preventDefault = function() {
996 self.scrolling = false;
997 self.firstXY = null;
998 cte.preventDefault();
999 };
1000 return e;
1001 },
1002 processTouches: function(inEvent, inFunction) {
1003 var tl = inEvent.changedTouches;
1004 this.currentTouchEvent = inEvent;
1005 for (var i = 0, t; i < tl.length; i++) {
1006 t = tl[i];
1007 inFunction.call(this, this.touchToPointer(t));
1008 }
1009 },
1010 // For single axis scrollers, determines whether the element should emit
1011 // pointer events or behave as a scroller
1012 shouldScroll: function(inEvent) {
1013 if (this.firstXY) {
1014 var ret;
1015 var scrollAxis = inEvent.currentTarget._scrollType;
1016 if (scrollAxis === 'none') {
1017 // this element is a touch-action: none, should never scroll
1018 ret = false;
1019 } else if (scrollAxis === 'XY') {
1020 // this element should always scroll
1021 ret = true;
1022 } else {
1023 var t = inEvent.changedTouches[0];
1024 // check the intended scroll axis, and other axis
1025 var a = scrollAxis;
1026 var oa = scrollAxis === 'Y' ? 'X' : 'Y';
1027 var da = Math.abs(t['client' + a] - this.firstXY[a]);
1028 var doa = Math.abs(t['client' + oa] - this.firstXY[oa]);
1029 // if delta in the scroll axis > delta other axis, scroll instead of
1030 // making events
1031 ret = da >= doa;
1032 }
1033 this.firstXY = null;
1034 return ret;
1035 }
1036 },
1037 findTouch: function(inTL, inId) {
1038 for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) {
1039 if (t.identifier === inId) {
1040 return true;
1041 }
1042 }
1043 },
1044 // In some instances, a touchstart can happen without a touchend. This
1045 // leaves the pointermap in a broken state.
1046 // Therefore, on every touchstart, we remove the touches that did not fire a
1047 // touchend event.
1048 // To keep state globally consistent, we fire a
1049 // pointercancel for this "abandoned" touch
1050 vacuumTouches: function(inEvent) {
1051 var tl = inEvent.touches;
1052 // pointermap.pointers() should be < tl.length here, as the touchstart has not
1053 // been processed yet.
1054 if (touch__pointermap.pointers() >= tl.length) {
1055 var d = [];
1056 touch__pointermap.forEach(function(value, key) {
1057 // Never remove pointerId == 1, which is mouse.
1058 // Touch identifiers are 2 smaller than their pointerId, which is the
1059 // index in pointermap.
1060 if (key !== 1 && !this.findTouch(tl, key - 2)) {
1061 var p = value.out;
1062 d.push(p);
1063 }
1064 }, this);
1065 d.forEach(this.cancelOut, this);
1066 }
1067 },
1068 touchstart: function(inEvent) {
1069 this.vacuumTouches(inEvent);
1070 this.setPrimaryTouch(inEvent.changedTouches[0]);
1071 this.dedupSynthMouse(inEvent);
1072 if (!this.scrolling) {
1073 this.clickCount++;
1074 this.processTouches(inEvent, this.overDown);
1075 }
1076 },
1077 overDown: function(inPointer) {
1078 var p = touch__pointermap.set(inPointer.pointerId, {
1079 target: inPointer.target,
1080 out: inPointer,
1081 outTarget: inPointer.target
1082 });
1083 _dispatcher.over(inPointer);
1084 _dispatcher.enter(inPointer);
1085 _dispatcher.down(inPointer);
1086 },
1087 touchmove: function(inEvent) {
1088 if (!this.scrolling) {
1089 if (this.shouldScroll(inEvent)) {
1090 this.scrolling = true;
1091 this.touchcancel(inEvent);
1092 } else {
1093 inEvent.preventDefault();
1094 this.processTouches(inEvent, this.moveOverOut);
1095 }
1096 }
1097 },
1098 moveOverOut: function(inPointer) {
1099 var event = inPointer;
1100 var pointer = touch__pointermap.get(event.pointerId);
1101 // a finger drifted off the screen, ignore it
1102 if (!pointer) {
1103 return;
1104 }
1105 var outEvent = pointer.out;
1106 var outTarget = pointer.outTarget;
1107 _dispatcher.move(event);
1108 if (outEvent && outTarget !== event.target) {
1109 outEvent.relatedTarget = event.target;
1110 event.relatedTarget = outTarget;
1111 // recover from retargeting by shadow
1112 outEvent.target = outTarget;
1113 if (event.target) {
1114 _dispatcher.leaveOut(outEvent);
1115 _dispatcher.enterOver(event);
1116 } else {
1117 // clean up case when finger leaves the screen
1118 event.target = outTarget;
1119 event.relatedTarget = null;
1120 this.cancelOut(event);
1121 }
1122 }
1123 pointer.out = event;
1124 pointer.outTarget = event.target;
1125 },
1126 touchend: function(inEvent) {
1127 this.dedupSynthMouse(inEvent);
1128 this.processTouches(inEvent, this.upOut);
1129 },
1130 upOut: function(inPointer) {
1131 if (!this.scrolling) {
1132 _dispatcher.up(inPointer);
1133 _dispatcher.out(inPointer);
1134 _dispatcher.leave(inPointer);
1135 }
1136 this.cleanUpPointer(inPointer);
1137 },
1138 touchcancel: function(inEvent) {
1139 this.processTouches(inEvent, this.cancelOut);
1140 },
1141 cancelOut: function(inPointer) {
1142 _dispatcher.cancel(inPointer);
1143 _dispatcher.out(inPointer);
1144 _dispatcher.leave(inPointer);
1145 this.cleanUpPointer(inPointer);
1146 },
1147 cleanUpPointer: function(inPointer) {
1148 touch__pointermap['delete'](inPointer.pointerId);
1149 this.removePrimaryPointer(inPointer);
1150 },
1151 // prevent synth mouse events from creating pointer events
1152 dedupSynthMouse: function(inEvent) {
1153 var lts = mouse.lastTouches;
1154 var t = inEvent.changedTouches[0];
1155 // only the primary finger will synth mouse events
1156 if (this.isPrimaryTouch(t)) {
1157 // remember x/y of last touch
1158 var lt = {x: t.clientX, y: t.clientY};
1159 lts.push(lt);
1160 var fn = (function(lts, lt){
1161 var i = lts.indexOf(lt);
1162 if (i > -1) {
1163 lts.splice(i, 1);
1164 }
1165 }).bind(null, lts, lt);
1166 setTimeout(fn, DEDUP_TIMEOUT);
1167 }
1168 }
1169 };
1170
1171 if (!HAS_TOUCH_ACTION_DELAY) {
1172 INSTALLER = new installer(touchEvents.elementAdded, touchEvents.elementRemoved, touchEvents.elementChanged, touchEvents);
1173 }
1174
1175 var touch = touchEvents;
1176
1177 var ms__pointermap = _dispatcher.pointermap;
1178 var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number';
1179 var msEvents = {
1180 events: [
1181 'MSPointerDown',
1182 'MSPointerMove',
1183 'MSPointerUp',
1184 'MSPointerOut',
1185 'MSPointerOver',
1186 'MSPointerCancel',
1187 'MSGotPointerCapture',
1188 'MSLostPointerCapture'
1189 ],
1190 register: function(target) {
1191 _dispatcher.listen(target, this.events);
1192 },
1193 unregister: function(target) {
1194 _dispatcher.unlisten(target, this.events);
1195 },
1196 POINTER_TYPES: [
1197 '',
1198 'unavailable',
1199 'touch',
1200 'pen',
1201 'mouse'
1202 ],
1203 prepareEvent: function(inEvent) {
1204 var e = inEvent;
1205 if (HAS_BITMAP_TYPE) {
1206 e = _dispatcher.cloneEvent(inEvent);
1207 e.pointerType = this.POINTER_TYPES[inEvent.pointerType];
1208 }
1209 return e;
1210 },
1211 cleanup: function(id) {
1212 ms__pointermap['delete'](id);
1213 },
1214 MSPointerDown: function(inEvent) {
1215 ms__pointermap.set(inEvent.pointerId, inEvent);
1216 var e = this.prepareEvent(inEvent);
1217 _dispatcher.down(e);
1218 },
1219 MSPointerMove: function(inEvent) {
1220 var e = this.prepareEvent(inEvent);
1221 _dispatcher.move(e);
1222 },
1223 MSPointerUp: function(inEvent) {
1224 var e = this.prepareEvent(inEvent);
1225 _dispatcher.up(e);
1226 this.cleanup(inEvent.pointerId);
1227 },
1228 MSPointerOut: function(inEvent) {
1229 var e = this.prepareEvent(inEvent);
1230 _dispatcher.leaveOut(e);
1231 },
1232 MSPointerOver: function(inEvent) {
1233 var e = this.prepareEvent(inEvent);
1234 _dispatcher.enterOver(e);
1235 },
1236 MSPointerCancel: function(inEvent) {
1237 var e = this.prepareEvent(inEvent);
1238 _dispatcher.cancel(e);
1239 this.cleanup(inEvent.pointerId);
1240 },
1241 MSLostPointerCapture: function(inEvent) {
1242 var e = _dispatcher.makeEvent('lostpointercapture', inEvent);
1243 _dispatcher.dispatchEvent(e);
1244 },
1245 MSGotPointerCapture: function(inEvent) {
1246 var e = _dispatcher.makeEvent('gotpointercapture', inEvent);
1247 _dispatcher.dispatchEvent(e);
1248 }
1249 };
1250
1251 var ms = msEvents;
1252
1253 function platform_events__applyPolyfill() {
1254 // only activate if this platform does not have pointer events
1255 if (!window.PointerEvent) {
1256 window.PointerEvent = _PointerEvent;
1257
1258 if (window.navigator.msPointerEnabled) {
1259 var tp = window.navigator.msMaxTouchPoints;
1260 Object.defineProperty(window.navigator, 'maxTouchPoints', {
1261 value: tp,
1262 enumerable: true
1263 });
1264 _dispatcher.registerSource('ms', ms);
1265 } else {
1266 _dispatcher.registerSource('mouse', mouse);
1267 if (window.ontouchstart !== undefined) {
1268 _dispatcher.registerSource('touch', touch);
1269 }
1270 }
1271
1272 _dispatcher.register(document);
1273 }
1274 }
1275
1276 var n = window.navigator;
1277 var s, r;
1278 function assertDown(id) {
1279 if (!_dispatcher.pointermap.has(id)) {
1280 throw new Error('InvalidPointerId');
1281 }
1282 }
1283 if (n.msPointerEnabled) {
1284 s = function(pointerId) {
1285 assertDown(pointerId);
1286 this.msSetPointerCapture(pointerId);
1287 };
1288 r = function(pointerId) {
1289 assertDown(pointerId);
1290 this.msReleasePointerCapture(pointerId);
1291 };
1292 } else {
1293 s = function setPointerCapture(pointerId) {
1294 assertDown(pointerId);
1295 _dispatcher.setCapture(pointerId, this);
1296 };
1297 r = function releasePointerCapture(pointerId) {
1298 assertDown(pointerId);
1299 _dispatcher.releaseCapture(pointerId, this);
1300 };
1301 }
1302
1303 function capture__applyPolyfill() {
1304 if (window.Element && !Element.prototype.setPointerCapture) {
1305 Object.defineProperties(Element.prototype, {
1306 'setPointerCapture': {
1307 value: s
1308 },
1309 'releasePointerCapture': {
1310 value: r
1311 }
1312 });
1313 }
1314 }
1315
1316 applyAttributeStyles();
1317 platform_events__applyPolyfill();
1318 capture__applyPolyfill();
1319
1320 var pointerevents = {
1321 dispatcher: _dispatcher,
1322 Installer: installer,
1323 PointerEvent: _PointerEvent,
1324 PointerMap: _pointermap,
1325 targetFinding: targeting
1326 };
1327
1328 return pointerevents;
1329
1330}));
Note: See TracBrowser for help on using the repository browser.