|
15 | 15 | */
|
16 | 16 | import type {BlockMap} from 'BlockMap';
|
17 | 17 |
|
| 18 | +const ContentBlockNode = require('ContentBlockNode'); |
| 19 | +const DraftTreeInvariants = require('DraftTreeInvariants'); |
| 20 | + |
| 21 | +const generateRandomKey = require('generateRandomKey'); |
| 22 | +const Immutable = require('immutable'); |
18 | 23 | const invariant = require('invariant');
|
19 | 24 |
|
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; |
82 | 88 | }
|
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 | +}; |
85 | 98 |
|
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(), |
103 | 206 | );
|
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; |
113 | 296 | };
|
114 | 297 |
|
115 |
| -module.exports = DraftTreeOperations; |
| 298 | +module.exports = { |
| 299 | + updateParentChild, |
| 300 | + replaceParentChild, |
| 301 | + updateSibling, |
| 302 | + createNewParent, |
| 303 | + updateAsSiblingsChild, |
| 304 | +}; |
0 commit comments