Navigation Menu

Skip to content

Commit

Permalink
dnd scroller (#1966)
Browse files Browse the repository at this point in the history
* feat

* feat

* feat

* feat

* Create witty-carrots-heal.md

* feat

* Update witty-carrots-heal.md

* feat
  • Loading branch information
zbeyens committed Nov 4, 2022
1 parent d11db00 commit 7e0e9c0
Show file tree
Hide file tree
Showing 16 changed files with 500 additions and 275 deletions.
7 changes: 7 additions & 0 deletions .changeset/witty-carrots-heal.md
@@ -0,0 +1,7 @@
---
"@udecode/plate-ui-dnd": minor
---

dnd plugin - new options:
- `enableScroller`: this adds a scroll area at the top and bottom of the window so the document scrolls when the mouse drags over. If you have another scroll container, you can either keep it disabled or override the props so the scroll areas are correctly positioned.
- `scrollerProps`
9 changes: 6 additions & 3 deletions examples/src/DndApp.tsx
Expand Up @@ -10,18 +10,21 @@ import {
import { basicElementsValue } from './basic-elements/basicElementsValue';
import { editableProps } from './common/editableProps';
import { plateUI } from './common/plateUI';
import { getNodesWithId } from './dnd/getNodesWithId';
import { withStyledDraggables } from './dnd/withStyledDraggables';
import { createMyPlugins, MyValue } from './typescript/plateTypes';

// set drag handle next to each block
const components = withStyledDraggables(plateUI);

// set id to each block
const initialValue = getNodesWithId(basicElementsValue);
const initialValue = basicElementsValue;

const plugins = createMyPlugins(
[createBasicElementsPlugin(), createNodeIdPlugin(), createDndPlugin()],
[
createBasicElementsPlugin(),
createNodeIdPlugin(),
createDndPlugin({ options: { enableScroller: true } }),
],
{
components,
}
Expand Down
2 changes: 1 addition & 1 deletion examples/src/PlaygroundApp.tsx
Expand Up @@ -125,7 +125,7 @@ const App = () => {
createKbdPlugin(),
createNodeIdPlugin(),
createBlockSelectionPlugin(),
createDndPlugin(),
createDndPlugin({ options: { enableScroller: true } }),
dragOverCursorPlugin,
createIndentPlugin(indentPlugin),
createAutoformatPlugin<
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -89,6 +89,7 @@
"@types/marked": "^4.0.3",
"@types/node": "^17.0.31",
"@types/prismjs": "^1.26.0",
"@types/raf": "^3.4.0",
"@types/react": "17.0.47",
"@types/react-dom": "17.0.17",
"@types/styled-components": "5.1.9",
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/dnd/package.json
Expand Up @@ -22,7 +22,8 @@
"@react-hook/merged-ref": "^1.3.2",
"@tippyjs/react": "^4.2.6",
"@udecode/plate-core": "18.7.0",
"@udecode/plate-styled-components": "18.7.0"
"@udecode/plate-styled-components": "18.7.0",
"raf": "^3.4.1"
},
"peerDependencies": {
"react": ">=16.8.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/dnd/src/components/Scroller/DndScroller.tsx
@@ -0,0 +1,9 @@
import React from 'react';
import { dndStore } from '../../dndStore';
import { Scroller, ScrollerProps } from './Scroller';

export const DndScroller = (props: Partial<ScrollerProps>) => {
const isDragging = dndStore.use.isDragging();

return <Scroller enabled={isDragging} {...props} />;
};
147 changes: 147 additions & 0 deletions packages/ui/dnd/src/components/Scroller/ScrollArea.tsx
@@ -0,0 +1,147 @@
import React, {
CSSProperties,
HTMLAttributes,
RefObject,
useEffect,
useRef,
} from 'react';
import { throttle } from 'lodash';
import raf from 'raf';

const getCoords = (e: any) => {
if (e.type === 'touchmove') {
return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
}

return { x: e.clientX, y: e.clientY };
};

export interface ScrollAreaProps {
placement: 'top' | 'bottom';
enabled?: boolean;
height?: number;
zIndex?: number;
minStrength?: number;
strengthMultiplier?: number;
containerRef?: RefObject<any>;
scrollAreaProps?: HTMLAttributes<HTMLDivElement>;
}

export const ScrollArea = ({
placement,
enabled = true,
height = 100,
zIndex = 10000,
minStrength = 0.15,
strengthMultiplier = 25,
containerRef,
scrollAreaProps,
}: ScrollAreaProps) => {
const ref = useRef<HTMLDivElement>();

const scaleYRef = useRef(0);
const frameRef = useRef<number | null>(null);

const direction = placement === 'top' ? -1 : 1;

// Drag a fixed, invisible box of custom height at the top, and bottom
// of the window. Make sure to show it only when dragging something.
const style: CSSProperties = {
position: 'fixed',
height,
width: '100%',
opacity: 0,
zIndex,
...scrollAreaProps?.style,
};

if (placement === 'top') {
style.top = 0;
} else if (placement === 'bottom') {
style.bottom = 0;
}

const stopScrolling = () => {
scaleYRef.current = 0;

if (frameRef.current) {
raf.cancel(frameRef.current);
frameRef.current = null;
}
};

const startScrolling = () => {
const tick = () => {
const scaleY = scaleYRef.current;

// stop scrolling if there's nothing to do
if (strengthMultiplier === 0 || scaleY === 0) {
stopScrolling();
return;
}

const container = containerRef?.current ?? window;
container.scrollBy(0, scaleY * strengthMultiplier * direction);

frameRef.current = raf(tick);

// there's a bug in safari where it seems like we can't get
// mousemove events from a container that also emits a scroll
// event that same frame. So we should double the strengthMultiplier and only adjust
// the scroll position at 30fps
};

tick();
};

// Update scaleY every 100ms or so
// and start scrolling if necessary
const updateScrolling = throttle(
(e) => {
const container = ref.current;
if (!container) return;

const { top: y, height: h } = container.getBoundingClientRect();
const coords = getCoords(e);

const strength = Math.max(Math.max(coords.y - y, 0) / h, minStrength);

// calculate strength
scaleYRef.current = direction === -1 ? 1 - strength : strength;

// start scrolling if we need to
if (!frameRef.current && scaleYRef.current) {
startScrolling();
}
},
100,
{ trailing: false }
);

const handleEvent = (e: any) => {
updateScrolling(e);
};

useEffect(() => {
if (!enabled) {
stopScrolling();
}
}, [enabled]);

if (!enabled) return null;

// Hide the element if not enabled, so it doesn't interfere with clicking things under it.
return (
<div
ref={ref as any}
style={style}
onDragOver={handleEvent}
onDragLeave={stopScrolling}
onDragEnd={stopScrolling}
// touchmove events don't seem to work across siblings, so we unfortunately
// would have to attach the listeners to the body
onTouchMove={handleEvent}
{...scrollAreaProps}
/>
);
};
16 changes: 16 additions & 0 deletions packages/ui/dnd/src/components/Scroller/Scroller.tsx
@@ -0,0 +1,16 @@
import React from 'react';
import { ScrollArea, ScrollAreaProps } from './ScrollArea';

export type ScrollerProps = Omit<ScrollAreaProps, 'placement'>;
/**
* Set up an edge scroller at the top of the page for scrolling up.
* One at the bottom for scrolling down.
*/
export const Scroller = (props: ScrollerProps) => {
return (
<>
<ScrollArea placement="top" {...props} />
<ScrollArea placement="bottom" {...props} />
</>
);
};
7 changes: 7 additions & 0 deletions packages/ui/dnd/src/components/Scroller/index.ts
@@ -0,0 +1,7 @@
/**
* @file Automatically generated by barrelsby.
*/

export * from './DndScroller';
export * from './ScrollArea';
export * from './Scroller';
1 change: 1 addition & 0 deletions packages/ui/dnd/src/components/index.ts
Expand Up @@ -7,3 +7,4 @@ export * from './Draggable';
export * from './Draggable.types';
export * from './grabberTooltipProps';
export * from './withDraggable';
export * from './Scroller/index';
10 changes: 0 additions & 10 deletions packages/ui/dnd/src/createDndPlugin.ts

This file was deleted.

25 changes: 25 additions & 0 deletions packages/ui/dnd/src/createDndPlugin.tsx
@@ -0,0 +1,25 @@
import React from 'react';
import { createPluginFactory } from '@udecode/plate-core';
import { DndScroller, ScrollerProps } from './components/index';
import { dndStore } from './dndStore';

export interface DndPlugin {
enableScroller?: boolean;
scrollerProps?: Partial<ScrollerProps>;
}

export const KEY_DND = 'dnd';

export const createDndPlugin = createPluginFactory<DndPlugin>({
key: KEY_DND,
handlers: {
onDragStart: () => () => dndStore.set.isDragging(true),
onDragEnd: () => () => dndStore.set.isDragging(false),
onDrop: (editor) => () => editor.isDragging as boolean,
},
then: (editor, { options }) => ({
renderAfterEditable: options.enableScroller
? () => <DndScroller {...options?.scrollerProps} />
: undefined,
}),
});
5 changes: 5 additions & 0 deletions packages/ui/dnd/src/dndStore.ts
@@ -0,0 +1,5 @@
import { createStore } from '@udecode/plate-core';

export const dndStore = createStore('dnd')({
isDragging: false,
});
3 changes: 3 additions & 0 deletions packages/ui/dnd/src/hooks/useDragNode.ts
@@ -1,5 +1,6 @@
import { DragSourceHookSpec, useDrag } from 'react-dnd';
import { TEditor, Value } from '@udecode/plate-core';
import { dndStore } from '../dndStore';
import { DragItemNode } from '../types';

export interface UseDragNodeOptions
Expand Down Expand Up @@ -28,6 +29,7 @@ export const useDragNode = <V extends Value>(
return useDrag<DragItemNode, unknown, { isDragging: boolean }>(
() => ({
item(monitor) {
dndStore.set.isDragging(true);
editor.isDragging = true;
document.body.classList.add('dragging');

Expand All @@ -42,6 +44,7 @@ export const useDragNode = <V extends Value>(
isDragging: monitor.isDragging(),
}),
end: () => {
dndStore.set.isDragging(false);
editor.isDragging = false;
document.body.classList.remove('dragging');
},
Expand Down
1 change: 1 addition & 0 deletions packages/ui/dnd/src/index.ts
Expand Up @@ -9,3 +9,4 @@ export * from './hooks/index';
export * from './queries/index';
export * from './transforms/index';
export * from './utils/index';
export * from './dndStore';

2 comments on commit 7e0e9c0

@vercel
Copy link

@vercel vercel bot commented on 7e0e9c0 Nov 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

plate-examples – ./

plate-examples-git-main-udecode.vercel.app
plate-examples-udecode.vercel.app
plate-examples.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 7e0e9c0 Nov 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

plate – ./

plate.udecode.io
plate-udecode.vercel.app
www.plate.udecode.io
plate-git-main-udecode.vercel.app

Please sign in to comment.