UNPKG

angular-material-npfixed

Version:

The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M

737 lines (649 loc) 25.5 kB
var HANDLERS = {}; /* The state of the current 'pointer' * The pointer represents the state of the current touch. * It contains normalized x and y coordinates from DOM events, * as well as other information abstracted from the DOM. */ var pointer, lastPointer, forceSkipClickHijack = false; /** * The position of the most recent click if that click was on a label element. * @type {{x: number, y: number}?} */ var lastLabelClickPos = null; // Used to attach event listeners once when multiple ng-apps are running. var isInitialized = false; angular .module('material.core.gestures', [ ]) .provider('$mdGesture', MdGestureProvider) .factory('$$MdGestureHandler', MdGestureHandler) .run( attachToDocument ); /** * @ngdoc service * @name $mdGestureProvider * @module material.core.gestures * * @description * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile * devices. * * <hljs lang="js"> * app.config(function($mdGestureProvider) { * * // For mobile devices without jQuery loaded, do not * // intercept click events during the capture phase. * $mdGestureProvider.skipClickHijack(); * * }); * </hljs> * */ function MdGestureProvider() { } MdGestureProvider.prototype = { // Publish access to setter to configure a variable BEFORE the // $mdGesture service is instantiated... skipClickHijack: function() { return forceSkipClickHijack = true; }, /** * $get is used to build an instance of $mdGesture * @ngInject */ $get : function($$MdGestureHandler, $$rAF, $timeout) { return new MdGesture($$MdGestureHandler, $$rAF, $timeout); } }; /** * MdGesture factory construction function * @ngInject */ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { var userAgent = navigator.userAgent || navigator.vendor || window.opera; var isIos = userAgent.match(/ipad|iphone|ipod/i); var isAndroid = userAgent.match(/android/i); var touchActionProperty = getTouchAction(); var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); var self = { handler: addHandler, register: register, // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack }; if (self.isHijackingClicks) { var maxClickDistance = 6; self.handler('click', { options: { maxDistance: maxClickDistance }, onEnd: checkDistanceAndEmit('click') }); self.handler('focus', { options: { maxDistance: maxClickDistance }, onEnd: function(ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { if (canFocus(ev.target)) { this.dispatchEvent(ev, 'focus', pointer); ev.target.focus(); } } function canFocus(element) { var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO']; return (element.getAttribute('tabindex') != '-1') && !element.hasAttribute('DISABLED') && (element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable || (focusableElements.indexOf(element.nodeName) != -1)); } } }); self.handler('mouseup', { options: { maxDistance: maxClickDistance }, onEnd: checkDistanceAndEmit('mouseup') }); self.handler('mousedown', { onStart: function(ev) { this.dispatchEvent(ev, 'mousedown'); } }); } function checkDistanceAndEmit(eventName) { return function(ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { this.dispatchEvent(ev, eventName, pointer); } }; } /* * Register an element to listen for a handler. * This allows an element to override the default options for a handler. * Additionally, some handlers like drag and hold only dispatch events if * the domEvent happens inside an element that's registered to listen for these events. * * @see GestureHandler for how overriding of default options works. * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) */ function register(element, handlerName, options) { var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; if (!handler) { throw new Error('Failed to register element with handler ' + handlerName + '. ' + 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); } return handler.registerElement(element, options); } /* * add a handler to $mdGesture. see below. */ function addHandler(name, definition) { var handler = new $$MdGestureHandler(name); angular.extend(handler, definition); HANDLERS[name] = handler; return self; } /* * Register handlers. These listen to touch/start/move events, interpret them, * and dispatch gesture events depending on options & conditions. These are all * instances of GestureHandler. * @see GestureHandler */ return self /* * The press handler dispatches an event on touchdown/touchend. * It's a simple abstraction of touch/mouse/pointer start and end. */ .handler('press', { onStart: function (ev, pointer) { this.dispatchEvent(ev, '$md.pressdown'); }, onEnd: function (ev, pointer) { this.dispatchEvent(ev, '$md.pressup'); } }) /* * The hold handler dispatches an event if the user keeps their finger within * the same <maxDistance> area for <delay> ms. * The hold handler will only run if a parent of the touch target is registered * to listen for hold events through $mdGesture.register() */ .handler('hold', { options: { maxDistance: 6, delay: 500 }, onCancel: function () { $timeout.cancel(this.state.timeout); }, onStart: function (ev, pointer) { // For hold, require a parent to be registered with $mdGesture.register() // Because we prevent scroll events, this is necessary. if (!this.state.registeredParent) return this.cancel(); this.state.pos = {x: pointer.x, y: pointer.y}; this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { this.dispatchEvent(ev, '$md.hold'); this.cancel(); //we're done! }), this.state.options.delay, false); }, onMove: function (ev, pointer) { // Don't scroll while waiting for hold. // If we don't preventDefault touchmove events here, Android will assume we don't // want to listen to anymore touch events. It will start scrolling and stop sending // touchmove events. if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); // If the user moves greater than <maxDistance> pixels, stop the hold timer // set in onStart var dx = this.state.pos.x - pointer.x; var dy = this.state.pos.y - pointer.y; if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { this.cancel(); } }, onEnd: function () { this.onCancel(); } }) /* * The drag handler dispatches a drag event if the user holds and moves his finger greater than * <minDistance> px in the x or y direction, depending on options.horizontal. * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier> * pixels vertically, this handler won't consider the move part of a drag. */ .handler('drag', { options: { minDistance: 6, horizontal: true, cancelMultiplier: 1.5 }, onSetup: function(element, options) { if (touchActionProperty) { // We check for horizontal to be false, because otherwise we would overwrite the default opts. this.oldTouchAction = element[0].style[touchActionProperty]; element[0].style[touchActionProperty] = options.horizontal === false ? 'pan-y' : 'pan-x'; } }, onCleanup: function(element) { if (this.oldTouchAction) { element[0].style[touchActionProperty] = this.oldTouchAction; } }, onStart: function (ev) { // For drag, require a parent to be registered with $mdGesture.register() if (!this.state.registeredParent) this.cancel(); }, onMove: function (ev, pointer) { var shouldStartDrag, shouldCancel; // Don't scroll while deciding if this touchmove qualifies as a drag event. // If we don't preventDefault touchmove events here, Android will assume we don't // want to listen to anymore touch events. It will start scrolling and stop sending // touchmove events. if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); if (!this.state.dragPointer) { if (this.state.options.horizontal) { shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; } else { shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; } if (shouldStartDrag) { // Create a new pointer representing this drag, starting at this point where the drag started. this.state.dragPointer = makeStartPointer(ev); updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); } else if (shouldCancel) { this.cancel(); } } else { this.dispatchDragMove(ev); } }, // Only dispatch dragmove events every frame; any more is unnecessary dispatchDragMove: $$rAF.throttle(function (ev) { // Make sure the drag didn't stop while waiting for the next frame if (this.state.isRunning) { updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); } }), onEnd: function (ev, pointer) { if (this.state.dragPointer) { updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); } } }) /* * The swipe handler will dispatch a swipe event if, on the end of a touch, * the velocity and distance were high enough. */ .handler('swipe', { options: { minVelocity: 0.65, minDistance: 10 }, onEnd: function (ev, pointer) { var eventType; if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && Math.abs(pointer.distanceX) > this.state.options.minDistance) { eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; this.dispatchEvent(ev, eventType); } else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && Math.abs(pointer.distanceY) > this.state.options.minDistance) { eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; this.dispatchEvent(ev, eventType); } } }); function getTouchAction() { var testEl = document.createElement('div'); var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; for (var i = 0; i < vendorPrefixes.length; i++) { var prefix = vendorPrefixes[i]; var property = prefix ? prefix + 'TouchAction' : 'touchAction'; if (angular.isDefined(testEl.style[property])) { return property; } } } } /** * MdGestureHandler * A GestureHandler is an object which is able to dispatch custom dom events * based on native dom {touch,pointer,mouse}{start,move,end} events. * * A gesture will manage its lifecycle through the start,move,end, and cancel * functions, which are called by native dom events. * * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be * overridden by elements registering through $mdGesture.register() */ function GestureHandler (name) { this.name = name; this.state = {}; } function MdGestureHandler() { var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); GestureHandler.prototype = { options: {}, // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events // differently when jQuery is loaded dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, // These are overridden by the registered handler onSetup: angular.noop, onCleanup: angular.noop, onStart: angular.noop, onMove: angular.noop, onEnd: angular.noop, onCancel: angular.noop, // onStart sets up a new state for the handler, which includes options from the // nearest registered parent element of ev.target. start: function (ev, pointer) { if (this.state.isRunning) return; var parentTarget = this.getNearestParent(ev.target); // Get the options from the nearest registered parent var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; this.state = { isRunning: true, // Override the default options with the nearest registered parent's options options: angular.extend({}, this.options, parentTargetOptions), // Pass in the registered parent node to the state so the onStart listener can use registeredParent: parentTarget }; this.onStart(ev, pointer); }, move: function (ev, pointer) { if (!this.state.isRunning) return; this.onMove(ev, pointer); }, end: function (ev, pointer) { if (!this.state.isRunning) return; this.onEnd(ev, pointer); this.state.isRunning = false; }, cancel: function (ev, pointer) { this.onCancel(ev, pointer); this.state = {}; }, // Find and return the nearest parent element that has been registered to // listen for this handler via $mdGesture.register(element, 'handlerName'). getNearestParent: function (node) { var current = node; while (current) { if ((current.$mdGesture || {})[this.name]) { return current; } current = current.parentNode; } return null; }, // Called from $mdGesture.register when an element registers itself with a handler. // Store the options the user gave on the DOMElement itself. These options will // be retrieved with getNearestParent when the handler starts. registerElement: function (element, options) { var self = this; element[0].$mdGesture = element[0].$mdGesture || {}; element[0].$mdGesture[this.name] = options || {}; element.on('$destroy', onDestroy); self.onSetup(element, options || {}); return onDestroy; function onDestroy() { delete element[0].$mdGesture[self.name]; element.off('$destroy', onDestroy); self.onCleanup(element, options || {}); } } }; return GestureHandler; /* * Dispatch an event with jQuery * TODO: Make sure this sends bubbling events * * @param srcEvent the original DOM touch event that started this. * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') * @param eventPointer the pointer object that matches this event. */ function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { eventPointer = eventPointer || pointer; var eventObj = new angular.element.Event(eventType); eventObj.$material = true; eventObj.pointer = eventPointer; eventObj.srcEvent = srcEvent; angular.extend(eventObj, { clientX: eventPointer.x, clientY: eventPointer.y, screenX: eventPointer.x, screenY: eventPointer.y, pageX: eventPointer.x, pageY: eventPointer.y, ctrlKey: srcEvent.ctrlKey, altKey: srcEvent.altKey, shiftKey: srcEvent.shiftKey, metaKey: srcEvent.metaKey }); angular.element(eventPointer.target).trigger(eventObj); } /* * NOTE: nativeDispatchEvent is very performance sensitive. * @param srcEvent the original DOM touch event that started this. * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') * @param eventPointer the pointer object that matches this event. */ function nativeDispatchEvent(srcEvent, eventType, eventPointer) { eventPointer = eventPointer || pointer; var eventObj; if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) { eventObj = document.createEvent('MouseEvents'); eventObj.initMouseEvent( eventType, true, true, window, srcEvent.detail, eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, srcEvent.button, srcEvent.relatedTarget || null ); } else { eventObj = document.createEvent('CustomEvent'); eventObj.initCustomEvent(eventType, true, true, {}); } eventObj.$material = true; eventObj.pointer = eventPointer; eventObj.srcEvent = srcEvent; eventPointer.target.dispatchEvent(eventObj); } } /** * Attach Gestures: hook document and check shouldHijack clicks * @ngInject */ function attachToDocument( $mdGesture, $$MdGestureHandler ) { // Polyfill document.contains for IE11. // TODO: move to util document.contains || (document.contains = function (node) { return document.body.contains(node); }); if (!isInitialized && $mdGesture.isHijackingClicks ) { /* * If hijack clicks is true, we preventDefault any click that wasn't * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost', * click event will be sent ~400ms after a touchend event happens. * The only way to know if this click is real is to prevent any normal * click events, and add a flag to events sent by material so we know not to prevent those. * * Two exceptions to click events that should be prevented are: * - click events sent by the keyboard (eg form submit) * - events that originate from an Ionic app */ document.addEventListener('click' , clickHijacker , true); document.addEventListener('mouseup' , mouseInputHijacker, true); document.addEventListener('mousedown', mouseInputHijacker, true); document.addEventListener('focus' , mouseInputHijacker, true); isInitialized = true; } function mouseInputHijacker(ev) { var isKeyClick = !ev.clientX && !ev.clientY; if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev)) { ev.preventDefault(); ev.stopPropagation(); } } function clickHijacker(ev) { var isKeyClick = ev.clientX === 0 && ev.clientY === 0; if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev)) { ev.preventDefault(); ev.stopPropagation(); lastLabelClickPos = null; } else { lastLabelClickPos = null; if (ev.target.tagName.toLowerCase() == 'label') { lastLabelClickPos = {x: ev.x, y: ev.y}; } } } // Listen to all events to cover all platforms. var START_EVENTS = 'mousedown touchstart pointerdown'; var MOVE_EVENTS = 'mousemove touchmove pointermove'; var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; angular.element(document) .on(START_EVENTS, gestureStart) .on(MOVE_EVENTS, gestureMove) .on(END_EVENTS, gestureEnd) // For testing .on('$$mdGestureReset', function gestureClearCache () { lastPointer = pointer = null; }); /* * When a DOM event happens, run all registered gesture handlers' lifecycle * methods which match the DOM event. * Eg when a 'touchstart' event happens, runHandlers('start') will call and * run `handler.cancel()` and `handler.start()` on all registered handlers. */ function runHandlers(handlerEvent, event) { var handler; for (var name in HANDLERS) { handler = HANDLERS[name]; if( handler instanceof $$MdGestureHandler ) { if (handlerEvent === 'start') { // Run cancel to reset any handlers' state handler.cancel(); } handler[handlerEvent](event, pointer); } } } /* * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) * If it is legitimate, we initiate the pointer state and mark the current pointer's type * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events * won't effect it. */ function gestureStart(ev) { // If we're already touched down, abort if (pointer) return; var now = +Date.now(); // iOS & old android bug: after a touch event, a click event is sent 350 ms later. // If <400ms have passed, don't allow an event of a different type than the previous event if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { return; } pointer = makeStartPointer(ev); runHandlers('start', ev); } /* * If a move event happens of the right type, update the pointer and run all the move handlers. * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. */ function gestureMove(ev) { if (!pointer || !typesMatch(ev, pointer)) return; updatePointerState(ev, pointer); runHandlers('move', ev); } /* * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' */ function gestureEnd(ev) { if (!pointer || !typesMatch(ev, pointer)) return; updatePointerState(ev, pointer); pointer.endTime = +Date.now(); runHandlers('end', ev); lastPointer = pointer; pointer = null; } } // ******************** // Module Functions // ******************** /* * Initiate the pointer. x, y, and the pointer's type. */ function makeStartPointer(ev) { var point = getEventPoint(ev); var startPointer = { startTime: +Date.now(), target: ev.target, // 'p' for pointer events, 'm' for mouse, 't' for touch type: ev.type.charAt(0) }; startPointer.startX = startPointer.x = point.pageX; startPointer.startY = startPointer.y = point.pageY; return startPointer; } /* * return whether the pointer's type matches the event's type. * Eg if a touch event happens but the pointer has a mouse type, return false. */ function typesMatch(ev, pointer) { return ev && pointer && ev.type.charAt(0) === pointer.type; } /** * Gets whether the given event is an input event that was caused by clicking on an * associated label element. * * This is necessary because the browser will, upon clicking on a label element, fire an * *extra* click event on its associated input (if any). mdGesture is able to flag the label * click as with `$material` correctly, but not the second input click. * * In order to determine whether an input event is from a label click, we compare the (x, y) for * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label * click occurs). Unfortunately, there are no event properties that tie the input and the label * together (such as relatedTarget). * * @param {MouseEvent} event * @returns {boolean} */ function isInputEventFromLabelClick(event) { return lastLabelClickPos && lastLabelClickPos.x == event.x && lastLabelClickPos.y == event.y; } /* * Update the given pointer based upon the given DOMEvent. * Distance, velocity, direction, duration, etc */ function updatePointerState(ev, pointer) { var point = getEventPoint(ev); var x = pointer.x = point.pageX; var y = pointer.y = point.pageY; pointer.distanceX = x - pointer.startX; pointer.distanceY = y - pointer.startY; pointer.distance = Math.sqrt( pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY ); pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; pointer.duration = +Date.now() - pointer.startTime; pointer.velocityX = pointer.distanceX / pointer.duration; pointer.velocityY = pointer.distanceY / pointer.duration; } /* * Normalize the point where the DOM event happened whether it's touch or mouse. * @returns point event obj with pageX and pageY on it. */ function getEventPoint(ev) { ev = ev.originalEvent || ev; // support jQuery events return (ev.touches && ev.touches[0]) || (ev.changedTouches && ev.changedTouches[0]) || ev; }