Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@noctaCrdt": "workspace:*",
"@pandabox/panda-plugins": "^0.0.8",
"@tanstack/react-query": "^5.60.5",
"@tanstack/react-virtual": "^3.11.2",
"axios": "^1.7.7",
"framer-motion": "^11.11.11",
"react": "^18.3.1",
Expand Down
8 changes: 0 additions & 8 deletions client/src/features/editor/Editor.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ export const editorContainer = css({
},
});

export const editorTitleContainer = css({
display: "flex",
gap: "4px",
flexDirection: "column",
width: "full",
padding: "spacing.sm",
});

export const editorTitle = css({
textStyle: "display-medium28",
outline: "none",
Expand Down
128 changes: 81 additions & 47 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@ import { EditorCRDT } from "@noctaCrdt/Crdt";
import { BlockLinkedList } from "@noctaCrdt/LinkedList";
import { Block as CRDTBlock } from "@noctaCrdt/Node";
import { serializedEditorDataProps } from "@noctaCrdt/types/Interfaces";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef, useState, useCallback, useEffect, useMemo, memo } from "react";
import { useSocketStore } from "@src/stores/useSocketStore.ts";
import { setCaretPosition, getAbsoluteCaretPosition } from "@src/utils/caretUtils.ts";
import {
editorContainer,
editorTitleContainer,
editorTitle,
addNewBlockButton,
} from "./Editor.style";
import { editorContainer, editorTitle, addNewBlockButton } from "./Editor.style";
import { Block } from "./components/block/Block";
import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop";
import { useBlockOperation } from "./hooks/useBlockOperation.ts";
Expand Down Expand Up @@ -50,7 +46,6 @@ export const Editor = memo(
const { clientId } = useSocketStore();
const [displayTitle, setDisplayTitle] = useState(pageTitle);
const [dragBlockList, setDragBlockList] = useState<string[]>([]);
console.log(serializedEditorData);

useEffect(() => {
if (pageTitle === "새로운 페이지" || pageTitle === "") {
Expand Down Expand Up @@ -273,7 +268,35 @@ export const Editor = memo(
if (isLocalChange.current || isSameLocalChange.current) {
setCaretPosition({
blockId: editorCRDT.current.currentBlock!.id,
linkedList: editorCRDT.current.LinkedList,
position: editorCRDT.current.currentBlock?.crdt.currentCaret,
pageId,
});
isLocalChange.current = false;
isSameLocalChange.current = false;
return;
}
}, [editorCRDT.current.currentBlock?.id.serialize()]);

// 리스트 가상화
const editorRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
count: editorState.linkedList.spread().length,
getScrollElement: () => editorRef.current,
estimateSize: () => 24,
overscan: 5,
});

useEffect(() => {
if (!editorCRDT || !editorCRDT.current.currentBlock) return;

const { activeElement } = document;
if (activeElement?.tagName.toLowerCase() === "input") {
return; // input에 포커스가 있으면 캐럿 위치 변경하지 않음
}
if (isLocalChange.current || isSameLocalChange.current) {
setCaretPosition({
blockId: editorCRDT.current.currentBlock!.id,
position: editorCRDT.current.currentBlock?.crdt.currentCaret,
pageId,
});
Expand Down Expand Up @@ -352,18 +375,23 @@ export const Editor = memo(
return <div>Loading editor data...</div>;
}
return (
<div data-testid={`editor-${testKey}`} className={editorContainer}>
<div className={editorTitleContainer}>
<input
data-testid={`editorTitle-${testKey}`}
type="text"
placeholder="제목을 입력하세요..."
onChange={handleTitleChange}
onBlur={handleBlur}
value={displayTitle}
className={editorTitle}
/>
<div style={{ height: "36px" }}></div>
<div data-testid={`editor-${testKey}`} className={editorContainer} ref={editorRef}>
<input
data-testid={`editorTitle-${testKey}`}
type="text"
placeholder="제목을 입력하세요..."
onChange={handleTitleChange}
onBlur={handleBlur}
value={displayTitle}
className={editorTitle}
/>
<div style={{ height: "36px" }}></div>
<div
style={{
height: virtualizer.getTotalSize(),
position: "relative",
}}
>
<DndContext
onDragEnd={(event: DragEndEvent) => {
handleDragEnd(event, dragBlockList, () => setDragBlockList([]));
Expand All @@ -379,37 +407,43 @@ export const Editor = memo(
.map((block) => `${block.id.client}-${block.id.clock}`)}
strategy={verticalListSortingStrategy}
>
{editorState.linkedList.spread().map((block, idx) => (
<Block
testKey={`block-${idx}`}
key={`${block.id.client}-${block.id.clock}`}
id={`${block.id.client}-${block.id.clock}`}
block={block}
isActive={block.id === editorCRDT.current.currentBlock?.id}
onInput={handleBlockInput}
onCompositionStart={handleCompositionStart}
onCompositionUpdate={handleCompositionUpdate}
onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeyDown}
onCopy={handleCopy}
onPaste={handlePaste}
onClick={handleBlockClick}
onAnimationSelect={handleAnimationSelect}
onTypeSelect={handleTypeSelect}
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
onTextStyleUpdate={onTextStyleUpdate}
onTextColorUpdate={onTextColorUpdate}
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
dragBlockList={dragBlockList}
onCheckboxToggle={handleCheckboxToggle}
/>
))}
{virtualizer.getVirtualItems().map((virtualRow) => {
const block = editorState.linkedList.spread()[virtualRow.index];
return (
<Block
testKey={`block-${virtualRow.index}`}
virtualStart={virtualRow.start}
virtualIndex={virtualRow.index}
virtualRef={virtualizer.measureElement}
key={`${block.id.client}-${block.id.clock}`}
id={`${block.id.client}-${block.id.clock}`}
block={block}
isActive={block.id === editorCRDT.current.currentBlock?.id}
onInput={handleBlockInput}
onCompositionStart={handleCompositionStart}
onCompositionUpdate={handleCompositionUpdate}
onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeyDown}
onCopy={handleCopy}
onPaste={handlePaste}
onClick={handleBlockClick}
onAnimationSelect={handleAnimationSelect}
onTypeSelect={handleTypeSelect}
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
onTextStyleUpdate={onTextStyleUpdate}
onTextColorUpdate={onTextColorUpdate}
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
dragBlockList={dragBlockList}
onCheckboxToggle={handleCheckboxToggle}
/>
);
})}
</SortableContext>
</DndContext>
{editorState.linkedList.spread().length === 0 && (
<div
data-testId="addNewBlockButton"
data-testid="addNewBlockButton"
className={addNewBlockButton}
onClick={addNewBlock}
>
Expand Down
1 change: 0 additions & 1 deletion client/src/features/editor/components/block/Block.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const baseBlockStyle = {
display: "flex",
flexDirection: "row",
alignItems: "center",
position: "relative",
width: "full",
minHeight: "16px",
backgroundColor: "transparent",
Expand Down
22 changes: 20 additions & 2 deletions client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import {
} from "./Block.style";

interface BlockProps {
virtualIndex: number;
virtualStart: number;
virtualRef: (node: Element | null | undefined) => void;
testKey: string;
id: string;
block: CRDTBlock;
Expand Down Expand Up @@ -71,6 +74,9 @@ interface BlockProps {
}
export const Block: React.FC<BlockProps> = memo(
({
virtualIndex,
virtualRef,
virtualStart,
testKey,
id,
block,
Expand Down Expand Up @@ -280,7 +286,6 @@ export const Block: React.FC<BlockProps> = memo(

useEffect(() => {
if (blockRef.current) {
console.log(block.crdt.serialize());
setInnerHTML({ element: blockRef.current, block });
}
}, [getTextAndStylesHash(block)]);
Expand All @@ -296,7 +301,20 @@ export const Block: React.FC<BlockProps> = memo(
return (
// TODO: eslint 규칙을 수정해야 할까?
// TODO: ol일때 index 순서 처리
<div data-testid={testKey} style={{ position: "relative" }}>
<div
data-testid={testKey}
data-id={id}
data-index={virtualIndex}
key={virtualIndex}
Comment on lines +306 to +308
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data-id는 caretUtil에서 특정 block을 찾기위해 넣어줬습니다.
data-index는 tanstack virtual 라이브러리에서 data-index를 기준으로 요소를 찾더라구요! 그래서 무조건 필요한 값입니다
key도 tanstack virtual 라이브러리에서 필요한 값입니다!

ref={virtualRef}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualStart}px)`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 start를 통해 어디에 위치시킬지 보여줍니다!

}}
>
{showTopIndicator && <Indicator />}
<motion.div
ref={setNodeRef}
Expand Down
14 changes: 7 additions & 7 deletions client/src/features/editor/hooks/useMarkdownGrammer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,

position: targetBlock.crdt.read().length,
pageId,
});
Expand Down Expand Up @@ -437,7 +437,7 @@ export const useMarkdownGrammer = ({
currentBlock.crdt.currentCaret = e.key === "Home" ? 0 : currentBlock.crdt.read().length;
setCaretPosition({
blockId: currentBlock.id,
linkedList: editorCRDT.LinkedList,

position: currentBlock.crdt.currentCaret,
pageId,
});
Expand All @@ -456,7 +456,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = headBlock;
setCaretPosition({
blockId: headBlock.id,
linkedList: editorCRDT.LinkedList,

position: currentCaretPosition,
pageId,
});
Expand All @@ -478,7 +478,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = lastBlock;
setCaretPosition({
blockId: lastBlock.id,
linkedList: editorCRDT.LinkedList,

position: currentCaretPosition,
pageId,
});
Expand Down Expand Up @@ -516,7 +516,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,

position: Math.min(caretPosition, targetBlock.crdt.read().length),
pageId,
});
Expand Down Expand Up @@ -547,7 +547,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,

position: targetBlock.crdt.read().length,
pageId,
});
Expand All @@ -573,7 +573,7 @@ export const useMarkdownGrammer = ({
editorCRDT.currentBlock = targetBlock;
setCaretPosition({
blockId: targetBlock.id,
linkedList: editorCRDT.LinkedList,

position: 0,
pageId,
});
Expand Down
18 changes: 4 additions & 14 deletions client/src/utils/caretUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { BlockLinkedList, TextLinkedList } from "@noctaCrdt/LinkedList";
import { BlockId } from "@noctaCrdt/NodeId";

interface SetCaretPositionProps {
blockId: BlockId;
linkedList: BlockLinkedList | TextLinkedList;
clientX?: number;
clientY?: number;
position?: number; // Used to set the caret at a specific position
Expand Down Expand Up @@ -72,28 +70,20 @@ export const getAbsoluteCaretPosition = (element: HTMLElement): number => {
return 0;
};

export const setCaretPosition = ({
blockId,
linkedList,
position,
pageId,
}: SetCaretPositionProps): void => {
export const setCaretPosition = ({ blockId, position, pageId }: SetCaretPositionProps): void => {
try {
if (position === undefined) return;
const selection = window.getSelection();
if (!selection) return;

const currentPage = document.getElementById(pageId);

const blockElements = Array.from(
currentPage?.querySelectorAll('.d_flex.pos_relative.w_full[data-group="true"]') || [],
const targetElement = currentPage?.querySelector(
`[data-id="${blockId.serialize().client}-${blockId.serialize().clock}"]`,
);

const currentIndex = linkedList.spread().findIndex((b) => b.id === blockId);
const targetElement = blockElements[currentIndex];
if (!targetElement) return;

const editableDiv = targetElement.querySelector('[contenteditable="true"]') as HTMLDivElement;
const editableDiv = targetElement.querySelector('[contenteditable="true"]') as HTMLElement;
if (!editableDiv) return;

editableDiv.focus();
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading