選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

DraftTreeOperations.js.flow 15 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. declare var __DEV__: boolean;
  2. /**
  3. * Copyright (c) Facebook, Inc. and its affiliates.
  4. *
  5. * This source code is licensed under the MIT license found in the
  6. * LICENSE file in the root directory of this source tree.
  7. *
  8. * @format
  9. * @flow strict-local
  10. * @emails oncall+draft_js
  11. *
  12. * This is unstable and not part of the public API and should not be used by
  13. * production systems. This file may be update/removed without notice.
  14. */
  15. import type { BlockMap } from "./BlockMap";
  16. const ContentBlockNode = require("./ContentBlockNode");
  17. const DraftTreeInvariants = require("./DraftTreeInvariants");
  18. const generateRandomKey = require("./generateRandomKey");
  19. const Immutable = require("immutable");
  20. const invariant = require("fbjs/lib/invariant");
  21. type SiblingInsertPosition = 'previous' | 'next';
  22. const verifyTree = (tree: BlockMap): void => {
  23. if (__DEV__) {
  24. invariant(DraftTreeInvariants.isValidTree(tree), 'The tree is not valid');
  25. }
  26. };
  27. /**
  28. * This is a utility method for setting B as a child of A, ensuring
  29. * that parent <-> child operations are correctly mirrored
  30. *
  31. * The child is inserted at 'position' index in the list
  32. *
  33. * The block map returned by this method may not be a valid tree (siblings are
  34. * unaffected)
  35. */
  36. const updateParentChild = (blockMap: BlockMap, parentKey: string, childKey: string, position: number): BlockMap => {
  37. const parent = blockMap.get(parentKey);
  38. const child = blockMap.get(childKey);
  39. invariant(parent != null && child != null, 'parent & child should exist in the block map');
  40. const newBlocks = {};
  41. const existingChildren = parent.getChildKeys();
  42. invariant(existingChildren != null && position >= 0 && position <= existingChildren.count(), 'position is not valid for the number of children'); // add as parent's child
  43. newBlocks[parentKey] = parent.merge({
  44. children: existingChildren.splice(position, 0, childKey)
  45. });
  46. let nextSiblingKey = null;
  47. let prevSiblingKey = null; // link new child as next sibling to the correct existing child
  48. if (position > 0) {
  49. prevSiblingKey = existingChildren.get(position - 1);
  50. newBlocks[prevSiblingKey] = blockMap.get(prevSiblingKey).merge({
  51. nextSibling: childKey
  52. });
  53. } // link new child as previous sibling to the correct existing child
  54. if (position < existingChildren.count()) {
  55. nextSiblingKey = existingChildren.get(position);
  56. newBlocks[nextSiblingKey] = blockMap.get(nextSiblingKey).merge({
  57. prevSibling: childKey
  58. });
  59. } // add parent & siblings to the child
  60. newBlocks[childKey] = child.merge({
  61. parent: parentKey,
  62. prevSibling: prevSiblingKey,
  63. nextSibling: nextSiblingKey
  64. });
  65. return blockMap.merge(newBlocks);
  66. };
  67. /**
  68. * This is a utility method for setting B as the next sibling of A, ensuring
  69. * that sibling operations are correctly mirrored
  70. *
  71. * The block map returned by this method may not be a valid tree (parent/child/
  72. * other siblings are unaffected)
  73. */
  74. const updateSibling = (blockMap: BlockMap, prevKey: string, nextKey: string): BlockMap => {
  75. const prevSibling = blockMap.get(prevKey);
  76. const nextSibling = blockMap.get(nextKey);
  77. invariant(prevSibling != null && nextSibling != null, 'siblings should exist in the block map');
  78. const newBlocks = {};
  79. newBlocks[prevKey] = prevSibling.merge({
  80. nextSibling: nextKey
  81. });
  82. newBlocks[nextKey] = nextSibling.merge({
  83. prevSibling: prevKey
  84. });
  85. return blockMap.merge(newBlocks);
  86. };
  87. /**
  88. * This is a utility method for replacing B by C as a child of A, ensuring
  89. * that parent <-> child connections between A & C are correctly mirrored
  90. *
  91. * The block map returned by this method may not be a valid tree (siblings are
  92. * unaffected)
  93. */
  94. const replaceParentChild = (blockMap: BlockMap, parentKey: string, existingChildKey: string, newChildKey: string): BlockMap => {
  95. const parent = blockMap.get(parentKey);
  96. const newChild = blockMap.get(newChildKey);
  97. invariant(parent != null && newChild != null, 'parent & child should exist in the block map');
  98. const existingChildren = parent.getChildKeys();
  99. const newBlocks = {};
  100. newBlocks[parentKey] = parent.merge({
  101. children: existingChildren.set(existingChildren.indexOf(existingChildKey), newChildKey)
  102. });
  103. newBlocks[newChildKey] = newChild.merge({
  104. parent: parentKey
  105. });
  106. return blockMap.merge(newBlocks);
  107. };
  108. /**
  109. * This is a utility method that abstracts the operation of creating a new parent
  110. * for a particular node in the block map.
  111. *
  112. * This operation respects the tree data invariants - it expects and returns a
  113. * valid tree.
  114. */
  115. const createNewParent = (blockMap: BlockMap, key: string): BlockMap => {
  116. verifyTree(blockMap);
  117. const block = blockMap.get(key);
  118. invariant(block != null, 'block must exist in block map');
  119. const newParent = new ContentBlockNode({
  120. key: generateRandomKey(),
  121. text: '',
  122. depth: block.depth,
  123. type: block.type,
  124. children: Immutable.List([])
  125. }); // add the parent just before the child in the block map
  126. 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
  127. newBlockMap = updateParentChild(newBlockMap, newParent.getKey(), key, 0); // set siblings & parent for the new parent key to child's siblings & parent
  128. const prevSibling = block.getPrevSiblingKey();
  129. const nextSibling = block.getNextSiblingKey();
  130. const parent = block.getParentKey();
  131. if (prevSibling != null) {
  132. newBlockMap = updateSibling(newBlockMap, prevSibling, newParent.getKey());
  133. }
  134. if (nextSibling != null) {
  135. newBlockMap = updateSibling(newBlockMap, newParent.getKey(), nextSibling);
  136. }
  137. if (parent != null) {
  138. newBlockMap = replaceParentChild(newBlockMap, parent, key, newParent.getKey());
  139. }
  140. verifyTree(newBlockMap);
  141. return newBlockMap;
  142. };
  143. /**
  144. * This is a utility method that abstracts the operation of adding a node as the child
  145. * of its previous or next sibling.
  146. *
  147. * The previous (or next) sibling must be a valid parent node.
  148. *
  149. * This operation respects the tree data invariants - it expects and returns a
  150. * valid tree.
  151. */
  152. const updateAsSiblingsChild = (blockMap: BlockMap, key: string, position: SiblingInsertPosition): BlockMap => {
  153. verifyTree(blockMap);
  154. const block = blockMap.get(key);
  155. invariant(block != null, 'block must exist in block map');
  156. const newParentKey = position === 'previous' ? block.getPrevSiblingKey() : block.getNextSiblingKey();
  157. invariant(newParentKey != null, 'sibling is null');
  158. const newParent = blockMap.get(newParentKey);
  159. invariant(newParent !== null && newParent.getText() === '', 'parent must be a valid node');
  160. let newBlockMap = blockMap;
  161. switch (position) {
  162. case 'next':
  163. newBlockMap = updateParentChild(newBlockMap, newParentKey, key, 0);
  164. const prevSibling = block.getPrevSiblingKey();
  165. if (prevSibling != null) {
  166. newBlockMap = updateSibling(newBlockMap, prevSibling, newParentKey);
  167. } else {
  168. newBlockMap = newBlockMap.set(newParentKey, newBlockMap.get(newParentKey).merge({
  169. prevSibling: null
  170. }));
  171. } // we also need to flip the order of the sibling & block in the ordered map
  172. // for this case
  173. 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));
  174. break;
  175. case 'previous':
  176. newBlockMap = updateParentChild(newBlockMap, newParentKey, key, newParent.getChildKeys().count());
  177. const nextSibling = block.getNextSiblingKey();
  178. if (nextSibling != null) {
  179. newBlockMap = updateSibling(newBlockMap, newParentKey, nextSibling);
  180. } else {
  181. newBlockMap = newBlockMap.set(newParentKey, newBlockMap.get(newParentKey).merge({
  182. nextSibling: null
  183. }));
  184. }
  185. break;
  186. } // remove the node as a child of its current parent
  187. const parentKey = block.getParentKey();
  188. if (parentKey != null) {
  189. const parent = newBlockMap.get(parentKey);
  190. newBlockMap = newBlockMap.set(parentKey, parent.merge({
  191. children: parent.getChildKeys().delete(parent.getChildKeys().indexOf(key))
  192. }));
  193. }
  194. verifyTree(newBlockMap);
  195. return newBlockMap;
  196. };
  197. /**
  198. * This is a utility method that abstracts the operation of moving a node up to become
  199. * a sibling of its parent. If the operation results in a parent with no children,
  200. * also delete the parent node.
  201. *
  202. * Can only operate on the first or last child (this is an invariant)
  203. *
  204. * This operation respects the tree data invariants - it expects and returns a
  205. * valid tree.
  206. */
  207. const moveChildUp = (blockMap: BlockMap, key: string): BlockMap => {
  208. verifyTree(blockMap);
  209. const block = blockMap.get(key);
  210. invariant(block != null, 'block must exist in block map'); // if there is no parent, do nothing
  211. const parentKey = block.getParentKey();
  212. if (parentKey == null) {
  213. return blockMap;
  214. }
  215. let parent = blockMap.get(parentKey);
  216. invariant(parent !== null, 'parent must exist in block map');
  217. let newBlockMap = blockMap;
  218. const childIndex = parent.getChildKeys().indexOf(key);
  219. invariant(childIndex === 0 || childIndex === parent.getChildKeys().count() - 1, 'block is not first or last child of its parent'); // If it's the first child, move as previous sibling of parent
  220. if (childIndex === 0) {
  221. const parentPrevSibling = parent.getPrevSiblingKey();
  222. newBlockMap = updateSibling(newBlockMap, key, parentKey); // link to parent's previous sibling
  223. if (parentPrevSibling != null) {
  224. newBlockMap = updateSibling(newBlockMap, parentPrevSibling, key);
  225. } // remove as parent's child
  226. parent = newBlockMap.get(parentKey);
  227. newBlockMap = newBlockMap.set(parentKey, parent.merge({
  228. children: parent.getChildKeys().slice(1)
  229. }));
  230. parent = newBlockMap.get(parentKey); // remove as previous sibling of parent's children
  231. if (parent.getChildKeys().count() > 0) {
  232. const firstChildKey = parent.getChildKeys().first();
  233. const firstChild = newBlockMap.get(firstChildKey);
  234. newBlockMap = newBlockMap.set(firstChildKey, firstChild.merge({
  235. prevSibling: null
  236. }));
  237. } // add the node just before its former parent in the block map
  238. newBlockMap = newBlockMap.takeUntil(block => block.getKey() === parentKey).concat(Immutable.OrderedMap([[key, newBlockMap.get(key)], [parentKey, newBlockMap.get(parentKey)]])).concat(newBlockMap.skipUntil(block => block.getKey() === key).slice(1)); // If it's the last child, move as next sibling of parent
  239. } else if (childIndex === parent.getChildKeys().count() - 1) {
  240. const parentNextSibling = parent.getNextSiblingKey();
  241. newBlockMap = updateSibling(newBlockMap, parentKey, key); // link to parent's next sibling
  242. if (parentNextSibling != null) {
  243. newBlockMap = updateSibling(newBlockMap, key, parentNextSibling);
  244. } // remove as parent's child
  245. parent = newBlockMap.get(parentKey);
  246. newBlockMap = newBlockMap.set(parentKey, parent.merge({
  247. children: parent.getChildKeys().slice(0, -1)
  248. }));
  249. parent = newBlockMap.get(parentKey); // remove as next sibling of parent's children
  250. if (parent.getChildKeys().count() > 0) {
  251. const lastChildKey = parent.getChildKeys().last();
  252. const lastChild = newBlockMap.get(lastChildKey);
  253. newBlockMap = newBlockMap.set(lastChildKey, lastChild.merge({
  254. nextSibling: null
  255. }));
  256. }
  257. } // For both cases, also link to parent's parent
  258. const grandparentKey = parent.getParentKey();
  259. if (grandparentKey != null) {
  260. const grandparentInsertPosition = newBlockMap.get(grandparentKey).getChildKeys().findIndex(n => n === parentKey);
  261. newBlockMap = updateParentChild(newBlockMap, grandparentKey, key, childIndex === 0 ? grandparentInsertPosition : grandparentInsertPosition + 1);
  262. } else {
  263. newBlockMap = newBlockMap.set(key, newBlockMap.get(key).merge({
  264. parent: null
  265. }));
  266. } // Delete parent if it has no children
  267. parent = newBlockMap.get(parentKey);
  268. if (parent.getChildKeys().count() === 0) {
  269. const prevSiblingKey = parent.getPrevSiblingKey();
  270. const nextSiblingKey = parent.getNextSiblingKey();
  271. if (prevSiblingKey != null && nextSiblingKey != null) {
  272. newBlockMap = updateSibling(newBlockMap, prevSiblingKey, nextSiblingKey);
  273. }
  274. if (prevSiblingKey == null && nextSiblingKey != null) {
  275. newBlockMap = newBlockMap.set(nextSiblingKey, newBlockMap.get(nextSiblingKey).merge({
  276. prevSibling: null
  277. }));
  278. }
  279. if (nextSiblingKey == null && prevSiblingKey != null) {
  280. newBlockMap = newBlockMap.set(prevSiblingKey, newBlockMap.get(prevSiblingKey).merge({
  281. nextSibling: null
  282. }));
  283. }
  284. if (grandparentKey != null) {
  285. const grandparent = newBlockMap.get(grandparentKey);
  286. const oldChildren = grandparent.getChildKeys();
  287. newBlockMap = newBlockMap.set(grandparentKey, grandparent.merge({
  288. children: oldChildren.delete(oldChildren.indexOf(parentKey))
  289. }));
  290. }
  291. newBlockMap = newBlockMap.delete(parentKey);
  292. }
  293. verifyTree(newBlockMap);
  294. return newBlockMap;
  295. };
  296. /**
  297. * This is a utility method to merge two non-leaf blocks into one. The next block's
  298. * children are added to the provided block & the next block is deleted.
  299. *
  300. * This operation respects the tree data invariants - it expects and returns a
  301. * valid tree.
  302. */
  303. const mergeBlocks = (blockMap: BlockMap, key: string): BlockMap => {
  304. verifyTree(blockMap); // current block must be a non-leaf
  305. const block = blockMap.get(key);
  306. invariant(block !== null, 'block must exist in block map');
  307. invariant(block.getChildKeys().count() > 0, 'block must be a non-leaf'); // next block must exist & be a non-leaf
  308. const nextBlockKey = block.getNextSiblingKey();
  309. invariant(nextBlockKey != null, 'block must have a next block');
  310. const nextBlock = blockMap.get(nextBlockKey);
  311. invariant(nextBlock != null, 'next block must exist in block map');
  312. invariant(nextBlock.getChildKeys().count() > 0, 'next block must be a non-leaf');
  313. const childKeys = block.getChildKeys().concat(nextBlock.getChildKeys());
  314. let newBlockMap = blockMap.set(key, block.merge({
  315. nextSibling: nextBlock.getNextSiblingKey(),
  316. children: childKeys
  317. }));
  318. newBlockMap = newBlockMap.merge(Immutable.OrderedMap(childKeys.map((k, i) => [k, blockMap.get(k).merge({
  319. parent: key,
  320. prevSibling: i - 1 < 0 ? null : childKeys.get(i - 1),
  321. nextSibling: i + 1 === childKeys.count() ? null : childKeys.get(i + 1)
  322. })])));
  323. newBlockMap = newBlockMap.delete(nextBlockKey);
  324. const nextNextBlockKey = nextBlock.getNextSiblingKey();
  325. if (nextNextBlockKey != null) {
  326. newBlockMap = newBlockMap.set(nextNextBlockKey, blockMap.get(nextNextBlockKey).merge({
  327. prevSibling: key
  328. }));
  329. }
  330. verifyTree(newBlockMap);
  331. return newBlockMap;
  332. };
  333. module.exports = {
  334. updateParentChild,
  335. replaceParentChild,
  336. updateSibling,
  337. createNewParent,
  338. updateAsSiblingsChild,
  339. moveChildUp,
  340. mergeBlocks
  341. };