You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
8.7 KiB
289 lines
8.7 KiB
import { useRef } from 'react';
|
|
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 { invariant } from '../../invariant';
|
|
import checkForNestedScrollContainers from './check-for-nested-scroll-container';
|
|
import * as dataAttr from '../data-attributes';
|
|
import { origin } from '../../state/position';
|
|
import getScroll from './get-scroll';
|
|
import type {
|
|
DroppableEntry,
|
|
DroppableCallbacks,
|
|
} from '../../state/registry/registry-types';
|
|
import getEnv from './get-env';
|
|
import type { Env } from './get-env';
|
|
import type {
|
|
Id,
|
|
DroppableId,
|
|
TypeId,
|
|
DroppableDimension,
|
|
DroppableDescriptor,
|
|
Direction,
|
|
ScrollOptions,
|
|
DroppableMode,
|
|
} from '../../types';
|
|
import getDimension from './get-dimension';
|
|
import AppContext from '../context/app-context';
|
|
import type { AppContextValue } from '../context/app-context';
|
|
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';
|
|
import useUniqueId from '../use-unique-id';
|
|
|
|
interface Props {
|
|
droppableId: DroppableId;
|
|
type: TypeId;
|
|
mode: DroppableMode;
|
|
direction: Direction;
|
|
isDropDisabled: boolean;
|
|
isCombineEnabled: boolean;
|
|
ignoreContainerClipping: boolean;
|
|
getDroppableRef: () => HTMLElement | null;
|
|
}
|
|
|
|
interface WhileDragging {
|
|
ref: HTMLElement;
|
|
descriptor: DroppableDescriptor;
|
|
env: Env;
|
|
scrollOptions: ScrollOptions;
|
|
}
|
|
|
|
const getClosestScrollableFromDrag = (
|
|
dragging?: WhileDragging | null,
|
|
): HTMLElement | null => (dragging && dragging.env.closestScrollable) || null;
|
|
|
|
export default function useDroppablePublisher(args: Props) {
|
|
const whileDraggingRef = useRef<WhileDragging | null>(null);
|
|
const appContext: AppContextValue = useRequiredContext(AppContext);
|
|
const uniqueId: Id = useUniqueId('droppable');
|
|
const { registry, marshal } = appContext;
|
|
const previousRef = usePreviousRef(args);
|
|
|
|
const descriptor = useMemo<DroppableDescriptor>(
|
|
() => ({
|
|
id: args.droppableId,
|
|
type: args.type,
|
|
mode: args.mode,
|
|
}),
|
|
[args.droppableId, args.mode, 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 | null = whileDraggingRef.current;
|
|
if (!dragging || !dragging.env.closestScrollable) {
|
|
return origin;
|
|
}
|
|
|
|
return getScroll(dragging.env.closestScrollable);
|
|
}, []);
|
|
|
|
const updateScroll = useCallback(() => {
|
|
// reading scroll value when called so value will be the latest
|
|
const scroll: Position = getClosestScroll();
|
|
memoizedUpdateScroll(scroll.x, scroll.y);
|
|
}, [getClosestScroll, memoizedUpdateScroll]);
|
|
|
|
const scheduleScrollUpdate = useMemo(
|
|
() => rafSchedule(updateScroll),
|
|
[updateScroll],
|
|
);
|
|
|
|
const onClosestScroll = useCallback(() => {
|
|
const dragging: WhileDragging | null = whileDraggingRef.current;
|
|
const closest: Element | null = 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 | null = 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,
|
|
});
|
|
|
|
const scrollable: Element | null = env.closestScrollable;
|
|
|
|
if (scrollable) {
|
|
scrollable.setAttribute(
|
|
dataAttr.scrollContainer.contextId,
|
|
appContext.contextId,
|
|
);
|
|
|
|
// bind scroll listener
|
|
scrollable.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(scrollable);
|
|
}
|
|
}
|
|
|
|
return dimension;
|
|
},
|
|
[appContext.contextId, descriptor, onClosestScroll, previousRef],
|
|
);
|
|
|
|
const getScrollWhileDragging = useCallback((): Position => {
|
|
const dragging: WhileDragging | null = whileDraggingRef.current;
|
|
const closest: Element | null = getClosestScrollableFromDrag(dragging);
|
|
invariant(
|
|
dragging && closest,
|
|
'Can only recollect Droppable client for Droppables that have a scroll container',
|
|
);
|
|
|
|
return getScroll(closest);
|
|
}, []);
|
|
|
|
const dragStopped = useCallback(() => {
|
|
const dragging: WhileDragging | null = whileDraggingRef.current;
|
|
invariant(dragging, 'Cannot stop drag when no active drag');
|
|
const closest = getClosestScrollableFromDrag(dragging);
|
|
|
|
// goodbye old friend
|
|
whileDraggingRef.current = null;
|
|
|
|
if (!closest) {
|
|
return;
|
|
}
|
|
|
|
// unwatch scroll
|
|
scheduleScrollUpdate.cancel();
|
|
closest.removeAttribute(dataAttr.scrollContainer.contextId);
|
|
closest.removeEventListener(
|
|
'scroll',
|
|
onClosestScroll,
|
|
// See: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
getListenerOptions(dragging.scrollOptions) as any,
|
|
);
|
|
}, [onClosestScroll, scheduleScrollUpdate]);
|
|
|
|
const scroll = useCallback((change: Position) => {
|
|
// arrange
|
|
const dragging: WhileDragging | null = whileDraggingRef.current;
|
|
invariant(dragging, 'Cannot scroll when there is no drag');
|
|
const closest: Element | null = 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,
|
|
getScrollWhileDragging,
|
|
dragStopped,
|
|
scroll,
|
|
};
|
|
}, [dragStopped, getDimensionAndWatchScroll, getScrollWhileDragging, scroll]);
|
|
|
|
const entry: DroppableEntry = useMemo(
|
|
() => ({
|
|
uniqueId,
|
|
descriptor,
|
|
callbacks,
|
|
}),
|
|
[callbacks, descriptor, uniqueId],
|
|
);
|
|
|
|
// Register with the marshal and let it know of:
|
|
// - any descriptor changes
|
|
// - when it unmounts
|
|
useLayoutEffect(() => {
|
|
publishedDescriptorRef.current = entry.descriptor;
|
|
registry.droppable.register(entry);
|
|
|
|
return () => {
|
|
if (whileDraggingRef.current) {
|
|
warning(
|
|
'Unsupported: changing the droppableId or type of a Droppable during a drag',
|
|
);
|
|
dragStopped();
|
|
}
|
|
|
|
registry.droppable.unregister(entry);
|
|
};
|
|
}, [callbacks, descriptor, dragStopped, entry, marshal, registry.droppable]);
|
|
|
|
// 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]);
|
|
}
|