react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
193 lines (158 loc) • 5.64 kB
JSX
// @flow
import { Component } from 'react';
import invariant from 'invariant';
import rafScheduler from 'raf-schd';
import memoizeOne from 'memoize-one';
import getWindowScrollPosition from '../get-window-scroll-position';
import getClientRect from '../../state/get-client-rect';
import { getDroppableDimension } from '../../state/dimension';
import getClosestScrollable from '../get-closest-scrollable';
// eslint-disable-next-line no-duplicate-imports
import type {
DroppableDimension,
Position,
HTMLElement,
ClientRect,
Spacing,
} from '../../types';
import type { Props } from './droppable-dimension-publisher-types';
const origin: Position = { x: 0, y: 0 };
export default class DroppableDimensionPublisher extends Component {
/* eslint-disable react/sort-comp */
props: Props;
isWatchingScroll: boolean = false;
closestScrollable: HTMLElement = null;
getScrollOffset = (): Position => {
if (!this.closestScrollable) {
return origin;
}
const offset: Position = {
x: this.closestScrollable.scrollLeft,
y: this.closestScrollable.scrollTop,
};
return offset;
}
getDimension = (): DroppableDimension => {
const {
droppableId,
direction,
ignoreContainerClipping,
isDropDisabled,
targetRef,
} = this.props;
invariant(targetRef, 'DimensionPublisher cannot calculate a dimension when not attached to the DOM');
const scroll: Position = this.getScrollOffset();
const style = window.getComputedStyle(targetRef);
// keeping it simple and always using the margin of the droppable
const margin: Spacing = {
top: parseInt(style.marginTop, 10),
right: parseInt(style.marginRight, 10),
bottom: parseInt(style.marginBottom, 10),
left: parseInt(style.marginLeft, 10),
};
const padding: Spacing = {
top: parseInt(style.paddingTop, 10),
right: parseInt(style.paddingRight, 10),
bottom: parseInt(style.paddingBottom, 10),
left: parseInt(style.paddingLeft, 10),
};
const clientRect: ClientRect = targetRef.getBoundingClientRect();
// The droppable's own bounds should be treated as the
// container bounds in the following situations:
// 1. The consumer has opted in to ignoring container clipping
// 2. There is no scroll container
// 3. The droppable has internal scrolling
const containerRect: ClientRect = ignoreContainerClipping ||
!this.closestScrollable ||
this.closestScrollable === targetRef
? clientRect
: getClientRect(this.closestScrollable.getBoundingClientRect());
const dimension: DroppableDimension = getDroppableDimension({
id: droppableId,
direction,
clientRect,
containerRect,
margin,
padding,
windowScroll: getWindowScrollPosition(),
scroll,
isEnabled: !isDropDisabled,
});
return dimension;
}
memoizedUpdateScroll = memoizeOne((x: number, y: number) => {
const offset: Position = { x, y };
this.props.updateScroll(this.props.droppableId, offset);
});
scheduleScrollUpdate = rafScheduler((offset: Position) => {
// might no longer be listening for scroll changes by the time a frame comes back
if (this.isWatchingScroll) {
this.memoizedUpdateScroll(offset.x, offset.y);
}
});
componentWillUnmount() {
if (this.isWatchingScroll) {
this.unwatchScroll();
}
}
onClosestScroll = () => {
this.scheduleScrollUpdate(this.getScrollOffset());
}
watchScroll = () => {
invariant(this.props.targetRef, 'cannot watch scroll if not in the dom');
// no closest parent
if (this.closestScrollable == null) {
return;
}
if (this.isWatchingScroll) {
return;
}
this.isWatchingScroll = true;
this.closestScrollable.addEventListener('scroll', this.onClosestScroll, { passive: true });
}
unwatchScroll = () => {
if (!this.isWatchingScroll) {
return;
}
this.isWatchingScroll = false;
this.closestScrollable.removeEventListener('scroll', this.onClosestScroll);
}
componentWillReceiveProps(nextProps: Props) {
// Because the dimension publisher wraps children - it might render even when its props do
// not change. We need to ensure that it does not publish when it should not.
const shouldStartPublishing = !this.props.shouldPublish && nextProps.shouldPublish;
const alreadyPublishing = this.props.shouldPublish && nextProps.shouldPublish;
const stopPublishing = this.props.shouldPublish && !nextProps.shouldPublish;
// should no longer watch for scrolling
if (stopPublishing) {
this.unwatchScroll();
return;
}
if (alreadyPublishing) {
// if ref changes and watching scroll - unwatch the scroll
if (nextProps.targetRef !== this.props.targetRef) {
if (this.isWatchingScroll) {
console.warn('changing targetRef while watching scroll!');
this.unwatchScroll();
}
}
// publish any changes to the disabled flag
if (nextProps.isDropDisabled !== this.props.isDropDisabled) {
this.props.updateIsEnabled(this.props.droppableId, !nextProps.isDropDisabled);
}
return;
}
// This will be the default when nothing is happening
if (!shouldStartPublishing) {
return;
}
// Need to start publishing
// discovering the closest scrollable for a drag
this.closestScrollable = getClosestScrollable(this.props.targetRef);
this.props.publish(this.getDimension());
this.watchScroll();
}
render() {
return this.props.children;
}
}