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/#131 블록 드래그앤드랍 구현 #132

Merged
merged 21 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7df8f82
build: dnd-kit 라이브러리 설치
Ludovico7 Nov 17, 2024
198148f
build: dnd-kit 라이브러리 설치
Ludovico7 Nov 17, 2024
4d4402b
build: @dnd-kit/sortable, @dnd-kit/utilities 라이브러리 설치
Ludovico7 Nov 17, 2024
cef534a
feat: 드래그 핸들 컴포넌트 구현
Ludovico7 Nov 17, 2024
1747bcf
feat: @noctaCRDT 라이브러리에 블록 노드간 reorder 메서드 구현
Ludovico7 Nov 17, 2024
46833d2
feat: 블록간 드래그 앤 드랍 구현
Ludovico7 Nov 17, 2024
5362b7d
feat: 블록 드래그 앤 드랍 구현
Ludovico7 Nov 17, 2024
18bf7d4
style: lint 설정
Ludovico7 Nov 17, 2024
02fc9a1
refactor: 블록 디렉토리 구조 수정
Ludovico7 Nov 17, 2024
74dbffe
style: 메뉴 블록이 indent와 관계없이 텍스트 블록 옆에 있도록 수정
Ludovico7 Nov 17, 2024
c45126e
style: lint 설정
Ludovico7 Nov 17, 2024
1963a0b
style: lint 설정 -> 블록 컴포넌트, 키입력 커스텀 훅 디렉토리 구조 수정으로 상대 경로 import로 수정
Ludovico7 Nov 17, 2024
29af574
refactor: import시 Block 컴포넌트를 찾지 못하는 문제 해결
Ludovico7 Nov 17, 2024
d84417c
refactor: import문 절대경로로 수정
Ludovico7 Nov 17, 2024
0d60c9a
refactor: Block.tsx 직접 import하도록 수정
Ludovico7 Nov 17, 2024
c6e6a98
refactor: Block.tsx 상대경로로 수정....
Ludovico7 Nov 17, 2024
c880ec7
refactor: Block.tsx 디렉토리명 block으로 변경
Ludovico7 Nov 17, 2024
32b0660
fix: 서윤님 피드백 반영해서 블록간 캐럿 이동시 이상한 위치에 이동하는 버그 수정
Ludovico7 Nov 17, 2024
5e7ae04
refactor: svg url방식으로 수정
Ludovico7 Nov 17, 2024
484140c
fix: 블록 클릭시 클릭한 위치에 캐럿 이동하도록 수정
Ludovico7 Nov 17, 2024
4ec0edd
Merge branch 'dev' of https://github.com/boostcampwm-2024/web33-Nocta…
pipisebastian Nov 18, 2024
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
79 changes: 79 additions & 0 deletions @noctaCrdt/LinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,85 @@ export class LinkedList<T extends Node<NodeId>> {
delete this.nodeMap[JSON.stringify(id)];
}

reorderNodes(oldIndex: number, newIndex: number): LinkedList<T> {
if (oldIndex === newIndex) return this;

const nodes = this.spread();
if (oldIndex < 0 || oldIndex >= nodes.length || newIndex < 0 || newIndex >= nodes.length) {
throw new Error("Invalid index for reordering");
}

// 이동할 노드
const targetNode = nodes[oldIndex];

// 1. 기존 연결 해제
if (targetNode.prev) {
const prevNode = this.getNode(targetNode.prev);
if (prevNode) {
prevNode.next = targetNode.next;
}
} else {
this.head = targetNode.next;
}

if (targetNode.next) {
const nextNode = this.getNode(targetNode.next);
if (nextNode) {
nextNode.prev = targetNode.prev;
}
}

// 2. 새로운 위치에 연결
// oldIndex가 newIndex보다 큰 경우(위로 이동)와 작은 경우(아래로 이동)를 구분
if (oldIndex < newIndex) {
// 아래로 이동하는 경우
if (newIndex === nodes.length - 1) {
// 맨 끝으로 이동
const lastNode = nodes[nodes.length - 1];
lastNode.next = targetNode.id;
targetNode.prev = lastNode.id;
targetNode.next = null;
} else {
const afterNode = nodes[newIndex + 1];
const beforeNode = nodes[newIndex];

targetNode.prev = beforeNode.id;
targetNode.next = afterNode.id;
beforeNode.next = targetNode.id;
afterNode.prev = targetNode.id;
}
} else {
// 위로 이동하는 경우
if (newIndex === 0) {
// 맨 앞으로 이동
const oldHead = this.head;
this.head = targetNode.id;
targetNode.prev = null;
targetNode.next = oldHead;

if (oldHead) {
const headNode = this.getNode(oldHead);
if (headNode) {
headNode.prev = targetNode.id;
}
}
} else {
const beforeNode = nodes[newIndex - 1];
const afterNode = nodes[newIndex];

targetNode.prev = beforeNode.id;
targetNode.next = afterNode.id;
beforeNode.next = targetNode.id;
afterNode.prev = targetNode.id;
}
}

// 노드맵 갱신
this.setNode(targetNode.id, targetNode);

return this;
}

