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/#162 블록 옵션창 구현 #170

Merged
merged 20 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
19 changes: 11 additions & 8 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
RemoteBlockInsertOperation,
RemoteCharInsertOperation,
CRDTSerializedProps,
RemoteReorderOperation,
RemoteBlockReorderOperation,
RemoteBlockUpdateOperation,
} from "./Interfaces";

Expand All @@ -22,17 +22,17 @@
this.LinkedList = new LinkedListClass();
}

localInsert(index: number, value: string, blockId?: BlockId, pageId?: string): any {

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'value' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'value' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 25 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

localDelete(index: number, blockId?: BlockId, pageId?: string): any {

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 30 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

remoteInsert(operation: any): void {

Check warning on line 35 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'operation' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 35 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'operation' is defined but never used. Allowed unused args must match /^_/u
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}
Expand Down Expand Up @@ -149,20 +149,25 @@
targetId: BlockId;
beforeId: BlockId | null;
afterId: BlockId | null;
}): RemoteReorderOperation {
const operation: RemoteReorderOperation = {
pageId: string;
}): RemoteBlockReorderOperation {
const operation: RemoteBlockReorderOperation = {
...params,
clock: this.clock,
client: this.client,
};

this.LinkedList.reorderNodes(params);
this.LinkedList.reorderNodes({
targetId: params.targetId,
beforeId: params.beforeId,
afterId: params.afterId,
});
this.clock += 1;

return operation;
}

