UNPKG

material-motion

Version:

Makes it easy to add rich, interactive motion to your application.

198 lines 9.02 kB
/** @license * Copyright 2016 - present The Material Motion Authors. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ import { not, when, } from '../aggregators'; import { Axis, GestureRecognitionState, State, } from '../enums'; import { MemorylessMotionSubject, MotionObservable, createProperty, } from '../observables'; import { isPointerEvent, } from '../typeGuards'; export class Draggable { constructor({ down$, move$, up$, cancel$, contextMenu$, capturedClick$, capturedDragStart$ }) { this.state$ = createProperty({ initialValue: State.AT_REST, }); this.recognitionState$ = createProperty({ initialValue: GestureRecognitionState.POSSIBLE, }); this.recognitionThreshold$ = createProperty({ initialValue: 16, }); this.axis$ = createProperty({ initialValue: Axis.ALL, }); this.cancellation$ = new MemorylessMotionSubject(); this.enabled$ = new MemorylessMotionSubject(); this.down$ = down$; this.move$ = move$; this.up$ = up$; this.cancel$ = cancel$; this.contextMenu$ = contextMenu$; this.capturedClick$ = capturedClick$; this.capturedDragStart$ = capturedDragStart$; this.value$ = new MotionObservable((observer) => { let downSubscription; let moveSubscription; let cancellationSubscription; let enabledSubscription; // If we've recognized a drag, we'll prevent any children from receiving // clicks. let preventClicks = false; // HTML's OS-integrated drag-and-drop will interrupt a PointerEvent stream // without emitting pointercancel; this is the best way I've found to // prevent that. // // See also https://github.com/w3c/pointerevents/issues/205 const capturedDragStartSubscription = capturedDragStart$.subscribe((dragStartEvent) => { dragStartEvent.preventDefault(); }); const capturedClickSubscription = capturedClick$.subscribe((clickEvent) => { if (preventClicks) { clickEvent.preventDefault(); clickEvent.stopImmediatePropagation(); } }); const subscribeToDown = () => { return down$.subscribe((downEvent) => { this.state$.write(State.ACTIVE); const currentAxis = this.axis$.read(); // If we get a new down event while we're already listening for // moves, ignore it. if (!moveSubscription) { preventClicks = false; if (isPointerEvent(downEvent)) { // The `as Element` is a workaround for // https://github.com/Microsoft/TypeScript/issues/299 downEvent.target.setPointerCapture(downEvent.pointerId); } moveSubscription = move$.merge([up$])._filter({ predicate: (nextEvent) => nextEvent.pointerId === downEvent.pointerId }).subscribe((nextEvent) => { const atRest = nextEvent.type.includes('up'); const translation = { x: currentAxis !== Axis.Y ? nextEvent.x - downEvent.x : 0, y: currentAxis !== Axis.X ? nextEvent.y - downEvent.y : 0, }; switch (this.recognitionState$.read()) { case GestureRecognitionState.POSSIBLE: if (Math.sqrt(Math.pow(translation.x, 2) + Math.pow(translation.y, 2)) > this.recognitionThreshold$.read()) { this.recognitionState$.write(GestureRecognitionState.BEGAN); preventClicks = true; } break; case GestureRecognitionState.BEGAN: this.recognitionState$.write(GestureRecognitionState.CHANGED); break; default: break; } if (atRest) { // This would be a takeWhile if we were using an Observable // implementation that supported completion. moveSubscription.unsubscribe(); moveSubscription = undefined; if (this.recognitionState$.read() === GestureRecognitionState.POSSIBLE) { this.recognitionState$.write(GestureRecognitionState.FAILED); } else { this.recognitionState$.write(GestureRecognitionState.ENDED); } // Doing the simple thing for now and setting AT_REST in up, // but it might be better on a delay to give time for clicks // to happen first. this.state$.write(State.AT_REST); this.recognitionState$.write(GestureRecognitionState.POSSIBLE); } else { observer.next(translation); } }); } }); }; downSubscription = subscribeToDown(); enabledSubscription = this.enabled$.subscribe((enabled) => { if (enabled && !downSubscription) { downSubscription = subscribeToDown(); } else if (!enabled && downSubscription) { downSubscription.unsubscribe(); downSubscription = undefined; preventClicks = false; // moveSubscription handled by cancellation flow } }); cancellationSubscription = this.cancellation$.merge([ when(not(this.enabled$)), cancel$, contextMenu$, ]).subscribe(() => { if (moveSubscription) { moveSubscription.unsubscribe(); moveSubscription = undefined; this.recognitionState$.write(GestureRecognitionState.CANCELLED); this.state$.write(State.AT_REST); this.recognitionState$.write(GestureRecognitionState.POSSIBLE); } }); return () => { capturedClickSubscription.unsubscribe(); capturedDragStartSubscription.unsubscribe(); if (downSubscription) { downSubscription.unsubscribe(); } if (moveSubscription) { moveSubscription.unsubscribe(); } if (cancellationSubscription) { cancellationSubscription.unsubscribe(); } if (enabledSubscription) { enabledSubscription.unsubscribe(); } }; })._multicast(); } get state() { return this.state$.read(); } get recognitionState() { return this.recognitionState$.read(); } get recognitionThreshold() { return this.recognitionThreshold$.read(); } set recognitionThreshold(value) { this.recognitionThreshold$.write(value); } get axis() { return this.axis$.read(); } set axis(value) { this.axis$.write(value); } cancel() { this.cancellation$.next(undefined); } enable() { this.enabled$.next(true); } disable() { this.enabled$.next(false); } } export default Draggable; //# sourceMappingURL=Draggable.js.map