mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(core): drop doc onto split view (#9487)
fix AF-2068, AF-2069, AF-1175, AF-2061, AF-2079, AF-2034, AF-2080, AF-1960, AF-2081 1. replace `dnd-kit` with `@atlaskit/pragmatic-drag-and-drop` 2. allow creating split views by drag & drop the following a. WorkbenchLinks (route links), like journals, trash, all docs b. doc refs c. tags/collection 3. style adjustments to split view 4. remove split view's feature flag and make it GA for electron https://github.com/user-attachments/assets/6a3e4a25-faa2-4215-8eb0-983f44db6e8c
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
|
||||
import { useTransition } from 'react-transition-state';
|
||||
import { useTransitionState } from 'react-transition-state';
|
||||
|
||||
import { useDropTarget } from '../../ui/dnd';
|
||||
import { Tooltip, type TooltipProps } from '../../ui/tooltip';
|
||||
import * as styles from './resize-panel.css';
|
||||
|
||||
@@ -22,6 +23,7 @@ export interface ResizeHandleProps
|
||||
tooltipShortcut?: TooltipProps['shortcut'];
|
||||
tooltipOptions?: Partial<Omit<TooltipProps, 'content' | 'shortcut'>>;
|
||||
tooltipShortcutClassName?: string;
|
||||
dropTargetOptions?: Parameters<typeof useDropTarget>[0];
|
||||
}
|
||||
|
||||
export interface ResizePanelProps
|
||||
@@ -40,6 +42,7 @@ export interface ResizePanelProps
|
||||
resizeHandleTooltipOptions?: Partial<
|
||||
Omit<TooltipProps, 'content' | 'shortcut'>
|
||||
>;
|
||||
resizeHandleDropTargetOptions?: Parameters<typeof useDropTarget>[0];
|
||||
enableAnimation?: boolean;
|
||||
width: number;
|
||||
unmountOnExit?: boolean;
|
||||
@@ -56,6 +59,7 @@ const ResizeHandle = ({
|
||||
resizeHandlePos,
|
||||
resizeHandleOffset,
|
||||
resizeHandleVerticalPadding,
|
||||
dropTargetOptions,
|
||||
open,
|
||||
onOpen,
|
||||
onResizing,
|
||||
@@ -115,6 +119,10 @@ const ResizeHandle = ({
|
||||
[maxWidth, resizeHandlePos, minWidth, onWidthChange, onResizing, onOpen]
|
||||
);
|
||||
|
||||
const { dropTargetRef } = useDropTarget(dropTargetOptions, [
|
||||
dropTargetOptions,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tooltip}
|
||||
@@ -125,7 +133,10 @@ const ResizeHandle = ({
|
||||
<div
|
||||
{...rest}
|
||||
data-testid="resize-handle"
|
||||
ref={ref}
|
||||
ref={node => {
|
||||
ref.current = node;
|
||||
dropTargetRef.current = node;
|
||||
}}
|
||||
style={assignInlineVars({
|
||||
[styles.resizeHandleOffsetVar]: `${resizeHandleOffset ?? 0}px`,
|
||||
[styles.resizeHandleVerticalPadding]: `${
|
||||
@@ -169,17 +180,18 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||
resizeHandleTooltipShortcut,
|
||||
resizeHandleTooltipShortcutClassName,
|
||||
resizeHandleTooltipOptions,
|
||||
resizeHandleDropTargetOptions,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const safeWidth = Math.min(maxWidth, Math.max(minWidth, width));
|
||||
const [{ status }, toggle] = useTransition({
|
||||
const [{ status }, toggle] = useTransitionState({
|
||||
timeout: animationTimeout,
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
toggle(open);
|
||||
}, [open]);
|
||||
}, [open, toggle]);
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
@@ -213,6 +225,7 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||
onWidthChange={onWidthChange}
|
||||
open={open}
|
||||
resizing={resizing}
|
||||
dropTargetOptions={resizeHandleDropTargetOptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
33
packages/frontend/component/src/ui/dnd/common.ts
Normal file
33
packages/frontend/component/src/ui/dnd/common.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { DNDData, fromExternalData } from './types';
|
||||
|
||||
export const isExternalDrag = <D extends DNDData>(args: {
|
||||
source: { data: D['draggable'] };
|
||||
}) => {
|
||||
return !args.source['data'];
|
||||
};
|
||||
|
||||
export const getAdaptedEventArgs = <
|
||||
D extends DNDData,
|
||||
Args extends { source: { data: D['draggable'] } },
|
||||
>(
|
||||
args: Args,
|
||||
fromExternalData?: fromExternalData<D>,
|
||||
isDropEvent = false
|
||||
): Args => {
|
||||
const data =
|
||||
isExternalDrag(args) && fromExternalData
|
||||
? fromExternalData(
|
||||
// @ts-expect-error hack for external data adapter (source has no data field)
|
||||
args as ExternalGetDataFeedbackArgs,
|
||||
isDropEvent
|
||||
)
|
||||
: args.source['data'];
|
||||
|
||||
return {
|
||||
...args,
|
||||
source: {
|
||||
...args.source,
|
||||
data,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -71,7 +71,6 @@ export const useDraggable = <D extends DNDData = DNDData>(
|
||||
|
||||
const context = useContext(DNDContext);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const options = useMemo(() => {
|
||||
const opts = getOptions();
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { shallowUpdater } from '../../utils';
|
||||
import { getAdaptedEventArgs, isExternalDrag } from './common';
|
||||
import { DNDContext } from './context';
|
||||
import type { DNDData, fromExternalData } from './types';
|
||||
|
||||
@@ -44,7 +46,7 @@ export type DropTargetTreeInstruction = Instruction;
|
||||
|
||||
export type ExternalDragPayload = ExternalDragType['payload'];
|
||||
|
||||
type DropTargetGetFeedback<D extends DNDData> = Parameters<
|
||||
export type DropTargetGetFeedback<D extends DNDData> = Parameters<
|
||||
NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']>
|
||||
>[0] & {
|
||||
source: {
|
||||
@@ -59,38 +61,6 @@ type DropTargetGet<T, D extends DNDData> =
|
||||
| T
|
||||
| ((data: DropTargetGetFeedback<D>) => T);
|
||||
|
||||
const isExternalDrag = <D extends DNDData>(
|
||||
args: Pick<DropTargetGetFeedback<D>, 'source'>
|
||||
) => {
|
||||
return !args.source['data'];
|
||||
};
|
||||
|
||||
const getAdaptedEventArgs = <
|
||||
D extends DNDData,
|
||||
Args extends Pick<DropTargetGetFeedback<D>, 'source'>,
|
||||
>(
|
||||
options: DropTargetOptions<D>,
|
||||
args: Args,
|
||||
isDropEvent = false
|
||||
): Args => {
|
||||
const data =
|
||||
isExternalDrag(args) && options.fromExternalData
|
||||
? options.fromExternalData(
|
||||
// @ts-expect-error hack for external data adapter (source has no data field)
|
||||
args as ExternalGetDataFeedbackArgs,
|
||||
isDropEvent
|
||||
)
|
||||
: args.source['data'];
|
||||
|
||||
return {
|
||||
...args,
|
||||
source: {
|
||||
...args.source,
|
||||
data,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function dropTargetGet<T, D extends DNDData>(
|
||||
get: T,
|
||||
options: DropTargetOptions<D>
|
||||
@@ -110,7 +80,7 @@ function dropTargetGet<T, D extends DNDData>(
|
||||
) => {
|
||||
if (typeof get === 'function') {
|
||||
return (get as any)({
|
||||
...getAdaptedEventArgs(options, args),
|
||||
...getAdaptedEventArgs(args, options.fromExternalData),
|
||||
get treeInstruction() {
|
||||
return options.treeInstruction
|
||||
? extractInstruction(
|
||||
@@ -145,8 +115,8 @@ function dropTargetGet<T, D extends DNDData>(
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
...getAdaptedEventArgs(args, options.fromExternalData),
|
||||
...get,
|
||||
...getAdaptedEventArgs(options, args),
|
||||
};
|
||||
}
|
||||
}) as any;
|
||||
@@ -168,6 +138,8 @@ export interface DropTargetOptions<D extends DNDData = DNDData> {
|
||||
};
|
||||
onDrop?: (data: DropTargetDropEvent<D>) => void;
|
||||
onDrag?: (data: DropTargetDragEvent<D>) => void;
|
||||
onDragEnter?: (data: DropTargetDragEvent<D>) => void;
|
||||
onDragLeave?: (data: DropTargetDragEvent<D>) => void;
|
||||
/**
|
||||
* external data adapter.
|
||||
* Will use the external data adapter from the context if not provided.
|
||||
@@ -232,6 +204,55 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
const dropTargetOptions = useMemo(() => {
|
||||
const wrappedCanDrop = dropTargetGet(options.canDrop, options);
|
||||
let _element: HTMLElement | null = null;
|
||||
|
||||
const updateDragOver = (
|
||||
args: DropTargetDragEvent<D>,
|
||||
handler?: (data: DropTargetDragEvent<D>) => void
|
||||
) => {
|
||||
args = getAdaptedEventArgs(args, options.fromExternalData);
|
||||
if (
|
||||
args.location.current.dropTargets[0]?.element === dropTargetRef.current
|
||||
) {
|
||||
if (enableDraggedOverDraggable.current) {
|
||||
setDraggedOverDraggable(shallowUpdater(args.source));
|
||||
}
|
||||
let instruction = null;
|
||||
let closestEdge = null;
|
||||
if (options.treeInstruction) {
|
||||
instruction = extractInstruction(args.self.data);
|
||||
setTreeInstruction(shallowUpdater(instruction));
|
||||
if (dropTargetRef.current) {
|
||||
dropTargetRef.current.dataset['treeInstruction'] =
|
||||
instruction?.type;
|
||||
}
|
||||
}
|
||||
if (options.closestEdge) {
|
||||
closestEdge = extractClosestEdge(args.self.data);
|
||||
setClosestEdge(shallowUpdater(closestEdge));
|
||||
}
|
||||
if (enableDropEffect.current) {
|
||||
setDropEffect(shallowUpdater(args.self.dropEffect));
|
||||
}
|
||||
if (enableDraggedOverPosition.current) {
|
||||
const rect = args.self.element.getBoundingClientRect();
|
||||
const { clientX, clientY } = args.location.current.input;
|
||||
setDraggedOverPosition(
|
||||
shallowUpdater({
|
||||
relativeX: clientX - rect.x,
|
||||
relativeY: clientY - rect.y,
|
||||
clientX: clientX,
|
||||
clientY: clientY,
|
||||
})
|
||||
);
|
||||
}
|
||||
handler?.({
|
||||
...args,
|
||||
treeInstruction: instruction,
|
||||
closestEdge,
|
||||
} as DropTargetDropEvent<D>);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
get element() {
|
||||
if (!_element) {
|
||||
@@ -285,7 +306,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
|
||||
// external data is only available in drop event thus
|
||||
// this is the only case for getAdaptedEventArgs
|
||||
const args = getAdaptedEventArgs(options, _args, true);
|
||||
const args = getAdaptedEventArgs(_args, options.fromExternalData, true);
|
||||
if (
|
||||
isExternalDrag(_args) &&
|
||||
options.fromExternalData &&
|
||||
@@ -309,7 +330,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
}
|
||||
},
|
||||
getData: (args: DropTargetGetFeedback<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
args = getAdaptedEventArgs(args, options.fromExternalData);
|
||||
const originData = dropTargetGet(options.data ?? {}, options)(args);
|
||||
const { input, element } = args;
|
||||
const withInstruction = options.treeInstruction
|
||||
@@ -332,50 +353,29 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
return withClosestEdge;
|
||||
},
|
||||
onDrag: (args: DropTargetDragEvent<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
if (
|
||||
args.location.current.dropTargets[0]?.element ===
|
||||
dropTargetRef.current
|
||||
) {
|
||||
if (enableDraggedOverDraggable.current) {
|
||||
setDraggedOverDraggable(args.source);
|
||||
}
|
||||
let instruction = null;
|
||||
let closestEdge = null;
|
||||
if (options.treeInstruction) {
|
||||
instruction = extractInstruction(args.self.data);
|
||||
setTreeInstruction(instruction);
|
||||
if (dropTargetRef.current) {
|
||||
dropTargetRef.current.dataset['treeInstruction'] =
|
||||
instruction?.type;
|
||||
}
|
||||
}
|
||||
if (options.closestEdge) {
|
||||
closestEdge = extractClosestEdge(args.self.data);
|
||||
setClosestEdge(closestEdge);
|
||||
}
|
||||
if (enableDropEffect.current) {
|
||||
setDropEffect(args.self.dropEffect);
|
||||
}
|
||||
if (enableDraggedOverPosition.current) {
|
||||
const rect = args.self.element.getBoundingClientRect();
|
||||
const { clientX, clientY } = args.location.current.input;
|
||||
setDraggedOverPosition({
|
||||
relativeX: clientX - rect.x,
|
||||
relativeY: clientY - rect.y,
|
||||
clientX: clientX,
|
||||
clientY: clientY,
|
||||
});
|
||||
}
|
||||
options.onDrag?.({
|
||||
...args,
|
||||
treeInstruction: instruction,
|
||||
closestEdge,
|
||||
} as DropTargetDropEvent<D>);
|
||||
}
|
||||
updateDragOver(args, options.onDrag);
|
||||
},
|
||||
onDragEnter: (args: DropTargetDragEvent<D>) => {
|
||||
updateDragOver(args, options.onDragEnter);
|
||||
},
|
||||
onDragLeave: (args: DropTargetDragEvent<D>) => {
|
||||
args = getAdaptedEventArgs(args, options.fromExternalData);
|
||||
|
||||
const withClosestEdge = options.closestEdge
|
||||
? attachClosestEdge(args.self.data, {
|
||||
element: args.self.element,
|
||||
input: args.location.current.input,
|
||||
allowedEdges: options.closestEdge.allowedEdges,
|
||||
})
|
||||
: args.self.data;
|
||||
|
||||
options.onDragLeave?.({
|
||||
...args,
|
||||
self: { ...args.self, data: withClosestEdge },
|
||||
});
|
||||
},
|
||||
onDropTargetChange: (args: DropTargetDropEvent<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
args = getAdaptedEventArgs(args, options.fromExternalData);
|
||||
if (
|
||||
args.location.current.dropTargets[0]?.element ===
|
||||
dropTargetRef.current
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './context';
|
||||
export * from './draggable';
|
||||
export * from './drop-indicator';
|
||||
export * from './drop-target';
|
||||
export * from './monitor';
|
||||
export * from './types';
|
||||
|
||||
121
packages/frontend/component/src/ui/dnd/monitor.tsx
Normal file
121
packages/frontend/component/src/ui/dnd/monitor.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import type {
|
||||
DragLocationHistory,
|
||||
ElementDragType,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/types';
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
|
||||
import { getAdaptedEventArgs } from './common';
|
||||
import { DNDContext } from './context';
|
||||
import type { DNDData, fromExternalData } from './types';
|
||||
|
||||
type MonitorGetFeedback<D extends DNDData = DNDData> = Parameters<
|
||||
NonNullable<Parameters<typeof monitorForElements>[0]['canMonitor']>
|
||||
>[0] & {
|
||||
source: {
|
||||
data: D['draggable'];
|
||||
};
|
||||
};
|
||||
|
||||
type MonitorGet<T, D extends DNDData = DNDData> =
|
||||
| T
|
||||
| ((data: MonitorGetFeedback<D>) => T);
|
||||
|
||||
type MonitorDragEvent<D extends DNDData = DNDData> = {
|
||||
/**
|
||||
* Location history for the drag operation
|
||||
*/
|
||||
location: DragLocationHistory;
|
||||
/**
|
||||
* Data associated with the entity that is being dragged
|
||||
*/
|
||||
source: Exclude<ElementDragType['payload'], 'data'> & {
|
||||
data: D['draggable'];
|
||||
};
|
||||
};
|
||||
|
||||
export interface MonitorOptions<D extends DNDData = DNDData> {
|
||||
canMonitor?: MonitorGet<boolean, D>;
|
||||
onDragStart?: (data: MonitorDragEvent<D>) => void;
|
||||
onDrag?: (data: MonitorDragEvent<D>) => void;
|
||||
onDrop?: (data: MonitorDragEvent<D>) => void;
|
||||
onDropTargetChange?: (data: MonitorDragEvent<D>) => void;
|
||||
/**
|
||||
* external data adapter.
|
||||
* Will use the external data adapter from the context if not provided.
|
||||
*/
|
||||
fromExternalData?: fromExternalData<D>;
|
||||
/**
|
||||
* Make the drop target allow external data.
|
||||
* If this is undefined, it will be set to true if fromExternalData is provided.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
allowExternal?: boolean;
|
||||
}
|
||||
|
||||
function monitorGet<D extends DNDData, T>(
|
||||
get: T,
|
||||
options: MonitorOptions<D>
|
||||
): T extends undefined
|
||||
? undefined
|
||||
: T extends MonitorGet<infer I>
|
||||
? (args: MonitorGetFeedback<D>) => I
|
||||
: never {
|
||||
if (get === undefined) {
|
||||
return undefined as any;
|
||||
}
|
||||
return ((args: MonitorGetFeedback<D>) => {
|
||||
const adaptedArgs = getAdaptedEventArgs(args, options.fromExternalData);
|
||||
return typeof get === 'function'
|
||||
? (get as any)(adaptedArgs)
|
||||
: {
|
||||
...adaptedArgs,
|
||||
...get,
|
||||
};
|
||||
}) as any;
|
||||
}
|
||||
|
||||
export const useDndMonitor = <D extends DNDData = DNDData>(
|
||||
getOptions: () => MonitorOptions<D> = () => ({}),
|
||||
deps: any[] = []
|
||||
) => {
|
||||
const dropTargetContext = useContext(DNDContext);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const opts = getOptions();
|
||||
const allowExternal = opts.allowExternal ?? !!opts.fromExternalData;
|
||||
return {
|
||||
...opts,
|
||||
allowExternal,
|
||||
fromExternalData: allowExternal
|
||||
? (opts.fromExternalData ??
|
||||
(dropTargetContext.fromExternalData as fromExternalData<D>))
|
||||
: undefined,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps, getOptions]);
|
||||
|
||||
const monitorOptions = useMemo(() => {
|
||||
return {
|
||||
canMonitor: monitorGet(options.canMonitor, options),
|
||||
onDragStart: monitorGet(options.onDragStart, options),
|
||||
onDrag: monitorGet(options.onDrag, options),
|
||||
onDrop: monitorGet(options.onDrop, options),
|
||||
onDropTargetChange: monitorGet(options.onDropTargetChange, options),
|
||||
};
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
return monitorForElements(monitorOptions);
|
||||
}, [monitorOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!options.fromExternalData) {
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error external & element adapter types have some subtle differences
|
||||
return monitorForExternal(monitorOptions);
|
||||
}, [monitorOptions, options.fromExternalData]);
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import type { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
|
||||
export type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
|
||||
export interface DNDData<
|
||||
Draggable extends Record<string, unknown> = Record<string, unknown>,
|
||||
DropTarget extends Record<string, unknown> = Record<string, unknown>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './observe-intersection';
|
||||
export * from './observe-resize';
|
||||
export * from './shallow-equal';
|
||||
export { startScopedViewTransition } from './view-transition';
|
||||
export * from './with-unit';
|
||||
|
||||
43
packages/frontend/component/src/utils/shallow-equal.ts
Normal file
43
packages/frontend/component/src/utils/shallow-equal.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
|
||||
export function shallowEqual(objA: any, objB: any) {
|
||||
if (Object.is(objA, objB)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof objA !== 'object' ||
|
||||
objA === null ||
|
||||
typeof objB !== 'object' ||
|
||||
objB === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test for A's keys different from B.
|
||||
for (const key of keysA) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(objB, key) ||
|
||||
!Object.is(objA[key], objB[key])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const shallowUpdater =
|
||||
<T>(newState: T) =>
|
||||
(state: T | null): T => {
|
||||
if (state && shallowEqual(state, newState)) {
|
||||
return state;
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
Reference in New Issue
Block a user