Skip to content

Commit 7f43e13

Browse files
se030dohun31
andauthored
Feat/#199-K: 블럭 추가/삭제에 포커스와 커서 이동 (#235)
* feat: Block 컴포넌트 ref를 Mom에도 전달 - 배열 ref에서 forwardRef 사용하면 타입과 값 할당이 제대로 되지 않는 이슈 - Mom 컴포넌트에서 RefObject 배열을 값으로 가지는 ref 객체를 하나 생성 - Block 컴포넌트에서 props로 전달받은 함수로 각각에서 생성한 ref 객체 등록 * refactor: 반복되는 형 변환 변수로 선언 * feat: 첫번째 블럭 삭제 방지 조건문 추가 - 부작용 방지하기 위해 추가 - 블럭 여러개인 경우에는 삭제 가능하도록 개선 * feat: 블럭 포커스 조정하는 useBlockFocus 훅 추가 * refactor: Mom blockRefs에 blockRef 등록하는 함수 네이밍 수정 * fix: remote 블럭 연산에는 setFocus 일어나지 않도록 수정 Co-authored-by: hodun <[email protected]> * feat: 블럭 삭제 시 직전 블럭 마지막 위치로 커서 이동 Co-authored-by: hodun <[email protected]> * refactor: 훅으로 분리되어 있던 블럭 포커스 이동 로직 Mom 컴포넌트로 이동 * feat: TextBlock props로 registerRef 전달 * fix: 블럭 포커스 잡히지 않는 버그 수정 Co-authored-by: hodun <[email protected]>
1 parent 0871a0c commit 7f43e13

File tree

4 files changed

+81
-9
lines changed

4 files changed

+81
-9
lines changed

client/src/components/Mom/Block/TextBlock.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@ interface BlockProps {
1818
onKeyDown: React.KeyboardEventHandler;
1919
type: BlockType;
2020
setType: (arg: BlockType) => void;
21+
registerRef: (arg: React.RefObject<HTMLElement>) => void;
2122
}
2223

