UNPKG

react-beautiful-dnd-next

Version:

Beautiful and accessible drag and drop for lists with React

280 lines (247 loc) 8.7 kB
// @flow import { useRef } from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; import { useMemo, useCallback } from 'use-memo-one'; import memoizeOne from 'memoize-one'; import checkForNestedScrollContainers from './check-for-nested-scroll-container'; import { origin } from '../../state/position'; import getScroll from './get-scroll'; import type { DimensionMarshal, DroppableCallbacks, RecollectDroppableOptions, } from '../../state/dimension-marshal/dimension-marshal-types'; import getEnv, { type Env } from './get-env'; import type { DroppableId, TypeId, DroppableDimension, DroppableDescriptor, Direction, ScrollOptions, } from '../../types'; import getDimension from './get-dimension'; import AppContext, { type AppContextValue } from '../context/app-context'; import withoutPlaceholder from './without-placeholder'; import { warning } from '../../dev-warning'; import getListenerOptions from './get-listener-options'; import useRequiredContext from '../use-required-context'; import usePreviousRef from '../use-previous-ref'; import useLayoutEffect from '../use-isomorphic-layout-effect'; type Props = {| droppableId: DroppableId, type: TypeId, direction: Direction, isDropDisabled: boolean, isCombineEnabled: boolean, ignoreContainerClipping: boolean, getPlaceholderRef: () => ?HTMLElement, getDroppableRef: () => ?HTMLElement, |}; type WhileDragging = {| ref: HTMLElement, descriptor: DroppableDescriptor, env: Env, scrollOptions: ScrollOptions, |}; const getClosestScrollableFromDrag = (dragging: ?WhileDragging): ?Element => (dragging && dragging.env.closestScrollable) || null; export default function useDroppableDimensionPublisher(args: Props) { const whileDraggingRef = useRef<?WhileDragging>(null); const appContext: AppContextValue = useRequiredContext(AppContext); const marshal: DimensionMarshal = appContext.marshal; const previousRef = usePreviousRef(args); const descriptor = useMemo<DroppableDescriptor>(() => { return { id: args.droppableId, type: args.type, }; }, [args.droppableId, args.type]); const publishedDescriptorRef = useRef<DroppableDescriptor>(descriptor); const memoizedUpdateScroll = useMemo( () => memoizeOne((x: number, y: number) => { invariant( whileDraggingRef.current, 'Can only update scroll when dragging', ); const scroll: Position = { x, y }; marshal.updateDroppableScroll(descriptor.id, scroll); }), [descriptor.id, marshal], ); const getClosestScroll = useCallback((): Position => { const dragging: ?WhileDragging = whileDraggingRef.current; if (!dragging || !dragging.env.closestScrollable) { return origin; } return getScroll(dragging.env.closestScrollable); }, []); const updateScroll = useCallback(() => { const scroll: Position = getClosestScroll(); memoizedUpdateScroll(scroll.x, scroll.y); }, [getClosestScroll, memoizedUpdateScroll]); const scheduleScrollUpdate = useMemo(() => rafSchedule(updateScroll), [ updateScroll, ]); const onClosestScroll = useCallback(() => { const dragging: ?WhileDragging = whileDraggingRef.current; const closest: ?Element = getClosestScrollableFromDrag(dragging); invariant( dragging && closest, 'Could not find scroll options while scrolling', ); const options: ScrollOptions = dragging.scrollOptions; if (options.shouldPublishImmediately) { updateScroll(); return; } scheduleScrollUpdate(); }, [scheduleScrollUpdate, updateScroll]); const getDimensionAndWatchScroll = useCallback( (windowScroll: Position, options: ScrollOptions) => { invariant( !whileDraggingRef.current, 'Cannot collect a droppable while a drag is occurring', ); const previous: Props = previousRef.current; const ref: ?HTMLElement = previous.getDroppableRef(); invariant(ref, 'Cannot collect without a droppable ref'); const env: Env = getEnv(ref); const dragging: WhileDragging = { ref, descriptor, env, scrollOptions: options, }; // side effect whileDraggingRef.current = dragging; const dimension: DroppableDimension = getDimension({ ref, descriptor, env, windowScroll, direction: previous.direction, isDropDisabled: previous.isDropDisabled, isCombineEnabled: previous.isCombineEnabled, shouldClipSubject: !previous.ignoreContainerClipping, }); if (env.closestScrollable) { // bind scroll listener env.closestScrollable.addEventListener( 'scroll', onClosestScroll, getListenerOptions(dragging.scrollOptions), ); // print a debug warning if using an unsupported nested scroll container setup if (process.env.NODE_ENV !== 'production') { checkForNestedScrollContainers(env.closestScrollable); } } return dimension; }, [descriptor, onClosestScroll, previousRef], ); const recollect = useCallback( (options: RecollectDroppableOptions): DroppableDimension => { const dragging: ?WhileDragging = whileDraggingRef.current; const closest: ?Element = getClosestScrollableFromDrag(dragging); invariant( dragging && closest, 'Can only recollect Droppable client for Droppables that have a scroll container', ); const previous: Props = previousRef.current; const execute = (): DroppableDimension => getDimension({ ref: dragging.ref, descriptor: dragging.descriptor, env: dragging.env, windowScroll: origin, direction: previous.direction, isDropDisabled: previous.isDropDisabled, isCombineEnabled: previous.isCombineEnabled, shouldClipSubject: !previous.ignoreContainerClipping, }); if (!options.withoutPlaceholder) { return execute(); } return withoutPlaceholder(previous.getPlaceholderRef(), execute); }, [previousRef], ); const dragStopped = useCallback(() => { const dragging: ?WhileDragging = whileDraggingRef.current; invariant(dragging, 'Cannot stop drag when no active drag'); const closest: ?Element = getClosestScrollableFromDrag(dragging); // goodbye old friend whileDraggingRef.current = null; if (!closest) { return; } // unwatch scroll scheduleScrollUpdate.cancel(); closest.removeEventListener( 'scroll', onClosestScroll, getListenerOptions(dragging.scrollOptions), ); }, [onClosestScroll, scheduleScrollUpdate]); const scroll = useCallback((change: Position) => { // arrange const dragging: ?WhileDragging = whileDraggingRef.current; invariant(dragging, 'Cannot scroll when there is no drag'); const closest: ?Element = getClosestScrollableFromDrag(dragging); invariant(closest, 'Cannot scroll a droppable with no closest scrollable'); // act closest.scrollTop += change.y; closest.scrollLeft += change.x; }, []); const callbacks: DroppableCallbacks = useMemo(() => { return { getDimensionAndWatchScroll, recollect, dragStopped, scroll, }; }, [dragStopped, getDimensionAndWatchScroll, recollect, scroll]); // Register with the marshal and let it know of: // - any descriptor changes // - when it unmounts useLayoutEffect(() => { publishedDescriptorRef.current = descriptor; marshal.registerDroppable(descriptor, callbacks); return () => { if (whileDraggingRef.current) { warning( 'Unsupported: changing the droppableId or type of a Droppable during a drag', ); dragStopped(); } marshal.unregisterDroppable(descriptor); }; }, [callbacks, descriptor, dragStopped, marshal]); // update is enabled with the marshal // only need to update when there is a drag useLayoutEffect(() => { if (!whileDraggingRef.current) { return; } marshal.updateDroppableIsEnabled( publishedDescriptorRef.current.id, !args.isDropDisabled, ); }, [args.isDropDisabled, marshal]); // update is combine enabled with the marshal // only need to update when there is a drag useLayoutEffect(() => { if (!whileDraggingRef.current) { return; } marshal.updateDroppableIsCombineEnabled( publishedDescriptorRef.current.id, args.isCombineEnabled, ); }, [args.isCombineEnabled, marshal]); }