다소 복잡한 코드로 보일 수 있으나, 사용한 라이브러리의 구조를 뜯어보고 조합해서 코드를 읽으면 이해할 수 있다 🤗
componentDidMount
최초 렌더링 시 마운트 되어야 하는 조건은 다음 2가지 경우에 따라 다르다.
- isFirstBlockWithoutHtml && isFirstBlockWithoutSibling: 텍스트 내용도, 다른 블록도 없이 새로운 단독 블록일 시
- 위의 경우가 아닌 경우: 기존의 블록이 존재하거나 입력한 텍스트가 있는 경우
1의 경우 전달받은 인자에 알맞게 내용을 입력해서 placeholder를 설정하고,
1 | if (!hasPlaceholder) { |
2의 경우 기존의 state가 존재하는 경우이므로 거기에 전달받은 props의 내용을 덧붙여 렌더링한다.
1 | this.addPlaceholder({ |
componentDidUpdate
최초의 렌더링 이후, 컴포넌트가 업데이트 될 때 (state/ props/ 부모 컴포넌트가 업데이트 됨에 따라) 실행되는 코드이다.
여기서 업데이트는 잦게 발생하고 보통 블록의 업데이트가 연관되는데, 다음과 같은 조건을 고려하여 블록을 업데이트 시킨다. 입력 할 때마다 업데이트하는 것이 아니라, 입력이 멈추었을 때 블록을 대상으로 업데이트 시켜야 렌더링 이슈가 발생하지 않는다.
1 | if (((stoppedTyping && htmlChanged) || tagChanged || imageChanged) && hasNoPlaceholder) { |
componentWillUnmount
컴포넌트가 업데이트 될 때마다 컴포넌트 리렌더링 직전 실행되는 코드로, 어떤 경우든 상관없이 업데이트 될 때마다 실행되어야 하는 코드를 넣는다.
현재 openActionMenu
에 addEventListener
로 이벤트 핸들러가 세팅되어 있는 상태라고 가정할 때, 아래와 같은 코드가 있다고 가정해보자.
1 | componentWillUnmount() { |
위 코드는 Component가 Will Unmount 할 때, remove Event Listener를 수행하게 될 것이다.
ComponentWillUnmount
는 useEffect
에서 return문이랑 동일한데, 하는 역할은 ComponentDidUpdate
직전, 즉, 리렌더링이 일어나기 직전 수행하고 다음 작업을 진행하는 것이다.
이와 같은 작업을 설정한 이유는, 여기서 closeActionMenu
는 action menu가 open 된 상태에서 어디를 클릭하든 menu가 close 되어야 하기 때문이다. 조건 없이 무조건 컴포넌트 상태에 변동이 있을 때 액션 메뉴가 닫혀야 하기 때문에 액션 메뉴를 열게 한 EventHandler를 제거해야 한다. 따라서 click 시 closeActionMenu가 실행되어야 하는 것이다.
handle key event
user가 /
커맨드를 keydown 했을 때 openSelectMenuHandler()
하는 것이 맞아보이지만, 실제로 keyup 을 했을 때 해당 메소드를 실행해야 한다. 사용자가 /
커맨드 입력을 완료해야 액션 메뉴가 떠야 하는데, 여기서 완료란 키 press가 떼어졌을 때 이기 때문이다. 따라서 handleKeyUp
에는 다음과 같은 코드를 작성한다.
1 | handleKeyUp(e: any) { |
그렇다면 onkeydown
에는 어떤 작업이 필요할까?
일단 사용자가 keydown하는 key에는 크게
command key (
/
): tag를 담은 tagSelectorMenu 오픈/
입력 시 현재까지 입력 중이던this.state.html
의 내용을 백업해두기 위해htmlBackup
에 저장한다.Backspace key
단일: 새로운 블록
html 내용이 없는 상태에서 backspace를 눌렀다면 블록을 지우는 것이다.
Enter key
- 이전 키가 shift가 아닌 경우 && tagSelectorMenu가 열려있지 않은 상태: 현재 블록 페이지 내 블록 리스트에 추가
- shift와 함께: 블록 내에서 Enter (html 내용의 일부)
타이핑을 위해:
html
(혹은htmlBackup
) 내용 입력
그리고 위 모든 경우에 관계없이 누른 키를 previousKey
로 등록하여 다른 키 이벤트 발생 시 조합해서 고려해야 한다. (Shift
+ Enter
처럼)
1 | handleKeyDown(e: any) { |
handle actionMenu
actionMenu
는 아래와 같다.
블록 관점에서 mouse 이벤트가 핸들링 되어야 하는 경우는 ContentEditable
의 current가 존재하는 (focus가 된, 수정 중인) 대상이 있을 때에 한해서
1의 경우, ContentEditable
의 text를 range 잡아 (드래그해서 단어문장문단 택) 클릭한 경우이다! 이 경우 actionMenu가 오픈되어야 하는 위치는 텍스트 range의 가운데 위이다. 해당 위치 계산을 위해 'TEXT_SELECTION'
을 키로 전달하여 actionMenu 오픈 시 위치가 계산되도록 한다.
이 경우에는 text가 range에 잡혀 selection에 추가한 range의 start와 end가 다르다. (블록의 text 시작 위치 === start, 끝 위치 === end) 이게 같은 경우에는 text 입력에 따른 actionMenu가 실행되는 경우가 아니므로 처리하지 않는다.
1 | handleMouseUp() { |
2의 경우 블록 오른쪽에 위치한, drag-n-drop 표시를 위한 아이콘을 onclick 한 상태이므로, 'DRAG_HANDLE_CLICK'
를 키로 전달한다. 이 경우 아이콘 바로 왼쪽에 actionMenu가 오픈되어야 한다.
1 | handleDragHandleClick(e: any) { |
actionMenu가 open된 후 발생하는 클릭 이벤트를 핸들링 하기 위해서는, range의 변경이 완료되었다고 판단하는 시간을 0.1초로 가정하고 이벤트 핸들러로 하여금 작동하도록 한다. 이러지 않으면, 사용자가 텍스트를 드래그하여 Range를 정하는 것과 맞물려 작동해 에러가 발생할 수 있으므로 불편을 초래할 수 있다.
1 | openActionMenu(parent: any, trigger: string) { |
또한 actionMenu가 닫힌 후에는 removeEventListener
를 통해 click에 대한 이벤트를 제거해야 이벤트 버블링을 방지할 수 있다.
handle tagSelectorMenu
tagSelectorMenu는 /
입력하는 html tag들을 담은 메뉴 아이템을 의미한다.
메뉴에서 tag를 select 했을 때 이벤트를 핸들링하는 handleTagSelection
을 살펴보자.
먼저, 이 핸들러는 전달된 tag
인자에 따라 추가된 블록의 html tag 타입을 설정해야 하는데, Enter 키 등으로 블록을 생성한 것이 아니라 페이지가 새로 생성된 최초의 상태에 default로 하나의 블록을 생성해 놓아야 한다.
여기서 사용자가 커맨드로 입력한 tag에 따라 기존 allowedTags
에서 sorting을 해야하기 때문에 match-sorter
라이브러리를 사용했다. 이 라이브러리에서 matchSorter
를 사용할 때 전달하는 인자 중 3번째 option 객체에 keys
를 넣어 전달할 수 있는데, 얘가 바로 default의 경우를 핸들링하는 키이다. 우리는 최초의 블록
1 | { |
을 렌더링 하기 위해 key로써 'tag'
를 핸들러의 인자로 전달한다.
이 외에, tag selector menu에서 클릭 이벤트 없이 typing할 경우 사용자가 내용을 입력하는 것이므로 기존에 입력 중이던 내용을 백업해놓는 htmlBackup
이 존재할 것이기 때문에 그에 이어서 내용을 덧붙인다.
최종적으로 위 모든 경우가 아닌 것은 메뉴 중 태그를 선택한 경우이므로 블록의 태그를 설정하고 메뉴를 닫으면 된다.
1 | handleTagSelection(tag: string) { |
그리고 이 메뉴를 여닫는 메소드는 actionMenu의 경우와 비슷하다. (여기서는 setTimeout
을 설정하지 않아도 된다. range의 변화와 같이 짧은 시간 내 변화가 많이 축적되는 이벤트가 발생하는 것이 아니기 때문이다. 클릭 이벤트 한 번일 뿐!)
1 | openTagSelectorMenu(trigger?: string) { |
위치를 계산하는 utils
setCaretToEnd: current 블록이 변경될 때 cursor (caret) 의 위치는 해당 블록 내 텍스트의 end 여야 한다.
1
range.collapse(false) // false === end, true === start
getSelection: 블록 내 텍스트를 드래그하여 텍스트 range를 생성(형성)할 때
getCaretCoordinates: caret을 내포하는 블록(=== coordinates)의 위치를 계산하기 위해