remoteReorder(operation: RemoteReorderOperation): void {
remoteReorder(operation: RemoteBlockReorderOperation): void {
const { targetId, beforeId, afterId, clock } = operation;

this.LinkedList.reorderNodes({
Expand All @@ -171,9 +176,7 @@
afterId,
});

if (this.clock <= clock) {
this.clock = clock + 1;
}
this.clock = Math.max(this.clock, clock) + 1;
}

serialize(): CRDTSerializedProps<Block> {
Expand Down
5 changes: 4 additions & 1 deletion @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { EditorCRDT } from "./Crdt";

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

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

export interface InsertOperation {
node: Block | Char;
}
Expand Down Expand Up @@ -93,10 +95,11 @@ export interface WorkSpaceSerializedProps {
pageList: Page[];
authUser: Map<string, string>;
}
export interface RemoteReorderOperation {
export interface RemoteBlockReorderOperation {
targetId: BlockId;
beforeId: BlockId | null;
afterId: BlockId | null;
clock: number;
client: number;
pageId: string;
}
6 changes: 3 additions & 3 deletions @noctaCrdt/Node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Node.ts
import { NodeId, BlockId, CharId } from "./NodeId";
import { ElementType } from "./Interfaces";
import { AnimationType, ElementType } from "./Interfaces";
import { BlockCRDT } from "./Crdt";

export abstract class Node<T extends NodeId> {
Expand Down Expand Up @@ -43,7 +43,7 @@ export abstract class Node<T extends NodeId> {
export class Block extends Node<BlockId> {
type: ElementType;
indent: number;
animation: string;
animation: AnimationType;
style: string[];
icon: string;
crdt: BlockCRDT;
Expand All @@ -52,7 +52,7 @@ export class Block extends Node<BlockId> {
super(value, id);
this.type = "p";
this.indent = 0;
this.animation = "";
this.animation = "none";
this.style = [];
this.icon = "";
this.crdt = new BlockCRDT(id.client);
Expand Down
39 changes: 39 additions & 0 deletions client/src/constants/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AnimationType, ElementType } from "@noctaCrdt/Interfaces";

export const OPTION_CATEGORIES = {
TYPE: {
id: "type",
label: "전환",
options: [
{ id: "p", label: "p" },
{ id: "h1", label: "h1" },
{ id: "h2", label: "h2" },
{ id: "h3", label: "h3" },
{ id: "ul", label: "ul" },
{ id: "ol", label: "ol" },
{ id: "checkbox", label: "checkbox" },
{ id: "blockquote", label: "blockquote" },
] as { id: ElementType; label: string }[],
Comment on lines +8 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분에서 id와 별개로 이름을 h1 -> 제목1, h2 -> 제목2, checkbox -> 체크박스 등으로 한글 이름을 사용하는것도 좋아보입니다!

},
ANIMATION: {
id: "animation",
label: "애니메이션",
options: [
{ id: "none", label: "없음" },
{ id: "highlight", label: "하이라이트" },
{ id: "gradation", label: "그라데이션" },
] as { id: AnimationType; label: string }[],
},
DUPLICATE: {
id: "duplicate",
label: "복제",
options: null,
},
DELETE: {
id: "delete",
label: "삭제",
options: null,
},
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

good !!! 👍


export type OptionCategory = keyof typeof OPTION_CATEGORIES;
40 changes: 30 additions & 10 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useSocketStore } from "@src/stores/useSocketStore.ts";
import { editorContainer, editorTitleContainer, editorTitle } from "./Editor.style";
import { Block } from "./components/block/Block.tsx";
import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop";
import { useBlockOptionSelect } from "./hooks/useBlockOption.ts";
import { useMarkdownGrammer } from "./hooks/useMarkdownGrammer";

interface EditorProps {
Expand All @@ -28,16 +29,6 @@ export interface EditorStateProps {
}
// TODO: pageId, editorCRDT를 props로 받아와야함
export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorProps) => {
/*
const {
sendCharInsertOperation,
sendCharDeleteOperation,
subscribeToRemoteOperations,
sendBlockInsertOperation,
sendBlockDeleteOperation,
sendBlockUpdateOperation,
} = useSocket();
*/
const {
sendCharInsertOperation,
sendCharDeleteOperation,
Expand All @@ -62,8 +53,21 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
editorCRDT: editorCRDT.current,
editorState,
setEditorState,
pageId,
});

const { handleTypeSelect, handleAnimationSelect, handleCopySelect, handleDeleteSelect } =
useBlockOptionSelect({
editorCRDT: editorCRDT.current,
editorState,
setEditorState,
pageId,
sendBlockUpdateOperation,
sendBlockDeleteOperation,
sendBlockInsertOperation,
sendCharInsertOperation,
});

const { handleKeyDown } = useMarkdownGrammer({
editorCRDT: editorCRDT.current,
editorState,
Expand Down Expand Up @@ -222,6 +226,18 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
currentBlock: prev.currentBlock,
}));
},

onRemoteBlockReorder: (operation) => {
console.log(operation, "block : 재정렬 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteReorder(operation);
setEditorState((prev) => ({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
},

onRemoteCursor: (position) => {
console.log(position, "커서위치 수신");
},
Expand Down Expand Up @@ -272,6 +288,10 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
onInput={handleBlockInput}
onKeyDown={handleKeyDown}
onClick={handleBlockClick}
onAnimationSelect={handleAnimationSelect}
onTypeSelect={handleTypeSelect}
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
/>
))}
</SortableContext>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// MenuBlock.style.ts
import { css } from "@styled-system/css";

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

position: "relative", // absolute에서 relative로 변경
position: "relative",
justifyContent: "center",
alignItems: "center",
width: "20px",
Expand All @@ -16,7 +14,6 @@ export const menuBlockStyle = css({
_groupHover: {
opacity: 1,
},

_active: {
cursor: "grabbing",
},
Expand Down
78 changes: 75 additions & 3 deletions client/src/features/editor/components/MenuBlock/MenuBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,89 @@
import { AnimationType, ElementType } from "@noctaCrdt/Interfaces";
import { useState, useRef } from "react";
import DraggableIcon from "@assets/icons/draggable.svg?url";
import { useModal } from "@src/components/modal/useModal";
import { OptionModal } from "../OptionModal/OptionModal";
import { menuBlockStyle, dragHandleIconStyle } from "./MenuBlock.style";

interface MenuBlockProps {
export interface MenuBlockProps {
attributes?: Record<string, any>;
listeners?: Record<string, any>;
onAnimationSelect: (animation: AnimationType) => void;
onTypeSelect: (type: ElementType) => void;
onCopySelect: () => void;
onDeleteSelect: () => void;
}

export const MenuBlock = ({ attributes, listeners }: MenuBlockProps) => {
export const MenuBlock = ({
attributes,
listeners,
onAnimationSelect,
onTypeSelect,
onCopySelect,
onDeleteSelect,
}: MenuBlockProps) => {
const menuBlockRef = useRef<HTMLDivElement>(null);

const [pressTime, setPressTime] = useState<NodeJS.Timeout | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [menuBlockPosition, setMenuBlockPosition] = useState<{ top: number; right: number }>({
top: 0,
right: 0,
});

const { isOpen, openModal, closeModal } = useModal();

const handlePressStart = () => {
const timer = setTimeout(() => {
setIsDragging(true);
}, 300);

setPressTime(timer);
};

const handlePressEnd = () => {
if (pressTime) {
clearTimeout(pressTime);
setPressTime(null);
}

if (!isDragging) {
if (menuBlockRef.current) {
const { top, right } = menuBlockRef.current.getBoundingClientRect();
setMenuBlockPosition({ top, right });
}
openModal();
}
setIsDragging(false);
};

const modifiedListeners = {
...listeners,
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 리스너 함수를 가져와서 이벤트를 덮어 쓸 수가 있군요..! 한수 배웠습니다 !

// dnd 이벤트 덮어쓰기
onMouseDown: (e: React.MouseEvent) => {
handlePressStart();
listeners?.onMouseDown?.(e);
},
onMouseUp: (e: React.MouseEvent) => {
handlePressEnd();
listeners?.onMouseUp?.(e);
},
};

return (
<div className={menuBlockStyle} {...attributes} {...listeners}>
<div ref={menuBlockRef} className={menuBlockStyle} {...attributes} {...modifiedListeners}>
<div className={dragHandleIconStyle}>
<img src={DraggableIcon} alt="drag handle" width="10" height="10" />
</div>
<OptionModal
isOpen={isOpen}
onClose={closeModal}
menuBlockPosition={menuBlockPosition}
onAnimationSelect={onAnimationSelect}
onTypeSelect={onTypeSelect}
onDeleteSelect={onDeleteSelect}
onCopySelect={onCopySelect}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const modal = {
initial: {
opacity: 0,
x: -5,
},
animate: {
opacity: 1,
x: 0,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { css } from "@styled-system/css";

export const optionModal = css({
zIndex: "10000",
position: "fixed",
borderRadius: "8px",
width: "160px",
padding: "8px",
background: "white",
boxShadow: "md",
});

export const optionButton = css({
borderRadius: "8px",
width: "100%",
paddingBlock: "4px",
paddingInline: "8px",
textAlign: "left",
_hover: {
backgroundColor: "gray.100/40",
cursor: "pointer",
},
});

export const modalContainer = css({
display: "flex",
gap: "1",
flexDirection: "column",
});
Loading
Loading