23-
function TextBlock({ id, index, onKeyDown, type, setType }: BlockProps) {
24+
function TextBlock({
25+
id,
26+
index,
27+
onKeyDown,
28+
type,
29+
setType,
30+
registerRef,
31+
}: BlockProps) {
2432
const { momSocket: socket } = useSocketContext();
2533
const [isOpen, setIsOpen] = useState<boolean>(false);
2634

@@ -74,7 +82,6 @@ function TextBlock({ id, index, onKeyDown, type, setType }: BlockProps) {
7482
if (!blockRef.current) return;
7583

7684
blockRef.current.innerText = readCRDT();
77-
blockRef.current.contentEditable = 'true';
7885
};
7986

8087
const onInsert = (op: RemoteInsertOperation) => {
@@ -103,6 +110,8 @@ function TextBlock({ id, index, onKeyDown, type, setType }: BlockProps) {
103110

104111
// crdt의 초기화와 소켓을 통해 전달받는 리모트 연산 처리
105112
useEffect(() => {
113+
registerRef(blockRef);
114+
106115
socket.emit(SOCKET_MESSAGE.BLOCK.INIT, id);
107116

108117
ee.on(`${SOCKET_MESSAGE.BLOCK.INIT}-${id}`, onInitialize);
@@ -217,6 +226,7 @@ function TextBlock({ id, index, onKeyDown, type, setType }: BlockProps) {
217226
'data-id': id,
218227
'date-index': index,
219228
...commonHandlers,
229+
contentEditable: true,
220230
suppressContentEditableWarning: true,
221231
},
222232
readCRDT(),

client/src/components/Mom/Block/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ interface BlockProps {
1818
id: string;
1919
index: number;
2020
onKeyDown: React.KeyboardEventHandler;
21+
registerRef: (arg: React.RefObject<HTMLElement>) => void;
2122
}
2223

23-
function Block({ id, index, onKeyDown }: BlockProps) {
24+
function Block({ id, index, onKeyDown, registerRef }: BlockProps) {
2425
const { momSocket: socket } = useSocketContext();
2526

2627
const [type, setType] = useState<BlockType>();
@@ -60,6 +61,7 @@ function Block({ id, index, onKeyDown }: BlockProps) {
6061
onKeyDown={onKeyDown}
6162
type={type}
6263
setType={setBlockType}
64+
registerRef={registerRef}
6365
/>
6466
);
6567
default:

client/src/components/Mom/index.tsx

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,52 @@ function Mom() {
4444

4545
const [blocks, setBlocks] = useState<string[]>([]);
4646

47+
const blockRefs = useRef<React.RefObject<HTMLElement>[]>([]);
48+
const focusIndex = useRef<number>();
49+
50+
const updateBlockFocus = (idx: number | undefined) => {
51+
focusIndex.current = idx;
52+
};
53+
54+
const setBlockFocus = () => {
55+
if (!blockRefs.current || focusIndex.current === undefined) return;
56+
57+
const idx = focusIndex.current;
58+
59+
const targetBlock = blockRefs.current[idx];
60+
61+
if (!targetBlock || !targetBlock.current) return;
62+
63+
targetBlock.current.focus();
64+
};
65+
66+
const setCaretToEnd = () => {
67+
const selection = getSelection();
68+
69+
if (!selection) return;
70+
71+
const range = selection.getRangeAt(0);
72+
73+
if (!range) return;
74+
75+
range.selectNodeContents(range.startContainer);
76+
range.collapse();
77+
};
78+
4779
const onKeyDown: React.KeyboardEventHandler = (e) => {
4880
const target = e.target as HTMLParagraphElement;
4981

50-
const { index } = target.dataset;
82+
const { index: indexString } = target.dataset;
83+
const index = Number(indexString);
5184

5285
if (e.key === 'Enter') {
5386
e.preventDefault();
5487

5588
const blockId = uuid();
5689

57-
const remoteInsertion = localInsertCRDT(Number(index), blockId);
90+
const remoteInsertion = localInsertCRDT(index, blockId);
91+
92+
updateBlockFocus(index + 1);
5893

5994
socket.emit(SOCKET_MESSAGE.MOM.INSERT_BLOCK, blockId, remoteInsertion);
6095
return;
@@ -67,7 +102,15 @@ function Mom() {
67102

68103
e.preventDefault();
69104

70-
const remoteDeletion = localDeleteCRDT(Number(index));
105+
if (index === 0) return;
106+
107+
const remoteDeletion = localDeleteCRDT(index);
108+
109+
updateBlockFocus(index - 1);
110+
111+
setBlocks(spreadCRDT());
112+
setBlockFocus();
113+
setCaretToEnd();
71114

72115
socket.emit(SOCKET_MESSAGE.MOM.DELETE_BLOCK, id, remoteDeletion);
73116
}
@@ -76,6 +119,10 @@ function Mom() {
76119
const initialOption: Option[] = [{ id: 1, text: '', count: 0 }];
77120
const [options, setOptions] = useState<Option[]>(initialOption);
78121

122+
useEffect(() => {
123+
setBlockFocus();
124+
}, [blocks]);
125+
79126
useEffect(() => {
80127
if (!selectedMom) return;
81128

@@ -92,15 +139,21 @@ function Mom() {
92139
titleRef.current.innerText = title;
93140
});
94141

95-
socket.on(SOCKET_MESSAGE.MOM.UPDATED, () => setBlocks(spreadCRDT()));
142+
socket.on(SOCKET_MESSAGE.MOM.UPDATED, () => {
143+
setBlocks(spreadCRDT());
144+
});
96145

97146
socket.on(SOCKET_MESSAGE.MOM.INSERT_BLOCK, (op) => {
98147
remoteInsertCRDT(op);
148+
149+
updateBlockFocus(undefined);
99150
setBlocks(spreadCRDT());
100151
});
101152

102153
socket.on(SOCKET_MESSAGE.MOM.DELETE_BLOCK, (op) => {
103154
remoteDeleteCRDT(op);
155+
156+
updateBlockFocus(undefined);
104157
setBlocks(spreadCRDT());
105158
});
106159

@@ -162,7 +215,15 @@ function Mom() {
162215

163216
<div className={style['mom-body']}>
164217
{blocks.map((id, index) => (
165-
<Block key={id} id={id} index={index} onKeyDown={onKeyDown} />
218+
<Block
219+
key={id}
220+
id={id}
221+
index={index}
222+
onKeyDown={onKeyDown}
223+
registerRef={(ref: React.RefObject<HTMLElement>) => {
224+
blockRefs.current[index] = ref;
225+
}}
226+
/>
166227
))}
167228
</div>
168229
{/* TODO: 임시로 놓은 투표 블록임 */}

server/socket/mom.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ async function momSocketServer(io: Server) {
8383

8484
await crdtManager.onDeleteBlock(momId, blockId, op);
8585

86-
socket.emit(SOCKET_MESSAGE.MOM.UPDATED);
8786
socket.to(momId).emit(SOCKET_MESSAGE.MOM.DELETE_BLOCK, op);
8887
});
8988

0 commit comments

Comments
 (0)