Skip to content

Commit

Permalink
Merge pull request #170 from boostcampwm-2024/Feature/#162_블록_옵션창_구현
Browse files Browse the repository at this point in the history
Feature/#162 블록 옵션창 구현
  • Loading branch information
github-actions[bot] authored Nov 22, 2024
2 parents 10d835f + 7e603eb commit 70ea86a
Show file tree
Hide file tree
Showing 12 changed files with 558 additions and 20 deletions.
2 changes: 2 additions & 0 deletions @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
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 }[],
},
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,
},
};

export type OptionCategory = keyof typeof OPTION_CATEGORIES;
17 changes: 17 additions & 0 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 Down Expand Up @@ -55,6 +56,18 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
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 @@ -275,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,
// 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

0 comments on commit 70ea86a

Please sign in to comment.