UNPKG

five-bells-visualization

Version:
1,715 lines (1,656 loc) 344 kB
/** * @license * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ // @version 0.5.5 window.PolymerGestures = {}; (function(scope) { var hasFullPath = false; // test for full event path support var pathTest = document.createElement('meta'); if (pathTest.createShadowRoot) { var sr = pathTest.createShadowRoot(); var s = document.createElement('span'); sr.appendChild(s); pathTest.addEventListener('testpath', function(ev) { if (ev.path) { // if the span is in the event path, then path[0] is the real source for all events hasFullPath = ev.path[0] === s; } ev.stopPropagation(); }); var ev = new CustomEvent('testpath', {bubbles: true}); // must add node to DOM to trigger event listener document.head.appendChild(pathTest); s.dispatchEvent(ev); pathTest.parentNode.removeChild(pathTest); sr = s = null; } pathTest = null; var target = { 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 = [], s = this.shadow(element); while(s) { shadows.push(s); s = this.olderShadow(s); } return shadows; }, searchRoot: function(inRoot, x, y) { var t, st, sr, os; if (inRoot) { t = inRoot.elementFromPoint(x, y); if (t) { // found element, check if it has a ShadowRoot sr = this.targetingShadow(t); } else if (inRoot !== document) { // check for sibling roots sr = this.olderShadow(inRoot); } // search other roots, fall back to light dom element return this.searchRoot(sr, x, y) || t; } }, owner: function(element) { if (!element) { return document; } 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) { if (hasFullPath && inEvent.path && inEvent.path.length) { return inEvent.path[0]; } var x = inEvent.clientX, 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); }, findTouchAction: function(inEvent) { var n; if (hasFullPath && inEvent.path && inEvent.path.length) { var path = inEvent.path; for (var i = 0; i < path.length; i++) { n = path[i]; if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { return n.getAttribute('touch-action'); } } } else { n = inEvent.target; while(n) { if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { return n.getAttribute('touch-action'); } n = n.parentNode || n.host; } } // auto is default return "auto"; }, LCA: function(a, b) { if (a === b) { return a; } if (a && !b) { return a; } if (b && !a) { return b; } if (!b && !a) { return document; } // fast case, a is a direct descendant of b or vice versa if (a.contains && a.contains(b)) { return a; } if (b.contains && b.contains(a)) { return b; } var adepth = this.depth(a); var bdepth = this.depth(b); var d = adepth - bdepth; if (d >= 0) { a = this.walk(a, d); } else { b = this.walk(b, -d); } while (a && b && a !== b) { a = a.parentNode || a.host; b = b.parentNode || b.host; } return a; }, walk: function(n, u) { for (var i = 0; n && (i < u); i++) { n = n.parentNode || n.host; } return n; }, depth: function(n) { var d = 0; while(n) { d++; n = n.parentNode || n.host; } return d; }, deepContains: function(a, b) { var common = this.LCA(a, b); // if a is the common ancestor, it must "deeply" contain b return common === a; }, insideNode: function(node, x, y) { var rect = node.getBoundingClientRect(); return (rect.left <= x) && (x <= rect.right) && (rect.top <= y) && (y <= rect.bottom); }, path: function(event) { var p; if (hasFullPath && event.path && event.path.length) { p = event.path; } else { p = []; var n = this.findTarget(event); while (n) { p.push(n); n = n.parentNode || n.host; } } return p; } }; scope.targetFinding = target; /** * Given an event, finds the "deepest" node that could have been the original target before ShadowDOM retargetting * * @param {Event} Event An event object with clientX and clientY properties * @return {Element} The probable event origninator */ scope.findTarget = target.findTarget.bind(target); /** * Determines if the "container" node deeply contains the "containee" node, including situations where the "containee" is contained by one or more ShadowDOM * roots. * * @param {Node} container * @param {Node} containee * @return {Boolean} */ scope.deepContains = target.deepContains.bind(target); /** * Determines if the x/y position is inside the given node. * * Example: * * function upHandler(event) { * var innode = PolymerGestures.insideNode(event.target, event.clientX, event.clientY); * if (innode) { * // wait for tap? * } else { * // tap will never happen * } * } * * @param {Node} node * @param {Number} x Screen X position * @param {Number} y screen Y position * @return {Boolean} */ scope.insideNode = target.insideNode; })(window.PolymerGestures); (function() { function shadowSelector(v) { return 'html /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' ] }, 'manipulation' ]; var styles = ''; // only install stylesheet if the browser has touch action support var hasTouchAction = typeof document.head.style.touchAction === 'string'; // only add shadow selectors if shadowdom is supported var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot; if (hasTouchAction) { 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); } })(); /** * 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` and initialized with properties from `inDict`. */ (function(scope) { 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 ]; var NOP_FACTORY = function(){ return function(){}; }; var eventFactory = { // TODO(dfreedm): this is overridden by tap recognizer, needs review preventTap: NOP_FACTORY, makeBaseEvent: function(inType, inDict) { var e = document.createEvent('Event'); e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false); e.preventTap = eventFactory.preventTap(e); return e; }, makeGestureEvent: function(inType, inDict) { inDict = inDict || Object.create(null); var e = this.makeBaseEvent(inType, inDict); for (var i = 0, keys = Object.keys(inDict), k; i < keys.length; i++) { k = keys[i]; if( k !== 'bubbles' && k !== 'cancelable' ) { e[k] = inDict[k]; } } return e; }, makePointerEvent: function(inType, inDict) { inDict = inDict || Object.create(null); var e = this.makeBaseEvent(inType, inDict); // define inherited MouseEvent properties 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) { 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.pointerType = inDict.pointerType || ''; e.hwTimestamp = inDict.hwTimestamp || 0; e.isPrimary = inDict.isPrimary || false; e._source = inDict._source || ''; return e; } }; scope.eventFactory = eventFactory; })(window.PolymerGestures); /** * This module implements an map of pointer states */ (function(scope) { var USE_MAP = window.Map && window.Map.prototype.forEach; var POINTERS_FN = function(){ return this.size; }; function PointerMap() { if (USE_MAP) { var m = new Map(); m.pointers = POINTERS_FN; return m; } else { this.keys = []; this.values = []; } } PointerMap.prototype = { set: function(inId, inEvent) { var i = this.keys.indexOf(inId); if (i > -1) { this.values[i] = inEvent; } else { this.keys.push(inId); this.values.push(inEvent); } }, has: function(inId) { return this.keys.indexOf(inId) > -1; }, 'delete': function(inId) { var i = this.keys.indexOf(inId); if (i > -1) { this.keys.splice(i, 1); this.values.splice(i, 1); } }, get: function(inId) { var i = this.keys.indexOf(inId); return this.values[i]; }, clear: function() { this.keys.length = 0; this.values.length = 0; }, // return value, key, map forEach: function(callback, thisArg) { this.values.forEach(function(v, i) { callback.call(thisArg, v, this.keys[i], this); }, this); }, pointers: function() { return this.keys.length; } }; scope.PointerMap = PointerMap; })(window.PolymerGestures); (function(scope) { 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', // gesture addons 'preventTap', 'tapPrevented', '_source' ]; 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, function(){}, false ]; var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined'); var eventFactory = scope.eventFactory; // set of recognizers to run for the currently handled event var currentGestures; /** * 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 = { IS_IOS: false, pointermap: new scope.PointerMap(), requiredGestures: new scope.PointerMap(), eventMap: Object.create(null), // Scope objects for native events. // This exists for ease of testing. eventSources: Object.create(null), eventSourceList: [], gestures: [], // map gesture event -> {listeners: int, index: gestures[int]} dependencyMap: { // make sure down and up are in the map to trigger "register" down: {listeners: 0, index: -1}, up: {listeners: 0, index: -1} }, gestureQueue: [], /** * 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); } }, registerGesture: function(name, source) { var obj = Object.create(null); obj.listeners = 0; obj.index = this.gestures.length; for (var i = 0, g; i < source.exposes.length; i++) { g = source.exposes[i].toLowerCase(); this.dependencyMap[g] = obj; } this.gestures.push(source); }, register: function(element, initial) { 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, initial); } }, 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); } }, // EVENTS down: function(inEvent) { this.requiredGestures.set(inEvent.pointerId, currentGestures); this.fireEvent('down', inEvent); }, move: function(inEvent) { // pipe move events into gesture queue directly inEvent.type = 'move'; this.fillGestureQueue(inEvent); }, up: function(inEvent) { this.fireEvent('up', inEvent); this.requiredGestures.delete(inEvent.pointerId); }, cancel: function(inEvent) { inEvent.tapPrevented = true; this.fireEvent('up', inEvent); this.requiredGestures.delete(inEvent.pointerId); }, addGestureDependency: function(node, currentGestures) { var gesturesWanted = node._pgEvents; if (gesturesWanted && currentGestures) { var gk = Object.keys(gesturesWanted); for (var i = 0, r, ri, g; i < gk.length; i++) { // gesture g = gk[i]; if (gesturesWanted[g] > 0) { // lookup gesture recognizer r = this.dependencyMap[g]; // recognizer index ri = r ? r.index : -1; currentGestures[ri] = true; } } } }, // LISTENER LOGIC eventHandler: function(inEvent) { // This is used to prevent multiple dispatch of events 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. var type = inEvent.type; // only generate the list of desired events on "down" if (type === 'touchstart' || type === 'mousedown' || type === 'pointerdown' || type === 'MSPointerDown') { if (!inEvent._handledByPG) { currentGestures = {}; } // in IOS mode, there is only a listener on the document, so this is not re-entrant if (this.IS_IOS) { var ev = inEvent; if (type === 'touchstart') { var ct = inEvent.changedTouches[0]; // set up a fake event to give to the path builder ev = {target: inEvent.target, clientX: ct.clientX, clientY: ct.clientY, path: inEvent.path}; } // use event path if available, otherwise build a path from target finding var nodes = inEvent.path || scope.targetFinding.path(ev); for (var i = 0, n; i < nodes.length; i++) { n = nodes[i]; this.addGestureDependency(n, currentGestures); } } else { this.addGestureDependency(inEvent.currentTarget, currentGestures); } } if (inEvent._handledByPG) { return; } var fn = this.eventMap && this.eventMap[type]; if (fn) { fn(inEvent); } inEvent._handledByPG = true; }, // set up event listeners listen: function(target, events) { for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) { this.addEvent(target, e); } }, // remove event listeners unlisten: function(target, events) { for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) { this.removeEvent(target, e); } }, addEvent: function(target, eventName) { target.addEventListener(eventName, this.boundHandler); }, 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) { var e = eventFactory.makePointerEvent(inType, inEvent); e.preventDefault = inEvent.preventDefault; e.tapPrevented = inEvent.tapPrevented; 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), 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 <use> element that is represented by the instance for Safari, Chrome, IE. // This is the behavior implemented by Firefox. if (p === 'target' || p === 'relatedTarget') { if (HAS_SVG_INSTANCE && eventCopy[p] instanceof SVGElementInstance) { eventCopy[p] = eventCopy[p].correspondingUseElement; } } } // keep the semantics of preventDefault eventCopy.preventDefault = function() { inEvent.preventDefault(); }; return eventCopy; }, /** * 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: function(inEvent) { var t = inEvent._target; if (t) { t.dispatchEvent(inEvent); // clone the event for the gesture system to process // clone after dispatch to pick up gesture prevention code var clone = this.cloneEvent(inEvent); clone.target = t; this.fillGestureQueue(clone); } }, gestureTrigger: function() { // process the gesture queue for (var i = 0, e, rg; i < this.gestureQueue.length; i++) { e = this.gestureQueue[i]; rg = e._requiredGestures; if (rg) { for (var j = 0, g, fn; j < this.gestures.length; j++) { // only run recognizer if an element in the source event's path is listening for those gestures if (rg[j]) { g = this.gestures[j]; fn = g[e.type]; if (fn) { fn.call(g, e); } } } } } this.gestureQueue.length = 0; }, fillGestureQueue: function(ev) { // only trigger the gesture queue once if (!this.gestureQueue.length) { requestAnimationFrame(this.boundGestureTrigger); } ev._requiredGestures = this.requiredGestures.get(ev.pointerId); this.gestureQueue.push(ev); } }; dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher); dispatcher.boundGestureTrigger = dispatcher.gestureTrigger.bind(dispatcher); scope.dispatcher = dispatcher; /** * Listen for `gesture` on `node` with the `handler` function * * If `handler` is the first listener for `gesture`, the underlying gesture recognizer is then enabled. * * @param {Element} node * @param {string} gesture * @return Boolean `gesture` is a valid gesture */ scope.activateGesture = function(node, gesture) { var g = gesture.toLowerCase(); var dep = dispatcher.dependencyMap[g]; if (dep) { var recognizer = dispatcher.gestures[dep.index]; if (!node._pgListeners) { dispatcher.register(node); node._pgListeners = 0; } // TODO(dfreedm): re-evaluate bookkeeping to avoid using attributes if (recognizer) { var touchAction = recognizer.defaultActions && recognizer.defaultActions[g]; var actionNode; switch(node.nodeType) { case Node.ELEMENT_NODE: actionNode = node; break; case Node.DOCUMENT_FRAGMENT_NODE: actionNode = node.host; break; default: actionNode = null; break; } if (touchAction && actionNode && !actionNode.hasAttribute('touch-action')) { actionNode.setAttribute('touch-action', touchAction); } } if (!node._pgEvents) { node._pgEvents = {}; } node._pgEvents[g] = (node._pgEvents[g] || 0) + 1; node._pgListeners++; } return Boolean(dep); }; /** * * Listen for `gesture` from `node` with `handler` function. * * @param {Element} node * @param {string} gesture * @param {Function} handler * @param {Boolean} capture */ scope.addEventListener = function(node, gesture, handler, capture) { if (handler) { scope.activateGesture(node, gesture); node.addEventListener(gesture, handler, capture); } }; /** * Tears down the gesture configuration for `node` * * If `handler` is the last listener for `gesture`, the underlying gesture recognizer is disabled. * * @param {Element} node * @param {string} gesture * @return Boolean `gesture` is a valid gesture */ scope.deactivateGesture = function(node, gesture) { var g = gesture.toLowerCase(); var dep = dispatcher.dependencyMap[g]; if (dep) { if (node._pgListeners > 0) { node._pgListeners--; } if (node._pgListeners === 0) { dispatcher.unregister(node); } if (node._pgEvents) { if (node._pgEvents[g] > 0) { node._pgEvents[g]--; } else { node._pgEvents[g] = 0; } } } return Boolean(dep); }; /** * Stop listening for `gesture` from `node` with `handler` function. * * @param {Element} node * @param {string} gesture * @param {Function} handler * @param {Boolean} capture */ scope.removeEventListener = function(node, gesture, handler, capture) { if (handler) { scope.deactivateGesture(node, gesture); node.removeEventListener(gesture, handler, capture); } }; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var pointermap = dispatcher.pointermap; // radius around touchend that swallows mouse events var DEDUP_DIST = 25; var WHICH_TO_BUTTONS = [0, 1, 4, 2]; var currentButtons = 0; var FIREFOX_LINUX = /Linux.*Firefox\//i; var HAS_BUTTONS = (function() { // firefox on linux returns spec-incorrect values for mouseup.buttons // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.buttons#See_also // https://codereview.chromium.org/727593003/#msg16 if (FIREFOX_LINUX.test(navigator.userAgent)) { return false; } try { return new MouseEvent('test', {buttons: 1}).buttons === 1; } catch (e) { return false; } })(); // handler block for native mouse events var mouseEvents = { POINTER_ID: 1, POINTER_TYPE: 'mouse', events: [ 'mousedown', 'mousemove', 'mouseup' ], exposes: [ 'down', 'up', 'move' ], register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { if (target.nodeType === Node.DOCUMENT_NODE) { return; } dispatcher.unlisten(target, this.events); }, lastTouches: [], // collide with the global mouse listener isEventSimulatedFromTouch: function(inEvent) { var lts = this.lastTouches; var x = inEvent.clientX, 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), dy = Math.abs(y - t.y); if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { return true; } } }, prepareEvent: function(inEvent) { var e = dispatcher.cloneEvent(inEvent); e.pointerId = this.POINTER_ID; e.isPrimary = true; e.pointerType = this.POINTER_TYPE; e._source = 'mouse'; if (!HAS_BUTTONS) { var type = inEvent.type; var bit = WHICH_TO_BUTTONS[inEvent.which] || 0; if (type === 'mousedown') { currentButtons |= bit; } else if (type === 'mouseup') { currentButtons &= ~bit; } e.buttons = currentButtons; } return e; }, mousedown: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var p = pointermap.has(this.POINTER_ID); var e = this.prepareEvent(inEvent); e.target = scope.findTarget(inEvent); pointermap.set(this.POINTER_ID, e.target); dispatcher.down(e); } }, mousemove: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var target = pointermap.get(this.POINTER_ID); if (target) { var e = this.prepareEvent(inEvent); e.target = target; // handle case where we missed a mouseup if ((HAS_BUTTONS ? e.buttons : e.which) === 0) { if (!HAS_BUTTONS) { currentButtons = e.buttons = 0; } dispatcher.cancel(e); this.cleanupMouse(e.buttons); } else { dispatcher.move(e); } } } }, mouseup: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(this.POINTER_ID); dispatcher.up(e); this.cleanupMouse(e.buttons); } }, cleanupMouse: function(buttons) { if (buttons === 0) { pointermap.delete(this.POINTER_ID); } } }; scope.mouseEvents = mouseEvents; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var allShadows = scope.targetFinding.allShadows.bind(scope.targetFinding); var pointermap = dispatcher.pointermap; var touchMap = Array.prototype.map.call.bind(Array.prototype.map); // This should be long enough to ignore compat mouse events made by touch var DEDUP_TIMEOUT = 2500; var DEDUP_DIST = 25; var CLICK_COUNT_TIMEOUT = 200; var HYSTERESIS = 20; var ATTRIB = 'touch-action'; // TODO(dfreedm): disable until http://crbug.com/399765 is resolved // var HAS_TOUCH_ACTION = ATTRIB in document.head.style; var HAS_TOUCH_ACTION = false; // handler block for native touch events var touchEvents = { IS_IOS: false, events: [ 'touchstart', 'touchmove', 'touchend', 'touchcancel' ], exposes: [ 'down', 'up', 'move' ], register: function(target, initial) { if (this.IS_IOS ? initial : !initial) { dispatcher.listen(target, this.events); } }, unregister: function(target) { if (!this.IS_IOS) { dispatcher.unlisten(target, this.events); } }, scrollTypes: { EMITTER: 'none', XSCROLLER: 'pan-x', YSCROLLER: 'pan-y', }, touchActionToScrollType: function(touchAction) { var t = touchAction; var st = this.scrollTypes; if (t === st.EMITTER) { return 'none'; } else if (t === st.XSCROLLER) { return 'X'; } else if (t === st.YSCROLLER) { return 'Y'; } else { 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.pointers() === 0 || (pointermap.pointers() === 1 && pointermap.has(1))) { this.firstTouch = inTouch.identifier; this.firstXY = {X: inTouch.clientX, Y: inTouch.clientY}; this.firstTarget = inTouch.target; this.scrolling = null; 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; }, findTarget: function(touch, id) { if (this.currentTouchEvent.type === 'touchstart') { if (this.isPrimaryTouch(touch)) { var fastPath = { clientX: touch.clientX, clientY: touch.clientY, path: this.currentTouchEvent.path, target: this.currentTouchEvent.target }; return scope.findTarget(fastPath); } else { return scope.findTarget(touch); } } // reuse target we found in touchstart return pointermap.get(id); }, touchToPointer: function(inTouch) { var cte = this.currentTouchEvent; var e = dispatcher.cloneEvent(inTouch); // Spec specifies that pointerId 1 is reserved 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 = this.findTarget(inTouch, id); e.bubbles = true; e.cancelable = true; e.detail = this.clickCount; e.buttons = this.typeToButtons(cte.type); e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0; e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0; e.pressure = inTouch.webkitForce || inTouch.force || 0.5; e.isPrimary = this.isPrimaryTouch(inTouch); e.pointerType = this.POINTER_TYPE; e._source = 'touch'; // 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, p; i < tl.length; i++) { t = tl[i]; p = this.touchToPointer(t); if (inEvent.type === 'touchstart') { pointermap.set(p.pointerId, p.target); } if (pointermap.has(p.pointerId)) { inFunction.call(this, p); } if (inEvent.type === 'touchend' || inEvent._cancel) { this.cleanUpPointer(p); } } }, // 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 touchAction = scope.targetFinding.findTouchAction(inEvent); var scrollAxis = this.touchActionToScrollType(touchAction); 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; } 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.pointers() should be < tl.length here, as the touchstart has not // been processed yet. if (pointermap.pointers() >= tl.length) { var d = []; pointermap.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; d.push(p); } }, this); d.forEach(function(p) { this.cancel(p); pointermap.delete(p.pointerId); }, this); } }, touchstart: function(inEvent) { this.vacuumTouches(inEvent); this.setPrimaryTouch(inEvent.changedTouches[0]); this.dedupSynthMouse(inEvent); if (!this.scrolling) { this.clickCount++; this.processTouches(inEvent, this.down); } }, down: function(inPointer) { dispatcher.down(inPointer); }, touchmove: function(inEvent) { if (HAS_TOUCH_ACTION) { // touchevent.cancelable == false is sent when the page is scrolling under native Touch Action in Chrome 36 // https://groups.google.com/a/chromium.org/d/msg/input-dev/wHnyukcYBcA/b9kmtwM1jJQJ if (inEvent.cancelable) { this.processTouches(inEvent, this.move); } } else { if (!this.scrolling) { if (this.scrolling === null && this.shouldScroll(inEvent)) { this.scrolling = true; } else { this.scrolling = false; inEvent.preventDefault(); this.processTouches(inEvent, this.move); } } else if (this.firstXY) { var t = inEvent.changedTouches[0]; var dx = t.clientX - this.firstXY.X; var dy = t.clientY - this.firstXY.Y; var dd = Math.sqrt(dx * dx + dy * dy); if (dd >= HYSTERESIS) { this.touchcancel(inEvent); this.scrolling = true; this.firstXY = null; } } } }, move: function(inPointer) { dispatcher.move(inPointer); }, touchend: function(inEvent) { this.dedupSynthMouse(inEvent); this.processTouches(inEvent, this.up); }, up: function(inPointer) { inPointer.relatedTarget = scope.findTarget(inPointer); dispatcher.up(inPointer); }, cancel: function(inPointer) { dispatcher.cancel(inPointer); }, touchcancel: function(inEvent) { inEvent._cancel = true; this.processTouches(inEvent, this.cancel); }, cleanUpPointer: function(inPointer) { pointermap['delete'](inPointer.pointerId); this.removePrimaryPointer(inPointer); }, // prevent synth mouse events from creating pointer events dedupSynthMouse: function(inEvent) { var lts = scope.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); } } }; // prevent "ghost clicks" that come from elements that were removed in a touch handler var STOP_PROP_FN = Event.prototype.stopImmediatePropagation || Event.prototype.stopPropagation; document.addEventListener('click', function(ev) { var x = ev.clientX, y = ev.clientY; // check if a click is within DEDUP_DIST px radius of the touchstart var closeTo = function(touch) { var dx = Math.abs(x - touch.x), dy = Math.abs(y - touch.y); return (dx <= DEDUP_DIST && dy <= DEDUP_DIST); }; // if click coordinates are close to touch coordinates, assume the click came from a touch var wasTouched = scope.mouseEvents.lastTouches.some(closeTo); // if the click came from touch, and the touchstart target is not in the path of the click event, // then the touchstart target was probably removed, and the click should be "busted" var path = scope.targetFinding.path(ev); if (wasTouched) { for (var i = 0; i < path.length; i++) { if (path[i] === touchEvents.firstTarget) { return; } } ev.preventDefault(); STOP_PROP_FN.call(ev); } }, true); scope.touchEvents = touchEvents; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var pointermap = dispatcher.pointermap; var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number'; var msEvents = { events: [ 'MSPointerDown', 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel', ], register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { if (target.nodeType === Node.DOCUMENT_NODE) { return; } dispatcher.unlisten(target, this.events); }, POINTER_TYPES: [ '', 'unavailable', 'touch', 'pen', 'mouse' ], prepareEvent: function(inEvent) { var e = inEvent; e = dispatcher.cloneEvent(inEvent); if (HAS_BITMAP_TYPE) { e.pointerType = this.POINTER_TYPES[inEvent.pointerType]; } e._source = 'ms'; return e; }, cleanup: function(id) { pointermap['delete'](id); }, MSPointerDown: function(inEvent) { var e = this.prepareEvent(inEvent); e.target = scope.findTarget(inEvent); pointermap.set(inEvent.pointerId, e.target); dispatcher.down(e); }, MSPointerMove: function(inEvent) { var target = pointermap.get(inEvent.pointerId); if (target) { var e = this.prepareEvent(inEvent); e.target = target; dispatcher.move(e); } }, MSPointerUp: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.up(e); this.cleanup(inEvent.pointerId); }, MSPointerCancel: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.cancel(e); this.cleanup(inEvent.pointerId); } }; scope.msEvents = msEvents; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var pointermap = dispatcher.pointermap; var pointerEvents = { events: [ 'pointerdown', 'pointermove', 'pointerup', 'pointercancel' ], prepareEvent: function(inEvent) { var e = dispatcher.cloneEvent(inEvent); e._source = 'pointer'; return e; }, register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { if (target.nodeType === Node.DOCUMENT_NODE) { return; } dispatcher.unlisten(target, this.events); }, cleanup: function(id) { pointermap['delete'](id); }, pointerdown: function(inEvent) { var e = this.prepareEvent(inEvent); e.target = scope.findTarget(inEvent); pointermap.set(e.pointerId, e.target); dispatcher.down(e); }, pointermove: function(inEvent) { var target = pointermap.get(inEvent.pointerId); if (target) { var e = this.prepareEvent(inEvent); e.target = target; dispatcher.move(e); } }, pointerup: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.up(e); this.cleanup(inEvent.pointerId); }, pointercancel: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.cancel(e); this.cleanup(inEvent.pointerId); } }; scope.pointerEvents = pointerEvents; })(window.PolymerGestures); /** * This module contains the handlers for native platform events. * From here, the dispatcher is called to create unified pointer events. * Included are touch events (v1), mouse events, and MSPointerEvents. */ (function(scope) { var dispatcher = scope.dispatcher; var nav = window.navigator; if (window.PointerEvent) { dispatcher.registerSource('pointer', scope.pointerEvents); } else if (nav.msPointerEnabled) { dispatcher.registerSource('ms', scope.msEvents); } else { dispatcher.registerSource('mouse', scope.mouseEvents); if (window.ontouchstart !== undefined) { dispatcher.registerSource('touch', scope.touchEvents); } } // Work around iOS bugs https://bugs.webkit.org/show_bug.cgi?id=135628 and https://bugs.webkit.org/show_bug.cgi?id=136506 var ua = navigator.userAgent; var IS_IOS = ua.match(/iPad|iPhone|iPod/) && 'ontouchstart' in window; dispatcher.IS_IOS = IS_IOS; scope.touchEvents.IS_IOS = IS_IOS; dispatcher.register(document, true); })(window.PolymerGestures); /** * This event denotes the beginning of a series of tracking events. * * @module PointerGestures * @submodule Events * @class trackstart */ /** * Pixels moved in the x direction since trackstart. * @type Number * @property dx */ /** * Pixes moved in the y direction since trackstart. * @type Number * @property dy */ /** * Pixels moved in the x direction since the last track. * @type Number * @property ddx */ /** * Pixles moved in the y direction since the last track. * @type Number * @property ddy */ /** * The clientX position of the track gesture. * @type Number * @property clientX */ /** * The clientY position of the track gesture. * @type Number * @property clientY */ /** * The pageX position of the track gesture. * @type Number * @property pageX */ /** * The pageY position of the track gesture. * @type Number * @property pageY */ /** * The screenX position of the track gesture. * @type Number * @property screenX */ /** * The screenY position of the track gesture. * @type Number * @property screenY */ /** * The last x axis direction of the pointer. * @type Number * @property xDirection */ /** * The last y axis direction of the pointer. * @type Number * @property yDirection */ /** * A shared object between all tracking events. * @type Object * @property trackInfo */ /** * The element currently under the pointer. * @type Element * @property relatedTarget */ /** * The type of pointer that make the track gesture. * @type String * @property pointerType */ /** * * This event fires for all pointer movement being tracked. * * @class track * @extends trackstart */ /** * This event fires when the pointer is no longer being tracked. * * @class trackend * @extends trackstart */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var pointermap = new scope.PointerMap(); var track = { events: [ 'down', 'move', 'up', ], exposes: [ 'trackstart', 'track', 'trackx', 'tracky', 'trackend' ], defaultActions: { 'track': 'none', 'trackx': 'pan-y', 'tracky': 'pan-x' }, WIGGLE_THRESHOLD: 4, clampDir: function(inDelta) { return inDelta > 0 ? 1 : -1; }, calcPositionDelta: function(inA, inB) { var x = 0, y = 0; if (inA && inB) { x = inB.pageX - inA.pageX; y = inB.pageY - inA.pageY; } return {x: x, y: y}; }, fireTrack: function(inType, inEvent, inTrackingData) { var t = inTrackingData; var d = this.calcPositionDelta(t.downEvent, inEvent); var dd = this.calcPositionDelta(t.lastMoveEvent, inEvent); if (dd.x) { t.xDirection = this.clampDir(dd.x); } else if (inType === 'trackx') { return; } if (dd.y) { t.yDirection = this.clampDir(dd.y); } else if (inType === 'tracky') { return; } var gestureProto = { bubbles: true, cancelable: true, trackInfo: t.trackInfo, relate