Skip to content

Commit

Permalink
Merge pull request #223 from boostcampwm-2024/Hotfix/#222_드래그하고_입력할때_…
Browse files Browse the repository at this point in the history
…텍스트_안지워지는_문제_해결

Hotfix/#222 드래그하고 입력할때 텍스트 안지워지는 문제 해결
  • Loading branch information
Ludovico7 authored Nov 27, 2024
2 parents 3d909c3 + 662557f commit 2b81990
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 77 deletions.
1 change: 1 addition & 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,7 @@ export const textContainerStyle = cva({
base: {
...baseTextStyle,
position: "relative",
wordBreak: "break-word",
overflowWrap: "break-word",
whiteSpace: "pre-wrap",
"&:empty::before": {
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

0 comments on commit 2b81990

Please sign in to comment.