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/#213 하위요소 블록 드래그앤드랍 구현 #226

Merged
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
878c480
feat: indicator 스타일 추가
pipisebastian Nov 27, 2024
39a8e08
feat: ul 아이콘 스타일 변경
pipisebastian Nov 27, 2024
6be2141
feat: tab의 경우 부모요소의 +1 까지만 가능하도록 수정
pipisebastian Nov 27, 2024
34bf566
feat: indicator 구현
pipisebastian Nov 27, 2024
7d9b68b
feat: blockId 배열대신 id string 배열로 관리
pipisebastian Nov 27, 2024
f10ae2b
feat: handleDragEnd 구현
pipisebastian Nov 27, 2024
6daeb57
refactor: 변수명 변경 및 함수 분리
pipisebastian Nov 27, 2024
8289631
refactor: const로 변경
pipisebastian Nov 27, 2024
06798c2
feat: handleDragStart 함수 구현
pipisebastian Nov 27, 2024
468a7b6
refactor: type string 변경
pipisebastian Nov 27, 2024
9d3fcdc
refactor: type string 변경
pipisebastian Nov 27, 2024
ad2aa42
refactor: indicator 다자인 수정
pipisebastian Nov 27, 2024
c6c0ae6
feat: backspace/shift tab 누를시 indent 감소 구현
pipisebastian Nov 27, 2024
e0a2745
feat: indent별 ul 아이콘 변경
pipisebastian Nov 27, 2024
b3f2f94
Merge branch 'dev' of https://github.com/boostcampwm-2024/web33-Nocta…
pipisebastian Nov 28, 2024
baab9fd
feat: 백스페이스 내용있을때도 indent 줄어들도록
pipisebastian Nov 28, 2024
b64288f
fix: 드래그 부모,자식관계가 제대로 업데이트 안되는 문제 해결
pipisebastian Nov 28, 2024
d7646d6
fix: prevBlock 존재여부 검사 추가
pipisebastian Nov 28, 2024
f67999e
feat: 하위요소까지 드래그 기능 구현
pipisebastian Nov 28, 2024
5f2c92a
refactor: 안쓰는 css import 제거
pipisebastian Nov 28, 2024
9a9d7e4
refactor: 기존코드 복구
pipisebastian Nov 28, 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
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;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

이게 밑으로 드래그했을때, 영상처럼 위에게 다 날라가는 원인이었습니다!

2024-11-28.4.49.09.mov

Copy link
Collaborator

Choose a reason for hiding this comment

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

아 이부분 관련해서 질문글을 쓰고있었는데.. ㅋㅋ 바로 답이 올라왔군요! 감사합니다.

저부분이 헤드에있던 녀석이 마지막으로 이동할때, 나머지 요소들이 재정렬될때 누락이 있었나보군요.. 대단하십니다 !!

Copy link
Member Author

@pipisebastian pipisebastian Nov 28, 2024

Choose a reason for hiding this comment

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

영상을 보시면 첫번째 block을 밑으로 드래그할때만 버그가 발생했습니다,
2,3번째 block은 버그가 발생하지 않았습니다.

이유는 첫번째 block은 prev가 없는 상태인데, 그걸 밑으로 드래그합니다.
LinkedList의 head가 여전히 첫 번째 블록을 가리키고 있기에 연결이 망가집니다.

그래서 prev가 없는 경우(= head인 경우), 현재 head의 next(targetNode.next)를 새로운 head로 업데이트해줘야합니다.


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[];
Copy link
Collaborator

Choose a reason for hiding this comment

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

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
Loading