Draft.js ブロック単位で削除する

imatomix
2021年12月4日 19:57

概要

Draft.js でブロック単位での削除をやってみます。当初こういうのは簡単にできるだろうと思っていたら、公式のドキュメントを見る限りでは、ブロックをターゲットにした削除APIが見当たらなかったので、割と手探りです。他にいいやり方があるのかもしれません。

まず削除するブロックを決めるにあたって、選択範囲を理解する必要がある。

選択範囲として取得できる情報は Start/EndAnchor/Focus の2種類がある。ユーザーが文末から文頭にかけて後方選択をした場合、Start/Endの場合は文頭が選択開始位置になるが、Anchor/Focusの場合は文末が選択開始位置になる。つまりStart/Endは結果としての選択範囲を見るが、Anchor/Focusはユーザー操作(ドラッグ開始地点がどこか)から見ている。なので、ユーザーが文頭から文末にかけて前方選択をした場合は、どちらも同じ情報になる。

なお、SelectionState がプロパティとして保持している情報は Anchor/Focus のみ。

削除方法

以下の2種類のやり方がありそう。
  • blockMapを編集する方法
  • Modifire.removeRangeを使う方法

BlockMapを編集する方法

getBlockMap を使用して ContentState のブロック情報を取得する。ブロック情報から任意のブロックを削除した後、 EditorStateを更新する。
const removeBlock = () => { const contentState = editorState.getCurrentContent() const selection = editorState.getSelection() const blockMap = contentState.getBlockMap() const blockKey = selection.getStartKey() const newBlockMap = blockMap.delete(blockKey) const newContentState = contentState.merge({ blockMap: newBlockMap, }) as ContentState const newEditorState = EditorState.push( editorState, newContentState, 'remove-range' ) setEditorState(newEditorState) }

Modifier.removeRange を使う方法

ブロックに限らず、範囲を指定して削除するModifierメソッドを使用する。ユーザーが指定した選択範囲を、スクリプト上でブロック全体に拡張して削除することでブロック自体を削除する。
removeRange( contentState: ContentState, rangeToRemove: SelectionState, removalDirection: DraftRemovalDirection ): ContentState
しかし、以下のように選択範囲をブロックの開始から終了までに広げただけでは、ブロック自体は削除されなかった。結果としては、空のブロックになるだけ。
const selection = editorState.getSelection() const contentState = editorState.getCurrentContent() const endBlock = contentState.getBlockForKey(selection.getEndKey()) const selectionState = new SelectionState({ anchorKey: selection.getStartKey(), // 開始ブロック anchorOffset: 0, // 開始ブロック内のスタート位置 focusKey: selection.getEndKey(), // 終了ブロック focusOffset: endBlock.getLength(), // 終了ブロック内の終了位置 }) const newContentState = Modifier.removeRange( contentState, selectionState, 'forward' )
ブロック自体を削除するには、開始ブロックの1つ前のブロックの終わりを選択範囲に含まなければならない。
const selectionState = new SelectionState({ anchorKey: beforeBlock?.getKey() || selection.getStartKey(), // 開始ブロック anchorOffset: beforeBlock?.getLength() || 0, // 開始ブロック内のスタート位置 focusKey: selection.getEndKey(), // 終了ブロック focusOffset: endBlock.getLength(), // 終了ブロック内の終了位置 })
最終的にこういう形になった。選択範囲が複数ブロックにまたがる場合、かかるすべてのブロックを削除するようにしたが、そういうユースケースはないかもしれない。
const removeBlock = () => { const selection = editorState.getSelection() const contentState = editorState.getCurrentContent() const beforeBlock = contentState.getBlockBefore(selection.getStartKey()) const endBlock = contentState.getBlockForKey(selection.getEndKey()) const selectionState = new SelectionState({ anchorKey: beforeBlock?.getKey() || selection.getStartKey(), anchorOffset: beforeBlock?.getLength() || 0, focusKey: selection.getEndKey(), focusOffset: endBlock.getLength(), }) const newContentState = Modifier.removeRange( contentState, selectionState, 'forward' ) const newEditorState = EditorState.push( editorState, newContentState, 'remove-range' ) setEditorState(newEditorState) }