Skip to content

Commit 7b80dd1

Browse files
authored
Merge pull request #119 from boostcamp-2020/feat/115
Block drag & drop ๊ตฌํ˜„
2 parents 9803294 + 1b08726 commit 7b80dd1

File tree

8 files changed

+176
-59
lines changed

8 files changed

+176
-59
lines changed

โ€Žfrontend/.eslintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"import/no-unresolved": 0,
3939
"import/prefer-default-export": "off",
4040
"jsx-a11y/no-static-element-interactions": 0,
41-
"@typescript-eslint/indent": 0
41+
"@typescript-eslint/indent": 0,
42+
"no-param-reassign": 0
4243
},
4344
"settings": {
4445
"import/resolver": {

โ€Žfrontend/src/components/atoms/BlockContent/BlockContent.tsx

+87-7
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
11
/** @jsx jsx */
22
/** @jsxRuntime classic */
3-
import { jsx, css, SerializedStyles } from '@emotion/react';
4-
import { useEffect, useRef, FormEvent, KeyboardEvent, useState } from 'react';
3+
import { jsx, css } from '@emotion/react';
4+
import React, {
5+
useEffect,
6+
useRef,
7+
FormEvent,
8+
KeyboardEvent,
9+
useState,
10+
} from 'react';
511
import { useRecoilState, useRecoilValue } from 'recoil';
612

7-
import { blockRefState, throttleState, blockMapState } from '@/stores';
13+
import {
14+
blockMapState,
15+
blockRefState,
16+
draggingBlockState,
17+
throttleState,
18+
} from '@/stores';
819
import { Block, BlockType } from '@/schemes';
9-
import { updateBlock } from '@/utils';
1020
import {
1121
regex,
1222
fontSize,
1323
placeHolder,
1424
listBlockType,
1525
} from '@utils/blockContent';
16-
import { useCommand } from '@/hooks';
26+
import { useCommand, useManager } from '@/hooks';
1727
import { focusState } from '@/stores/page';
28+
import { moveBlock, updateBlock } from '@/utils';
1829

1930
const isGridOrColumn = (block: Block): boolean =>
2031
block.type === BlockType.GRID || block.type === BlockType.COLUMN;
2132

2233
const blockContentCSS = css`
34+
position: relative;
2335
display: flex;
2436
align-items: stretch;
2537
`;
26-
const editableDivCSS = (block: Block): SerializedStyles => css`
38+
const editableDivCSS = (block: Block) => css`
2739
margin: 5;
2840
font-size: ${fontSize[block.type]};
2941
display: ${!isGridOrColumn(block) ? 'flex' : 'none'};
@@ -50,6 +62,13 @@ const editableDivCSS = (block: Block): SerializedStyles => css`
5062
cursor: text;
5163
}
5264
`;
65+
const dragOverCss = () => css`
66+
position: absolute;
67+
bottom: 0;
68+
width: 100%;
69+
height: 15%;
70+
background-color: rgba(80, 188, 223, 0.7);
71+
`;
5372

5473
function BlockContent(blockDTO: Block) {
5574
const contentEditableRef = useRef(null);
@@ -59,6 +78,29 @@ function BlockContent(blockDTO: Block) {
5978
const listCnt = useRef(1);
6079
const [Dispatcher] = useCommand();
6180
const [isBlur, setIsBlur] = useState(false);
81+
const draggingBlock = useRecoilValue(draggingBlockState);
82+
const [{ blockIndex }] = useManager(blockDTO.id);
83+
const [dragOverToggle, setDragOverToggle] = useState(false);
84+
85+
useEffect(() => {
86+
blockRefState[blockDTO.id] = contentEditableRef;
87+
return () => {
88+
blockRefState[blockDTO.id] = null;
89+
};
90+
}, []);
91+
92+
useEffect(() => {
93+
if (focusId === blockDTO.id) contentEditableRef.current.focus();
94+
}, [focusId]);
95+
96+
useEffect(() => {
97+
const selection = window.getSelection();
98+
const nodeLength = selection.focusNode?.nodeValue?.length ?? 0;
99+
if (caretRef.current > nodeLength) {
100+
caretRef.current = nodeLength;
101+
}
102+
selection.collapse(selection.focusNode, caretRef.current);
103+
}, [blockDTO.value]);
62104

63105
const indexInSibling: number = blockMap[
64106
blockDTO.parentId
@@ -207,8 +249,45 @@ function BlockContent(blockDTO: Block) {
207249
}
208250
}, [blockDTO.value]);
209251

252+
const dragOverHandler = (event: React.DragEvent<HTMLDivElement>) => {
253+
event.dataTransfer.dropEffect = 'move';
254+
255+
event.preventDefault();
256+
};
257+
258+
const dropHandler = async (event: React.DragEvent<HTMLDivElement>) => {
259+
setDragOverToggle(false);
260+
event.dataTransfer.dropEffect = 'move';
261+
262+
const blockId = draggingBlock?.id;
263+
if (!blockId || blockId === blockDTO.id) {
264+
return;
265+
}
266+
267+
const { block, from: fromBlock, to } = await moveBlock({
268+
blockId,
269+
toId: blockDTO.parentId,
270+
index: blockIndex + 1,
271+
});
272+
setBlockMap((prev) => {
273+
const next = { ...prev };
274+
next[block.id] = block;
275+
fromBlock && (next[fromBlock.id] = fromBlock);
276+
next[to.id] = to;
277+
return next;
278+
});
279+
280+
event.preventDefault();
281+
};
282+
210283
return (
211-
<div css={blockContentCSS}>
284+
<div
285+
css={blockContentCSS}
286+
onDragOver={dragOverHandler}
287+
onDrop={dropHandler}
288+
onDragEnter={() => setDragOverToggle(true)}
289+
onDragLeave={() => setDragOverToggle(false)}
290+
>
212291
{listBlockType(blockDTO, listCnt.current)}
213292
<div
214293
ref={contentEditableRef}
@@ -223,6 +302,7 @@ function BlockContent(blockDTO: Block) {
223302
>
224303
{blockDTO.value}
225304
</div>
305+
{dragOverToggle && <div css={dragOverCss()} />}
226306
</div>
227307
);
228308
}

โ€Žfrontend/src/components/molecules/BlockComponent/BlockComponent.tsx

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/** @jsx jsx */
22
/** @jsxRuntime classic */
3-
import { jsx, css, SerializedStyles } from '@emotion/react';
3+
import { jsx, css } from '@emotion/react';
4+
import React, { useRef } from 'react';
5+
import { useRecoilState, useRecoilValue } from 'recoil';
46

57
import { BlockContent } from '@atoms/index';
68
import { BlockHandler, HoverArea } from '@components/molecules';
@@ -11,7 +13,6 @@ import {
1113
blockRefState,
1214
blockMapState,
1315
} from '@stores/page';
14-
import { useRecoilState, useRecoilValue } from 'recoil';
1516

1617
const isGridOrColumn = (block: Block): boolean =>
1718
block.type === BlockType.GRID || block.type === BlockType.COLUMN;
@@ -26,22 +27,27 @@ const blockCss = css`
2627
color: inherit;
2728
fill: inherit;
2829
`;
29-
const descendantsCss = (block: Block): SerializedStyles => css`
30+
const descendantsCss = (block: Block) => css`
3031
display: flex;
3132
padding-left: ${!isGridOrColumn(block) ? '1.5rem' : 0};
3233
flex-direction: ${block.type !== BlockType.GRID ? 'column' : 'row'};
3334
color: inherit;
3435
fill: inherit;
3536
`;
3637

37-
function BlockComponent({ blockDTO }: { blockDTO: Block }): JSX.Element {
38+
interface Props {
39+
blockDTO: Block;
40+
}
41+
42+
function BlockComponent({ blockDTO }: Props): JSX.Element {
3843
const blockMap = useRecoilValue(blockMapState);
3944
const [focusId, setFocusId] = useRecoilState<string>(focusState);
4045
const [hoverId, setHoverId] = useRecoilState(hoverState);
4146
const blockRef: any = blockRefState[blockDTO.id];
47+
const blockComponentRef = useRef(null);
4248

4349
return (
44-
<div css={blockCss}>
50+
<div css={blockCss} ref={blockComponentRef}>
4551
<div
4652
css={{ position: 'relative' }}
4753
onMouseEnter={() => setHoverId(blockDTO.id)}
@@ -51,8 +57,13 @@ function BlockComponent({ blockDTO }: { blockDTO: Block }): JSX.Element {
5157
}}
5258
>
5359
<BlockContent {...blockDTO} />
54-
<HoverArea handleClick={() => blockRef.current.focus()} />
55-
{hoverId === blockDTO.id && <BlockHandler />}
60+
<HoverArea clickHandler={() => blockRef.current.focus()} />
61+
{hoverId === blockDTO.id && (
62+
<BlockHandler
63+
blockDTO={blockDTO}
64+
blockComponentRef={blockComponentRef}
65+
/>
66+
)}
5667
</div>
5768
{blockDTO.childIdList.length ? (
5869
<div css={descendantsCss(blockDTO)}>

โ€Žfrontend/src/components/molecules/BlockHandler/BlockHandler.tsx

+38-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
/** @jsxRuntime classic */
22
/** @jsx jsx */
3-
import { jsx } from '@emotion/react';
4-
import styled from '@emotion/styled';
3+
import { jsx, css } from '@emotion/react';
54
import React from 'react';
65

76
import { ReactComponent as DraggableIcon } from '@assets/draggable.svg';
87
import { ReactComponent as PlusIcon } from '@assets/plus.svg';
8+
import { useSetRecoilState } from 'recoil';
99

10-
const ButtonWrapper = styled.div`
10+
import { draggingBlockState } from '@/stores';
11+
import { Block } from '@/schemes';
12+
13+
const buttonWrapperCss = () => css`
1114
display: flex;
1215
height: 100%;
1316
position: absolute;
@@ -23,13 +26,40 @@ const ButtonWrapper = styled.div`
2326
border-radius: 3px;
2427
}
2528
`;
29+
const buttonCss = () => css`
30+
display: inline-block;
31+
height: 16px;
32+
`;
33+
34+
interface Props {
35+
blockDTO: Block;
36+
blockComponentRef: any;
37+
}
38+
39+
function BlockHandler({ blockDTO, blockComponentRef }: Props): JSX.Element {
40+
const setDraggingBlock = useSetRecoilState(draggingBlockState);
41+
42+
const dragStartHandler = (event: React.DragEvent<HTMLDivElement>) => {
43+
event.dataTransfer.effectAllowed = 'move';
44+
event.dataTransfer.dropEffect = 'move';
45+
event.dataTransfer.setDragImage(blockComponentRef.current, 0, 0);
46+
47+
setDraggingBlock(blockDTO);
48+
};
2649

27-
function BlockHandler(): React.ReactElement {
2850
return (
29-
<ButtonWrapper>
30-
<PlusIcon css={{ height: '16px' }} />
31-
<DraggableIcon css={{ height: '16px' }} />
32-
</ButtonWrapper>
51+
<div css={buttonWrapperCss()}>
52+
<PlusIcon css={buttonCss()} />
53+
<div
54+
css={buttonCss()}
55+
draggable="true"
56+
onDragStart={dragStartHandler}
57+
onMouseEnter={() => setDraggingBlock(blockDTO)}
58+
onMouseLeave={() => setDraggingBlock(null)}
59+
>
60+
<DraggableIcon css={buttonCss()} />
61+
</div>
62+
</div>
3363
);
3464
}
3565

โ€Žfrontend/src/components/molecules/Editor/Editor.tsx

+3-11
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import { jsx, css } from '@emotion/react';
44

55
import { BlockComponent } from '@components/molecules';
6-
import { useRecoilState, useRecoilValue } from 'recoil';
6+
import { useRecoilValue } from 'recoil';
77
import { pageState, blockMapState } from '@/stores';
8-
import { Suspense, useEffect } from 'react';
9-
import { readBlockMap } from '@/utils';
8+
import { Suspense } from 'react';
109

1110
const wrapperCss = () => css`
1211
padding-left: calc(96px + env(safe-area-inset-left));
@@ -18,14 +17,7 @@ const wrapperCss = () => css`
1817

1918
function Editor(): JSX.Element {
2019
const page = useRecoilValue(pageState);
21-
// const blockMap = useRecoilValue(blockMapState);
22-
const [blockMap, setBlockMap] = useRecoilState(blockMapState);
23-
24-
useEffect(() => {
25-
(async () => {
26-
setBlockMap((await readBlockMap(page.id)).blockMap);
27-
})();
28-
}, [page, setBlockMap]);
20+
const blockMap = useRecoilValue(blockMapState);
2921

3022
return (
3123
<div css={wrapperCss()}>

โ€Žfrontend/src/components/molecules/HoverArea/HoverArea.tsx

+16-11
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,42 @@
22
/** @jsxRuntime classic */
33
import { jsx, css } from '@emotion/react';
44
import { MouseEvent } from 'react';
5+
import { useRecoilValue } from 'recoil';
6+
import { draggingBlockState } from '@/stores';
57

6-
const LeftHoverAreaCss = css`
8+
const leftHoverAreaCss = css`
79
position: absolute;
810
top: 0;
911
right: 100%;
1012
width: calc(10% + 36px);
1113
height: 100%;
1214
`;
13-
const RightHoverAreaCss = css`
15+
const rightHoverAreaCss = css`
1416
position: absolute;
1517
top: 0;
1618
left: 100%;
1719
width: 10%;
1820
height: 100%;
1921
`;
20-
const CommonHoverAreaCss = css`
22+
const commonHoverAreaCss = css`
2123
&:hover {
2224
cursor: text;
2325
}
2426
`;
25-
function HoverArea({
26-
handleClick,
27-
}: {
28-
handleClick: (
27+
28+
interface Props {
29+
clickHandler: (
2930
event: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
3031
) => void;
31-
}): React.ReactElement {
32+
}
33+
34+
function HoverArea({ clickHandler }: Props): JSX.Element {
35+
const draggingBlock = useRecoilValue(draggingBlockState);
36+
3237
return (
33-
<div css={CommonHoverAreaCss} onClick={handleClick} onKeyDown={() => {}}>
34-
<div css={LeftHoverAreaCss} />
35-
<div css={RightHoverAreaCss} />
38+
<div css={commonHoverAreaCss} onClick={clickHandler} onKeyDown={() => {}}>
39+
{!draggingBlock && <div css={leftHoverAreaCss} />}
40+
<div css={rightHoverAreaCss} />
3641
</div>
3742
);
3843
}

โ€Žfrontend/src/components/molecules/Menu/Menu.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,10 @@ function Menu(): JSX.Element {
6565

6666
const CreatingPageHandler = async () => {
6767
const { pages: updated, page: created } = await createPage();
68-
const { blockMap } = await readBlockMap(created.id);
69-
console.log({ blockMap });
70-
setBlockMap(blockMap);
71-
setPages(updated);
68+
69+
setBlockMap((await readBlockMap(created.id)).blockMap);
7270
setSelectedPage(created);
71+
setPages(updated);
7372
};
7473

7574
const clickCloseHandler = () => {

0 commit comments

Comments
ย (0)