const onUntab = (blockMap: BlockMap, block: ContentBlockNode): BlockMap => { const key = block.getKey(); const parentKey = block.getParentKey(); const nextSiblingKey = block.getNextSiblingKey(); if (parentKey == null) { return blockMap; } const parent = blockMap.get(parentKey); const existingChildren = parent.getChildKeys(); const blockIndex = existingChildren.indexOf(key); if (blockIndex === 0 || blockIndex === existingChildren.count() - 1) { blockMap = DraftTreeOperations.moveChildUp(blockMap, key); } else { // split the block into [0, blockIndex] in parent & the rest in a new block const prevChildren = existingChildren.slice(0, blockIndex + 1); const nextChildren = existingChildren.slice(blockIndex + 1); blockMap = blockMap.set(parentKey, parent.merge({children: prevChildren})); const newBlock = new ContentBlockNode({ key: generateRandomKey(), text: '', depth: parent.getDepth(), type: parent.getType(), children: nextChildren, parent: parent.getParentKey(), }); // add new block just before its the original next sibling in the block map // TODO(T33894878): Remove the map reordering code & fix converter after launch invariant(nextSiblingKey != null, 'block must have a next sibling here'); const blocks = blockMap.toSeq(); blockMap = blocks .takeUntil(block => block.getKey() === nextSiblingKey) .concat( [[newBlock.getKey(), newBlock]], blocks.skipUntil(block => block.getKey() === nextSiblingKey), ) .toOrderedMap(); // set the nextChildren's parent to the new block blockMap = blockMap.map( block => nextChildren.includes(block.getKey()) ? block.merge({parent: newBlock.getKey()}) : block, ); // update the next/previous pointers for the children at the split blockMap = blockMap .set(key, block.merge({nextSibling: null})) .set( nextSiblingKey, blockMap.get(nextSiblingKey).merge({prevSibling: null}), ); const parentNextSiblingKey = parent.getNextSiblingKey(); if (parentNextSiblingKey != null) { blockMap = DraftTreeOperations.updateSibling( blockMap, newBlock.getKey(), parentNextSiblingKey, ); } blockMap = DraftTreeOperations.updateSibling( blockMap, parentKey, newBlock.getKey(), ); blockMap = DraftTreeOperations.moveChildUp(blockMap, key); } // on untab, we also want to unnest any sibling blocks that become two levels deep // ensure that block's old parent does not have a non-leaf as its first child. let childWasUntabbed = false; if (parentKey != null) { let parent = blockMap.get(parentKey); while (parent != null) { const children = parent.getChildKeys(); const firstChildKey = children.first(); invariant(firstChildKey != null, 'parent must have at least one child'); const firstChild = blockMap.get(firstChildKey); if (firstChild.getChildKeys().count() === 0) { break; } else { blockMap = DraftTreeOperations.moveChildUp(blockMap, firstChildKey); parent = blockMap.get(parentKey); childWasUntabbed = true; } } } // now, we may be in a state with two non-leaf blocks of the same type // next to each other if (childWasUntabbed && parentKey != null) { const parent = blockMap.get(parentKey); const prevSiblingKey = parent != null // parent may have been deleted ? parent.getPrevSiblingKey() : null; if (prevSiblingKey != null && parent.getChildKeys().count() > 0) { const prevSibling = blockMap.get(prevSiblingKey); if (prevSibling != null && prevSibling.getChildKeys().count() > 0) { blockMap = DraftTreeOperations.mergeBlocks(blockMap, prevSiblingKey); } } } return blockMap; };
onTab: ( event: SyntheticKeyboardEvent<>, editorState: EditorState, maxDepth: number, ): EditorState => { const selection = editorState.getSelection(); const key = selection.getAnchorKey(); if (key !== selection.getFocusKey()) { return editorState; } let content = editorState.getCurrentContent(); const block = content.getBlockForKey(key); const type = block.getType(); if (type !== 'unordered-list-item' && type !== 'ordered-list-item') { return editorState; } event.preventDefault(); const depth = block.getDepth(); if (!event.shiftKey && depth === maxDepth) { return editorState; } // implement nested tree behaviour for onTab let blockMap = editorState.getCurrentContent().getBlockMap(); const prevSiblingKey = block.getPrevSiblingKey(); const nextSiblingKey = block.getNextSiblingKey(); if (!event.shiftKey) { // if there is no previous sibling, we do nothing if (prevSiblingKey == null) { return editorState; } // if previous sibling is a non-leaf move node as child of previous sibling const prevSibling = blockMap.get(prevSiblingKey); const nextSibling = nextSiblingKey != null ? blockMap.get(nextSiblingKey) : null; const prevSiblingNonLeaf = prevSibling != null && prevSibling.getChildKeys().count() > 0; const nextSiblingNonLeaf = nextSibling != null && nextSibling.getChildKeys().count() > 0; if (prevSiblingNonLeaf) { blockMap = DraftTreeOperations.updateAsSiblingsChild( blockMap, key, 'previous', ); // if next sibling is also non-leaf, merge the previous & next siblings if (nextSiblingNonLeaf) { blockMap = DraftTreeOperations.mergeBlocks(blockMap, prevSiblingKey); } // else, if only next sibling is non-leaf move node as child of next sibling } else if (nextSiblingNonLeaf) { blockMap = DraftTreeOperations.updateAsSiblingsChild( blockMap, key, 'next', ); // if none of the siblings are non-leaf, we need to create a new parent } else { blockMap = DraftTreeOperations.createNewParent(blockMap, key); } // on un-tab } else { // if the block isn't nested, do nothing if (block.getParentKey() == null) { return editorState; } blockMap = onUntab(blockMap, block); } content = editorState.getCurrentContent().merge({ blockMap: blockMap, }); const withAdjustment = adjustBlockDepthForContentState( content, selection, event.shiftKey ? -1 : 1, maxDepth, ); return EditorState.push(editorState, withAdjustment, 'adjust-depth'); },