Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotfix/#222 드래그하고 입력할때 텍스트 안지워지는 문제 해결 #223

Merged
2 changes: 2 additions & 0 deletions client/src/features/editor/components/block/Block.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export const textContainerStyle = cva({
base: {
...baseTextStyle,
position: "relative",
wordBreak: "break-word",
whiteSpace: "pre-wrap",
"&:empty::before": {
color: "gray.300",
pointerEvents: "none",
Expand Down
8 changes: 6 additions & 2 deletions client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ interface BlockProps {
blockRef: HTMLDivElement | null,
block: CRDTBlock,
) => void;
onPaste: (e: React.ClipboardEvent<HTMLDivElement>, block: CRDTBlock) => void;
onPaste: (
e: React.ClipboardEvent<HTMLDivElement>,
blockRef: HTMLDivElement | null,
block: CRDTBlock,
) => void;
onClick: (blockId: BlockId, e: React.MouseEvent<HTMLDivElement>) => void;
onAnimationSelect: (blockId: BlockId, animation: AnimationType) => void;
onTypeSelect: (blockId: BlockId, type: ElementType) => void;
Expand Down Expand Up @@ -253,7 +257,7 @@ export const Block: React.FC<BlockProps> = memo(
onInput={handleInput}
onClick={(e) => onClick(block.id, e)}
onCopy={(e) => onCopy(e, blockRef.current, block)}
onPaste={(e) => onPaste(e, block)}
onPaste={(e) => onPaste(e, blockRef.current, block)}
onMouseUp={handleMouseUp}
onCompositionEnd={(e) => onCompositionEnd(e, block)}
contentEditable={block.type !== "hr"}
Expand Down
128 changes: 105 additions & 23 deletions client/src/features/editor/hooks/useBlockOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,39 +127,121 @@ export const useBlockOperation = ({
[sendCharInsertOperation, sendCharDeleteOperation, editorCRDT, pageId],
);

const deleteSelectedText = useCallback(
(block: Block, startOffset: number, endOffset: number) => {
for (let i = endOffset - 1; i >= startOffset; i--) {
const operationNode = block.crdt.localDelete(i, block.id, pageId);
sendCharDeleteOperation(operationNode);
}
block.crdt.currentCaret = startOffset;
},
[pageId, sendCharDeleteOperation],
);

const handleKeyWithSelection = useCallback(
(
e: React.KeyboardEvent<HTMLDivElement>,
block: Block,
startOffset: number,
endOffset: number,
) => {
switch (e.key) {
case "Backspace":
case "Delete": {
e.preventDefault();
deleteSelectedText(block, startOffset, endOffset);
setEditorState({
clock: editorCRDT.clock,
linkedList: editorCRDT.LinkedList,
});
break;
}
// 복사, 잘라내기, 실행취소 등 조합 키는 기본 동작 허용
case "c":
case "v":
case "x":
case "z":
case "y": {
if (e.metaKey || e.ctrlKey) {
// 기본 브라우저 동작 허용
return;
}
deleteSelectedText(block, startOffset, endOffset);
onKeyDown(e);
break;
}
// 탐색 및 선택 관련 키
case "Tab":
case "ArrowLeft":
case "ArrowRight":
case "ArrowUp":
case "ArrowDown":
case "Home":
case "End":
case "PageUp":
case "PageDown": {
e.preventDefault();
onKeyDown(e);
break;
}
// 기능 키들은 기본 동작 허용
case "F1":
case "F2":
case "F3":
case "F4":
case "F5":
case "F6":
case "F7":
case "F8":
case "F9":
case "F10":
case "F11":
case "F12": {
return;
}
case "Enter": {
deleteSelectedText(block, startOffset, endOffset);
onKeyDown(e);
break;
}
case "Escape": {
// 선택 해제만 하고 다른 동작은 하지 않음
window.getSelection()?.removeAllRanges();
return;
}
default: {
// 일반 입력 키의 경우
if (e.metaKey || e.ctrlKey || e.altKey) {
// 다른 단축키들 허용
return;
}
deleteSelectedText(block, startOffset, endOffset);
onKeyDown(e);
}
}
},
[deleteSelectedText, editorCRDT, onKeyDown],
);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>, blockRef: HTMLDivElement | null, block: Block) => {
if (!blockRef || !block) return;
const selection = window.getSelection();
if (!selection || selection.isCollapsed || !blockRef) {
// 선택된 텍스트가 없으면 기존 onKeyDown 로직 실행
if (!selection) return;

// 선택된 텍스트가 없으면 기본 키 핸들러 실행
if (selection.isCollapsed) {
onKeyDown(e);
return;
}

if (e.key === "Backspace") {
e.preventDefault();
const range = selection.getRangeAt(0);
if (!blockRef.contains(range.commonAncestorContainer)) return;

const range = selection.getRangeAt(0);
if (!blockRef.contains(range.commonAncestorContainer)) return;
const startOffset = getTextOffset(blockRef, range.startContainer, range.startOffset);
const endOffset = getTextOffset(blockRef, range.endContainer, range.endOffset);

const startOffset = getTextOffset(blockRef, range.startContainer, range.startOffset);
const endOffset = getTextOffset(blockRef, range.endContainer, range.endOffset);

// 선택된 범위의 문자들을 역순으로 삭제
for (let i = endOffset - 1; i >= startOffset; i--) {
const operationNode = block.crdt.localDelete(i, block.id, pageId);
sendCharDeleteOperation(operationNode);
}

block.crdt.currentCaret = startOffset;
setEditorState({
clock: editorCRDT.clock,
linkedList: editorCRDT.LinkedList,
});
} else {
onKeyDown(e);
}
handleKeyWithSelection(e, block, startOffset, endOffset);
},
[editorCRDT.LinkedList, sendCharDeleteOperation, pageId, onKeyDown],
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/features/editor/hooks/useBlockOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
RemoteBlockUpdateOperation,
RemoteCharInsertOperation,
} from "@noctaCrdt/Interfaces";
import { Block } from "@noctaCrdt/Node";
import { BlockId } from "@noctaCrdt/NodeId";
import { BlockLinkedList } from "node_modules/@noctaCrdt/LinkedList";
import { EditorStateProps } from "../Editor";
import { Block } from "@noctaCrdt/Node";

interface useBlockOptionSelectProps {
editorCRDT: EditorCRDT;
Expand Down
122 changes: 71 additions & 51 deletions client/src/features/editor/hooks/useCopyAndPaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface UseCopyAndPasteProps {
}

export const useCopyAndPaste = ({ editorCRDT, pageId, setEditorState }: UseCopyAndPasteProps) => {
const { sendCharInsertOperation } = useSocketStore();
const { sendCharInsertOperation, sendCharDeleteOperation } = useSocketStore();

const handleCopy = useCallback(
(e: React.ClipboardEvent<HTMLDivElement>, blockRef: HTMLDivElement | null, block: Block) => {
Expand Down Expand Up @@ -61,64 +61,84 @@ export const useCopyAndPaste = ({ editorCRDT, pageId, setEditorState }: UseCopyA
[],
);

const handlePaste = useCallback((e: React.ClipboardEvent<HTMLDivElement>, block: Block) => {
e.preventDefault();

const customData = e.clipboardData.getData("application/x-nocta-formatted");

if (customData) {
const { metadata } = JSON.parse(customData);
const caretPosition = block.crdt.currentCaret;

metadata.forEach((char: ClipboardMetadata, index: number) => {
const insertPosition = caretPosition + index;
const charNode = block.crdt.localInsert(
insertPosition,
char.value,
block.id,
pageId,
char.style,
char.color,
char.backgroundColor,
);
sendCharInsertOperation({
node: charNode.node,
blockId: block.id,
pageId,
style: char.style,
color: char.color,
backgroundColor: char.backgroundColor,
const handlePaste = useCallback(
(e: React.ClipboardEvent<HTMLDivElement>, blockRef: HTMLDivElement | null, block: Block) => {
e.preventDefault();
if (!blockRef) return;

const selection = window.getSelection();

if (selection && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
if (!blockRef.contains(range.commonAncestorContainer)) return;

const startOffset = getTextOffset(blockRef, range.startContainer, range.startOffset);
const endOffset = getTextOffset(blockRef, range.endContainer, range.endOffset);

// 선택된 범위의 문자들을 역순으로 삭제
for (let i = endOffset - 1; i >= startOffset; i--) {
const operationNode = block.crdt.localDelete(i, block.id, pageId);
sendCharDeleteOperation(operationNode);
}
}

const customData = e.clipboardData.getData("application/x-nocta-formatted");

if (customData) {
const { metadata } = JSON.parse(customData);
const caretPosition = block.crdt.currentCaret;

metadata.forEach((char: ClipboardMetadata, index: number) => {
const insertPosition = caretPosition + index;
const charNode = block.crdt.localInsert(
insertPosition,
char.value,
block.id,
pageId,
char.style,
char.color,
char.backgroundColor,
);
sendCharInsertOperation({
node: charNode.node,
blockId: block.id,
pageId,
style: char.style,
color: char.color,
backgroundColor: char.backgroundColor,
});
});
});

editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + metadata.length;
} else {
const text = e.clipboardData.getData("text/plain");
editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + metadata.length;
} else {
const text = e.clipboardData.getData("text/plain");

if (!block || text.length === 0) return;
if (!block || text.length === 0) return;

const caretPosition = block.crdt.currentCaret;
const caretPosition = block.crdt.currentCaret;

// 텍스트를 한 글자씩 순차적으로 삽입
text.split("").forEach((char, index) => {
const insertPosition = caretPosition + index;
const charNode = block.crdt.localInsert(insertPosition, char, block.id, pageId);
sendCharInsertOperation({
node: charNode.node,
blockId: block.id,
pageId,
// 텍스트를 한 글자씩 순차적으로 삽입
text.split("").forEach((char, index) => {
const insertPosition = caretPosition + index;
const charNode = block.crdt.localInsert(insertPosition, char, block.id, pageId);
sendCharInsertOperation({
node: charNode.node,
blockId: block.id,
pageId,
});
});
});

// 캐럿 위치 업데이트
editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + text.length;
}
// 캐럿 위치 업데이트
editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + text.length;
}

setEditorState({
clock: editorCRDT.clock,
linkedList: editorCRDT.LinkedList,
});
}, []);
setEditorState({
clock: editorCRDT.clock,
linkedList: editorCRDT.LinkedList,
});
},
[],
);

return { handleCopy, handlePaste };
};
Loading
Loading