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

Commit 6f73657

Browse files
niveditcfacebook-github-bot
authored andcommitted
Add utilty methods for creating a new parent & updating node to become 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
1 parent e2c24cf commit 6f73657

File tree

3 files changed

+1700
-91
lines changed

3 files changed

+1700
-91
lines changed

src/model/modifier/exploration/DraftTreeOperations.js

Lines changed: 280 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -15,101 +15,290 @@
1515
*/
1616
import type {BlockMap} from 'BlockMap';
1717

18+
const ContentBlockNode = require('ContentBlockNode');
19+
const DraftTreeInvariants = require('DraftTreeInvariants');
20+
21+
const generateRandomKey = require('generateRandomKey');
22+
const Immutable = require('immutable');
1823
const invariant = require('invariant');
1924

20-
const DraftTreeOperations = {
21-
/**
22-
* This is a utility method for setting B as a first/last child of A, ensuring
23-
* that parent <-> child operations are correctly mirrored
24-
*
25-
* The block map returned by this method may not be a valid tree (siblings are
26-
* unaffected)
27-
*/
28-
updateParentChild(
29-
blockMap: BlockMap,
30-
parentKey: string,
31-
childKey: string,
32-
position: 'first' | 'last',
33-
): BlockMap {
34-
const parent = blockMap.get(parentKey);
35-
const child = blockMap.get(childKey);
36-
invariant(
37-
parent != null && child != null,
38-
'parent & child should exist in the block map',
39-
);
40-
const existingChildren = parent.getChildKeys();
41-
const newBlocks = {};
42-
// add as parent's child
43-
newBlocks[parentKey] = parent.merge({
44-
children:
45-
position === 'first'
46-
? existingChildren.unshift(childKey)
47-
: existingChildren.push(childKey),
48-
});
49-
// add as child's parent
50-
if (existingChildren.count() !== 0) {
51-
// link child as sibling to the existing children
52-
switch (position) {
53-
case 'first':
54-
const nextSiblingKey = existingChildren.first();
55-
newBlocks[childKey] = child.merge({
56-
parent: parentKey,
57-
nextSibling: nextSiblingKey,
58-
prevSibling: null,
59-
});
60-
newBlocks[nextSiblingKey] = blockMap.get(nextSiblingKey).merge({
61-
prevSibling: childKey,
62-
});
63-
break;
64-
case 'last':
65-
const prevSiblingKey = existingChildren.last();
66-
newBlocks[childKey] = child.merge({
67-
parent: parentKey,
68-
prevSibling: prevSiblingKey,
69-
nextSibling: null,
70-
});
71-
newBlocks[prevSiblingKey] = blockMap.get(prevSiblingKey).merge({
72-
nextSibling: childKey,
73-
});
74-
break;
75-
}
76-
} else {
77-
newBlocks[childKey] = child.merge({
78-
parent: parentKey,
79-
prevSibling: null,
80-
nextSibling: null,
81-
});
25+
type SiblingInsertPosition = 'previous' | 'next';
26+
type ChildInsertPosition = 'first' | 'last';
27+
28+
const verifyTree = (tree: BlockMap): void => {
29+
if (__DEV__) {
30+
invariant(DraftTreeInvariants.isValidTree(tree), 'The tree is not valid');
31+
}
32+
};
33+
34+
/**
35+
* This is a utility method for setting B as a first/last child of A, ensuring
36+
* that parent <-> child operations are correctly mirrored
37+
*
38+
* The block map returned by this method may not be a valid tree (siblings are
39+
* unaffected)
40+
*/
41+
const updateParentChild = (
42+
blockMap: BlockMap,
43+
parentKey: string,
44+
childKey: string,
45+
position: ChildInsertPosition,
46+
): BlockMap => {
47+
const parent = blockMap.get(parentKey);
48+
const child = blockMap.get(childKey);
49+
invariant(
50+
parent != null && child != null,
51+
'parent & child should exist in the block map',
52+
);
53+
const existingChildren = parent.getChildKeys();
54+
const newBlocks = {};
55+
// add as parent's child
56+
newBlocks[parentKey] = parent.merge({
57+
children:
58+
position === 'first'
59+
? existingChildren.unshift(childKey)
60+
: existingChildren.push(childKey),
61+
});
62+
// add as child's parent
63+
if (existingChildren.count() !== 0) {
64+
// link child as sibling to the existing children
65+
switch (position) {
66+
case 'first':
67+
const nextSiblingKey = existingChildren.first();
68+
newBlocks[childKey] = child.merge({
69+
parent: parentKey,
70+
nextSibling: nextSiblingKey,
71+
prevSibling: null,
72+
});
73+
newBlocks[nextSiblingKey] = blockMap.get(nextSiblingKey).merge({
74+
prevSibling: childKey,
75+
});
76+
break;
77+
case 'last':
78+
const prevSiblingKey = existingChildren.last();
79+
newBlocks[childKey] = child.merge({
80+
parent: parentKey,
81+
prevSibling: prevSiblingKey,
82+
nextSibling: null,
83+
});
84+
newBlocks[prevSiblingKey] = blockMap.get(prevSiblingKey).merge({
85+
nextSibling: childKey,
86+
});
87+
break;
8288
}
83-
return blockMap.merge(newBlocks);
84-
},
89+
} else {
90+
newBlocks[childKey] = child.merge({
91+
parent: parentKey,
92+
prevSibling: null,
93+
nextSibling: null,
94+
});
95+
}
96+
return blockMap.merge(newBlocks);
97+
};
8598

86-
/**
87-
* This is a utility method for setting B as the next sibling of A, ensuring
88-
* that sibling operations are correctly mirrored
89-
*
90-
* The block map returned by this method may not be a valid tree (parent/child/
91-
* other siblings are unaffected)
92-
*/
93-
updateSibling(
94-
blockMap: BlockMap,
95-
prevKey: string,
96-
nextKey: string,
97-
): BlockMap {
98-
const prevSibling = blockMap.get(prevKey);
99-
const nextSibling = blockMap.get(nextKey);
100-
invariant(
101-
prevSibling != null && nextSibling != null,
102-
'siblings should exist in the block map',
99+
/**
100+
* This is a utility method for setting B as the next sibling of A, ensuring
101+
* that sibling operations are correctly mirrored
102+
*
103+
* The block map returned by this method may not be a valid tree (parent/child/
104+
* other siblings are unaffected)
105+
*/
106+
const updateSibling = (
107+
blockMap: BlockMap,
108+
prevKey: string,
109+
nextKey: string,
110+
): BlockMap => {
111+
const prevSibling = blockMap.get(prevKey);
112+
const nextSibling = blockMap.get(nextKey);
113+
invariant(
114+
prevSibling != null && nextSibling != null,
115+
'siblings should exist in the block map',
116+
);
117+
const newBlocks = {};
118+
newBlocks[prevKey] = prevSibling.merge({
119+
nextSibling: nextKey,
120+
});
121+
newBlocks[nextKey] = nextSibling.merge({
122+
prevSibling: prevKey,
123+
});
124+
return blockMap.merge(newBlocks);
125+
};
126+
127+
/**
128+
* This is a utility method for replacing B by C as a child of A, ensuring
129+
* that parent <-> child connections between A & C are correctly mirrored
130+
*
131+
* The block map returned by this method may not be a valid tree (siblings are
132+
* unaffected)
133+
*/
134+
const replaceParentChild = (
135+
blockMap: BlockMap,
136+
parentKey: string,
137+
existingChildKey: string,
138+
newChildKey: string,
139+
): BlockMap => {
140+
const parent = blockMap.get(parentKey);
141+
const newChild = blockMap.get(newChildKey);
142+
invariant(
143+
parent != null && newChild != null,
144+
'parent & child should exist in the block map',
145+
);
146+
const existingChildren = parent.getChildKeys();
147+
const newBlocks = {};
148+
newBlocks[parentKey] = parent.merge({
149+
children: existingChildren.set(
150+
existingChildren.indexOf(existingChildKey),
151+
newChildKey,
152+
),
153+
});
154+
newBlocks[newChildKey] = newChild.merge({
155+
parent: parentKey,
156+
});
157+
return blockMap.merge(newBlocks);
158+
};
159+
160+
/**
161+
* This is a utility method that abstracts the operation of creating a new parent
162+
* for a particular node in the block map.
163+
*
164+
* This operation respects the tree data invariants - it expects and returns a
165+
* valid tree.
166+
*/
167+
const createNewParent = (blockMap: BlockMap, key: string): BlockMap => {
168+
verifyTree(blockMap);
169+
const block = blockMap.get(key);
170+
invariant(block != null, 'block must exist in block map');
171+
const newParent = new ContentBlockNode({
172+
key: generateRandomKey(),
173+
text: '',
174+
depth: block.depth,
175+
type: block.type,
176+
children: Immutable.List([]),
177+
});
178+
// add the parent just before the child in the block map
179+
let newBlockMap = blockMap
180+
.takeUntil(block => block.getKey() === key)
181+
.concat(Immutable.OrderedMap([[newParent.getKey(), newParent]]))
182+
.concat(blockMap.skipUntil(block => block.getKey() === key));
183+
// set parent <-> child connection
184+
newBlockMap = updateParentChild(
185+
newBlockMap,
186+
newParent.getKey(),
187+
key,
188+
'first',
189+
);
190+
// set siblings & parent for the new parent key to child's siblings & parent
191+
const prevSibling = block.getPrevSiblingKey();
192+
const nextSibling = block.getNextSiblingKey();
193+
const parent = block.getParentKey();
194+
if (prevSibling != null) {
195+
newBlockMap = updateSibling(newBlockMap, prevSibling, newParent.getKey());
196+
}
197+
if (nextSibling != null) {
198+
newBlockMap = updateSibling(newBlockMap, newParent.getKey(), nextSibling);
199+
}
200+
if (parent != null) {
201+
newBlockMap = replaceParentChild(
202+
newBlockMap,
203+
parent,
204+
key,
205+
newParent.getKey(),
103206
);
104-
const newBlocks = {};
105-
newBlocks[prevKey] = prevSibling.merge({
106-
nextSibling: nextKey,
107-
});
108-
newBlocks[nextKey] = nextSibling.merge({
109-
prevSibling: prevKey,
110-
});
111-
return blockMap.merge(newBlocks);
112-
},
207+
}
208+
verifyTree(newBlockMap);
209+
return newBlockMap;
210+
};
211+
212+
/**
213+
* This is a utility method that abstracts the operation of adding a node as the child
214+
* of its previous or next sibling.
215+
*
216+
* The previous (or next) sibling must be a valid parent node.
217+
*
218+
* This operation respects the tree data invariants - it expects and returns a
219+
* valid tree.
220+
*/
221+
const updateAsSiblingsChild = (
222+
blockMap: BlockMap,
223+
key: string,
224+
position: SiblingInsertPosition,
225+
): BlockMap => {
226+
verifyTree(blockMap);
227+
const block = blockMap.get(key);
228+
invariant(block != null, 'block must exist in block map');
229+
const newParentKey =
230+
position === 'previous'
231+
? block.getPrevSiblingKey()
232+
: block.getNextSiblingKey();
233+
invariant(newParentKey != null, 'sibling is null');
234+
const newParent = blockMap.get(newParentKey);
235+
invariant(
236+
newParent !== null && newParent.getText() === '',
237+
'parent must be a valid node',
238+
);
239+
let newBlockMap = blockMap;
240+
switch (position) {
241+
case 'next':
242+
newBlockMap = updateParentChild(newBlockMap, newParentKey, key, 'first');
243+
const prevSibling = block.getPrevSiblingKey();
244+
if (prevSibling != null) {
245+
newBlockMap = updateSibling(newBlockMap, prevSibling, newParentKey);
246+
} else {
247+
newBlockMap = newBlockMap.set(
248+
newParentKey,
249+
newBlockMap.get(newParentKey).merge({prevSibling: null}),
250+
);
251+
}
252+
// we also need to flip the order of the sibling & block in the ordered map
253+
// for this case
254+
newBlockMap = newBlockMap
255+
.takeUntil(block => block.getKey() === key)
256+
.concat(
257+
Immutable.OrderedMap([
258+
[newParentKey, newBlockMap.get(newParentKey)],
259+
[key, newBlockMap.get(key)],
260+
]),
261+
)
262+
.concat(
263+
newBlockMap
264+
.skipUntil(block => block.getKey() === newParentKey)
265+
.slice(1),
266+
);
267+
break;
268+
case 'previous':
269+
newBlockMap = updateParentChild(newBlockMap, newParentKey, key, 'last');
270+
const nextSibling = block.getNextSiblingKey();
271+
if (nextSibling != null) {
272+
newBlockMap = updateSibling(newBlockMap, newParentKey, nextSibling);
273+
} else {
274+
newBlockMap = newBlockMap.set(
275+
newParentKey,
276+
newBlockMap.get(newParentKey).merge({nextSibling: null}),
277+
);
278+
}
279+
break;
280+
}
281+
// remove the node as a child of its current parent
282+
const parentKey = block.getParentKey();
283+
if (parentKey != null) {
284+
const parent = newBlockMap.get(parentKey);
285+
newBlockMap = newBlockMap.set(
286+
parentKey,
287+
parent.merge({
288+
children: parent
289+
.getChildKeys()
290+
.delete(parent.getChildKeys().indexOf(key)),
291+
}),
292+
);
293+
}
294+
verifyTree(newBlockMap);
295+
return newBlockMap;
113296
};
114297

115-
module.exports = DraftTreeOperations;
298+
module.exports = {
299+
updateParentChild,
300+
replaceParentChild,
301+
updateSibling,
302+
createNewParent,
303+
updateAsSiblingsChild,
304+
};

0 commit comments

Comments
 (0)