feat: outer drag 、fix new page selection

This commit is contained in:
SaikaSakura 2022-08-05 19:29:26 +08:00
parent 812af254aa
commit 5c51457761
16 changed files with 358 additions and 72 deletions

View File

@ -124,7 +124,12 @@ const EditorContainer = ({
return (
<StyledEditorContainer
lockScroll={lockScroll}
ref={scrollContainerRef}
ref={ref => {
scrollContainerRef.current = ref;
if (editorRef.current?.scrollManager) {
editorRef.current.scrollManager.scrollContainer = ref;
}
}}
onScroll={onScroll}
>
{pageId ? (

View File

@ -2,13 +2,13 @@ import { FC, useEffect, useLayoutEffect, useRef } from 'react';
import { ChildrenView } from '@toeverything/framework/virgo';
import { styled } from '@toeverything/components/ui';
import { sleep } from '@toeverything/utils';
import { GRID_ITEM_MIN_WIDTH, GRID_PROPERTY_KEY, removePercent } from '../grid';
import { GRID_PROPERTY_KEY, removePercent } from '../grid';
export const GRID_ITEM_CLASS_NAME = 'grid-item';
export const GRID_ITEM_CONTENT_CLASS_NAME = `${GRID_ITEM_CLASS_NAME}-content`;
export const GridItem: FC<ChildrenView> = function (props) {
const { children, block } = props;
const { children, block, editor } = props;
const RENDER_DELAY_TIME = 100;
const ref = useRef<HTMLDivElement>();
@ -25,6 +25,7 @@ export const GridItem: FC<ChildrenView> = function (props) {
const checkAndRefreshWidth = async () => {
const currentWidth = block.getProperty(GRID_PROPERTY_KEY);
const gridItemMinWidth = editor.configManager.grid.gridItemMinWidth;
if (currentWidth) {
setWidth(currentWidth);
} else if (!block.dom?.style.width) {
@ -64,26 +65,23 @@ export const GridItem: FC<ChildrenView> = function (props) {
if new width less then min width,
set min width and next block will be fix width
*/
if (newWidth < GRID_ITEM_MIN_WIDTH) {
needFixWidth += GRID_ITEM_MIN_WIDTH - newWidth;
newWidth = GRID_ITEM_MIN_WIDTH;
if (newWidth < gridItemMinWidth) {
needFixWidth += gridItemMinWidth - newWidth;
newWidth = gridItemMinWidth;
}
// if can fix width, fix width
if (
newWidth > GRID_ITEM_MIN_WIDTH &&
needFixWidth
) {
if (newWidth > gridItemMinWidth && needFixWidth) {
if (
newWidth - needFixWidth >=
GRID_ITEM_MIN_WIDTH
gridItemMinWidth
) {
newWidth = newWidth - needFixWidth;
needFixWidth = 0;
} else {
needFixWidth =
needFixWidth -
(newWidth - GRID_ITEM_MIN_WIDTH);
newWidth = GRID_ITEM_MIN_WIDTH;
(newWidth - gridItemMinWidth);
newWidth = gridItemMinWidth;
}
}
if (index === children.length - 2) {

View File

@ -12,10 +12,8 @@ import { debounce, domToRect, Point } from '@toeverything/utils';
import clsx from 'clsx';
import { Protocol } from '@toeverything/datasource/db-service';
const MAX_ITEM_COUNT = 6;
const DB_UPDATE_DELAY = 50;
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
export const GRID_ITEM_MIN_WIDTH = 100 / MAX_ITEM_COUNT;
export const GRID_PROPERTY_KEY = 'gridItemWidth';
export function removePercent(str: string) {
@ -24,13 +22,14 @@ export function removePercent(str: string) {
export const Grid: FC<CreateView> = function (props) {
const { block, editor } = props;
const gridItemMinWidth = editor.configManager.grid.gridItemMinWidth;
const [isOnDrag, setIsOnDrag] = useState<boolean>(false);
const isSetMouseUp = useRef<boolean>(false);
const gridContainerRef = useRef<HTMLDivElement>();
const mouseStartPoint = useRef<Point>();
const gridItemCountRef = useRef<number>();
const originalLeftWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
const originalRightWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
const originalLeftWidth = useRef<number>(gridItemMinWidth);
const originalRightWidth = useRef<number>(gridItemMinWidth);
const [alertHandleId, setAlertHandleId] = useState<string>(null);
const getLeftRightGridItemDomByIndex = (index: number) => {
@ -126,8 +125,8 @@ export const Grid: FC<CreateView> = function (props) {
editor.mouseManager.onMouseupEventOnce(() => {
setIsOnDrag(false);
isSetMouseUp.current = false;
originalLeftWidth.current = GRID_ITEM_MIN_WIDTH;
originalRightWidth.current = GRID_ITEM_MIN_WIDTH;
originalLeftWidth.current = gridItemMinWidth;
originalRightWidth.current = gridItemMinWidth;
mouseStartPoint.current = null;
});
} else {
@ -153,12 +152,12 @@ export const Grid: FC<CreateView> = function (props) {
const newLeftWidth = originalLeftWidth.current - xDistance;
let newLeftPercent = (newLeftWidth / containerWidth) * 100;
let newRightPercent = Number(totalWidth) - newLeftPercent;
if (newLeftPercent < GRID_ITEM_MIN_WIDTH) {
newLeftPercent = GRID_ITEM_MIN_WIDTH;
newRightPercent = totalWidth - GRID_ITEM_MIN_WIDTH;
} else if (newRightPercent < GRID_ITEM_MIN_WIDTH) {
newRightPercent = GRID_ITEM_MIN_WIDTH;
newLeftPercent = totalWidth - GRID_ITEM_MIN_WIDTH;
if (newLeftPercent < gridItemMinWidth) {
newLeftPercent = gridItemMinWidth;
newRightPercent = totalWidth - gridItemMinWidth;
} else if (newRightPercent < gridItemMinWidth) {
newRightPercent = gridItemMinWidth;
newLeftPercent = totalWidth - gridItemMinWidth;
}
//XXX first change dom style is for animation speed, maybe not a good idea
const newLeft = `${newLeftPercent}%`;
@ -213,6 +212,7 @@ export const Grid: FC<CreateView> = function (props) {
<GridContainer
className={clsx({ [GRID_ON_DRAG_CLASS]: isOnDrag })}
ref={gridContainerRef}
gridItemMinWidth={gridItemMinWidth}
isOnDrag={isOnDrag}
>
{block.childrenIds.map((id, i) => {
@ -233,7 +233,8 @@ export const Grid: FC<CreateView> = function (props) {
onMouseDown={event => handleMouseDown(event, i)}
blockId={id}
enabledAddItem={
block.childrenIds.length < MAX_ITEM_COUNT
block.childrenIds.length <
editor.configManager.grid.maxGridItemCount
}
onMouseEnter={event =>
handleHandleMouseEnter(event, i)
@ -252,24 +253,25 @@ export const Grid: FC<CreateView> = function (props) {
);
};
const GridContainer = styled('div')<{ isOnDrag: boolean }>(
({ isOnDrag, theme }) => ({
position: 'relative',
display: 'flex',
alignItems: 'stretch',
borderRadius: '10px',
border: '1px solid #FFF',
minWidth: `${GRID_ITEM_MIN_WIDTH}%`,
[`&:hover .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
const GridContainer = styled('div')<{
isOnDrag: boolean;
gridItemMinWidth: number;
}>(({ isOnDrag, theme, gridItemMinWidth }) => ({
position: 'relative',
display: 'flex',
alignItems: 'stretch',
borderRadius: '10px',
border: '1px solid #FFF',
minWidth: `${gridItemMinWidth}%`,
[`&:hover .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
},
...(isOnDrag && {
[`& .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
},
...(isOnDrag && {
[`& .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
},
}),
})
);
}),
}));
const GridMask = styled('div')({
position: 'fixed',

View File

@ -3,7 +3,7 @@ import { Protocol } from '@toeverything/datasource/db-service';
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
import { GridItem } from '../grid-item/GridItem';
import { GridRender } from './GridRender';
export { GRID_ITEM_MIN_WIDTH, GRID_PROPERTY_KEY, removePercent } from './Grid';
export { GRID_PROPERTY_KEY, removePercent } from './Grid';
export class GridBlock extends BaseView {
public override selectable = false;

View File

@ -187,7 +187,6 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
)
)
);
const scrollDirections = getScrollDirections(
endPointRef.current,
scrollManager.verticalScrollTriggerDistance,
@ -204,6 +203,7 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
mouseType.current = 'up';
startPointBlock.current = null;
setShow(false);
setRect(Rect.fromLTRB(0, 0, 0, 0));
scrollManager.stopAutoScroll();
};

View File

@ -159,6 +159,33 @@ export class BlockCommands {
return [];
}
public async moveInNewGridItem(
blockId: string,
gridItemId: string,
isBefore = false
) {
const block = await this._editor.getBlockById(blockId);
if (block) {
const gridItemBlock = await this._editor.createBlock(
Protocol.Block.Type.gridItem
);
const targetGridItemBlock = await this._editor.getBlockById(
gridItemId
);
await block.remove();
await gridItemBlock.append(block);
if (targetGridItemBlock && gridItemBlock) {
if (isBefore) {
await targetGridItemBlock.before(gridItemBlock);
} else {
await targetGridItemBlock.after(gridItemBlock);
}
}
return gridItemBlock;
}
return undefined;
}
public async splitGroupFromBlock(blockId: string) {
const block = await this._editor.getBlockById(blockId);
await splitGroup(this._editor, block);

View File

@ -0,0 +1,27 @@
import { BlockEditor } from '../..';
/**
*
* the global config for the editor
* @class GridConfig
*/
export class GridConfig {
private _maxGridItemCount = 6;
private _editor: BlockEditor;
constructor(editor: BlockEditor) {
this._editor = editor;
}
get maxGridItemCount() {
return this._maxGridItemCount;
}
set maxGridItemCount(value) {
this._maxGridItemCount = value;
}
get gridItemMinWidth() {
return 100 / this.maxGridItemCount;
}
}

View File

@ -0,0 +1,23 @@
import { BlockEditor } from '../..';
import { GridConfig } from './grid';
// TODO: if config be complex, add children config abstract
/**
*
* the global config for the editor
* @class EditorConfig
*/
export class EditorConfig {
private _maxGridItemCount = 6;
private _editor: BlockEditor;
private _grid: GridConfig;
constructor(editor: BlockEditor) {
this._editor = editor;
this._grid = new GridConfig(editor);
}
get grid() {
return this._grid;
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { domToRect, Point } from '@toeverything/utils';
import { AsyncBlock } from '../..';
import { GridDropType } from '../commands/types';
@ -5,6 +6,7 @@ import { Editor } from '../editor';
import { BlockDropPlacement, GroupDirection } from '../types';
// TODO: Evaluate implementing custom events with Rxjs
import EventEmitter from 'eventemitter3';
import { Protocol } from '@toeverything/datasource/db-service';
enum DragType {
dragBlock = 'dragBlock',
@ -86,6 +88,7 @@ export class DragDropManager {
while (curr !== this._editor.getRootBlockId()) {
if (curr === blockId) return false;
const block = await this._editor.getBlockById(curr);
if (!block) return false;
curr = block.parentId;
}
return true;
@ -114,6 +117,48 @@ export class DragDropManager {
: GridDropType.right
);
}
if (
[
BlockDropPlacement.outerLeft,
BlockDropPlacement.outerRight,
].includes(this._blockDragDirection)
) {
const targetBlock = await this._editor.getBlockById(
this._blockDragTargetId
);
if (targetBlock.type !== Protocol.Block.Type.grid) {
await this._editor.commands.blockCommands.createLayoutBlock(
blockId,
this._blockDragTargetId,
this._blockDragDirection ===
BlockDropPlacement.outerLeft
? GridDropType.left
: GridDropType.right
);
}
if (targetBlock.type === Protocol.Block.Type.grid) {
const gridItems = await targetBlock.children();
if (
BlockDropPlacement.outerRight ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[gridItems.length - 1].id
);
}
if (
BlockDropPlacement.outerLeft ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[0].id,
true
);
}
}
}
}
}
@ -209,6 +254,93 @@ export class DragDropManager {
);
}
/**
*
* check if drag block is out of blocks and return direction
* @param {React.DragEvent<Element>} event
* @return {
* direction: BlockDropPlacement.none, // none, outerLeft, outerRight
* block: undefined, // the block in the same clientY
* isOuter: false, // if is drag over outer
* }
*
* @memberof DragDropManager
*/
public async checkOuterBlockDragTypes(event: React.DragEvent<Element>) {
const { clientX, clientY } = event;
const mousePoint = new Point(clientX, clientY);
const rootBlock = await this._editor.getBlockById(
this._editor.getRootBlockId()
);
let direction = BlockDropPlacement.none;
const rootBlockRect = domToRect(rootBlock.dom);
let targetBlock: AsyncBlock | undefined;
let typesInfo = {
direction: BlockDropPlacement.none,
block: undefined,
isOuter: false,
} as {
direction: BlockDropPlacement;
block: AsyncBlock | undefined;
isOuter: boolean;
};
if (rootBlockRect.isPointLeft(mousePoint)) {
direction = BlockDropPlacement.outerLeft;
typesInfo.isOuter = true;
}
if (rootBlockRect.isPointRight(mousePoint)) {
direction = BlockDropPlacement.outerRight;
typesInfo.isOuter = true;
}
if (direction !== BlockDropPlacement.none) {
const blockList = await this._editor.getBlockListByLevelOrder();
targetBlock = blockList.find(block => {
const domRect = domToRect(block.dom);
const pointChecker =
direction === BlockDropPlacement.outerLeft
? domRect.isPointLeft.bind(domRect)
: domRect.isPointRight.bind(domRect);
return (
block.type !== Protocol.Block.Type.page &&
block.type !== Protocol.Block.Type.group &&
pointChecker(mousePoint)
);
});
if (targetBlock) {
if (targetBlock.type !== Protocol.Block.Type.grid) {
this._setBlockDragDirection(direction);
this._setBlockDragTargetId(targetBlock.id);
typesInfo = {
direction,
block: targetBlock,
isOuter: true,
};
}
if (targetBlock.type === Protocol.Block.Type.grid) {
const children = await targetBlock.children();
if (
children.length <
this._editor.configManager.grid.maxGridItemCount
) {
typesInfo = {
direction,
block: targetBlock,
isOuter: true,
};
}
}
}
}
if (
typesInfo.direction !== BlockDropPlacement.none &&
typesInfo.block
) {
this._setBlockDragTargetId(targetBlock.id);
}
this._setBlockDragDirection(typesInfo.direction);
return typesInfo;
}
public async checkBlockDragTypes(
event: React.DragEvent<Element>,
blockDom: HTMLElement,
@ -216,10 +348,25 @@ export class DragDropManager {
) {
const { clientX, clientY } = event;
this._setBlockDragTargetId(blockId);
const path = await this._editor.getBlockPath(blockId);
const mousePoint = new Point(clientX, clientY);
const rect = domToRect(blockDom);
/**
* IMP: compute the level of the target block
* future feature drag drop has level support do not delete
* const levelUnderGrid = Array.from(path)
.reverse()
.findIndex(block => block.type === Protocol.Block.Type.gridItem);
const levelUnderGroup = Array.from(path)
.reverse()
.findIndex(block => block.type === Protocol.Block.Type.group);
const blockLevel =
levelUnderGrid > 0 ? levelUnderGrid : levelUnderGroup;
console.log({ blockLevel, levelUnderGrid, levelUnderGroup });
*
*/
let direction = BlockDropPlacement.bottom;
if (mousePoint.x - rect.left <= this._dragBlockHotDistance) {
direction = BlockDropPlacement.left;
}
@ -236,9 +383,10 @@ export class DragDropManager {
direction === BlockDropPlacement.left ||
direction === BlockDropPlacement.right
) {
const path = await this._editor.getBlockPath(blockId);
const gridBlocks = path.filter(block => block.type === 'grid');
// limit grid block floor counts
const gridBlocks = path.filter(
block => block.type === Protocol.Block.Type.grid
);
// limit grid block floor counts, when drag block to init grid
if (gridBlocks.length >= MAX_GRID_BLOCK_FLOOR) {
direction = BlockDropPlacement.none;
}

View File

@ -3,6 +3,8 @@ export enum BlockDropPlacement {
right = 'right',
top = 'top',
bottom = 'bottom',
outerLeft = 'outer-left',
outerRight = 'outer-right',
none = 'none',
}

View File

@ -35,6 +35,7 @@ import { BrowserClipboard } from './clipboard/browser-clipboard';
import { ClipboardPopulator } from './clipboard/clipboard-populator';
import { BlockHelper } from './block/block-helper';
import { DragDropManager } from './drag-drop';
import { EditorConfig } from './config';
export interface EditorCtorProps {
workspace: string;
@ -56,6 +57,7 @@ export class Editor implements Virgo {
public dragDropManager = new DragDropManager(this);
public commands = new EditorCommands(this);
public blockHelper = new BlockHelper(this);
public configManager = new EditorConfig(this);
public bdCommands: Commands;
public ui_container?: HTMLDivElement;
public version = '0.0.1';
@ -343,6 +345,23 @@ export class Editor implements Virgo {
return [...blockList, ...(await this.getOffspring(rootBlockId))];
}
async getBlockListByLevelOrder() {
const rootBlockId = this.getRootBlockId();
const rootBlock = await this.getBlockById(rootBlockId);
const blockList: Array<AsyncBlock> = [];
let nextToVisit: Array<AsyncBlock> = rootBlock ? [rootBlock] : [];
while (nextToVisit.length) {
let next: Array<AsyncBlock> = [];
for (const block of nextToVisit) {
const children = await block.children();
blockList.push(block);
next = next.concat(children);
}
nextToVisit = next;
}
return blockList;
}
/**
*
* get all offspring of block

View File

@ -110,7 +110,6 @@ export class ScrollManager {
}
public emitScrollEvent(event: UIEvent) {
this.scrollContainer = event.target as HTMLElement;
this._scrollDirection = this._getScrollDirection();
this._scrollMoveOffset = Math.abs(
this.scrollContainer.scrollTop - this._scrollRecord[0]

View File

@ -73,6 +73,7 @@ export interface Virgo {
getBlockById(blockId: string): Promise<AsyncBlock | null>;
setHotKeysScope(scope?: string): void;
getBlockList: () => Promise<AsyncBlock[]>;
getBlockListByLevelOrder: () => Promise<AsyncBlock[]>;
// removeBlocks: () => void;
storageManager: StorageManager | undefined;
selection: VirgoSelection;

View File

@ -52,7 +52,6 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
return null;
}
const { direction, blockInfo } = lineInfo;
const finalDirection = direction;
const lineStyle = {
zIndex: 2,
position: 'absolute' as const,
@ -91,14 +90,14 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
left: intersectionRect.right + 10 - rootRect.x,
};
const styleMap = {
left: leftLineStyle,
right: rightLineStyle,
top: topLineStyle,
bottom: bottomLineStyle,
[BlockDropPlacement.left]: leftLineStyle,
[BlockDropPlacement.right]: rightLineStyle,
[BlockDropPlacement.top]: topLineStyle,
[BlockDropPlacement.bottom]: bottomLineStyle,
[BlockDropPlacement.outerLeft]: leftLineStyle,
[BlockDropPlacement.outerRight]: rightLineStyle,
};
return (
<div className="editor-menu-line" style={styleMap[finalDirection]} />
);
return <div className="editor-menu-line" style={styleMap[direction]} />;
}
function DragComponent(props: {

View File

@ -5,8 +5,9 @@ import { ignoreBlockTypes } from './menu-config';
import { LineInfoSubject, LeftMenuDraggable } from './LeftMenuDraggable';
import { PluginRenderRoot } from '../../utils';
import { Subject } from 'rxjs';
import { domToRect, last, Point } from '@toeverything/utils';
import { domToRect, last, Point, throttle } from '@toeverything/utils';
import { BlockDropPlacement } from '@toeverything/framework/virgo';
const DRAG_THROTTLE_DELAY = 150;
export class LeftMenuPlugin extends BasePlugin {
private _mousedown?: boolean;
private _root?: PluginRenderRoot;
@ -35,11 +36,7 @@ export class LeftMenuPlugin extends BasePlugin {
.get(HookType.ON_ROOTNODE_MOUSE_UP)
.subscribe(this._handleMouseUp)
);
this.sub.add(
this.hooks
.get(HookType.ON_ROOTNODE_DRAG_OVER)
.subscribe(this._handleDragOverBlockNode)
);
this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_MOUSE_LEAVE).subscribe(() => {
this._hideLeftMenu();
@ -60,8 +57,47 @@ export class LeftMenuPlugin extends BasePlugin {
this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_DROP).subscribe(this._onDrop)
);
this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_DRAG_OVER).subscribe(
throttle(
this._handleRootNodeDragover.bind(this),
DRAG_THROTTLE_DELAY,
{
leading: true,
}
)
)
);
}
private _handleRootNodeDragover = async (
event: React.DragEvent<Element>
) => {
event.preventDefault();
if (this.editor.dragDropManager.isDragBlock(event)) {
const { direction, block, isOuter } =
await this.editor.dragDropManager.checkOuterBlockDragTypes(
event
);
if (direction !== BlockDropPlacement.none && block && block.dom) {
this._lineInfo.next({
direction,
blockInfo: {
blockId: block.id,
dom: block.dom,
type: block.type,
rect: block.dom.getBoundingClientRect(),
properties: block.getProperties(),
},
});
} else if (!isOuter) {
this._handleDragOverBlockNode(event);
} else {
this._lineInfo.next(undefined);
}
}
};
private _onDrop = () => {
this._lineInfo.next(undefined);
};

View File

@ -121,20 +121,20 @@ export class Rect {
}
}
isPointDown({ y }: Point) {
return this.bottom < y;
isPointDown({ x, y }: Point) {
return this.bottom < y && this.left <= x && this.right >= x;
}
isPointUp({ y }: Point) {
return y < this.top;
isPointUp({ x, y }: Point) {
return y < this.top && this.left <= x && this.right >= x;
}
isPointLeft({ x }: Point) {
return x < this.left;
isPointLeft({ x, y }: Point) {
return x < this.left && this.top <= y && this.bottom >= y;
}
isPointRight({ x }: Point) {
return x > this.right;
isPointRight({ x, y }: Point) {
return x > this.right && this.top <= y && this.bottom >= y;
}
fromNewLeft(left: number) {