material-motion
Version:
Makes it easy to add rich, interactive motion to your application.
198 lines • 9.02 kB
JavaScript
/** @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