/** * @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 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, relatedTarget: inEvent.relatedTarget, pointerType: inEvent.pointerType, pointerId: inEvent.pointerId, _source: 'track' }; if (inType !== 'tracky') { gestureProto.x = inEvent.x; gestureProto.dx = d.x; gestureProto.ddx = dd.x; gestureProto.clientX = inEvent.clientX; gestureProto.pageX = inEvent.pageX; gestureProto.screenX = inEvent.screenX; gestureProto.xDirection = t.xDirection; } if (inType !== 'trackx') { gestureProto.dy = d.y; gestureProto.ddy = dd.y; gestureProto.y = inEvent.y; gestureProto.clientY = inEvent.clientY; gestureProto.pageY = inEvent.pageY; gestureProto.screenY = inEvent.screenY; gestureProto.yDirection = t.yDirection; } var e = eventFactory.makeGestureEvent(inType, gestureProto); t.downTarget.dispatchEvent(e); }, down: function(inEvent) { if (inEvent.isPrimary && (inEvent.pointerType === 'mouse' ? inEvent.buttons === 1 : true)) { var p = { downEvent: inEvent, downTarget: inEvent.target, trackInfo: {}, lastMoveEvent: null, xDirection: 0, yDirection: 0, tracking: false }; pointermap.set(inEvent.pointerId, p); } }, move: function(inEvent) { var p = pointermap.get(inEvent.pointerId); if (p) { if (!p.tracking) { var d = this.calcPositionDelta(p.downEvent, inEvent); var move = d.x * d.x + d.y * d.y; // start tracking only if finger moves more than WIGGLE_THRESHOLD if (move > this.WIGGLE_THRESHOLD) { p.tracking = true; p.lastMoveEvent = p.downEvent; this.fireTrack('trackstart', inEvent, p); } } if (p.tracking) { this.fireTrack('track', inEvent, p); this.fireTrack('trackx', inEvent, p); this.fireTrack('tracky', inEvent, p); } p.lastMoveEvent = inEvent; } }, up: function(inEvent) { var p = pointermap.get(inEvent.pointerId); if (p) { if (p.tracking) { this.fireTrack('trackend', inEvent, p); } pointermap.delete(inEvent.pointerId); } } }; dispatcher.registerGesture('track', track); })(window.PolymerGestures); /** * This event is fired when a pointer is held down for 200ms. * * @module PointerGestures * @submodule Events * @class hold */ /** * Type of pointer that made the holding event. * @type String * @property pointerType */ /** * Screen X axis position of the held pointer * @type Number * @property clientX */ /** * Screen Y axis position of the held pointer * @type Number * @property clientY */ /** * Type of pointer that made the holding event. * @type String * @property pointerType */ /** * This event is fired every 200ms while a pointer is held down. * * @class holdpulse * @extends hold */ /** * Milliseconds pointer has been held down. * @type Number * @property holdTime */ /** * This event is fired when a held pointer is released or moved. * * @class release */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var hold = { // wait at least HOLD_DELAY ms between hold and pulse events HOLD_DELAY: 200, // pointer can move WIGGLE_THRESHOLD pixels before not counting as a hold WIGGLE_THRESHOLD: 16, events: [ 'down', 'move', 'up', ], exposes: [ 'hold', 'holdpulse', 'release' ], heldPointer: null, holdJob: null, pulse: function() { var hold = Date.now() - this.heldPointer.timeStamp; var type = this.held ? 'holdpulse' : 'hold'; this.fireHold(type, hold); this.held = true; }, cancel: function() { clearInterval(this.holdJob); if (this.held) { this.fireHold('release'); } this.held = false; this.heldPointer = null; this.target = null; this.holdJob = null; }, down: function(inEvent) { if (inEvent.isPrimary && !this.heldPointer) { this.heldPointer = inEvent; this.target = inEvent.target; this.holdJob = setInterval(this.pulse.bind(this), this.HOLD_DELAY); } }, up: function(inEvent) { if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { this.cancel(); } }, move: function(inEvent) { if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { var x = inEvent.clientX - this.heldPointer.clientX; var y = inEvent.clientY - this.heldPointer.clientY; if ((x * x + y * y) > this.WIGGLE_THRESHOLD) { this.cancel(); } } }, fireHold: function(inType, inHoldTime) { var p = { bubbles: true, cancelable: true, pointerType: this.heldPointer.pointerType, pointerId: this.heldPointer.pointerId, x: this.heldPointer.clientX, y: this.heldPointer.clientY, _source: 'hold' }; if (inHoldTime) { p.holdTime = inHoldTime; } var e = eventFactory.makeGestureEvent(inType, p); this.target.dispatchEvent(e); } }; dispatcher.registerGesture('hold', hold); })(window.PolymerGestures); /** * This event is fired when a pointer quickly goes down and up, and is used to * denote activation. * * Any gesture event can prevent the tap event from being created by calling * `event.preventTap`. * * Any pointer event can prevent the tap by setting the `tapPrevented` property * on itself. * * @module PointerGestures * @submodule Events * @class tap */ /** * X axis position of the tap. * @property x * @type Number */ /** * Y axis position of the tap. * @property y * @type Number */ /** * Type of the pointer that made the tap. * @property pointerType * @type String */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var pointermap = new scope.PointerMap(); var tap = { events: [ 'down', 'up' ], exposes: [ 'tap' ], down: function(inEvent) { if (inEvent.isPrimary && !inEvent.tapPrevented) { pointermap.set(inEvent.pointerId, { target: inEvent.target, buttons: inEvent.buttons, x: inEvent.clientX, y: inEvent.clientY }); } }, shouldTap: function(e, downState) { var tap = true; if (e.pointerType === 'mouse') { // only allow left click to tap for mouse tap = (e.buttons ^ 1) && (downState.buttons & 1); } return tap && !e.tapPrevented; }, up: function(inEvent) { var start = pointermap.get(inEvent.pointerId); if (start && this.shouldTap(inEvent, start)) { // up.relatedTarget is target currently under finger var t = scope.targetFinding.LCA(start.target, inEvent.relatedTarget); if (t) { var e = eventFactory.makeGestureEvent('tap', { bubbles: true, cancelable: true, x: inEvent.clientX, y: inEvent.clientY, detail: inEvent.detail, pointerType: inEvent.pointerType, pointerId: inEvent.pointerId, altKey: inEvent.altKey, ctrlKey: inEvent.ctrlKey, metaKey: inEvent.metaKey, shiftKey: inEvent.shiftKey, _source: 'tap' }); t.dispatchEvent(e); } } pointermap.delete(inEvent.pointerId); } }; // patch eventFactory to remove id from tap's pointermap for preventTap calls eventFactory.preventTap = function(e) { return function() { e.tapPrevented = true; pointermap.delete(e.pointerId); }; }; dispatcher.registerGesture('tap', tap); })(window.PolymerGestures); /* * Basic strategy: find the farthest apart points, use as diameter of circle * react to size change and rotation of the chord */ /** * @module pointer-gestures * @submodule Events * @class pinch */ /** * Scale of the pinch zoom gesture * @property scale * @type Number */ /** * Center X position of pointers causing pinch * @property centerX * @type Number */ /** * Center Y position of pointers causing pinch * @property centerY * @type Number */ /** * @module pointer-gestures * @submodule Events * @class rotate */ /** * Angle (in degrees) of rotation. Measured from starting positions of pointers. * @property angle * @type Number */ /** * Center X position of pointers causing rotation * @property centerX * @type Number */ /** * Center Y position of pointers causing rotation * @property centerY * @type Number */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var pointermap = new scope.PointerMap(); var RAD_TO_DEG = 180 / Math.PI; var pinch = { events: [ 'down', 'up', 'move', 'cancel' ], exposes: [ 'pinchstart', 'pinch', 'pinchend', 'rotate' ], defaultActions: { 'pinch': 'none', 'rotate': 'none' }, reference: {}, down: function(inEvent) { pointermap.set(inEvent.pointerId, inEvent); if (pointermap.pointers() == 2) { var points = this.calcChord(); var angle = this.calcAngle(points); this.reference = { angle: angle, diameter: points.diameter, target: scope.targetFinding.LCA(points.a.target, points.b.target) }; this.firePinch('pinchstart', points.diameter, points); } }, up: function(inEvent) { var p = pointermap.get(inEvent.pointerId); var num = pointermap.pointers(); if (p) { if (num === 2) { // fire 'pinchend' before deleting pointer var points = this.calcChord(); this.firePinch('pinchend', points.diameter, points); } pointermap.delete(inEvent.pointerId); } }, move: function(inEvent) { if (pointermap.has(inEvent.pointerId)) { pointermap.set(inEvent.pointerId, inEvent); if (pointermap.pointers() > 1) { this.calcPinchRotate(); } } }, cancel: function(inEvent) { this.up(inEvent); }, firePinch: function(type, diameter, points) { var zoom = diameter / this.reference.diameter; var e = eventFactory.makeGestureEvent(type, { bubbles: true, cancelable: true, scale: zoom, centerX: points.center.x, centerY: points.center.y, _source: 'pinch' }); this.reference.target.dispatchEvent(e); }, fireRotate: function(angle, points) { var diff = Math.round((angle - this.reference.angle) % 360); var e = eventFactory.makeGestureEvent('rotate', { bubbles: true, cancelable: true, angle: diff, centerX: points.center.x, centerY: points.center.y, _source: 'pinch' }); this.reference.target.dispatchEvent(e); }, calcPinchRotate: function() { var points = this.calcChord(); var diameter = points.diameter; var angle = this.calcAngle(points); if (diameter != this.reference.diameter) { this.firePinch('pinch', diameter, points); } if (angle != this.reference.angle) { this.fireRotate(angle, points); } }, calcChord: function() { var pointers = []; pointermap.forEach(function(p) { pointers.push(p); }); var dist = 0; // start with at least two pointers var points = {a: pointers[0], b: pointers[1]}; var x, y, d; for (var i = 0; i < pointers.length; i++) { var a = pointers[i]; for (var j = i + 1; j < pointers.length; j++) { var b = pointers[j]; x = Math.abs(a.clientX - b.clientX); y = Math.abs(a.clientY - b.clientY); d = x + y; if (d > dist) { dist = d; points = {a: a, b: b}; } } } x = Math.abs(points.a.clientX + points.b.clientX) / 2; y = Math.abs(points.a.clientY + points.b.clientY) / 2; points.center = { x: x, y: y }; points.diameter = dist; return points; }, calcAngle: function(points) { var x = points.a.clientX - points.b.clientX; var y = points.a.clientY - points.b.clientY; return (360 + Math.atan2(y, x) * RAD_TO_DEG) % 360; } }; dispatcher.registerGesture('pinch', pinch); })(window.PolymerGestures); (function (global) { 'use strict'; var Token, TokenName, Syntax, Messages, source, index, length, delegate, lookahead, state; Token = { BooleanLiteral: 1, EOF: 2, Identifier: 3, Keyword: 4, NullLiteral: 5, NumericLiteral: 6, Punctuator: 7, StringLiteral: 8 }; TokenName = {}; TokenName[Token.BooleanLiteral] = 'Boolean'; TokenName[Token.EOF] = ''; TokenName[Token.Identifier] = 'Identifier'; TokenName[Token.Keyword] = 'Keyword'; TokenName[Token.NullLiteral] = 'Null'; TokenName[Token.NumericLiteral] = 'Numeric'; TokenName[Token.Punctuator] = 'Punctuator'; TokenName[Token.StringLiteral] = 'String'; Syntax = { ArrayExpression: 'ArrayExpression', BinaryExpression: 'BinaryExpression', CallExpression: 'CallExpression', ConditionalExpression: 'ConditionalExpression', EmptyStatement: 'EmptyStatement', ExpressionStatement: 'ExpressionStatement', Identifier: 'Identifier', Literal: 'Literal', LabeledStatement: 'LabeledStatement', LogicalExpression: 'LogicalExpression', MemberExpression: 'MemberExpression', ObjectExpression: 'ObjectExpression', Program: 'Program', Property: 'Property', ThisExpression: 'ThisExpression', UnaryExpression: 'UnaryExpression' }; // Error messages should be identical to V8. Messages = { UnexpectedToken: 'Unexpected token %0', UnknownLabel: 'Undefined label \'%0\'', Redeclaration: '%0 \'%1\' has already been declared' }; // Ensure the condition is true, otherwise throw an error. // This is only to have a better contract semantic, i.e. another safety net // to catch a logic error. The condition shall be fulfilled in normal case. // Do NOT use this to enforce a certain condition on any user input. function assert(condition, message) { if (!condition) { throw new Error('ASSERT: ' + message); } } function isDecimalDigit(ch) { return (ch >= 48 && ch <= 57); // 0..9 } // 7.2 White Space function isWhiteSpace(ch) { return (ch === 32) || // space (ch === 9) || // tab (ch === 0xB) || (ch === 0xC) || (ch === 0xA0) || (ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0); } // 7.3 Line Terminators function isLineTerminator(ch) { return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029); } // 7.6 Identifier Names and Identifiers function isIdentifierStart(ch) { return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) (ch >= 65 && ch <= 90) || // A..Z (ch >= 97 && ch <= 122); // a..z } function isIdentifierPart(ch) { return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) (ch >= 65 && ch <= 90) || // A..Z (ch >= 97 && ch <= 122) || // a..z (ch >= 48 && ch <= 57); // 0..9 } // 7.6.1.1 Keywords function isKeyword(id) { return (id === 'this') } // 7.4 Comments function skipWhitespace() { while (index < length && isWhiteSpace(source.charCodeAt(index))) { ++index; } } function getIdentifier() { var start, ch; start = index++; while (index < length) { ch = source.charCodeAt(index); if (isIdentifierPart(ch)) { ++index; } else { break; } } return source.slice(start, index); } function scanIdentifier() { var start, id, type; start = index; id = getIdentifier(); // There is no keyword or literal with only one character. // Thus, it must be an identifier. if (id.length === 1) { type = Token.Identifier; } else if (isKeyword(id)) { type = Token.Keyword; } else if (id === 'null') { type = Token.NullLiteral; } else if (id === 'true' || id === 'false') { type = Token.BooleanLiteral; } else { type = Token.Identifier; } return { type: type, value: id, range: [start, index] }; } // 7.7 Punctuators function scanPunctuator() { var start = index, code = source.charCodeAt(index), code2, ch1 = source[index], ch2; switch (code) { // Check for most common single-character punctuators. case 46: // . dot case 40: // ( open bracket case 41: // ) close bracket case 59: // ; semicolon case 44: // , comma case 123: // { open curly brace case 125: // } close curly brace case 91: // [ case 93: // ] case 58: // : case 63: // ? ++index; return { type: Token.Punctuator, value: String.fromCharCode(code), range: [start, index] }; default: code2 = source.charCodeAt(index + 1); // '=' (char #61) marks an assignment or comparison operator. if (code2 === 61) { switch (code) { case 37: // % case 38: // & case 42: // *: case 43: // + case 45: // - case 47: // / case 60: // < case 62: // > case 124: // | index += 2; return { type: Token.Punctuator, value: String.fromCharCode(code) + String.fromCharCode(code2), range: [start, index] }; case 33: // ! case 61: // = index += 2; // !== and === if (source.charCodeAt(index) === 61) { ++index; } return { type: Token.Punctuator, value: source.slice(start, index), range: [start, index] }; default: break; } } break; } // Peek more characters. ch2 = source[index + 1]; // Other 2-character punctuators: && || if (ch1 === ch2 && ('&|'.indexOf(ch1) >= 0)) { index += 2; return { type: Token.Punctuator, value: ch1 + ch2, range: [start, index] }; } if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { ++index; return { type: Token.Punctuator, value: ch1, range: [start, index] }; } throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } // 7.8.3 Numeric Literals function scanNumericLiteral() { var number, start, ch; ch = source[index]; assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'), 'Numeric literal must start with a decimal digit or a decimal point'); start = index; number = ''; if (ch !== '.') { number = source[index++]; ch = source[index]; // Hex number starts with '0x'. // Octal number starts with '0'. if (number === '0') { // decimal number starts with '0' such as '09' is illegal. if (ch && isDecimalDigit(ch.charCodeAt(0))) { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } } while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; } ch = source[index]; } if (ch === '.') { number += source[index++]; while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; } ch = source[index]; } if (ch === 'e' || ch === 'E') { number += source[index++]; ch = source[index]; if (ch === '+' || ch === '-') { number += source[index++]; } if (isDecimalDigit(source.charCodeAt(index))) { while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; } } else { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } } if (isIdentifierStart(source.charCodeAt(index))) { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } return { type: Token.NumericLiteral, value: parseFloat(number), range: [start, index] }; } // 7.8.4 String Literals function scanStringLiteral() { var str = '', quote, start, ch, octal = false; quote = source[index]; assert((quote === '\'' || quote === '"'), 'String literal must starts with a quote'); start = index; ++index; while (index < length) { ch = source[index++]; if (ch === quote) { quote = ''; break; } else if (ch === '\\') { ch = source[index++]; if (!ch || !isLineTerminator(ch.charCodeAt(0))) { switch (ch) { case 'n': str += '\n'; break; case 'r': str += '\r'; break; case 't': str += '\t'; break; case 'b': str += '\b'; break; case 'f': str += '\f'; break; case 'v': str += '\x0B'; break; default: str += ch; break; } } else { if (ch === '\r' && source[index] === '\n') { ++index; } } } else if (isLineTerminator(ch.charCodeAt(0))) { break; } else { str += ch; } } if (quote !== '') { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } return { type: Token.StringLiteral, value: str, octal: octal, range: [start, index] }; } function isIdentifierName(token) { return token.type === Token.Identifier || token.type === Token.Keyword || token.type === Token.BooleanLiteral || token.type === Token.NullLiteral; } function advance() { var ch; skipWhitespace(); if (index >= length) { return { type: Token.EOF, range: [index, index] }; } ch = source.charCodeAt(index); // Very common: ( and ) and ; if (ch === 40 || ch === 41 || ch === 58) { return scanPunctuator(); } // String literal starts with single quote (#39) or double quote (#34). if (ch === 39 || ch === 34) { return scanStringLiteral(); } if (isIdentifierStart(ch)) { return scanIdentifier(); } // Dot (.) char #46 can also start a floating-point number, hence the need // to check the next character. if (ch === 46) { if (isDecimalDigit(source.charCodeAt(index + 1))) { return scanNumericLiteral(); } return scanPunctuator(); } if (isDecimalDigit(ch)) { return scanNumericLiteral(); } return scanPunctuator(); } function lex() { var token; token = lookahead; index = token.range[1]; lookahead = advance(); index = token.range[1]; return token; } function peek() { var pos; pos = index; lookahead = advance(); index = pos; } // Throw an exception function throwError(token, messageFormat) { var error, args = Array.prototype.slice.call(arguments, 2), msg = messageFormat.replace( /%(\d)/g, function (whole, index) { assert(index < args.length, 'Message reference must be in range'); return args[index]; } ); error = new Error(msg); error.index = index; error.description = msg; throw error; } // Throw an exception because of the token. function throwUnexpected(token) { throwError(token, Messages.UnexpectedToken, token.value); } // Expect the next token to match the specified punctuator. // If not, an exception will be thrown. function expect(value) { var token = lex(); if (token.type !== Token.Punctuator || token.value !== value) { throwUnexpected(token); } } // Return true if the next token matches the specified punctuator. function match(value) { return lookahead.type === Token.Punctuator && lookahead.value === value; } // Return true if the next token matches the specified keyword function matchKeyword(keyword) { return lookahead.type === Token.Keyword && lookahead.value === keyword; } function consumeSemicolon() { // Catch the very common case first: immediately a semicolon (char #59). if (source.charCodeAt(index) === 59) { lex(); return; } skipWhitespace(); if (match(';')) { lex(); return; } if (lookahead.type !== Token.EOF && !match('}')) { throwUnexpected(lookahead); } } // 11.1.4 Array Initialiser function parseArrayInitialiser() { var elements = []; expect('['); while (!match(']')) { if (match(',')) { lex(); elements.push(null); } else { elements.push(parseExpression()); if (!match(']')) { expect(','); } } } expect(']'); return delegate.createArrayExpression(elements); } // 11.1.5 Object Initialiser function parseObjectPropertyKey() { var token; skipWhitespace(); token = lex(); // Note: This function is called only from parseObjectProperty(), where // EOF and Punctuator tokens are already filtered out. if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { return delegate.createLiteral(token); } return delegate.createIdentifier(token.value); } function parseObjectProperty() { var token, key; token = lookahead; skipWhitespace(); if (token.type === Token.EOF || token.type === Token.Punctuator) { throwUnexpected(token); } key = parseObjectPropertyKey(); expect(':'); return delegate.createProperty('init', key, parseExpression()); } function parseObjectInitialiser() { var properties = []; expect('{'); while (!match('}')) { properties.push(parseObjectProperty()); if (!match('}')) { expect(','); } } expect('}'); return delegate.createObjectExpression(properties); } // 11.1.6 The Grouping Operator function parseGroupExpression() { var expr; expect('('); expr = parseExpression(); expect(')'); return expr; } // 11.1 Primary Expressions function parsePrimaryExpression() { var type, token, expr; if (match('(')) { return parseGroupExpression(); } type = lookahead.type; if (type === Token.Identifier) { expr = delegate.createIdentifier(lex().value); } else if (type === Token.StringLiteral || type === Token.NumericLiteral) { expr = delegate.createLiteral(lex()); } else if (type === Token.Keyword) { if (matchKeyword('this')) { lex(); expr = delegate.createThisExpression(); } } else if (type === Token.BooleanLiteral) { token = lex(); token.value = (token.value === 'true'); expr = delegate.createLiteral(token); } else if (type === Token.NullLiteral) { token = lex(); token.value = null; expr = delegate.createLiteral(token); } else if (match('[')) { expr = parseArrayInitialiser(); } else if (match('{')) { expr = parseObjectInitialiser(); } if (expr) { return expr; } throwUnexpected(lex()); } // 11.2 Left-Hand-Side Expressions function parseArguments() { var args = []; expect('('); if (!match(')')) { while (index < length) { args.push(parseExpression()); if (match(')')) { break; } expect(','); } } expect(')'); return args; } function parseNonComputedProperty() { var token; token = lex(); if (!isIdentifierName(token)) { throwUnexpected(token); } return delegate.createIdentifier(token.value); } function parseNonComputedMember() { expect('.'); return parseNonComputedProperty(); } function parseComputedMember() { var expr; expect('['); expr = parseExpression(); expect(']'); return expr; } function parseLeftHandSideExpression() { var expr, args, property; expr = parsePrimaryExpression(); while (true) { if (match('[')) { property = parseComputedMember(); expr = delegate.createMemberExpression('[', expr, property); } else if (match('.')) { property = parseNonComputedMember(); expr = delegate.createMemberExpression('.', expr, property); } else if (match('(')) { args = parseArguments(); expr = delegate.createCallExpression(expr, args); } else { break; } } return expr; } // 11.3 Postfix Expressions var parsePostfixExpression = parseLeftHandSideExpression; // 11.4 Unary Operators function parseUnaryExpression() { var token, expr; if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) { expr = parsePostfixExpression(); } else if (match('+') || match('-') || match('!')) { token = lex(); expr = parseUnaryExpression(); expr = delegate.createUnaryExpression(token.value, expr); } else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { throwError({}, Messages.UnexpectedToken); } else { expr = parsePostfixExpression(); } return expr; } function binaryPrecedence(token) { var prec = 0; if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { return 0; } switch (token.value) { case '||': prec = 1; break; case '&&': prec = 2; break; case '==': case '!=': case '===': case '!==': prec = 6; break; case '<': case '>': case '<=': case '>=': case 'instanceof': prec = 7; break; case 'in': prec = 7; break; case '+': case '-': prec = 9; break; case '*': case '/': case '%': prec = 11; break; default: break; } return prec; } // 11.5 Multiplicative Operators // 11.6 Additive Operators // 11.7 Bitwise Shift Operators // 11.8 Relational Operators // 11.9 Equality Operators // 11.10 Binary Bitwise Operators // 11.11 Binary Logical Operators function parseBinaryExpression() { var expr, token, prec, stack, right, operator, left, i; left = parseUnaryExpression(); token = lookahead; prec = binaryPrecedence(token); if (prec === 0) { return left; } token.prec = prec; lex(); right = parseUnaryExpression(); stack = [left, token, right]; while ((prec = binaryPrecedence(lookahead)) > 0) { // Reduce: make a binary expression from the three topmost entries. while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { right = stack.pop(); operator = stack.pop().value; left = stack.pop(); expr = delegate.createBinaryExpression(operator, left, right); stack.push(expr); } // Shift. token = lex(); token.prec = prec; stack.push(token); expr = parseUnaryExpression(); stack.push(expr); } // Final reduce to clean-up the stack. i = stack.length - 1; expr = stack[i]; while (i > 1) { expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr); i -= 2; } return expr; } // 11.12 Conditional Operator function parseConditionalExpression() { var expr, consequent, alternate; expr = parseBinaryExpression(); if (match('?')) { lex(); consequent = parseConditionalExpression(); expect(':'); alternate = parseConditionalExpression(); expr = delegate.createConditionalExpression(expr, consequent, alternate); } return expr; } // Simplification since we do not support AssignmentExpression. var parseExpression = parseConditionalExpression; // Polymer Syntax extensions // Filter :: // Identifier // Identifier "(" ")" // Identifier "(" FilterArguments ")" function parseFilter() { var identifier, args; identifier = lex(); if (identifier.type !== Token.Identifier) { throwUnexpected(identifier); } args = match('(') ? parseArguments() : []; return delegate.createFilter(identifier.value, args); } // Filters :: // "|" Filter // Filters "|" Filter function parseFilters() { while (match('|')) { lex(); parseFilter(); } } // TopLevel :: // LabelledExpressions // AsExpression // InExpression // FilterExpression // AsExpression :: // FilterExpression as Identifier // InExpression :: // Identifier, Identifier in FilterExpression // Identifier in FilterExpression // FilterExpression :: // Expression // Expression Filters function parseTopLevel() { skipWhitespace(); peek(); var expr = parseExpression(); if (expr) { if (lookahead.value === ',' || lookahead.value == 'in' && expr.type === Syntax.Identifier) { parseInExpression(expr); } else { parseFilters(); if (lookahead.value === 'as') { parseAsExpression(expr); } else { delegate.createTopLevel(expr); } } } if (lookahead.type !== Token.EOF) { throwUnexpected(lookahead); } } function parseAsExpression(expr) { lex(); // as var identifier = lex().value; delegate.createAsExpression(expr, identifier); } function parseInExpression(identifier) { var indexName; if (lookahead.value === ',') { lex(); if (lookahead.type !== Token.Identifier) throwUnexpected(lookahead); indexName = lex().value; } lex(); // in var expr = parseExpression(); parseFilters(); delegate.createInExpression(identifier.name, indexName, expr); } function parse(code, inDelegate) { delegate = inDelegate; source = code; index = 0; length = source.length; lookahead = null; state = { labelSet: {} }; return parseTopLevel(); } global.esprima = { parse: parse }; })(this); // 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 (function (global) { 'use strict'; function prepareBinding(expressionText, name, node, filterRegistry) { var expression; try { expression = getExpression(expressionText); if (expression.scopeIdent && (node.nodeType !== Node.ELEMENT_NODE || node.tagName !== 'TEMPLATE' || (name !== 'bind' && name !== 'repeat'))) { throw Error('as and in can only be used within