Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Commit

Permalink
Add utilty methods for creating a new parent & updating node to becom…
Browse files Browse the repository at this point in the history
…e sibling's child

Summary:
Implement some tree operations that will be used in the `NestedRichTextEditorUtil onTab` method.

Specifically:
- Creating a new parent for a node
- Updating the node so that it is a child of its previous or next sibling

The `onTab` use case is sketched out in the diagram below. The red stars represent the `createNewParent` operation & the red squares represent the `updateAsSiblingsChild` operation.

{F137497472}

(also added a new utility method `replaceParentChild` that switches one child for another & is used in createNewParent)

Reviewed By: mitermayer

Differential Revision: D9624285

fbshipit-source-id: f5701373ce1734cb8f21aca154ed45d656a47c54
  • Loading branch information
niveditc authored and facebook-github-bot committed Sep 4, 2018
1 parent e2c24cf commit 6f73657
Show file tree
Hide file tree
Showing 3 changed files with 1,700 additions and 91 deletions.
371 changes: 280 additions & 91 deletions src/model/modifier/exploration/DraftTreeOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,101 +15,290 @@
*/
import type {BlockMap} from 'BlockMap';

const ContentBlockNode = require('ContentBlockNode');
const DraftTreeInvariants = require('DraftTreeInvariants');

const generateRandomKey = require('generateRandomKey');
const Immutable = require('immutable');
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,
});
type SiblingInsertPosition = 'previous' | 'next';
type ChildInsertPosition = 'first' | 'last';

const verifyTree = (tree: BlockMap): void => {
if (__DEV__) {
invariant(DraftTreeInvariants.isValidTree(tree), 'The tree is not valid');
}
};

/**
* 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)
*/
const updateParentChild = (
blockMap: BlockMap,
parentKey: string,
childKey: string,
position: ChildInsertPosition,
): 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;
}
return blockMap.merge(newBlocks);
},
} 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',
/**
* 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)
*/
const 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);
};

/**
* This is a utility method for replacing B by C as a child of A, ensuring
* that parent <-> child connections between A & C are correctly mirrored
*
* The block map returned by this method may not be a valid tree (siblings are
* unaffected)
*/
const replaceParentChild = (
blockMap: BlockMap,
parentKey: string,
existingChildKey: string,
newChildKey: string,
): BlockMap => {
const parent = blockMap.get(parentKey);
const newChild = blockMap.get(newChildKey);
invariant(
parent != null && newChild != null,
'parent & child should exist in the block map',
);
const existingChildren = parent.getChildKeys();
const newBlocks = {};
newBlocks[parentKey] = parent.merge({
children: existingChildren.set(
existingChildren.indexOf(existingChildKey),
newChildKey,
),
});
newBlocks[newChildKey] = newChild.merge({
parent: parentKey,
});
return blockMap.merge(newBlocks);
};

/**
* This is a utility method that abstracts the operation of creating a new parent
* for a particular node in the block map.
*
* This operation respects the tree data invariants - it expects and returns a
* valid tree.
*/
const createNewParent = (blockMap: BlockMap, key: string): BlockMap => {
verifyTree(blockMap);
const block = blockMap.get(key);
invariant(block != null, 'block must exist in block map');
const newParent = new ContentBlockNode({
key: generateRandomKey(),
text: '',
depth: block.depth,
type: block.type,
children: Immutable.List([]),
});
// add the parent just before the child in the block map
let newBlockMap = blockMap
.takeUntil(block => block.getKey() === key)
.concat(Immutable.OrderedMap([[newParent.getKey(), newParent]]))
.concat(blockMap.skipUntil(block => block.getKey() === key));
// set parent <-> child connection
newBlockMap = updateParentChild(
newBlockMap,
newParent.getKey(),
key,
'first',
);
// set siblings & parent for the new parent key to child's siblings & parent
const prevSibling = block.getPrevSiblingKey();
const nextSibling = block.getNextSiblingKey();
const parent = block.getParentKey();
if (prevSibling != null) {
newBlockMap = updateSibling(newBlockMap, prevSibling, newParent.getKey());
}
if (nextSibling != null) {
newBlockMap = updateSibling(newBlockMap, newParent.getKey(), nextSibling);
}
if (parent != null) {
newBlockMap = replaceParentChild(
newBlockMap,
parent,
key,
newParent.getKey(),
);
const newBlocks = {};
newBlocks[prevKey] = prevSibling.merge({
nextSibling: nextKey,
});
newBlocks[nextKey] = nextSibling.merge({
prevSibling: prevKey,
});
return blockMap.merge(newBlocks);
},
}
verifyTree(newBlockMap);
return newBlockMap;
};

/**
* This is a utility method that abstracts the operation of adding a node as the child
* of its previous or next sibling.
*
* The previous (or next) sibling must be a valid parent node.
*
* This operation respects the tree data invariants - it expects and returns a
* valid tree.
*/
const updateAsSiblingsChild = (
blockMap: BlockMap,
key: string,
position: SiblingInsertPosition,
): BlockMap => {
verifyTree(blockMap);
const block = blockMap.get(key);
invariant(block != null, 'block must exist in block map');
const newParentKey =
position === 'previous'
? block.getPrevSiblingKey()
: block.getNextSiblingKey();
invariant(newParentKey != null, 'sibling is null');
const newParent = blockMap.get(newParentKey);
invariant(
newParent !== null && newParent.getText() === '',
'parent must be a valid node',
);
let newBlockMap = blockMap;
switch (position) {
case 'next':
newBlockMap = updateParentChild(newBlockMap, newParentKey, key, 'first');
const prevSibling = block.getPrevSiblingKey();
if (prevSibling != null) {
newBlockMap = updateSibling(newBlockMap, prevSibling, newParentKey);
} else {
newBlockMap = newBlockMap.set(
newParentKey,
newBlockMap.get(newParentKey).merge({prevSibling: null}),
);
}
// we also need to flip the order of the sibling & block in the ordered map
// for this case
newBlockMap = newBlockMap
.takeUntil(block => block.getKey() === key)
.concat(
Immutable.OrderedMap([
[newParentKey, newBlockMap.get(newParentKey)],
[key, newBlockMap.get(key)],
]),
)
.concat(
newBlockMap
.skipUntil(block => block.getKey() === newParentKey)
.slice(1),
);
break;
case 'previous':
newBlockMap = updateParentChild(newBlockMap, newParentKey, key, 'last');
const nextSibling = block.getNextSiblingKey();
if (nextSibling != null) {
newBlockMap = updateSibling(newBlockMap, newParentKey, nextSibling);
} else {
newBlockMap = newBlockMap.set(
newParentKey,
newBlockMap.get(newParentKey).merge({nextSibling: null}),
);
}
break;
}
// remove the node as a child of its current parent
const parentKey = block.getParentKey();
if (parentKey != null) {
const parent = newBlockMap.get(parentKey);
newBlockMap = newBlockMap.set(
parentKey,
parent.merge({
children: parent
.getChildKeys()
.delete(parent.getChildKeys().indexOf(key)),
}),
);
}
verifyTree(newBlockMap);
return newBlockMap;
};

module.exports = DraftTreeOperations;
module.exports = {
updateParentChild,
replaceParentChild,
updateSibling,
createNewParent,
updateAsSiblingsChild,
};
Loading

0 comments on commit 6f73657

Please sign in to comment.