From 328ddc63525956e55027abb1a6a81d56e8fa5fae Mon Sep 17 00:00:00 2001 From: mitermayer Date: Mon, 6 Aug 2018 14:14:51 -0700 Subject: [PATCH] 1/n splitting PR #1828: Initial forking of RichTextEditorUtil (#1828) Summary: We are splitting up and updating pieces of this PR by Mitermayer. This chunk forks the RichTextEditorUtil and puts them both under the same Flow interface, also adding a test. The forked NestedRichTextEditorUtil is a WIP version of this utility. This is directly taken from mitermayer 's fork, props to him Pull Request resolved: https://github.com/facebook/draft-js/pull/1828 Reviewed By: mitermayer Differential Revision: D9181952 Pulled By: mitermayer fbshipit-source-id: 75f08fdb228ae1b45f97ae29aeae2f3d5ea53650 --- src/model/modifier/RichTextEditorUtil.js | 9 +- src/model/modifier/RichTextUtils.js | 67 + .../exploration/NestedRichTextEditorUtil.js | 423 +++ .../NestedRichTextEditorUtil-test.js | 374 +++ .../NestedRichTextEditorUtil-test.js.snap | 2576 +++++++++++++++++ 5 files changed, 3445 insertions(+), 4 deletions(-) create mode 100644 src/model/modifier/RichTextUtils.js create mode 100644 src/model/modifier/exploration/NestedRichTextEditorUtil.js create mode 100644 src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js create mode 100644 src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap diff --git a/src/model/modifier/RichTextEditorUtil.js b/src/model/modifier/RichTextEditorUtil.js index a47e5a3c6c..31420b11e7 100644 --- a/src/model/modifier/RichTextEditorUtil.js +++ b/src/model/modifier/RichTextEditorUtil.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @format - * @flow + * @flow strict-local */ 'use strict'; @@ -15,16 +15,17 @@ import type ContentState from 'ContentState'; import type {DraftBlockType} from 'DraftBlockType'; import type {DraftEditorCommand} from 'DraftEditorCommand'; +import type {DataObjectForLink, RichTextUtils} from 'RichTextUtils'; +import type SelectionState from 'SelectionState'; import type URI from 'URI'; const DraftModifier = require('DraftModifier'); const EditorState = require('EditorState'); -const SelectionState = require('SelectionState'); const adjustBlockDepthForContentState = require('adjustBlockDepthForContentState'); const nullthrows = require('nullthrows'); -const RichTextEditorUtil = { +const RichTextEditorUtil: RichTextUtils = { currentBlockContainsLink: function(editorState: EditorState): boolean { const selection = editorState.getSelection(); const contentState = editorState.getCurrentContent(); @@ -47,7 +48,7 @@ const RichTextEditorUtil = { .getType(); }, - getDataObjectForLinkURL: function(uri: URI): Object { + getDataObjectForLinkURL: function(uri: URI): DataObjectForLink { return {url: uri.toString()}; }, diff --git a/src/model/modifier/RichTextUtils.js b/src/model/modifier/RichTextUtils.js new file mode 100644 index 0000000000..1eb7577373 --- /dev/null +++ b/src/model/modifier/RichTextUtils.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @format + * @flow strict-local + */ + +import type ContentState from 'ContentState'; +import type {DraftBlockType} from 'DraftBlockType'; +import type {DraftEditorCommand} from 'DraftEditorCommand'; +import type EditorState from 'EditorState'; +import type SelectionState from 'SelectionState'; +import type URI from 'URI'; + +export type DataObjectForLink = { + url: string, +}; + +export type RichTextUtils = { + currentBlockContainsLink: (editorState: EditorState) => boolean, + + getCurrentBlockType: (editorState: EditorState) => DraftBlockType, + + getDataObjectForLinkURL: (uri: URI) => DataObjectForLink, + + handleKeyCommand: ( + editorState: EditorState, + command: DraftEditorCommand | string, + ) => ?EditorState, + + insertSoftNewline: (editorState: EditorState) => EditorState, + + onBackspace: (editorState: EditorState) => ?EditorState, + + onDelete: (editorState: EditorState) => ?EditorState, + + onTab: ( + event: SyntheticKeyboardEvent<>, + editorState: EditorState, + maxDepth: number, + ) => EditorState, + + toggleBlockType: ( + editorState: EditorState, + blockType: DraftBlockType, + ) => EditorState, + + toggleCode: (editorState: EditorState) => EditorState, + + toggleInlineStyle: ( + editorState: EditorState, + inlineStyle: string, + ) => EditorState, + + toggleLink: ( + editorState: EditorState, + targetSelection: SelectionState, + entityKey: ?string, + ) => EditorState, + + tryToRemoveBlockStyle: (editorState: EditorState) => ?ContentState, +}; diff --git a/src/model/modifier/exploration/NestedRichTextEditorUtil.js b/src/model/modifier/exploration/NestedRichTextEditorUtil.js new file mode 100644 index 0000000000..b40d1c23f2 --- /dev/null +++ b/src/model/modifier/exploration/NestedRichTextEditorUtil.js @@ -0,0 +1,423 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @format + * @flow strict-local + * + * This is unstable and not part of the public API and should not be used by + * production systems. This file may be update/removed without notice. + */ +import type ContentState from 'ContentState'; +import type {DraftBlockType} from 'DraftBlockType'; +import type {DraftEditorCommand} from 'DraftEditorCommand'; +import type {DataObjectForLink, RichTextUtils} from 'RichTextUtils'; +import type SelectionState from 'SelectionState'; +import type URI from 'URI'; + +const DraftModifier = require('DraftModifier'); +const EditorState = require('EditorState'); +const RichTextEditorUtil = require('RichTextEditorUtil'); + +const adjustBlockDepthForContentState = require('adjustBlockDepthForContentState'); + +// Eventually we could allow to control this list by either allowing user configuration +// and/or a schema in conjunction to DraftBlockRenderMap +const NESTING_DISABLED_TYPES = ['code-block', 'atomic']; + +const NestedRichTextEditorUtil: RichTextUtils = { + handleKeyCommand: ( + editorState: EditorState, + command: DraftEditorCommand | string, + ): ?EditorState => { + switch (command) { + case 'bold': + return NestedRichTextEditorUtil.toggleInlineStyle(editorState, 'BOLD'); + case 'italic': + return NestedRichTextEditorUtil.toggleInlineStyle( + editorState, + 'ITALIC', + ); + case 'underline': + return NestedRichTextEditorUtil.toggleInlineStyle( + editorState, + 'UNDERLINE', + ); + case 'code': + return NestedRichTextEditorUtil.toggleCode(editorState); + case 'backspace': + case 'backspace-word': + case 'backspace-to-start-of-line': + return NestedRichTextEditorUtil.onBackspace(editorState); + case 'delete': + case 'delete-word': + case 'delete-to-end-of-block': + return NestedRichTextEditorUtil.onDelete(editorState); + default: + // they may have custom editor commands; ignore those + return null; + } + }, + + onDelete: (editorState: EditorState): ?EditorState => { + const selection = editorState.getSelection(); + if (!selection.isCollapsed()) { + return null; + } + + const content = editorState.getCurrentContent(); + const startKey = selection.getStartKey(); + const block = content.getBlockForKey(startKey); + const length = block.getLength(); + + // The cursor is somewhere within the text. Behave normally. + if (selection.getStartOffset() < length) { + return null; + } + + const blockAfter = content.getBlockAfter(startKey); + + if (!blockAfter || blockAfter.getType() !== 'atomic') { + return null; + } + + const atomicBlockTarget = selection.merge({ + focusKey: blockAfter.getKey(), + focusOffset: blockAfter.getLength(), + }); + + const withoutAtomicBlock = DraftModifier.removeRange( + content, + atomicBlockTarget, + 'forward', + ); + + if (withoutAtomicBlock !== content) { + return EditorState.push(editorState, withoutAtomicBlock, 'remove-range'); + } + + return null; + }, + + /** + * Ensures that if on the beginning of unstyled block and first child of + * a nested parent we add its text to the neareast previous leaf node + */ + onBackspace: (editorState: EditorState): ?EditorState => { + const selection = editorState.getSelection(); + const content = editorState.getCurrentContent(); + const currentBlock = content.getBlockForKey(selection.getStartKey()); + const previousBlockKey = currentBlock.getPrevSiblingKey(); + + if ( + !selection.isCollapsed() || + selection.getAnchorOffset() || + selection.getFocusOffset() || + (currentBlock.getType() === 'unstyled' && + (previousBlockKey && + content.getBlockForKey(previousBlockKey).getType() !== 'atomic')) + ) { + return null; + } + + const startKey = selection.getStartKey(); + const blockBefore = content.getBlockBefore(startKey); + + // we want to delete that block completely + if (blockBefore && blockBefore.getType() === 'atomic') { + const withoutAtomicBlock = DraftModifier.removeRange( + content, + selection.merge({ + focusKey: blockBefore.getKey(), + focusOffset: blockBefore.getText().length, + anchorKey: startKey, + anchorOffset: content.getBlockForKey(startKey).getText().length, + isBackward: false, + }), + 'forward', + ).merge({ + selectionAfter: selection, + }); + + if (withoutAtomicBlock !== content) { + return EditorState.push( + editorState, + withoutAtomicBlock, + 'remove-range', + ); + } + } + + // if we have a next sibbling we should not allow the normal backspace + // behaviour of moving this text into its parent + // if (currentBlock.getPrevSiblingKey()) { + // return editorState; + // } + + // If that doesn't succeed, try to remove the current block style. + const withoutBlockStyle = NestedRichTextEditorUtil.tryToRemoveBlockStyle( + editorState, + ); + + if (withoutBlockStyle) { + return EditorState.push( + editorState, + withoutBlockStyle, + 'change-block-type', + ); + } + + return null; + }, + + // Todo (T32099101) + // onSplitNestedBlock() {}, + + // Todo (T32099101) + // onSplitParent() {}, + + /** + * Ensures that we can create nested blocks by changing the block type of + * a ranged selection + */ + toggleBlockType: ( + editorState: EditorState, + blockType: DraftBlockType, + ): EditorState => { + const selection = editorState.getSelection(); + const content = editorState.getCurrentContent(); + const currentBlock = content.getBlockForKey(selection.getStartKey()); + const haveChildren = !currentBlock.getChildKeys().isEmpty(); + const isSelectionCollapsed = selection.isCollapsed(); + const isMultiBlockSelection = + selection.getAnchorKey() !== selection.getFocusKey(); + const isUnsupportedNestingBlockType = NESTING_DISABLED_TYPES.includes( + blockType, + ); + const isCurrentBlockOfUnsupportedNestingBlockType = NESTING_DISABLED_TYPES.includes( + currentBlock.getType(), + ); + + // we don't allow this operations to avoid corrupting the document data model + // to make sure that non nested blockTypes wont inherit children + if ( + (isMultiBlockSelection || haveChildren) && + isUnsupportedNestingBlockType + ) { + return editorState; + } + + // we can treat this operations the same way as we would for flat data structures + if ( + isCurrentBlockOfUnsupportedNestingBlockType || + isSelectionCollapsed || + isUnsupportedNestingBlockType || + isMultiBlockSelection || + currentBlock.getType() === blockType || + !currentBlock.getChildKeys().isEmpty() + ) { + return RichTextEditorUtil.toggleBlockType(editorState, blockType); + } + + // TODO + // if we have full range selection on the block: + // extract text and insert a block after it with the text as its content + // else + // split the block into before range and after unstyled blocks + // + return editorState; + }, + + currentBlockContainsLink: (editorState: EditorState): boolean => { + const selection = editorState.getSelection(); + const contentState = editorState.getCurrentContent(); + const entityMap = contentState.getEntityMap(); + return contentState + .getBlockForKey(selection.getAnchorKey()) + .getCharacterList() + .slice(selection.getStartOffset(), selection.getEndOffset()) + .some(v => { + const entity = v.getEntity(); + return !!entity && entityMap.__get(entity).getType() === 'LINK'; + }); + }, + + getCurrentBlockType: (editorState: EditorState): DraftBlockType => { + const selection = editorState.getSelection(); + return editorState + .getCurrentContent() + .getBlockForKey(selection.getStartKey()) + .getType(); + }, + + getDataObjectForLinkURL: (uri: URI): DataObjectForLink => { + return {url: uri.toString()}; + }, + + insertSoftNewline: (editorState: EditorState): EditorState => { + const contentState = DraftModifier.insertText( + editorState.getCurrentContent(), + editorState.getSelection(), + '\n', + editorState.getCurrentInlineStyle(), + null, + ); + + const newEditorState = EditorState.push( + editorState, + contentState, + 'insert-characters', + ); + + return EditorState.forceSelection( + newEditorState, + contentState.getSelectionAfter(), + ); + }, + + onTab: ( + event: SyntheticKeyboardEvent<>, + editorState: EditorState, + maxDepth: number, + ): EditorState => { + const selection = editorState.getSelection(); + const key = selection.getAnchorKey(); + if (key !== selection.getFocusKey()) { + return editorState; + } + + const 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; + } + + const withAdjustment = adjustBlockDepthForContentState( + content, + selection, + event.shiftKey ? -1 : 1, + maxDepth, + ); + + return EditorState.push(editorState, withAdjustment, 'adjust-depth'); + }, + + toggleCode: (editorState: EditorState): EditorState => { + const selection = editorState.getSelection(); + const anchorKey = selection.getAnchorKey(); + const focusKey = selection.getFocusKey(); + + if (selection.isCollapsed() || anchorKey !== focusKey) { + return RichTextEditorUtil.toggleBlockType(editorState, 'code-block'); + } + + return RichTextEditorUtil.toggleInlineStyle(editorState, 'CODE'); + }, + + /** + * Toggle the specified inline style for the selection. If the + * user's selection is collapsed, apply or remove the style for the + * internal state. If it is not collapsed, apply the change directly + * to the document state. + */ + toggleInlineStyle: ( + editorState: EditorState, + inlineStyle: string, + ): EditorState => { + const selection = editorState.getSelection(); + const currentStyle = editorState.getCurrentInlineStyle(); + + // If the selection is collapsed, toggle the specified style on or off and + // set the result as the new inline style override. This will then be + // used as the inline style for the next character to be inserted. + if (selection.isCollapsed()) { + return EditorState.setInlineStyleOverride( + editorState, + currentStyle.has(inlineStyle) + ? currentStyle.remove(inlineStyle) + : currentStyle.add(inlineStyle), + ); + } + + // If characters are selected, immediately apply or remove the + // inline style on the document state itself. + const content = editorState.getCurrentContent(); + let newContent; + + // If the style is already present for the selection range, remove it. + // Otherwise, apply it. + if (currentStyle.has(inlineStyle)) { + newContent = DraftModifier.removeInlineStyle( + content, + selection, + inlineStyle, + ); + } else { + newContent = DraftModifier.applyInlineStyle( + content, + selection, + inlineStyle, + ); + } + + return EditorState.push(editorState, newContent, 'change-inline-style'); + }, + + toggleLink: ( + editorState: EditorState, + targetSelection: SelectionState, + entityKey: ?string, + ): EditorState => { + const withoutLink = DraftModifier.applyEntity( + editorState.getCurrentContent(), + targetSelection, + entityKey, + ); + + return EditorState.push(editorState, withoutLink, 'apply-entity'); + }, + + /** + * When a collapsed cursor is at the start of a styled block, changes block + * type to 'unstyled'. Returns null if selection does not meet that criteria. + */ + tryToRemoveBlockStyle: (editorState: EditorState): ?ContentState => { + const selection = editorState.getSelection(); + const offset = selection.getAnchorOffset(); + if (selection.isCollapsed() && offset === 0) { + const key = selection.getAnchorKey(); + const content = editorState.getCurrentContent(); + const block = content.getBlockForKey(key); + + const type = block.getType(); + const blockBefore = content.getBlockBefore(key); + if ( + type === 'code-block' && + blockBefore && + blockBefore.getType() === 'code-block' && + blockBefore.getLength() !== 0 + ) { + return null; + } + + if (type !== 'unstyled') { + return DraftModifier.setBlockType(content, selection, 'unstyled'); + } + } + return null; + }, +}; + +module.exports = NestedRichTextEditorUtil; diff --git a/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js b/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js new file mode 100644 index 0000000000..3793de35f2 --- /dev/null +++ b/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js @@ -0,0 +1,374 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails oncall+ui_infra + * @format + */ + +jest.disableAutomock(); + +jest.mock('generateRandomKey'); + +const AtomicBlockUtils = require('AtomicBlockUtils'); +const BlockMapBuilder = require('BlockMapBuilder'); +const ContentBlockNode = require('ContentBlockNode'); +const EditorState = require('EditorState'); +const NestedRichTextEditorUtil = require('NestedRichTextEditorUtil'); +const SelectionState = require('SelectionState'); + +const getSampleStateForTesting = require('getSampleStateForTesting'); +const Immutable = require('immutable'); + +const {List} = Immutable; + +const {editorState, contentState, selectionState} = getSampleStateForTesting(); +const {onBackspace, onDelete} = NestedRichTextEditorUtil; + +const contentBlockNodes = [ + new ContentBlockNode({ + key: 'A', + nextSibling: 'B', + text: 'Alpha', + type: 'blockquote', + }), + new ContentBlockNode({ + key: 'B', + prevSibling: 'A', + nextSibling: 'G', + type: 'ordered-list-item', + children: List(['C', 'F']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + nextSibling: 'F', + type: 'blockquote', + children: List(['D', 'E']), + }), + new ContentBlockNode({ + parent: 'C', + key: 'D', + nextSibling: 'E', + type: 'header-two', + text: 'Delta', + }), + new ContentBlockNode({ + parent: 'C', + key: 'E', + prevSibling: 'D', + type: 'unstyled', + text: 'Elephant', + }), + new ContentBlockNode({ + parent: 'B', + key: 'F', + prevSibling: 'C', + type: 'code-block', + text: 'Fire', + }), + new ContentBlockNode({ + key: 'G', + prevSibling: 'B', + nextSibling: 'H', + type: 'ordered-list-item', + text: 'Gorila', + }), + new ContentBlockNode({ + key: 'H', + prevSibling: 'G', + nextSibling: 'I', + text: ' ', + type: 'atomic', + }), + new ContentBlockNode({ + key: 'I', + prevSibling: 'H', + text: 'last', + type: 'unstyled', + }), +]; + +const toggleExperimentalTreeDataSupport = enabled => { + jest.doMock('gkx', () => name => { + return name === 'draft_tree_data_support' ? enabled : false; + }); +}; + +const insertAtomicBlock = targetEditorState => { + const entityKey = targetEditorState + .getCurrentContent() + .createEntity('TEST', 'IMMUTABLE', null) + .getLastCreatedEntityKey(); + const character = ' '; + const movedSelection = EditorState.moveSelectionToEnd(targetEditorState); + return AtomicBlockUtils.insertAtomicBlock( + movedSelection, + entityKey, + character, + ); +}; + +const assertNestedUtilOperation = ( + operation, + selection = {}, + content = contentBlockNodes, +) => { + const result = operation( + EditorState.forceSelection( + EditorState.createWithContent( + contentState.set('blockMap', BlockMapBuilder.createFromArray(content)), + ), + SelectionState.createEmpty(content[0].key).merge(selection), + ), + ); + + const expected = + result instanceof EditorState + ? result + .getCurrentContent() + .getBlockMap() + .toJS() + : result; + + expect(expected).toMatchSnapshot(); +}; + +toggleExperimentalTreeDataSupport(true); + +test(`toggleBlockType does not handle nesting when selection is collapsed`, () => { + assertNestedUtilOperation(editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'header-one'), + ); +}); + +test(`toggleBlockType does not handle nesting for multi block selection`, () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'header-one'), + { + anchorKey: 'D', + focusKey: 'E', + focusOffset: contentBlockNodes[4].getLength(), + }, + ); +}); + +test(`toggleBlockType does not allow block type change for multi block selection to unsupported type`, () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'code-block'), + { + anchorKey: 'D', + focusKey: 'E', + focusOffset: contentBlockNodes[4].getLength(), + }, + ); +}); + +test(`toggleBlockType does not handle nesting for blockType: "atomic"`, () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'header-one'), + { + anchorKey: 'H', + focusKey: 'H', + focusOffset: contentBlockNodes[7].getLength(), + }, + ); +}); + +test(`toggleBlockType does not handle nesting for blockType: "code-block"`, () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'header-two'), + { + anchorKey: 'F', + focusKey: 'F', + focusOffset: contentBlockNodes[5].getLength(), + }, + ); +}); + +test(`toggleBlockType does not handle nesting for block that has children`, () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'header-one'), + { + anchorKey: 'C', + focusKey: 'C', + focusOffset: contentBlockNodes[2].getLength(), + }, + ); +}); + +test(`toggleBlockType does not allow block type change for block that has children to unsupported type`, () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'code-block'), + { + anchorKey: 'C', + focusKey: 'C', + focusOffset: contentBlockNodes[2].getLength(), + }, + ); +}); + +/** + * Example: + * + * Having the cursor on the H1 and trying to change blocktype to unordered-list + * it should not update h1 instead it should udate its parent block type + * + * blockquote > foo + * should not become + * blockquote > blockquote > foo + */ +test('toggleBlockType does not handle nesting enabled blocks with same blockType', () => { + assertNestedUtilOperation( + editorState => + NestedRichTextEditorUtil.toggleBlockType(editorState, 'blockquote'), + { + anchorKey: 'A', + focusKey: 'A', + focusOffset: contentBlockNodes[0].getLength(), + }, + ); +}); + +/** + * Example: + * + * Having the cursor on the H1 and trying to change blocktype to unordered-list + * it should not update h1 instead it should udate its parent block type + * + * ordered-list > h1 + * should become + * unordered-list > h1 + */ +test('toggleBlockType should change parent block type when changing type for same tag element', () => { + expect(true).toBe(true); +}); + +/** + * Example: + * + * Changing the block type inside a nested enable block that has text should + * transfer it's text to a nested unstyled block example + * + * blockquote > ordered-list-item + * should become + * blockquote > ordered-list-item > unstyled + */ +test('toggleBlockType with ranged selection should retain parent type and create a new nested block with text from parent', () => { + expect(true).toBe(true); +}); + +test('onBackspace does not handle non-zero-offset selections', () => { + assertNestedUtilOperation(editorState => onBackspace(editorState), { + anchorKey: 'F', + focusKey: 'F', + anchorOffset: 2, + focusOffset: 2, + }); +}); + +test('onBackspace does not handle non-collapsed selections', () => { + assertNestedUtilOperation(editorState => onBackspace(editorState), { + anchorKey: 'F', + focusKey: 'F', + focusOffset: 2, + }); +}); + +test('onBackspace resets the current block type if empty', () => { + assertNestedUtilOperation(editorState => onBackspace(editorState), { + anchorKey: 'F', + focusKey: 'F', + }); +}); + +test('onBackspace removes a preceding atomic block', () => { + assertNestedUtilOperation(editorState => onBackspace(editorState), { + focusKey: 'I', + anchorKey: 'I', + }); +}); + +test('onBackspace on the start of a leaf unstyled block should remove block and merge text to previous leaf', () => { + expect(true).toBe(true); +}); + +test('onDelete does not handle if it is the last block on the blockMap', () => { + expect(true).toBe(true); +}); + +test('onDelete does not handle if the next block has no children', () => { + expect(true).toBe(true); +}); + +test('onDelete on the end of a leaf block should remove block and merge text to previous leaf', () => { + expect(true).toBe(true); +}); + +test('onSplitParent must split a nested block retaining parent', () => { + expect(true).toBe(true); +}); + +/** + * => Note: + * + * The bellow tests are a port from RichTextEditorUtil-test.js to ensure that + * NestedRichTextEditorUtil can provide the same guarantees as its flat counterpart. + */ +test('onDelete does not handle non-block-end or non-collapsed selections', () => { + const nonZero = selectionState.merge({anchorOffset: 2, focusOffset: 2}); + expect( + onDelete(EditorState.forceSelection(editorState, nonZero)) === null, + ).toMatchSnapshot(); + + const nonCollapsed = nonZero.merge({anchorOffset: 0}); + expect( + onDelete(EditorState.forceSelection(editorState, nonCollapsed)) === null, + ).toMatchSnapshot(); +}); + +test('onDelete removes a following atomic block', () => { + const blockSizeBeforeRemove = editorState.getCurrentContent().getBlockMap() + .size; + const withAtomicBlock = insertAtomicBlock(editorState); + const content = withAtomicBlock.getCurrentContent(); + const atomicKey = content + .getBlockMap() + .find(block => block.getType() === 'atomic') + .getKey(); + + const blockBefore = content.getBlockBefore(atomicKey); + const keyBefore = blockBefore.getKey(); + const lengthBefore = blockBefore.getLength(); + + const withSelectionAboveAtomic = EditorState.forceSelection( + withAtomicBlock, + new SelectionState({ + anchorKey: keyBefore, + anchorOffset: lengthBefore, + focusKey: keyBefore, + focusOffset: lengthBefore, + }), + ); + + const afterDelete = onDelete(withSelectionAboveAtomic); + const blockMapAfterDelete = afterDelete.getCurrentContent().getBlockMap(); + + expect( + blockMapAfterDelete.some(block => block.getType() === 'atomic'), + ).toMatchSnapshot(); + + expect( + blockMapAfterDelete.size === blockSizeBeforeRemove + 1, + ).toMatchSnapshot(); +}); diff --git a/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap b/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap new file mode 100644 index 0000000000..4df14ec433 --- /dev/null +++ b/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap @@ -0,0 +1,2576 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`onBackspace does not handle non-collapsed selections 1`] = `null`; + +exports[`onBackspace does not handle non-zero-offset selections 1`] = `null`; + +exports[`onBackspace removes a preceding atomic block 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "I", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "G", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`onBackspace resets the current block type if empty 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "unstyled", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`onDelete does not handle non-block-end or non-collapsed selections 1`] = `true`; + +exports[`onDelete does not handle non-block-end or non-collapsed selections 2`] = `true`; + +exports[`onDelete removes a following atomic block 1`] = `false`; + +exports[`onDelete removes a following atomic block 2`] = `true`; + +exports[`toggleBlockType does not allow block type change for block that has children to unsupported type 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not allow block type change for multi block selection to unsupported type 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not handle nesting enabled blocks with same blockType 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "unstyled", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not handle nesting for block that has children 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "header-one", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not handle nesting for blockType: "atomic" 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not handle nesting for blockType: "code-block" 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "header-two", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not handle nesting for multi block selection 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-one", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "header-one", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`toggleBlockType does not handle nesting when selection is collapsed 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "header-one", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`;