findByIndex(index: number): T {
if (index < 0) {
throw new Error(`Invalid negative index: ${index}`);
Expand Down
3 changes: 3 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"prepare": "panda codegen"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@noctaCrdt": "workspace:*",
"@pandabox/panda-plugins": "^0.0.8",
"framer-motion": "^11.11.11",
Expand Down
6 changes: 6 additions & 0 deletions client/src/assets/icons/draggable.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 @@ -5,6 +5,7 @@ export const editorContainer = css({
height: "full", // 부모 컴포넌트의 header(60px)를 제외한 높이
margin: "spacing.lg", // 16px margin
padding: "24px", // 24px padding
overflowX: "hidden",
overflowY: "auto", // 내용이 많을 경우 스크롤
_focus: {
outline: "none",
Expand Down
57 changes: 43 additions & 14 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { EditorCRDT } from "@noctaCrdt/Crdt";
import { BlockLinkedList } from "@noctaCrdt/LinkedList";
import { Block as CRDTBlock } from "@noctaCrdt/Node";
import { BlockId } from "@noctaCrdt/NodeId";
import { useRef, useState, useCallback, useEffect } from "react";
import { Block } from "@src/features/editor/components/block/Block";
import { useMarkdownGrammer } from "@src/features/editor/hooks/useMarkdownGrammer";
import { editorContainer, editorTitleContainer, editorTitle } from "./Editor.style";
import { Block } from "./components/block/Block.tsx";
import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop";
import { useMarkdownGrammer } from "./hooks/useMarkdownGrammer";

interface EditorProps {
onTitleChange: (title: string) => void;
Expand All @@ -26,6 +29,12 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
currentBlock: null as BlockId | null,
});

const { sensors, handleDragEnd } = useBlockDragAndDrop({
editorCRDT: editorCRDT.current,
editorState,
setEditorState,
});

const { handleKeyDown } = useMarkdownGrammer({
editorCRDT: editorCRDT.current,
editorState,
Expand All @@ -36,14 +45,22 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
onTitleChange(e.target.value);
};

const handleBlockClick = (blockId: BlockId) => {
const handleBlockClick = (blockId: BlockId, e: React.MouseEvent<HTMLDivElement>) => {
const block = editorState.linkedList.getNode(blockId);
if (!block) return;

// 클릭된 요소 내에서의 위치를 가져오기 위해
const range = document.caretRangeFromPoint(e.clientX, e.clientY);
if (!range) return;

const selection = window.getSelection();
if (!selection) return;

// 클릭한 위치의 offset을 currentCaret으로 저장
// 새로운 Range로 Selection 설정
selection.removeAllRanges();
selection.addRange(range);

// 현재 캐럿 위치를 저장
block.crdt.currentCaret = selection.focusOffset;

setEditorState((prev) => ({
Expand Down Expand Up @@ -110,6 +127,8 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
});
}, []);

console.log("block list", editorState.linkedList.spread());

return (
<div className={editorContainer}>
<div className={editorTitleContainer}>
Expand All @@ -119,16 +138,26 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
onChange={handleTitleChange}
className={editorTitle}
/>
{editorState.linkedList.spread().map((block) => (
<Block
key={`${block.id.client}-${block.id.clock}`}
block={block}
isActive={block.id === editorState.currentBlock}
onInput={handleBlockInput}
onKeyDown={handleKeyDown}
onClick={handleBlockClick}
/>
))}
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext
items={editorState.linkedList
.spread()
.map((block) => `${block.id.client}-${block.id.clock}`)}
strategy={verticalListSortingStrategy}
>
{editorState.linkedList.spread().map((block) => (
<Block
key={`${block.id.client}-${block.id.clock}`}
id={`${block.id.client}-${block.id.clock}`}
block={block}
isActive={block.id === editorState.currentBlock}
onInput={handleBlockInput}
onKeyDown={handleKeyDown}
onClick={handleBlockClick}
/>
))}
</SortableContext>
</DndContext>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// IconBlock.style.ts
import { css, cva } from "@styled-system/css";

export const iconContainerStyle = css({
display: "flex",
justifyContent: "center",
alignItems: "center",
minWidth: "24px",
marginRight: "8px",
});

export const iconStyle = cva({
base: {
display: "flex",
justifyContent: "center",
alignItems: "center",
color: "gray.600",
fontSize: "14px",
},
variants: {
type: {
ul: {
fontSize: "20px", // bullet point size
},
ol: {
paddingRight: "4px",
},
checkbox: {
borderRadius: "2px",
width: "16px",
height: "16px",
backgroundColor: "white",
},
},
},
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import { ElementType } from "@noctaCrdt/Interfaces";
import { iconContainerStyle, iconStyle } from "./IconBlock.style";

// Fix: 서윤님 피드백 반영
interface IconBlockProps {
type: ElementType;
index?: number;
}

export const IconBlock = ({ type, index = 1 }: IconBlockProps) => {
const getIcon = () => {
switch (type) {
case "ul":
return "•";
return <span className={iconStyle({ type: "ul" })}>•</span>;
case "ol":
return `${index}.`;
return <span className={iconStyle({ type: "ol" })}>{`${index}.`}</span>;
case "checkbox":
return <input type="checkbox" />;
return <span className={iconStyle({ type: "checkbox" })} />;
default:
return null;
}
};

const icon = getIcon();
if (!icon) return null;
return (
<div style={{ marginRight: "8px" }}>
<span>{icon}</span>
</div>
);

return <div className={iconContainerStyle}>{icon}</div>;
};
31 changes: 31 additions & 0 deletions client/src/features/editor/components/MenuBlock/MenuBlock.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// MenuBlock.style.ts
import { css } from "@styled-system/css";

export const menuBlockStyle = css({
display: "flex",
zIndex: 1,

position: "relative", // absolute에서 relative로 변경
justifyContent: "center",
alignItems: "center",
width: "20px",
height: "20px",
marginLeft: "-20px",
transition: "opacity 0.2s ease-in-out",
cursor: "grab",
_groupHover: {
opacity: 1,
},

_active: {
cursor: "grabbing",
},
});

export const dragHandleIconStyle = css({
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
});
17 changes: 17 additions & 0 deletions client/src/features/editor/components/MenuBlock/MenuBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import DraggableIcon from "@assets/icons/draggable.svg?url";
import { menuBlockStyle, dragHandleIconStyle } from "./MenuBlock.style";

interface MenuBlockProps {
attributes?: Record<string, any>;

Check warning on line 5 in client/src/features/editor/components/MenuBlock/MenuBlock.tsx

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 5 in client/src/features/editor/components/MenuBlock/MenuBlock.tsx

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
listeners?: Record<string, any>;

Check warning on line 6 in client/src/features/editor/components/MenuBlock/MenuBlock.tsx

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 6 in client/src/features/editor/components/MenuBlock/MenuBlock.tsx

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
}

export const MenuBlock = ({ attributes, listeners }: MenuBlockProps) => {
return (
<div className={menuBlockStyle} {...attributes} {...listeners}>
<div className={dragHandleIconStyle}>
<img src={DraggableIcon} alt="drag handle" width="10" height="10" />
</div>
</div>
);
};
10 changes: 10 additions & 0 deletions client/src/features/editor/components/block/Block.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ export const blockContainerStyle = cva({
},
});

export const contentWrapperStyle = cva({
base: {
display: "flex",
position: "relative",
flex: 1,
flexDirection: "row",
alignItems: "center",
},
});

const baseTextStyle = {
textStyle: "display-medium16",
flex: "1 1 auto", // 변경: flex-grow: 1, flex-shrink: 1, flex-basis: auto
Expand Down
Loading
Loading