/*! * PEP v0.4.3 | https://github.com/jquery/PEP * Copyright jQuery Foundation and other contributors | http://jquery.org/license */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.PointerEventsPolyfill = factory()); }(this, function () { 'use strict'; /** * This is the constructor for new PointerEvents. * * New Pointer Events must be given a type, and an optional dictionary of * initialization properties. * * Due to certain platform requirements, events returned from the constructor * identify as MouseEvents. * * @constructor * @param {String} inType The type of the event to create. * @param {Object} [inDict] An optional dictionary of initial event properties. * @return {Event} A new PointerEvent of type `inType`, initialized with properties from `inDict`. */ var MOUSE_PROPS = [ 'bubbles', 'cancelable', 'view', 'detail', 'screenX', 'screenY', 'clientX', 'clientY', 'ctrlKey', 'altKey', 'shiftKey', 'metaKey', 'button', 'relatedTarget', 'pageX', 'pageY' ]; var MOUSE_DEFAULTS = [ false, false, null, null, 0, 0, 0, 0, false, false, false, false, 0, null, 0, 0 ]; function PointerEvent(inType, inDict) { inDict = inDict || Object.create(null); var e = document.createEvent('Event'); e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false); // define inherited MouseEvent properties // skip bubbles and cancelable since they're set above in initEvent() for (var i = 2, p; i < MOUSE_PROPS.length; i++) { p = MOUSE_PROPS[i]; e[p] = inDict[p] || MOUSE_DEFAULTS[i]; } e.buttons = inDict.buttons || 0; // Spec requires that pointers without pressure specified use 0.5 for down // state and 0 for up state. var pressure = 0; if (inDict.pressure && e.buttons) { pressure = inDict.pressure; } else { pressure = e.buttons ? 0.5 : 0; } // add x/y properties aliased to clientX/Y e.x = e.clientX; e.y = e.clientY; // define the properties of the PointerEvent interface e.pointerId = inDict.pointerId || 0; e.width = inDict.width || 0; e.height = inDict.height || 0; e.pressure = pressure; e.tiltX = inDict.tiltX || 0; e.tiltY = inDict.tiltY || 0; e.twist = inDict.twist || 0; e.tangentialPressure = inDict.tangentialPressure || 0; e.pointerType = inDict.pointerType || ''; e.hwTimestamp = inDict.hwTimestamp || 0; e.isPrimary = inDict.isPrimary || false; return e; } /** * This module implements a map of pointer states */ var USE_MAP = window.Map && window.Map.prototype.forEach; var PointerMap = USE_MAP ? Map : SparseArrayMap; function SparseArrayMap() { this.array = []; this.size = 0; } SparseArrayMap.prototype = { set: function(k, v) { if (v === undefined) { return this.delete(k); } if (!this.has(k)) { this.size++; } this.array[k] = v; }, has: function(k) { return this.array[k] !== undefined; }, delete: function(k) { if (this.has(k)) { delete this.array[k]; this.size--; } }, get: function(k) { return this.array[k]; }, clear: function() { this.array.length = 0; this.size = 0; }, // return value, key, map forEach: function(callback, thisArg) { return this.array.forEach(function(v, k) { callback.call(thisArg, v, k, this); }, this); } }; var CLONE_PROPS = [ // MouseEvent 'bubbles', 'cancelable', 'view', 'detail', 'screenX', 'screenY', 'clientX', 'clientY', 'ctrlKey', 'altKey', 'shiftKey', 'metaKey', 'button', 'relatedTarget', // DOM Level 3 'buttons', // PointerEvent 'pointerId', 'width', 'height', 'pressure', 'tiltX', 'tiltY', 'pointerType', 'hwTimestamp', 'isPrimary', // event instance 'type', 'target', 'currentTarget', 'which', 'pageX', 'pageY', 'timeStamp' ]; var CLONE_DEFAULTS = [ // MouseEvent false, false, null, null, 0, 0, 0, 0, false, false, false, false, 0, null, // DOM Level 3 0, // PointerEvent 0, 0, 0, 0, 0, 0, '', 0, false, // event instance '', null, null, 0, 0, 0, 0 ]; var BOUNDARY_EVENTS = { 'pointerover': 1, 'pointerout': 1, 'pointerenter': 1, 'pointerleave': 1 }; var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined'); /** * This module is for normalizing events. Mouse and Touch events will be * collected here, and fire PointerEvents that have the same semantics, no * matter the source. * Events fired: * - pointerdown: a pointing is added * - pointerup: a pointer is removed * - pointermove: a pointer is moved * - pointerover: a pointer crosses into an element * - pointerout: a pointer leaves an element * - pointercancel: a pointer will no longer generate events */ var dispatcher = { pointermap: new PointerMap(), eventMap: Object.create(null), captureInfo: Object.create(null), // Scope objects for native events. // This exists for ease of testing. eventSources: Object.create(null), eventSourceList: [], /** * Add a new event source that will generate pointer events. * * `inSource` must contain an array of event names named `events`, and * functions with the names specified in the `events` array. * @param {string} name A name for the event source * @param {Object} source A new source of platform events. */ registerSource: function(name, source) { var s = source; var newEvents = s.events; if (newEvents) { newEvents.forEach(function(e) { if (s[e]) { this.eventMap[e] = s[e].bind(s); } }, this); this.eventSources[name] = s; this.eventSourceList.push(s); } }, register: function(element) { var l = this.eventSourceList.length; for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { // call eventsource register es.register.call(es, element); } }, unregister: function(element) { var l = this.eventSourceList.length; for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { // call eventsource register es.unregister.call(es, element); } }, contains: /*scope.external.contains || */function(container, contained) { try { return container.contains(contained); } catch (ex) { // most likely: https://bugzilla.mozilla.org/show_bug.cgi?id=208427 return false; } }, // EVENTS down: function(inEvent) { inEvent.bubbles = true; this.fireEvent('pointerdown', inEvent); }, move: function(inEvent) { inEvent.bubbles = true; this.fireEvent('pointermove', inEvent); }, up: function(inEvent) { inEvent.bubbles = true; this.fireEvent('pointerup', inEvent); }, enter: function(inEvent) { inEvent.bubbles = false; this.fireEvent('pointerenter', inEvent); }, leave: function(inEvent) { inEvent.bubbles = false; this.fireEvent('pointerleave', inEvent); }, over: function(inEvent) { inEvent.bubbles = true; this.fireEvent('pointerover', inEvent); }, out: function(inEvent) { inEvent.bubbles = true; this.fireEvent('pointerout', inEvent); }, cancel: function(inEvent) { inEvent.bubbles = true; this.fireEvent('pointercancel', inEvent); }, leaveOut: function(event) { this.out(event); this.propagate(event, this.leave, false); }, enterOver: function(event) { this.over(event); this.propagate(event, this.enter, true); }, // LISTENER LOGIC eventHandler: function(inEvent) { // This is used to prevent multiple dispatch of pointerevents from // platform events. This can happen when two elements in different scopes // are set up to create pointer events, which is relevant to Shadow DOM. if (inEvent._handledByPE) { return; } var type = inEvent.type; var fn = this.eventMap && this.eventMap[type]; if (fn) { fn(inEvent); } inEvent._handledByPE = true; }, // set up event listeners listen: function(target, events) { events.forEach(function(e) { this.addEvent(target, e); }, this); }, // remove event listeners unlisten: function(target, events) { events.forEach(function(e) { this.removeEvent(target, e); }, this); }, addEvent: /*scope.external.addEvent || */function(target, eventName) { target.addEventListener(eventName, this.boundHandler); }, removeEvent: /*scope.external.removeEvent || */function(target, eventName) { target.removeEventListener(eventName, this.boundHandler); }, // EVENT CREATION AND TRACKING /** * Creates a new Event of type `inType`, based on the information in * `inEvent`. * * @param {string} inType A string representing the type of event to create * @param {Event} inEvent A platform event with a target * @return {Event} A PointerEvent of type `inType` */ makeEvent: function(inType, inEvent) { // relatedTarget must be null if pointer is captured if (this.captureInfo[inEvent.pointerId]) { inEvent.relatedTarget = null; } var e = new PointerEvent(inType, inEvent); if (inEvent.preventDefault) { e.preventDefault = inEvent.preventDefault; } e._target = e._target || inEvent.target; return e; }, // make and dispatch an event in one call fireEvent: function(inType, inEvent) { var e = this.makeEvent(inType, inEvent); return this.dispatchEvent(e); }, /** * Returns a snapshot of inEvent, with writable properties. * * @param {Event} inEvent An event that contains properties to copy. * @return {Object} An object containing shallow copies of `inEvent`'s * properties. */ cloneEvent: function(inEvent) { var eventCopy = Object.create(null); var p; for (var i = 0; i < CLONE_PROPS.length; i++) { p = CLONE_PROPS[i]; eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i]; // Work around SVGInstanceElement shadow tree // Return the element that is represented by the instance for Safari, Chrome, IE. // This is the behavior implemented by Firefox. if (HAS_SVG_INSTANCE && (p === 'target' || p === 'relatedTarget')) { if (eventCopy[p] instanceof SVGElementInstance) { eventCopy[p] = eventCopy[p].correspondingUseElement; } } } // keep the semantics of preventDefault if (inEvent.preventDefault) { eventCopy.preventDefault = function() { inEvent.preventDefault(); }; } return eventCopy; }, getTarget: function(inEvent) { var capture = this.captureInfo[inEvent.pointerId]; if (!capture) { return inEvent._target; } if (inEvent._target === capture || !(inEvent.type in BOUNDARY_EVENTS)) { return capture; } }, propagate: function(event, fn, propagateDown) { var target = event.target; var targets = []; // Order of conditions due to document.contains() missing in IE. while (target !== document && !target.contains(event.relatedTarget)) { targets.push(target); target = target.parentNode; // Touch: Do not propagate if node is detached. if (!target) { return; } } if (propagateDown) { targets.reverse(); } targets.forEach(function(target) { event.target = target; fn.call(this, event); }, this); }, setCapture: function(inPointerId, inTarget, skipDispatch) { if (this.captureInfo[inPointerId]) { this.releaseCapture(inPointerId, skipDispatch); } this.captureInfo[inPointerId] = inTarget; this.implicitRelease = this.releaseCapture.bind(this, inPointerId, skipDispatch); document.addEventListener('pointerup', this.implicitRelease); document.addEventListener('pointercancel', this.implicitRelease); var e = new PointerEvent('gotpointercapture'); e.pointerId = inPointerId; e._target = inTarget; if (!skipDispatch) { this.asyncDispatchEvent(e); } }, releaseCapture: function(inPointerId, skipDispatch) { var t = this.captureInfo[inPointerId]; if (!t) { return; } this.captureInfo[inPointerId] = undefined; document.removeEventListener('pointerup', this.implicitRelease); document.removeEventListener('pointercancel', this.implicitRelease); var e = new PointerEvent('lostpointercapture'); e.pointerId = inPointerId; e._target = t; if (!skipDispatch) { this.asyncDispatchEvent(e); } }, /** * Dispatches the event to its target. * * @param {Event} inEvent The event to be dispatched. * @return {Boolean} True if an event handler returns true, false otherwise. */ dispatchEvent: /*scope.external.dispatchEvent || */function(inEvent) { var t = this.getTarget(inEvent); if (t) { return t.dispatchEvent(inEvent); } }, asyncDispatchEvent: function(inEvent) { requestAnimationFrame(this.dispatchEvent.bind(this, inEvent)); } }; dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher); var targeting = { shadow: function(inEl) { if (inEl) { return inEl.shadowRoot || inEl.webkitShadowRoot; } }, canTarget: function(shadow) { return shadow && Boolean(shadow.elementFromPoint); }, targetingShadow: function(inEl) { var s = this.shadow(inEl); if (this.canTarget(s)) { return s; } }, olderShadow: function(shadow) { var os = shadow.olderShadowRoot; if (!os) { var se = shadow.querySelector('shadow'); if (se) { os = se.olderShadowRoot; } } return os; }, allShadows: function(element) { var shadows = []; var s = this.shadow(element); while (s) { shadows.push(s); s = this.olderShadow(s); } return shadows; }, searchRoot: function(inRoot, x, y) { if (inRoot) { var t = inRoot.elementFromPoint(x, y); var st, sr; // is element a shadow host? sr = this.targetingShadow(t); while (sr) { // find the the element inside the shadow root st = sr.elementFromPoint(x, y); if (!st) { // check for older shadows sr = this.olderShadow(sr); } else { // shadowed element may contain a shadow root var ssr = this.targetingShadow(st); return this.searchRoot(ssr, x, y) || st; } } // light dom element is the target return t; } }, owner: function(element) { var s = element; // walk up until you hit the shadow root or document while (s.parentNode) { s = s.parentNode; } // the owner element is expected to be a Document or ShadowRoot if (s.nodeType !== Node.DOCUMENT_NODE && s.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { s = document; } return s; }, findTarget: function(inEvent) { var x = inEvent.clientX; var y = inEvent.clientY; // if the listener is in the shadow root, it is much faster to start there var s = this.owner(inEvent.target); // if x, y is not in this root, fall back to document search if (!s.elementFromPoint(x, y)) { s = document; } return this.searchRoot(s, x, y); } }; var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); var map = Array.prototype.map.call.bind(Array.prototype.map); var toArray = Array.prototype.slice.call.bind(Array.prototype.slice); var filter = Array.prototype.filter.call.bind(Array.prototype.filter); var MO = window.MutationObserver || window.WebKitMutationObserver; var SELECTOR = '[touch-action]'; var OBSERVER_INIT = { subtree: true, childList: true, attributes: true, attributeOldValue: true, attributeFilter: ['touch-action'] }; function Installer(add, remove, changed, binder) { this.addCallback = add.bind(binder); this.removeCallback = remove.bind(binder); this.changedCallback = changed.bind(binder); if (MO) { this.observer = new MO(this.mutationWatcher.bind(this)); } } Installer.prototype = { watchSubtree: function(target) { // Only watch scopes that can target find, as these are top-level. // Otherwise we can see duplicate additions and removals that add noise. // // TODO(dfreedman): For some instances with ShadowDOMPolyfill, we can see // a removal without an insertion when a node is redistributed among // shadows. Since it all ends up correct in the document, watching only // the document will yield the correct mutations to watch. if (this.observer && targeting.canTarget(target)) { this.observer.observe(target, OBSERVER_INIT); } }, enableOnSubtree: function(target) { this.watchSubtree(target); if (target === document && document.readyState !== 'complete') { this.installOnLoad(); } else { this.installNewSubtree(target); } }, installNewSubtree: function(target) { forEach(this.findElements(target), this.addElement, this); }, findElements: function(target) { if (target.querySelectorAll) { return target.querySelectorAll(SELECTOR); } return []; }, removeElement: function(el) { this.removeCallback(el); }, addElement: function(el) { this.addCallback(el); }, elementChanged: function(el, oldValue) { this.changedCallback(el, oldValue); }, concatLists: function(accum, list) { return accum.concat(toArray(list)); }, // register all touch-action = none nodes on document load installOnLoad: function() { document.addEventListener('readystatechange', function() { if (document.readyState === 'complete') { this.installNewSubtree(document); } }.bind(this)); }, isElement: function(n) { return n.nodeType === Node.ELEMENT_NODE; }, flattenMutationTree: function(inNodes) { // find children with touch-action var tree = map(inNodes, this.findElements, this); // make sure the added nodes are accounted for tree.push(filter(inNodes, this.isElement)); // flatten the list return tree.reduce(this.concatLists, []); }, mutationWatcher: function(mutations) { mutations.forEach(this.mutationHandler, this); }, mutationHandler: function(m) { if (m.type === 'childList') { var added = this.flattenMutationTree(m.addedNodes); added.forEach(this.addElement, this); var removed = this.flattenMutationTree(m.removedNodes); removed.forEach(this.removeElement, this); } else if (m.type === 'attributes') { this.elementChanged(m.target, m.oldValue); } } }; function shadowSelector(v) { return 'body /shadow-deep/ ' + selector(v); } function selector(v) { return '[touch-action="' + v + '"]'; } function rule(v) { return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + '; }'; } var attrib2css = [ 'none', 'auto', 'pan-x', 'pan-y', { rule: 'pan-x pan-y', selectors: [ 'pan-x pan-y', 'pan-y pan-x' ] } ]; var styles = ''; // only install stylesheet if the browser has touch action support var hasNativePE = window.PointerEvent || window.MSPointerEvent; // only add shadow selectors if shadowdom is supported var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot; function applyAttributeStyles() { if (hasNativePE) { attrib2css.forEach(function(r) { if (String(r) === r) { styles += selector(r) + rule(r) + '\n'; if (hasShadowRoot) { styles += shadowSelector(r) + rule(r) + '\n'; } } else { styles += r.selectors.map(selector) + rule(r.rule) + '\n'; if (hasShadowRoot) { styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n'; } } }); var el = document.createElement('style'); el.textContent = styles; document.head.appendChild(el); } } var pointermap = dispatcher.pointermap; // radius around touchend that swallows mouse events var DEDUP_DIST = 25; // left, middle, right, back, forward var BUTTON_TO_BUTTONS = [1, 4, 2, 8, 16]; var HAS_BUTTONS = false; try { HAS_BUTTONS = new MouseEvent('test', { buttons: 1 }).buttons === 1; } catch (e) {} // handler block for native mouse events var mouseEvents = { POINTER_ID: 1, POINTER_TYPE: 'mouse', events: [ 'mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseout' ], register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { dispatcher.unlisten(target, this.events); }, lastTouches: [], // collide with the global mouse listener isEventSimulatedFromTouch: function(inEvent) { var lts = this.lastTouches; var x = inEvent.clientX; var y = inEvent.clientY; for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { // simulated mouse events will be swallowed near a primary touchend var dx = Math.abs(x - t.x); var dy = Math.abs(y - t.y); if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { return true; } } }, prepareEvent: function(inEvent) { var e = dispatcher.cloneEvent(inEvent); // forward mouse preventDefault var pd = e.preventDefault; e.preventDefault = function() { inEvent.preventDefault(); pd(); }; e.pointerId = this.POINTER_ID; e.isPrimary = true; e.pointerType = this.POINTER_TYPE; return e; }, prepareButtonsForMove: function(e, inEvent) { var p = pointermap.get(this.POINTER_ID); // Update buttons state after possible out-of-document mouseup. if (inEvent.which === 0 || !p) { e.buttons = 0; } else { e.buttons = p.buttons; } inEvent.buttons = e.buttons; }, mousedown: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var p = pointermap.get(this.POINTER_ID); var e = this.prepareEvent(inEvent); if (!HAS_BUTTONS) { e.buttons = BUTTON_TO_BUTTONS[e.button]; if (p) { e.buttons |= p.buttons; } inEvent.buttons = e.buttons; } pointermap.set(this.POINTER_ID, inEvent); if (!p || p.buttons === 0) { dispatcher.down(e); } else { dispatcher.move(e); } } }, mousemove: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var e = this.prepareEvent(inEvent); if (!HAS_BUTTONS) { this.prepareButtonsForMove(e, inEvent); } e.button = -1; pointermap.set(this.POINTER_ID, inEvent); dispatcher.move(e); } }, mouseup: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var p = pointermap.get(this.POINTER_ID); var e = this.prepareEvent(inEvent); if (!HAS_BUTTONS) { var up = BUTTON_TO_BUTTONS[e.button]; // Produces wrong state of buttons in Browsers without `buttons` support // when a mouse button that was pressed outside the document is released // inside and other buttons are still pressed down. e.buttons = p ? p.buttons & ~up : 0; inEvent.buttons = e.buttons; } pointermap.set(this.POINTER_ID, inEvent); // Support: Firefox <=44 only // FF Ubuntu includes the lifted button in the `buttons` property on // mouseup. // https://bugzilla.mozilla.org/show_bug.cgi?id=1223366 e.buttons &= ~BUTTON_TO_BUTTONS[e.button]; if (e.buttons === 0) { dispatcher.up(e); } else { dispatcher.move(e); } } }, mouseover: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var e = this.prepareEvent(inEvent); if (!HAS_BUTTONS) { this.prepareButtonsForMove(e, inEvent); } e.button = -1; pointermap.set(this.POINTER_ID, inEvent); dispatcher.enterOver(e); } }, mouseout: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var e = this.prepareEvent(inEvent); if (!HAS_BUTTONS) { this.prepareButtonsForMove(e, inEvent); } e.button = -1; dispatcher.leaveOut(e); } }, cancel: function(inEvent) { var e = this.prepareEvent(inEvent); dispatcher.cancel(e); this.deactivateMouse(); }, deactivateMouse: function() { pointermap.delete(this.POINTER_ID); } }; var captureInfo = dispatcher.captureInfo; var findTarget = targeting.findTarget.bind(targeting); var allShadows = targeting.allShadows.bind(targeting); var pointermap$1 = dispatcher.pointermap; // This should be long enough to ignore compat mouse events made by touch var DEDUP_TIMEOUT = 2500; var CLICK_COUNT_TIMEOUT = 200; var ATTRIB = 'touch-action'; var INSTALLER; // handler block for native touch events var touchEvents = { events: [ 'touchstart', 'touchmove', 'touchend', 'touchcancel' ], register: function(target) { INSTALLER.enableOnSubtree(target); }, unregister: function() { // TODO(dfreedman): is it worth it to disconnect the MO? }, elementAdded: function(el) { var a = el.getAttribute(ATTRIB); var st = this.touchActionToScrollType(a); if (st) { el._scrollType = st; dispatcher.listen(el, this.events); // set touch-action on shadows as well allShadows(el).forEach(function(s) { s._scrollType = st; dispatcher.listen(s, this.events); }, this); } }, elementRemoved: function(el) { el._scrollType = undefined; dispatcher.unlisten(el, this.events); // remove touch-action from shadow allShadows(el).forEach(function(s) { s._scrollType = undefined; dispatcher.unlisten(s, this.events); }, this); }, elementChanged: function(el, oldValue) { var a = el.getAttribute(ATTRIB); var st = this.touchActionToScrollType(a); var oldSt = this.touchActionToScrollType(oldValue); // simply update scrollType if listeners are already established if (st && oldSt) { el._scrollType = st; allShadows(el).forEach(function(s) { s._scrollType = st; }, this); } else if (oldSt) { this.elementRemoved(el); } else if (st) { this.elementAdded(el); } }, scrollTypes: { EMITTER: 'none', XSCROLLER: 'pan-x', YSCROLLER: 'pan-y', SCROLLER: /^(?:pan-x pan-y)|(?:pan-y pan-x)|auto$/ }, touchActionToScrollType: function(touchAction) { var t = touchAction; var st = this.scrollTypes; if (t === 'none') { return 'none'; } else if (t === st.XSCROLLER) { return 'X'; } else if (t === st.YSCROLLER) { return 'Y'; } else if (st.SCROLLER.exec(t)) { return 'XY'; } }, POINTER_TYPE: 'touch', firstTouch: null, isPrimaryTouch: function(inTouch) { return this.firstTouch === inTouch.identifier; }, setPrimaryTouch: function(inTouch) { // set primary touch if there no pointers, or the only pointer is the mouse if (pointermap$1.size === 0 || (pointermap$1.size === 1 && pointermap$1.has(1))) { this.firstTouch = inTouch.identifier; this.firstXY = { X: inTouch.clientX, Y: inTouch.clientY }; this.scrolling = false; this.cancelResetClickCount(); } }, removePrimaryPointer: function(inPointer) { if (inPointer.isPrimary) { this.firstTouch = null; this.firstXY = null; this.resetClickCount(); } }, clickCount: 0, resetId: null, resetClickCount: function() { var fn = function() { this.clickCount = 0; this.resetId = null; }.bind(this); this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT); }, cancelResetClickCount: function() { if (this.resetId) { clearTimeout(this.resetId); } }, typeToButtons: function(type) { var ret = 0; if (type === 'touchstart' || type === 'touchmove') { ret = 1; } return ret; }, touchToPointer: function(inTouch) { var cte = this.currentTouchEvent; var e = dispatcher.cloneEvent(inTouch); // We reserve pointerId 1 for Mouse. // Touch identifiers can start at 0. // Add 2 to the touch identifier for compatibility. var id = e.pointerId = inTouch.identifier + 2; e.target = captureInfo[id] || findTarget(e); e.bubbles = true; e.cancelable = true; e.detail = this.clickCount; e.button = 0; e.buttons = this.typeToButtons(cte.type); e.width = (inTouch.radiusX || inTouch.webkitRadiusX || 0) * 2; e.height = (inTouch.radiusY || inTouch.webkitRadiusY || 0) * 2; e.pressure = inTouch.force || inTouch.webkitForce || 0.5; e.isPrimary = this.isPrimaryTouch(inTouch); e.pointerType = this.POINTER_TYPE; // forward modifier keys e.altKey = cte.altKey; e.ctrlKey = cte.ctrlKey; e.metaKey = cte.metaKey; e.shiftKey = cte.shiftKey; // forward touch preventDefaults var self = this; e.preventDefault = function() { self.scrolling = false; self.firstXY = null; cte.preventDefault(); }; return e; }, processTouches: function(inEvent, inFunction) { var tl = inEvent.changedTouches; this.currentTouchEvent = inEvent; for (var i = 0, t; i < tl.length; i++) { t = tl[i]; inFunction.call(this, this.touchToPointer(t)); } }, // For single axis scrollers, determines whether the element should emit // pointer events or behave as a scroller shouldScroll: function(inEvent) { if (this.firstXY) { var ret; var scrollAxis = inEvent.currentTarget._scrollType; if (scrollAxis === 'none') { // this element is a touch-action: none, should never scroll ret = false; } else if (scrollAxis === 'XY') { // this element should always scroll ret = true; } else { var t = inEvent.changedTouches[0]; // check the intended scroll axis, and other axis var a = scrollAxis; var oa = scrollAxis === 'Y' ? 'X' : 'Y'; var da = Math.abs(t['client' + a] - this.firstXY[a]); var doa = Math.abs(t['client' + oa] - this.firstXY[oa]); // if delta in the scroll axis > delta other axis, scroll instead of // making events ret = da >= doa; } this.firstXY = null; return ret; } }, findTouch: function(inTL, inId) { for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) { if (t.identifier === inId) { return true; } } }, // In some instances, a touchstart can happen without a touchend. This // leaves the pointermap in a broken state. // Therefore, on every touchstart, we remove the touches that did not fire a // touchend event. // To keep state globally consistent, we fire a // pointercancel for this "abandoned" touch vacuumTouches: function(inEvent) { var tl = inEvent.touches; // pointermap.size should be < tl.length here, as the touchstart has not // been processed yet. if (pointermap$1.size >= tl.length) { var d = []; pointermap$1.forEach(function(value, key) { // Never remove pointerId == 1, which is mouse. // Touch identifiers are 2 smaller than their pointerId, which is the // index in pointermap. if (key !== 1 && !this.findTouch(tl, key - 2)) { var p = value.out; d.push(p); } }, this); d.forEach(this.cancelOut, this); } }, touchstart: function(inEvent) { this.vacuumTouches(inEvent); this.setPrimaryTouch(inEvent.changedTouches[0]); this.dedupSynthMouse(inEvent); if (!this.scrolling) { this.clickCount++; this.processTouches(inEvent, this.overDown); } }, overDown: function(inPointer) { pointermap$1.set(inPointer.pointerId, { target: inPointer.target, out: inPointer, outTarget: inPointer.target }); dispatcher.enterOver(inPointer); dispatcher.down(inPointer); }, touchmove: function(inEvent) { if (!this.scrolling) { if (this.shouldScroll(inEvent)) { this.scrolling = true; this.touchcancel(inEvent); } else { inEvent.preventDefault(); this.processTouches(inEvent, this.moveOverOut); } } }, moveOverOut: function(inPointer) { var event = inPointer; var pointer = pointermap$1.get(event.pointerId); // a finger drifted off the screen, ignore it if (!pointer) { return; } var outEvent = pointer.out; var outTarget = pointer.outTarget; dispatcher.move(event); if (outEvent && outTarget !== event.target) { outEvent.relatedTarget = event.target; event.relatedTarget = outTarget; // recover from retargeting by shadow outEvent.target = outTarget; if (event.target) { dispatcher.leaveOut(outEvent); dispatcher.enterOver(event); } else { // clean up case when finger leaves the screen event.target = outTarget; event.relatedTarget = null; this.cancelOut(event); } } pointer.out = event; pointer.outTarget = event.target; }, touchend: function(inEvent) { this.dedupSynthMouse(inEvent); this.processTouches(inEvent, this.upOut); }, upOut: function(inPointer) { if (!this.scrolling) { dispatcher.up(inPointer); dispatcher.leaveOut(inPointer); } this.cleanUpPointer(inPointer); }, touchcancel: function(inEvent) { this.processTouches(inEvent, this.cancelOut); }, cancelOut: function(inPointer) { dispatcher.cancel(inPointer); dispatcher.leaveOut(inPointer); this.cleanUpPointer(inPointer); }, cleanUpPointer: function(inPointer) { pointermap$1.delete(inPointer.pointerId); this.removePrimaryPointer(inPointer); }, // prevent synth mouse events from creating pointer events dedupSynthMouse: function(inEvent) { var lts = mouseEvents.lastTouches; var t = inEvent.changedTouches[0]; // only the primary finger will synth mouse events if (this.isPrimaryTouch(t)) { // remember x/y of last touch var lt = { x: t.clientX, y: t.clientY }; lts.push(lt); var fn = (function(lts, lt) { var i = lts.indexOf(lt); if (i > -1) { lts.splice(i, 1); } }).bind(null, lts, lt); setTimeout(fn, DEDUP_TIMEOUT); } } }; INSTALLER = new Installer(touchEvents.elementAdded, touchEvents.elementRemoved, touchEvents.elementChanged, touchEvents); var pointermap$2 = dispatcher.pointermap; var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number'; var msEvents = { events: [ 'MSPointerDown', 'MSPointerMove', 'MSPointerUp', 'MSPointerOut', 'MSPointerOver', 'MSPointerCancel', 'MSGotPointerCapture', 'MSLostPointerCapture' ], register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { dispatcher.unlisten(target, this.events); }, POINTER_TYPES: [ '', 'unavailable', 'touch', 'pen', 'mouse' ], prepareEvent: function(inEvent) { var e = inEvent; if (HAS_BITMAP_TYPE) { e = dispatcher.cloneEvent(inEvent); e.pointerType = this.POINTER_TYPES[inEvent.pointerType]; } return e; }, cleanup: function(id) { pointermap$2.delete(id); }, MSPointerDown: function(inEvent) { pointermap$2.set(inEvent.pointerId, inEvent); var e = this.prepareEvent(inEvent); dispatcher.down(e); }, MSPointerMove: function(inEvent) { var e = this.prepareEvent(inEvent); dispatcher.move(e); }, MSPointerUp: function(inEvent) { var e = this.prepareEvent(inEvent); dispatcher.up(e); this.cleanup(inEvent.pointerId); }, MSPointerOut: function(inEvent) { var e = this.prepareEvent(inEvent); dispatcher.leaveOut(e); }, MSPointerOver: function(inEvent) { var e = this.prepareEvent(inEvent); dispatcher.enterOver(e); }, MSPointerCancel: function(inEvent) { var e = this.prepareEvent(inEvent); dispatcher.cancel(e); this.cleanup(inEvent.pointerId); }, MSLostPointerCapture: function(inEvent) { var e = dispatcher.makeEvent('lostpointercapture', inEvent); dispatcher.dispatchEvent(e); }, MSGotPointerCapture: function(inEvent) { var e = dispatcher.makeEvent('gotpointercapture', inEvent); dispatcher.dispatchEvent(e); } }; function applyPolyfill() { // only activate if this platform does not have pointer events if (!window.PointerEvent) { window.PointerEvent = PointerEvent; if (window.navigator.msPointerEnabled) { var tp = window.navigator.msMaxTouchPoints; Object.defineProperty(window.navigator, 'maxTouchPoints', { value: tp, enumerable: true }); dispatcher.registerSource('ms', msEvents); } else { Object.defineProperty(window.navigator, 'maxTouchPoints', { value: 0, enumerable: true }); dispatcher.registerSource('mouse', mouseEvents); if (window.ontouchstart !== undefined) { dispatcher.registerSource('touch', touchEvents); } } dispatcher.register(document); } } var n = window.navigator; var s; var r; var h; function assertActive(id) { if (!dispatcher.pointermap.has(id)) { var error = new Error('InvalidPointerId'); error.name = 'InvalidPointerId'; throw error; } } function assertConnected(elem) { var parent = elem.parentNode; while (parent && parent !== elem.ownerDocument) { parent = parent.parentNode; } if (!parent) { var error = new Error('InvalidStateError'); error.name = 'InvalidStateError'; throw error; } } function inActiveButtonState(id) { var p = dispatcher.pointermap.get(id); return p.buttons !== 0; } if (n.msPointerEnabled) { s = function(pointerId) { assertActive(pointerId); assertConnected(this); if (inActiveButtonState(pointerId)) { dispatcher.setCapture(pointerId, this, true); this.msSetPointerCapture(pointerId); } }; r = function(pointerId) { assertActive(pointerId); dispatcher.releaseCapture(pointerId, true); this.msReleasePointerCapture(pointerId); }; } else { s = function setPointerCapture(pointerId) { assertActive(pointerId); assertConnected(this); if (inActiveButtonState(pointerId)) { dispatcher.setCapture(pointerId, this); } }; r = function releasePointerCapture(pointerId) { assertActive(pointerId); dispatcher.releaseCapture(pointerId); }; } h = function hasPointerCapture(pointerId) { return !!dispatcher.captureInfo[pointerId]; }; function applyPolyfill$1() { if (window.Element && !Element.prototype.setPointerCapture) { Object.defineProperties(Element.prototype, { 'setPointerCapture': { value: s }, 'releasePointerCapture': { value: r }, 'hasPointerCapture': { value: h } }); } } applyAttributeStyles(); applyPolyfill(); applyPolyfill$1(); var pointerevents = { dispatcher: dispatcher, Installer: Installer, PointerEvent: PointerEvent, PointerMap: PointerMap, targetFinding: targeting }; return pointerevents; }));