diff --git a/src/model/modifier/exploration/DraftTreeOperations.js b/src/model/modifier/exploration/DraftTreeOperations.js new file mode 100644 index 0000000000..559261a414 --- /dev/null +++ b/src/model/modifier/exploration/DraftTreeOperations.js @@ -0,0 +1,114 @@ +/** + * 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 {BlockMap} from 'BlockMap'; + +const invariant = require('invariant'); + +const DraftTreeOperations = { + /** + * This is a utility method for setting B as a first/last child of A, ensuring + * that parent <-> child operations are correctly mirrored + * + * The block map returned by this method may not be a valid tree (siblings are + * unaffected) + */ + updateParentChild( + blockMap: BlockMap, + parentKey: string, + childKey: string, + position: 'first' | 'last', + ): BlockMap { + const parent = blockMap.get(parentKey); + const child = blockMap.get(childKey); + invariant( + parent != null && child != null, + 'parent & child should exist in the block map', + ); + const existingChildren = parent.getChildKeys(); + const newBlocks = {}; + // add as parent's child + newBlocks[parentKey] = parent.merge({ + children: + position === 'first' + ? existingChildren.unshift(childKey) + : existingChildren.push(childKey), + }); + // add as child's parent + if (existingChildren.count() !== 0) { + // link child as sibling to the existing children + switch (position) { + case 'first': + const nextSiblingKey = existingChildren.first(); + newBlocks[childKey] = child.merge({ + parent: parentKey, + nextSibling: nextSiblingKey, + prevSibling: null, + }); + newBlocks[nextSiblingKey] = blockMap.get(nextSiblingKey).merge({ + prevSibling: childKey, + }); + break; + case 'last': + const prevSiblingKey = existingChildren.last(); + newBlocks[childKey] = child.merge({ + parent: parentKey, + prevSibling: prevSiblingKey, + nextSibling: null, + }); + newBlocks[prevSiblingKey] = blockMap.get(prevSiblingKey).merge({ + nextSibling: childKey, + }); + break; + } + } else { + newBlocks[childKey] = child.merge({ + parent: parentKey, + prevSibling: null, + nextSibling: null, + }); + } + return blockMap.merge(newBlocks); + }, + + /** + * This is a utility method for setting B as the next sibling of A, ensuring + * that sibling operations are correctly mirrored + * + * The block map returned by this method may not be a valid tree (parent/child/ + * other siblings are unaffected) + */ + updateSibling( + blockMap: BlockMap, + prevKey: string, + nextKey: string, + ): BlockMap { + const prevSibling = blockMap.get(prevKey); + const nextSibling = blockMap.get(nextKey); + invariant( + prevSibling != null && nextSibling != null, + 'siblings should exist in the block map', + ); + const newBlocks = {}; + newBlocks[prevKey] = prevSibling.merge({ + nextSibling: nextKey, + }); + newBlocks[nextKey] = nextSibling.merge({ + prevSibling: prevKey, + }); + return blockMap.merge(newBlocks); + }, +}; + +module.exports = DraftTreeOperations; diff --git a/src/model/modifier/exploration/__tests__/DraftTreeOperations-test.js b/src/model/modifier/exploration/__tests__/DraftTreeOperations-test.js new file mode 100644 index 0000000000..8d2a2548bc --- /dev/null +++ b/src/model/modifier/exploration/__tests__/DraftTreeOperations-test.js @@ -0,0 +1,159 @@ +/** + * 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 + * @flow strict-local + * @format + */ + +'use strict'; + +jest.disableAutomock(); + +const ContentBlockNode = require('ContentBlockNode'); +const DraftTreeOperations = require('DraftTreeOperations'); + +const Immutable = require('immutable'); +const blockMap1 = Immutable.OrderedMap({ + A: new ContentBlockNode({ + key: 'A', + parent: null, + text: 'alpha', + children: Immutable.List([]), + prevSibling: null, + nextSibling: 'X', + }), + X: new ContentBlockNode({ + key: 'X', + parent: null, + text: '', + children: Immutable.List(['B', 'C']), + prevSibling: 'A', + nextSibling: 'D', + }), + B: new ContentBlockNode({ + key: 'B', + parent: 'X', + text: 'beta', + children: Immutable.List([]), + prevSibling: null, + nextSibling: 'C', + }), + C: new ContentBlockNode({ + key: 'C', + parent: 'X', + text: 'charlie', + children: Immutable.List([]), + prevSibling: 'B', + nextSibling: null, + }), + D: new ContentBlockNode({ + key: 'D', + parent: null, + text: 'delta', + children: Immutable.List([]), + prevSibling: 'X', + nextSibling: null, + }), +}); + +test('test adding a last child to parent', () => { + let newBlockMap = DraftTreeOperations.updateParentChild( + blockMap1, + 'X', + 'D', + 'last', + ); + newBlockMap = newBlockMap.merge({ + X: newBlockMap.get('X').merge({ + nextSibling: null, + }), + }); + expect(newBlockMap).toMatchSnapshot(); +}); + +test('test adding a first child to parent', () => { + let newBlockMap = DraftTreeOperations.updateParentChild( + blockMap1, + 'X', + 'D', + 'first', + ); + newBlockMap = newBlockMap.merge({ + X: newBlockMap.get('X').merge({ + nextSibling: null, + }), + }); + expect(newBlockMap).toMatchSnapshot(); +}); + +test('test adding a sibling', () => { + let newBlockMap = DraftTreeOperations.updateSibling(blockMap1, 'D', 'C'); + newBlockMap = newBlockMap.merge({ + B: newBlockMap.get('B').merge({ + nextSibling: null, + }), + C: newBlockMap.get('C').merge({ + parent: null, + }), + X: newBlockMap.get('X').merge({ + children: ['B'], + }), + }); + expect(newBlockMap).toMatchSnapshot(); +}); + +const blockMap2 = Immutable.OrderedMap({ + A: new ContentBlockNode({ + key: 'A', + parent: null, + text: 'alpha', + children: Immutable.List([]), + prevSibling: null, + nextSibling: 'X', + }), + X: new ContentBlockNode({ + key: 'X', + parent: null, + text: '', + children: Immutable.List([]), + prevSibling: 'A', + nextSibling: 'B', + }), + B: new ContentBlockNode({ + key: 'B', + parent: null, + text: 'beta', + children: Immutable.List([]), + prevSibling: 'X', + nextSibling: 'C', + }), + C: new ContentBlockNode({ + key: 'C', + parent: null, + text: 'charlie', + children: Immutable.List([]), + prevSibling: 'B', + nextSibling: null, + }), +}); + +test('test adding an only child to parent', () => { + let newBlockMap = DraftTreeOperations.updateParentChild( + blockMap2, + 'X', + 'C', + 'first', + ); + newBlockMap = newBlockMap.merge({ + B: newBlockMap.get('B').merge({ + nextSibling: null, + }), + }); + expect(newBlockMap).toMatchSnapshot(); +}); diff --git a/src/model/modifier/exploration/__tests__/__snapshots__/DraftTreeOperations-test.js.snap b/src/model/modifier/exploration/__tests__/__snapshots__/DraftTreeOperations-test.js.snap new file mode 100644 index 0000000000..635a1524b8 --- /dev/null +++ b/src/model/modifier/exploration/__tests__/__snapshots__/DraftTreeOperations-test.js.snap @@ -0,0 +1,592 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test adding a first child to parent 1`] = ` +Immutable.OrderedMap { + "A": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "A", + "text": "alpha", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": "X", + }, + "X": Immutable.Record { + "parent": null, + "characterList": Immutable.List [], + "data": Immutable.Map {}, + "depth": 0, + "key": "X", + "text": "", + "type": "unstyled", + "children": Immutable.List [ + "D", + "B", + "C", + ], + "prevSibling": "A", + "nextSibling": null, + }, + "B": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "B", + "text": "beta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "D", + "nextSibling": "C", + }, + "C": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "C", + "text": "charlie", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "B", + "nextSibling": null, + }, + "D": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "D", + "text": "delta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": "B", + }, +} +`; + +exports[`test adding a last child to parent 1`] = ` +Immutable.OrderedMap { + "A": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "A", + "text": "alpha", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": "X", + }, + "X": Immutable.Record { + "parent": null, + "characterList": Immutable.List [], + "data": Immutable.Map {}, + "depth": 0, + "key": "X", + "text": "", + "type": "unstyled", + "children": Immutable.List [ + "B", + "C", + "D", + ], + "prevSibling": "A", + "nextSibling": null, + }, + "B": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "B", + "text": "beta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": "C", + }, + "C": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "C", + "text": "charlie", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "B", + "nextSibling": "D", + }, + "D": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "D", + "text": "delta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "C", + "nextSibling": null, + }, +} +`; + +exports[`test adding a sibling 1`] = ` +Immutable.OrderedMap { + "A": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "A", + "text": "alpha", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": "X", + }, + "X": Immutable.Record { + "parent": null, + "characterList": Immutable.List [], + "data": Immutable.Map {}, + "depth": 0, + "key": "X", + "text": "", + "type": "unstyled", + "children": Immutable.List [ + "B", + ], + "prevSibling": "A", + "nextSibling": "D", + }, + "B": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "B", + "text": "beta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": null, + }, + "C": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "C", + "text": "charlie", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "D", + "nextSibling": null, + }, + "D": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "D", + "text": "delta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "X", + "nextSibling": "C", + }, +} +`; + +exports[`test adding an only child to parent 1`] = ` +Immutable.OrderedMap { + "A": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "A", + "text": "alpha", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": "X", + }, + "X": Immutable.Record { + "parent": null, + "characterList": Immutable.List [], + "data": Immutable.Map {}, + "depth": 0, + "key": "X", + "text": "", + "type": "unstyled", + "children": Immutable.List [ + "C", + ], + "prevSibling": "A", + "nextSibling": "B", + }, + "B": Immutable.Record { + "parent": null, + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "B", + "text": "beta", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": "X", + "nextSibling": null, + }, + "C": Immutable.Record { + "parent": "X", + "characterList": Immutable.List [ + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + Immutable.Record { + "style": Immutable.OrderedSet [], + "entity": null, + }, + ], + "data": Immutable.Map {}, + "depth": 0, + "key": "C", + "text": "charlie", + "type": "unstyled", + "children": Immutable.List [], + "prevSibling": null, + "nextSibling": null, + }, +} +`;