Skip to content

Commit

Permalink
Merge pull request #226 from boostcampwm-2024/Feature/#213_하위요소_블록_드래…
Browse files Browse the repository at this point in the history
…그앤드랍_구현

Feature/#213 하위요소 블록 드래그앤드랍 구현
  • Loading branch information
github-actions[bot] authored Nov 28, 2024
2 parents 53ce3e4 + 9a9d7e4 commit 56741ab
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 112 deletions.
2 changes: 2 additions & 0 deletions @noctaCrdt/LinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ export class BlockLinkedList extends LinkedList<Block> {
if (targetNode.prev) {
const prevNode = this.getNode(targetNode.prev);
if (prevNode) prevNode.next = targetNode.next;
} else {
this.head = targetNode.next;
}

if (targetNode.next) {
Expand Down
16 changes: 13 additions & 3 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DndContext } from "@dnd-kit/core";
import { DndContext, DragEndEvent } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { EditorCRDT } from "@noctaCrdt/Crdt";
import { BlockLinkedList } from "@noctaCrdt/LinkedList";
Expand Down Expand Up @@ -48,6 +48,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
const [displayTitle, setDisplayTitle] = useState(
pageTitle === "새로운 페이지" || pageTitle === "" ? "" : pageTitle,
);
const [dragBlockList, setDragBlockList] = useState<string[]>([]);

const editorCRDTInstance = useMemo(() => {
let newEditorCRDT;
Expand Down Expand Up @@ -80,7 +81,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
addNewBlock,
} = useEditorOperation({ editorCRDT, pageId, setEditorState });

const { sensors, handleDragEnd } = useBlockDragAndDrop({
const { sensors, handleDragEnd, handleDragStart } = useBlockDragAndDrop({
editorCRDT: editorCRDT.current,
editorState,
setEditorState,
Expand Down Expand Up @@ -272,7 +273,15 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
className={editorTitle}
/>
<div style={{ height: "36px" }}></div>
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<DndContext
onDragEnd={(event: DragEndEvent) => {
handleDragEnd(event, dragBlockList, () => setDragBlockList([]));
}}
onDragStart={(event) => {
handleDragStart(event, setDragBlockList);
}}
sensors={sensors}
>
<SortableContext
items={editorState.linkedList
.spread()
Expand All @@ -298,6 +307,7 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData
onTextStyleUpdate={onTextStyleUpdate}
onTextColorUpdate={onTextColorUpdate}
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
dragBlockList={dragBlockList}
/>
))}
</SortableContext>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const iconStyle = cva({
variants: {
type: {
ul: {
fontSize: "20px", // bullet point size
fontSize: "6px", // bullet point size
},
ol: {
paddingRight: "4px",
Expand Down
11 changes: 9 additions & 2 deletions client/src/features/editor/components/IconBlock/IconBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import { iconContainerStyle, iconStyle } from "./IconBlock.style";
interface IconBlockProps {
type: ElementType;
index: number | undefined;
indent?: number;
}

export const IconBlock = ({ type, index = 1 }: IconBlockProps) => {
export const IconBlock = ({ type, index = 1, indent = 0 }: IconBlockProps) => {
const getIcon = () => {
switch (type) {
case "ul":
return <span className={iconStyle({ type: "ul" })}></span>;
return (
<span className={iconStyle({ type: "ul" })}>
{indent === 0 && "●"}
{indent === 1 && "○"}
{indent === 2 && "■"}
</span>
);
case "ol":
return <span className={iconStyle({ type: "ol" })}>{`${index}.`}</span>;
case "checkbox":
Expand Down
27 changes: 27 additions & 0 deletions client/src/features/editor/components/block/Block.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,30 @@ export const textContainerStyle = cva({
type: "p",
},
});

export const dropIndicatorStyle = cva({
base: {
zIndex: "10",
position: "absolute",
height: "2px",
},
variants: {
indent: {
first: {
left: "0",
width: "100%",
backgroundColor: "#ADADFF",
},
second: {
left: "10px",
width: "calc(100% - 10px)",
backgroundColor: "#9B9BFF ",
},
third: {
left: "20px",
width: "calc(100% - 20px)",
backgroundColor: "#8989FF",
},
},
},
});
152 changes: 88 additions & 64 deletions client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
AnimationType,
ElementType,
Expand All @@ -20,11 +19,17 @@ import { MenuBlock } from "../MenuBlock/MenuBlock";
import { TextOptionModal } from "../TextOptionModal/TextOptionModal";
import { TypeOptionModal } from "../TypeOptionModal/TypeOptionModal";
import { blockAnimation } from "./Block.animation";
import { textContainerStyle, blockContainerStyle, contentWrapperStyle } from "./Block.style";
import {
textContainerStyle,
blockContainerStyle,
contentWrapperStyle,
dropIndicatorStyle,
} from "./Block.style";

interface BlockProps {
id: string;
block: CRDTBlock;
dragBlockList: string[];
isActive: boolean;
onInput: (e: React.FormEvent<HTMLDivElement>, block: CRDTBlock) => void;
onCompositionEnd: (e: React.CompositionEvent<HTMLDivElement>, block: CRDTBlock) => void;
Expand Down Expand Up @@ -64,6 +69,7 @@ export const Block: React.FC<BlockProps> = memo(
({
id,
block,
dragBlockList,
isActive,
onInput,
onCompositionEnd,
Expand All @@ -83,13 +89,23 @@ export const Block: React.FC<BlockProps> = memo(
const { isOpen, openModal, closeModal } = useModal();
const [selectedNodes, setSelectedNodes] = useState<Array<Char> | null>(null);
const { isAnimationStart } = useBlockAnimation(blockRef);
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id,
data: {
type: "block",
block,
},
});
const { attributes, listeners, setNodeRef, isDragging, isOver, activeIndex, overIndex, data } =
useSortable({
id,
data: {
id,
type: "block",
block,
},
});

// 현재 드래그 중인 부모 블록의 indent 확인
const isChildOfDragging = dragBlockList.some((item) => item === data.id);

// NOTE 드롭 인디케이터 위치 계산
// 현재 over 중인 블럭 위치 + 위/아래로 모두 인디케이터 표시 + 부모요소는 자식요소 내부로는 이동하지 못함
const showTopIndicator = isOver && !isChildOfDragging && activeIndex >= overIndex;
const showBottomIndicator = isOver && !isChildOfDragging && activeIndex < overIndex;

const [slashModalOpen, setSlashModalOpen] = useState(false);
const [slashModalPosition, setSlashModalPosition] = useState({ top: 0, left: 0 });
Expand Down Expand Up @@ -214,6 +230,14 @@ export const Block: React.FC<BlockProps> = memo(
}
};

const Indicator = () => (
<div
className={dropIndicatorStyle({
indent: block.indent === 0 ? "first" : block.indent === 1 ? "second" : "third",
})}
/>
);

useEffect(() => {
if (blockRef.current) {
setInnerHTML({ element: blockRef.current, block });
Expand All @@ -223,66 +247,66 @@ export const Block: React.FC<BlockProps> = memo(
return (
// TODO: eslint 규칙을 수정해야 할까?
// TODO: ol일때 index 순서 처리
<motion.div
ref={setNodeRef}
className={blockContainerStyle({ isActive })}
style={{
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : undefined,
}}
initial={blockAnimation[block.animation || "none"].initial}
animate={isAnimationStart && blockAnimation[block.animation || "none"].animate}
data-group
>
<div style={{ position: "relative" }}>
{showTopIndicator && <Indicator />}
<motion.div
className={contentWrapperStyle()}
style={{ paddingLeft: `${block.indent * 12}px` }}
ref={setNodeRef}
className={blockContainerStyle({ isActive })}
style={{ opacity: isDragging || isChildOfDragging ? 0.3 : undefined }}
initial={blockAnimation[block.animation || "none"].initial}
animate={isAnimationStart && blockAnimation[block.animation || "none"].animate}
data-group
>
<MenuBlock
attributes={attributes}
listeners={listeners}
onAnimationSelect={handleAnimationSelect}
onTypeSelect={handleTypeSelect}
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
<motion.div
className={contentWrapperStyle()}
style={{ paddingLeft: `${block.indent * 12}px` }}
>
<MenuBlock
attributes={attributes}
listeners={listeners}
onAnimationSelect={handleAnimationSelect}
onTypeSelect={handleTypeSelect}
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
/>
<IconBlock type={block.type} index={block.listIndex} indent={block.indent} />
<div
ref={blockRef}
onKeyDown={(e) => onKeyDown(e, blockRef.current, block)}
onInput={handleInput}
onClick={(e) => onClick(block.id, e)}
onCopy={(e) => onCopy(e, blockRef.current, block)}
onPaste={(e) => onPaste(e, blockRef.current, block)}
onMouseUp={handleMouseUp}
onCompositionEnd={(e) => onCompositionEnd(e, block)}
contentEditable={block.type !== "hr"}
spellCheck={false}
suppressContentEditableWarning
className={textContainerStyle({
type: block.type,
})}
/>
</motion.div>
<TextOptionModal
selectedNodes={selectedNodes}
isOpen={isOpen}
onClose={closeModal}
onBoldSelect={() => handleStyleSelect("bold")}
onItalicSelect={() => handleStyleSelect("italic")}
onUnderlineSelect={() => handleStyleSelect("underline")}
onStrikeSelect={() => handleStyleSelect("strikethrough")}
onTextColorSelect={handleTextColorSelect}
onTextBackgroundColorSelect={handleTextBackgroundColorSelect}
/>
<IconBlock type={block.type} index={block.listIndex} />
<div
ref={blockRef}
onKeyDown={(e) => onKeyDown(e, blockRef.current, block)}
onInput={handleInput}
onClick={(e) => onClick(block.id, e)}
onCopy={(e) => onCopy(e, blockRef.current, block)}
onPaste={(e) => onPaste(e, blockRef.current, block)}
onMouseUp={handleMouseUp}
onCompositionEnd={(e) => onCompositionEnd(e, block)}
contentEditable={block.type !== "hr"}
spellCheck={false}
suppressContentEditableWarning
className={textContainerStyle({
type: block.type,
})}
<TypeOptionModal
isOpen={slashModalOpen}
onClose={() => setSlashModalOpen(false)}
onTypeSelect={(type) => handleTypeSelect(type)}
position={slashModalPosition}
/>
</motion.div>
<TextOptionModal
selectedNodes={selectedNodes}
isOpen={isOpen}
onClose={closeModal}
onBoldSelect={() => handleStyleSelect("bold")}
onItalicSelect={() => handleStyleSelect("italic")}
onUnderlineSelect={() => handleStyleSelect("underline")}
onStrikeSelect={() => handleStyleSelect("strikethrough")}
onTextColorSelect={handleTextColorSelect}
onTextBackgroundColorSelect={handleTextBackgroundColorSelect}
/>
<TypeOptionModal
isOpen={slashModalOpen}
onClose={() => setSlashModalOpen(false)}
onTypeSelect={(type) => handleTypeSelect(type)}
position={slashModalPosition}
/>
</motion.div>
{showBottomIndicator && <Indicator />}
</div>
);
},
);
Expand Down
Loading

0 comments on commit 56741ab

Please sign in to comment.