mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: Optimize the re-render node when the selection changes
This commit is contained in:
parent
3039f0427f
commit
a38c213744
@ -2,10 +2,11 @@ import { BaseEditor, BaseSelection, Descendant } from "slate";
|
|||||||
import { TreeNode } from '$app/block_editor/view/tree_node';
|
import { TreeNode } from '$app/block_editor/view/tree_node';
|
||||||
import { Operation } from "$app/block_editor/core/operation";
|
import { Operation } from "$app/block_editor/core/operation";
|
||||||
import { TextBlockSelectionManager } from './text_selection';
|
import { TextBlockSelectionManager } from './text_selection';
|
||||||
|
import { BlockType } from "@/appflowy_app/interfaces";
|
||||||
|
|
||||||
export class TextBlockManager {
|
export class TextBlockManager {
|
||||||
public selectionManager: TextBlockSelectionManager;
|
public selectionManager: TextBlockSelectionManager;
|
||||||
constructor(private operation: Operation) {
|
constructor(private rootId: string, private operation: Operation) {
|
||||||
this.selectionManager = new TextBlockSelectionManager();
|
this.selectionManager = new TextBlockSelectionManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,6 +19,24 @@ export class TextBlockManager {
|
|||||||
this.operation.updateNode(node.id, path, data);
|
this.operation.updateNode(node.id, path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteNode(node: TreeNode) {
|
||||||
|
if (node.type !== BlockType.TextBlock) {
|
||||||
|
this.operation.updateNode(node.id, ['type'], BlockType.TextBlock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.parent!.id !== this.rootId) {
|
||||||
|
const newParent = node.parent!.parent!;
|
||||||
|
const newPrev = node.parent;
|
||||||
|
this.operation.moveNode(node.id, newParent.id, newPrev?.id || '');
|
||||||
|
}
|
||||||
|
if (!node.prevLine) return;
|
||||||
|
this.operation.updateNode(node.prevLine.id, ['data', 'content'], [
|
||||||
|
...node.prevLine.data.content,
|
||||||
|
...node.data.content,
|
||||||
|
]);
|
||||||
|
this.operation.deleteNode(node.id);
|
||||||
|
}
|
||||||
|
|
||||||
splitNode(node: TreeNode, editor: BaseEditor) {
|
splitNode(node: TreeNode, editor: BaseEditor) {
|
||||||
const focus = editor.selection?.focus;
|
const focus = editor.selection?.focus;
|
||||||
const path = focus?.path || [0, editor.children.length - 1];
|
const path = focus?.path || [0, editor.children.length - 1];
|
||||||
|
@ -5,6 +5,7 @@ import { Block } from './block';
|
|||||||
|
|
||||||
export class Operation {
|
export class Operation {
|
||||||
private sync: BlockEditorSync;
|
private sync: BlockEditorSync;
|
||||||
|
|
||||||
constructor(private blockChain: BlockChain) {
|
constructor(private blockChain: BlockChain) {
|
||||||
this.sync = new BlockEditorSync();
|
this.sync = new BlockEditorSync();
|
||||||
}
|
}
|
||||||
@ -66,6 +67,19 @@ export class Operation {
|
|||||||
});
|
});
|
||||||
this.sync.sendOps([op]);
|
this.sync.sendOps([op]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveNode(blockId: string, newParentId: string, newPrevId: string) {
|
||||||
|
const op = this.getMoveOp(blockId, newParentId, newPrevId);
|
||||||
|
this.blockChain.move(blockId, newParentId, newPrevId);
|
||||||
|
this.sync.sendOps([op]);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNode(blockId: string) {
|
||||||
|
const op = this.getRemoveOp(blockId);
|
||||||
|
this.blockChain.remove(blockId);
|
||||||
|
this.sync.sendOps([op]);
|
||||||
|
}
|
||||||
|
|
||||||
private getUpdateNodeOp<T>(blockId: string, path: string[], value: T): {
|
private getUpdateNodeOp<T>(blockId: string, path: string[], value: T): {
|
||||||
type: 'update',
|
type: 'update',
|
||||||
data: UpdateOpData
|
data: UpdateOpData
|
||||||
|
@ -3,30 +3,31 @@ export class BlockPositionManager {
|
|||||||
private regionGrid: RegionGrid;
|
private regionGrid: RegionGrid;
|
||||||
private viewportBlocks: Set<string> = new Set();
|
private viewportBlocks: Set<string> = new Set();
|
||||||
private blockPositions: Map<string, BlockPosition> = new Map();
|
private blockPositions: Map<string, BlockPosition> = new Map();
|
||||||
private observer: IntersectionObserver;
|
|
||||||
private container: HTMLDivElement | null = null;
|
private container: HTMLDivElement | null = null;
|
||||||
|
|
||||||
constructor(container: HTMLDivElement) {
|
constructor(container: HTMLDivElement) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.regionGrid = new RegionGrid(container.offsetHeight);
|
this.regionGrid = new RegionGrid(container.offsetHeight);
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
|
||||||
for (const entry of entries) {
|
}
|
||||||
const blockId = entry.target.getAttribute('data-block-id');
|
|
||||||
if (!blockId) return;
|
isInViewport(nodeId: string) {
|
||||||
if (entry.isIntersecting) {
|
return this.viewportBlocks.has(nodeId);
|
||||||
this.updateBlockPosition(blockId);
|
|
||||||
this.viewportBlocks.add(blockId);
|
|
||||||
} else {
|
|
||||||
this.viewportBlocks.delete(blockId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { root: container });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observeBlock(node: HTMLDivElement) {
|
observeBlock(node: HTMLDivElement) {
|
||||||
this.observer.observe(node);
|
const blockId = node.getAttribute('data-block-id');
|
||||||
|
if (blockId) {
|
||||||
|
this.updateBlockPosition(blockId);
|
||||||
|
this.viewportBlocks.add(blockId);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
unobserve: () => this.observer.unobserve(node),
|
unobserve: () => {
|
||||||
|
if (blockId) {
|
||||||
|
this.viewportBlocks.delete(blockId);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +68,6 @@ export class BlockPositionManager {
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.container = null;
|
this.container = null;
|
||||||
this.observer.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -133,21 +133,25 @@ export class RenderTree {
|
|||||||
updateSelections(selections: string[]) {
|
updateSelections(selections: string[]) {
|
||||||
const newSelections = filterSelections<TreeNode>(selections, this.map);
|
const newSelections = filterSelections<TreeNode>(selections, this.map);
|
||||||
|
|
||||||
let isDiff = false;
|
|
||||||
if (newSelections.length !== this.selections.size) {
|
|
||||||
isDiff = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedBlocksSet = new Set(newSelections);
|
const selectedBlocksSet = new Set(newSelections);
|
||||||
if (Array.from(this.selections).some((id) => !selectedBlocksSet.has(id))) {
|
|
||||||
isDiff = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDiff) {
|
const updateNotSelected: string[] = [];
|
||||||
const shouldUpdateIds = new Set([...this.selections, ...newSelections]);
|
const updateSelected: string[] = [];
|
||||||
this.selections = selectedBlocksSet;
|
Array.from(this.selections).forEach((id) => {
|
||||||
shouldUpdateIds.forEach((id) => this.forceUpdate(id));
|
if (!selectedBlocksSet.has(id)) {
|
||||||
}
|
updateNotSelected.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newSelections.forEach(id => {
|
||||||
|
if (!this.selections.has(id)) {
|
||||||
|
updateSelected.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selections = selectedBlocksSet;
|
||||||
|
[...updateNotSelected, ...updateSelected].forEach((id) => {
|
||||||
|
this.forceUpdate(id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelected(nodeId: string) {
|
isSelected(nodeId: string) {
|
||||||
|
@ -52,6 +52,24 @@ export class TreeNode {
|
|||||||
this.children.push(node);
|
this.children.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastChild() {
|
||||||
|
return this.children[this.children.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get prevLine(): TreeNode | null {
|
||||||
|
if (!this.parent) return null;
|
||||||
|
const index = this.parent?.children.findIndex(item => item.id === this.id);
|
||||||
|
if (index === 0) {
|
||||||
|
return this.parent;
|
||||||
|
}
|
||||||
|
const prev = this.parent.children[index - 1];
|
||||||
|
let line = prev;
|
||||||
|
while(line.lastChild) {
|
||||||
|
line = line.lastChild;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
get block() {
|
get block() {
|
||||||
return this._block;
|
return this._block;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ export function useBlockList({ blockId, blockEditor }: BlockListProps) {
|
|||||||
const rowVirtualizer = useVirtualizer({
|
const rowVirtualizer = useVirtualizer({
|
||||||
count: root?.children.length || 0,
|
count: root?.children.length || 0,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
overscan: 5,
|
|
||||||
estimateSize: () => {
|
estimateSize: () => {
|
||||||
return defaultSize;
|
return defaultSize;
|
||||||
},
|
},
|
||||||
@ -71,7 +70,7 @@ export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: Fa
|
|||||||
|
|
||||||
export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) {
|
export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) {
|
||||||
return (props: BlockListProps) => {
|
return (props: BlockListProps) => {
|
||||||
const textBlockManager = new TextBlockManager(props.blockEditor.operation);
|
const textBlockManager = new TextBlockManager(props.blockId, props.blockEditor.operation);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -49,6 +49,9 @@ export function useTextBlock({
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'Backspace': {
|
||||||
|
console.log(editor.selection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerHotkey(event, editor);
|
triggerHotkey(event, editor);
|
||||||
|
Loading…
Reference in New Issue
Block a user