UNPKG

react-beautiful-dnd

Version:

Beautiful, accessible drag and drop for lists with React.js

302 lines (266 loc) 8.59 kB
// @flow /* eslint-disable no-use-before-define */ import createScheduler from '../util/create-scheduler'; import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import * as keyCodes from '../../key-codes'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import createPostDragEventPreventer, { type EventPreventer } from '../util/create-post-drag-event-preventer'; import { bindEvents, unbindEvents } from '../util/bind-events'; import createEventMarshal, { type EventMarshal } from '../util/create-event-marshal'; import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; import type { EventBinding } from '../util/event-types'; import type { Position, } from '../../../types'; import type { MouseSensor, CreateSensorArgs } from './sensor-types'; // Custom event format for force press inputs type MouseForceChangedEvent = MouseEvent & { webkitForce?: number, } type State = {| isDragging: boolean, pending: ?Position, |} // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button const primaryButton: number = 0; const noop = () => { }; // shared management of mousedown without needing to call preventDefault() const mouseDownMarshal: EventMarshal = createEventMarshal(); export default ({ callbacks, getWindow, canStartCapturing, }: CreateSensorArgs): MouseSensor => { let state: State = { isDragging: false, pending: null, }; const setState = (newState: State): void => { state = newState; }; const isDragging = (): boolean => state.isDragging; const isCapturing = (): boolean => Boolean(state.pending || state.isDragging); const schedule = createScheduler(callbacks); const postDragEventPreventer: EventPreventer = createPostDragEventPreventer(getWindow); const startDragging = (fn?: Function = noop) => { setState({ pending: null, isDragging: true, }); fn(); }; const stopDragging = (fn?: Function = noop, shouldBlockClick?: boolean = true) => { schedule.cancel(); unbindWindowEvents(); mouseDownMarshal.reset(); if (shouldBlockClick) { postDragEventPreventer.preventNext(); } setState({ isDragging: false, pending: null, }); fn(); }; const startPendingDrag = (point: Position) => { setState({ pending: point, isDragging: false }); bindWindowEvents(); }; const stopPendingDrag = () => { stopDragging(noop, false); }; const kill = (fn?: Function = noop) => { if (state.pending) { stopPendingDrag(); return; } stopDragging(fn); }; const unmount = (): void => { kill(); postDragEventPreventer.abort(); }; const cancel = () => { kill(callbacks.onCancel); }; const windowBindings: EventBinding[] = [ { eventName: 'mousemove', fn: (event: MouseEvent) => { const { button, clientX, clientY } = event; if (button !== primaryButton) { return; } const point: Position = { x: clientX, y: clientY, }; // Already dragging if (state.isDragging) { // preventing default as we are using this event event.preventDefault(); schedule.move(point); return; } if (!state.pending) { console.error('invalid state'); return; } // drag is pending // threshold not yet exceeded if (!isSloppyClickThresholdExceeded(state.pending, point)) { return; } // preventing default as we are using this event event.preventDefault(); startDragging(() => callbacks.onLift({ client: point, autoScrollMode: 'FLUID', })); }, }, { eventName: 'mouseup', fn: (event: MouseEvent) => { if (state.pending) { stopPendingDrag(); return; } // preventing default as we are using this event event.preventDefault(); stopDragging(callbacks.onDrop); }, }, { eventName: 'mousedown', fn: (event: MouseEvent) => { // this can happen during a drag when the user clicks a button // other than the primary mouse button if (state.isDragging) { event.preventDefault(); } stopDragging(callbacks.onCancel); }, }, { eventName: 'keydown', fn: (event: KeyboardEvent) => { // firing a keyboard event before the drag has started // treat this as an indirect cancel if (!state.isDragging) { cancel(); return; } // cancelling a drag if (event.keyCode === keyCodes.escape) { event.preventDefault(); cancel(); return; } preventStandardKeyEvents(event); }, }, { eventName: 'resize', fn: cancel, }, { eventName: 'scroll', // ## Passive: true // Eventual consistency is fine because we use position: fixed on the item // ## Capture: false // Scroll events on elements do not bubble, but they go through the capture phase // https://twitter.com/alexandereardon/status/985994224867819520 // Using capture: false here as we want to avoid intercepting droppable scroll requests options: { passive: true, capture: false }, fn: () => { // stop a pending drag if (state.pending) { stopPendingDrag(); return; } schedule.windowScrollMove(); }, }, // Need to opt out of dragging if the user is a force press // Only for safari which has decided to introduce its own custom way of doing things // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html { eventName: 'webkitmouseforcechanged', fn: (event: MouseForceChangedEvent) => { if ( event.webkitForce == null || (MouseEvent: any).WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN == null ) { console.error('handling a mouse force changed event when it is not supported'); return; } const forcePressThreshold: number = (MouseEvent: any).WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN; const isForcePressing: boolean = event.webkitForce >= forcePressThreshold; if (isForcePressing) { // it is considered a indirect cancel so we do not // prevent default in any situation. cancel(); } }, }, // Cancel on page visibility change { eventName: supportedPageVisibilityEventName, fn: cancel, }, ]; const bindWindowEvents = () => { const win: HTMLElement = getWindow(); bindEvents(win, windowBindings, { capture: true }); }; const unbindWindowEvents = () => { const win: HTMLElement = getWindow(); unbindEvents(win, windowBindings, { capture: true }); }; const onMouseDown = (event: MouseEvent): void => { if (mouseDownMarshal.isHandled()) { return; } if (!canStartCapturing(event)) { return; } if (isCapturing()) { console.error('should not be able to perform a mouse down while a drag or pending drag is occurring'); cancel(); return; } // only starting a drag if dragging with the primary mouse button if (event.button !== primaryButton) { return; } // Do not start a drag if any modifier key is pressed if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) { return; } // Registering that this event has been handled. // This is to prevent parent draggables using this event // to start also. // Ideally we would not use preventDefault() as we are not sure // if this mouse down is part of a drag interaction // Unfortunately we do to prevent the element obtaining focus (see below). mouseDownMarshal.handle(); // Unfortunately we do need to prevent the drag handle from getting focus on mousedown. // This goes against our policy on not blocking events before a drag has started. // See [How we use dom events](/docs/guides/how-we-use-dom-events.md). event.preventDefault(); const point: Position = { x: event.clientX, y: event.clientY, }; startPendingDrag(point); }; const sensor: MouseSensor = { onMouseDown, kill, isCapturing, isDragging, unmount, }; return sensor; };