This repository has been archived by the owner on Feb 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add utility methods for parent-child & sibling pointer updates
Summary: When the tree is updated, we need to make sure that we correctly mirror the parent-child & prevSibling-nextSibling links. The utility methods in this diff help make these pointer updates more resilient. This diff introduces methods to add: - a child to a parent (either as the first or last child) - a prev <-> next sibling relationship These functions will only respect the local invariants on the nodes being updated, and can leave the rest of the tree in an inconsistent state that requires a few more updates (see the test file for examples of this). In the next diff, I'll be codifying some common paradigms (such as creating a new parent node & adding an existing node as its child) which will use the methods implemented in this diff to simplify the logic. The paradigm-based operations will preserve the tree invariant after the operation. Differential Revision: D9601093 fbshipit-source-id: 3152ffc24a49e7a591958aada3333f331aaef9dd
- Loading branch information
1 parent
c5b785a
commit 0cb80b7
Showing
3 changed files
with
865 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
159 changes: 159 additions & 0 deletions
159
src/model/modifier/exploration/__tests__/DraftTreeOperations-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); |
Oops, something went wrong.