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

Feature/#17 수평선 구현 #207

Merged
merged 9 commits into from
Nov 26, 2024
12 changes: 11 additions & 1 deletion @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import { Block, Char } from "./Node";
import { Page } from "./Page";
import { EditorCRDT } from "./Crdt";

export type ElementType = "p" | "h1" | "h2" | "h3" | "ul" | "ol" | "li" | "checkbox" | "blockquote";
export type ElementType =
| "p"
| "h1"
| "h2"
| "h3"
| "ul"
| "ol"
| "li"
| "checkbox"
| "blockquote"
| "hr";

export type AnimationType = "none" | "highlight" | "gradation";

Expand Down
1 change: 1 addition & 0 deletions client/src/features/editor/Editor.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const editorContainer = css({

export const editorTitleContainer = css({
display: "flex",
gap: "4px",
flexDirection: "column",
width: "full",
padding: "spacing.sm",
Expand Down
11 changes: 5 additions & 6 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
sendCharInsertOperation,
});

const { handleKeyDown: onKeyDown } = useMarkdownGrammer({
const { handleKeyDown: onKeyDown, handleInput: handleHrInput } = useMarkdownGrammer({
editorCRDT: editorCRDT.current,
editorState,
setEditorState,
Expand Down Expand Up @@ -151,11 +151,10 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
const newContent = element.textContent || "";
const currentContent = block.crdt.read();
const caretPosition = getAbsoluteCaretPosition(element);
console.log({
newContent,
currentContent,
caretPosition,
});

if (handleHrInput(block, newContent)) {
return;
}

if (newContent.length > currentContent.length) {
let charNode: RemoteCharInsertOperation;
Expand Down
7 changes: 6 additions & 1 deletion client/src/features/editor/components/block/Block.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ export const textContainerStyle = cva({
},
blockquote: {
borderLeft: "4px solid token(colors.gray.300)",
paddingLeft: "spacing.md",
borderRadius: "none",
paddingLeft: "8px",
color: "gray.500",
fontStyle: "italic",
"&:empty::before": {
Expand All @@ -127,6 +128,10 @@ export const textContainerStyle = cva({
content: '"텍스트를 입력하세요..."',
},
},
hr: {
borderTop: "2px solid token(colors.gray.300)",
height: "1px",
},
},
},
defaultVariants: {
Expand Down
2 changes: 1 addition & 1 deletion client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export const Block: React.FC<BlockProps> = memo(
onPaste={(e) => onPaste(e, block)}
onMouseUp={handleMouseUp}
onCompositionEnd={(e) => onCompositionEnd(e, block)}
contentEditable
contentEditable={block.type !== "hr"}
spellCheck={false}
suppressContentEditableWarning
className={textContainerStyle({
Expand Down
6 changes: 5 additions & 1 deletion client/src/features/editor/hooks/useBlockOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ export const useBlockOptionSelect = ({

block.type = type;
editorCRDT.currentBlock = block;
editorCRDT.remoteUpdate(block, pageId);

// block의 crdt 초기화. hr 은 문자 노드가 없기 때문에
if (block.type === "hr") {
block.crdt = new BlockCRDT(editorCRDT.client);
}

sendBlockUpdateOperation({
node: block,
Expand Down
144 changes: 124 additions & 20 deletions client/src/features/editor/hooks/useMarkdownGrammer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useCallback } from "react";
import { EditorStateProps } from "@features/editor/Editor";
import { checkMarkdownPattern } from "@src/features/editor/utils/markdownPatterns";
import { setCaretPosition, getAbsoluteCaretPosition } from "@src/utils/caretUtils";
import { Block } from "node_modules/@noctaCrdt/Node";

interface useMarkdownGrammerProps {
editorCRDT: EditorCRDT;
Expand Down Expand Up @@ -56,6 +57,13 @@ export const useMarkdownGrammer = ({
});
};

const findBlock = (index: number) => {
if (index < 0) return null;
if (index >= editorCRDT.LinkedList.spread().length) return null;

return editorCRDT.LinkedList.findByIndex(index);
};

const currentBlockId = editorCRDT.currentBlock ? editorCRDT.currentBlock.id : null;
if (!currentBlockId) return;

Expand Down Expand Up @@ -164,9 +172,30 @@ export const useMarkdownGrammer = ({
const prevBlock =
currentIndex > 0 ? editorCRDT.LinkedList.findByIndex(currentIndex - 1) : null;
if (prevBlock) {
// 현재 블록 삭제
sendBlockDeleteOperation(editorCRDT.localDelete(currentIndex, undefined, pageId));
prevBlock.crdt.currentCaret = prevBlock.crdt.spread().length;
editorCRDT.currentBlock = prevBlock;

// 이전 편집 가능한 블록 찾기
let targetIndex = currentIndex - 1;
let targetBlock = findBlock(targetIndex);

while (targetBlock && targetBlock.type === "hr") {
targetIndex -= 1;
targetBlock = findBlock(targetIndex);
}

// 편집 가능한 블록을 찾았다면 캐럿 이동
if (targetBlock && targetBlock.type !== "hr") {
targetBlock.crdt.currentCaret = targetBlock.crdt.read().length;
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,
position: targetBlock.crdt.read().length,
pageId,
});
}

updateEditorState();
}
break;
Expand All @@ -190,7 +219,22 @@ export const useMarkdownGrammer = ({
// FIX: 서윤님 피드백 반영
const prevBlock =
currentIndex > 0 ? editorCRDT.LinkedList.findByIndex(currentIndex - 1) : null;

if (prevBlock) {
let targetIndex = currentIndex - 1;
let targetBlock = findBlock(targetIndex);

while (targetBlock && targetBlock.type === "hr") {
targetIndex -= 1;
targetBlock = findBlock(targetIndex);
}
if (targetBlock && prevBlock.type === "hr") {
editorCRDT.currentBlock = targetBlock;
editorCRDT.currentBlock.crdt.currentCaret = targetBlock.crdt.read().length; // 커서 이동
updateEditorState();
break;
}

const prevBlockEndCaret = prevBlock.crdt.spread().length;
for (let i = 0; i < currentContent.length; i++) {
const currentCharNode = currentCharNodes[i];
Expand All @@ -214,6 +258,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = prevBlock;
editorCRDT.currentBlock.crdt.currentCaret = prevBlockEndCaret;
sendBlockDeleteOperation(editorCRDT.localDelete(currentIndex, undefined, pageId));

updateEditorState();
e.preventDefault();
}
Expand Down Expand Up @@ -283,14 +328,18 @@ export const useMarkdownGrammer = ({
return;
}

// const selection = window.getSelection();
// const caretPosition = selection?.focusOffset || 0;
const caretPosition = getAbsoluteCaretPosition(e.currentTarget);

// 이동할 블록 결정
const targetIndex = e.key === "ArrowUp" ? currentIndex - 1 : currentIndex + 1;
const targetBlock = editorCRDT.LinkedList.findByIndex(targetIndex);
if (!targetBlock) return;
let targetIndex = e.key === "ArrowUp" ? currentIndex - 1 : currentIndex + 1;
let targetBlock = findBlock(targetIndex);

while (targetBlock && targetBlock.type === "hr") {
targetIndex = e.key === "ArrowUp" ? targetIndex - 1 : targetIndex + 1;
targetBlock = findBlock(targetIndex);
}

if (!targetBlock || targetBlock.type === "hr") return;

e.preventDefault();
targetBlock.crdt.currentCaret = Math.min(caretPosition, targetBlock.crdt.read().length);
editorCRDT.currentBlock = targetBlock;
Expand All @@ -312,14 +361,22 @@ export const useMarkdownGrammer = ({
// 왼쪽 끝에서 이전 블록으로
if (e.key === "ArrowLeft" && caretPosition === 0 && currentIndex > 0) {
e.preventDefault(); // 기본 동작 방지
const prevBlock = editorCRDT.LinkedList.findByIndex(currentIndex - 1);
if (prevBlock) {
prevBlock.crdt.currentCaret = prevBlock.crdt.read().length;
editorCRDT.currentBlock = prevBlock;

let targetIndex = currentIndex - 1;
let targetBlock = findBlock(targetIndex);

while (targetBlock && targetBlock.type === "hr") {
targetIndex -= 1;
targetBlock = findBlock(targetIndex);
}

if (targetBlock && targetBlock.type !== "hr") {
targetBlock.crdt.currentCaret = targetBlock.crdt.read().length;
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: prevBlock.id,
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,
position: prevBlock.crdt.read().length,
position: targetBlock.crdt.read().length,
pageId,
});
}
Expand All @@ -331,12 +388,19 @@ export const useMarkdownGrammer = ({
currentIndex < editorCRDT.LinkedList.spread().length - 1
) {
e.preventDefault(); // 기본 동작 방지
const nextBlock = editorState.linkedList.findByIndex(currentIndex + 1);
if (nextBlock) {
nextBlock.crdt.currentCaret = 0;
editorCRDT.currentBlock = nextBlock;
let targetIndex = currentIndex + 1;
let targetBlock = findBlock(targetIndex);

while (targetBlock && targetBlock.type === "hr") {
targetIndex += 1;
targetBlock = findBlock(targetIndex);
}

if (targetBlock && targetBlock.type !== "hr") {
targetBlock.crdt.currentCaret = 0;
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: nextBlock.id,
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,
position: 0,
pageId,
Expand All @@ -359,5 +423,45 @@ export const useMarkdownGrammer = ({
[editorCRDT, editorState, setEditorState, pageId],
);

return { handleKeyDown };
// hr은 --- 입력 시 생성되는 input 이벤트. KeyDown 입력과 관련이 없으므로 함수 분리
const handleInput = useCallback(
(block: Block, newContent: string) => {
if (newContent === "---") {
const currentContent = block.crdt.read();
currentContent.split("").forEach((_) => {
const operationNode = block.crdt.localDelete(0, block.id, pageId);
sendCharDeleteOperation(operationNode);
});

block.type = "hr";
sendBlockUpdateOperation(editorCRDT.localUpdate(block, pageId));

// 새로운 블록 생성
const currentIndex = editorCRDT.LinkedList.spread().findIndex((b) => b.id.equals(block.id));
const operation = editorCRDT.localInsert(currentIndex + 1, "");
operation.node.type = "p";
sendBlockInsertOperation({ node: operation.node, pageId });

editorCRDT.currentBlock = operation.node;
editorCRDT.currentBlock.crdt.currentCaret = 0;

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

return true;
}
return false;
},
[
editorCRDT,
sendCharDeleteOperation,
sendBlockUpdateOperation,
sendBlockInsertOperation,
pageId,
],
);

return { handleKeyDown, handleInput };
};
Loading