|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- declare var __DEV__: boolean;
-
- /**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @format
- * @flow strict-local
- * @emails oncall+draft_js
- *
- * 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 ContentBlockNode = require("./ContentBlockNode");
-
- const DraftTreeInvariants = require("./DraftTreeInvariants");
-
- const generateRandomKey = require("./generateRandomKey");
-
- const Immutable = require("immutable");
-
- const invariant = require("fbjs/lib/invariant");
-
- type SiblingInsertPosition = 'previous' | 'next';
-
- 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 child of A, ensuring
- * that parent <-> child operations are correctly mirrored
- *
- * The child is inserted at 'position' index in the list
- *
- * 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: number): 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 newBlocks = {};
- const existingChildren = parent.getChildKeys();
- invariant(existingChildren != null && position >= 0 && position <= existingChildren.count(), 'position is not valid for the number of children'); // add as parent's child
-
- newBlocks[parentKey] = parent.merge({
- children: existingChildren.splice(position, 0, childKey)
- });
- let nextSiblingKey = null;
- let prevSiblingKey = null; // link new child as next sibling to the correct existing child
-
- if (position > 0) {
- prevSiblingKey = existingChildren.get(position - 1);
- newBlocks[prevSiblingKey] = blockMap.get(prevSiblingKey).merge({
- nextSibling: childKey
- });
- } // link new child as previous sibling to the correct existing child
-
-
- if (position < existingChildren.count()) {
- nextSiblingKey = existingChildren.get(position);
- newBlocks[nextSiblingKey] = blockMap.get(nextSiblingKey).merge({
- prevSibling: childKey
- });
- } // add parent & siblings to the child
-
-
- newBlocks[childKey] = child.merge({
- parent: parentKey,
- prevSibling: prevSiblingKey,
- nextSibling: nextSiblingKey
- });
- 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)
- */
-
-
- 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, 0); // 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());
- }
-
- 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, 0);
- 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, newParent.getChildKeys().count());
- 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;
- };
- /**
- * This is a utility method that abstracts the operation of moving a node up to become
- * a sibling of its parent. If the operation results in a parent with no children,
- * also delete the parent node.
- *
- * Can only operate on the first or last child (this is an invariant)
- *
- * This operation respects the tree data invariants - it expects and returns a
- * valid tree.
- */
-
-
- const moveChildUp = (blockMap: BlockMap, key: string): BlockMap => {
- verifyTree(blockMap);
- const block = blockMap.get(key);
- invariant(block != null, 'block must exist in block map'); // if there is no parent, do nothing
-
- const parentKey = block.getParentKey();
-
- if (parentKey == null) {
- return blockMap;
- }
-
- let parent = blockMap.get(parentKey);
- invariant(parent !== null, 'parent must exist in block map');
- let newBlockMap = blockMap;
- const childIndex = parent.getChildKeys().indexOf(key);
- 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
-
- if (childIndex === 0) {
- const parentPrevSibling = parent.getPrevSiblingKey();
- newBlockMap = updateSibling(newBlockMap, key, parentKey); // link to parent's previous sibling
-
- if (parentPrevSibling != null) {
- newBlockMap = updateSibling(newBlockMap, parentPrevSibling, key);
- } // remove as parent's child
-
-
- parent = newBlockMap.get(parentKey);
- newBlockMap = newBlockMap.set(parentKey, parent.merge({
- children: parent.getChildKeys().slice(1)
- }));
- parent = newBlockMap.get(parentKey); // remove as previous sibling of parent's children
-
- if (parent.getChildKeys().count() > 0) {
- const firstChildKey = parent.getChildKeys().first();
- const firstChild = newBlockMap.get(firstChildKey);
- newBlockMap = newBlockMap.set(firstChildKey, firstChild.merge({
- prevSibling: null
- }));
- } // add the node just before its former parent in the block map
-
-
- 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
- } else if (childIndex === parent.getChildKeys().count() - 1) {
- const parentNextSibling = parent.getNextSiblingKey();
- newBlockMap = updateSibling(newBlockMap, parentKey, key); // link to parent's next sibling
-
- if (parentNextSibling != null) {
- newBlockMap = updateSibling(newBlockMap, key, parentNextSibling);
- } // remove as parent's child
-
-
- parent = newBlockMap.get(parentKey);
- newBlockMap = newBlockMap.set(parentKey, parent.merge({
- children: parent.getChildKeys().slice(0, -1)
- }));
- parent = newBlockMap.get(parentKey); // remove as next sibling of parent's children
-
- if (parent.getChildKeys().count() > 0) {
- const lastChildKey = parent.getChildKeys().last();
- const lastChild = newBlockMap.get(lastChildKey);
- newBlockMap = newBlockMap.set(lastChildKey, lastChild.merge({
- nextSibling: null
- }));
- }
- } // For both cases, also link to parent's parent
-
-
- const grandparentKey = parent.getParentKey();
-
- if (grandparentKey != null) {
- const grandparentInsertPosition = newBlockMap.get(grandparentKey).getChildKeys().findIndex(n => n === parentKey);
- newBlockMap = updateParentChild(newBlockMap, grandparentKey, key, childIndex === 0 ? grandparentInsertPosition : grandparentInsertPosition + 1);
- } else {
- newBlockMap = newBlockMap.set(key, newBlockMap.get(key).merge({
- parent: null
- }));
- } // Delete parent if it has no children
-
-
- parent = newBlockMap.get(parentKey);
-
- if (parent.getChildKeys().count() === 0) {
- const prevSiblingKey = parent.getPrevSiblingKey();
- const nextSiblingKey = parent.getNextSiblingKey();
-
- if (prevSiblingKey != null && nextSiblingKey != null) {
- newBlockMap = updateSibling(newBlockMap, prevSiblingKey, nextSiblingKey);
- }
-
- if (prevSiblingKey == null && nextSiblingKey != null) {
- newBlockMap = newBlockMap.set(nextSiblingKey, newBlockMap.get(nextSiblingKey).merge({
- prevSibling: null
- }));
- }
-
- if (nextSiblingKey == null && prevSiblingKey != null) {
- newBlockMap = newBlockMap.set(prevSiblingKey, newBlockMap.get(prevSiblingKey).merge({
- nextSibling: null
- }));
- }
-
- if (grandparentKey != null) {
- const grandparent = newBlockMap.get(grandparentKey);
- const oldChildren = grandparent.getChildKeys();
- newBlockMap = newBlockMap.set(grandparentKey, grandparent.merge({
- children: oldChildren.delete(oldChildren.indexOf(parentKey))
- }));
- }
-
- newBlockMap = newBlockMap.delete(parentKey);
- }
-
- verifyTree(newBlockMap);
- return newBlockMap;
- };
- /**
- * This is a utility method to merge two non-leaf blocks into one. The next block's
- * children are added to the provided block & the next block is deleted.
- *
- * This operation respects the tree data invariants - it expects and returns a
- * valid tree.
- */
-
-
- const mergeBlocks = (blockMap: BlockMap, key: string): BlockMap => {
- verifyTree(blockMap); // current block must be a non-leaf
-
- const block = blockMap.get(key);
- invariant(block !== null, 'block must exist in block map');
- invariant(block.getChildKeys().count() > 0, 'block must be a non-leaf'); // next block must exist & be a non-leaf
-
- const nextBlockKey = block.getNextSiblingKey();
- invariant(nextBlockKey != null, 'block must have a next block');
- const nextBlock = blockMap.get(nextBlockKey);
- invariant(nextBlock != null, 'next block must exist in block map');
- invariant(nextBlock.getChildKeys().count() > 0, 'next block must be a non-leaf');
- const childKeys = block.getChildKeys().concat(nextBlock.getChildKeys());
- let newBlockMap = blockMap.set(key, block.merge({
- nextSibling: nextBlock.getNextSiblingKey(),
- children: childKeys
- }));
- newBlockMap = newBlockMap.merge(Immutable.OrderedMap(childKeys.map((k, i) => [k, blockMap.get(k).merge({
- parent: key,
- prevSibling: i - 1 < 0 ? null : childKeys.get(i - 1),
- nextSibling: i + 1 === childKeys.count() ? null : childKeys.get(i + 1)
- })])));
- newBlockMap = newBlockMap.delete(nextBlockKey);
- const nextNextBlockKey = nextBlock.getNextSiblingKey();
-
- if (nextNextBlockKey != null) {
- newBlockMap = newBlockMap.set(nextNextBlockKey, blockMap.get(nextNextBlockKey).merge({
- prevSibling: key
- }));
- }
-
- verifyTree(newBlockMap);
- return newBlockMap;
- };
-
- module.exports = {
- updateParentChild,
- replaceParentChild,
- updateSibling,
- createNewParent,
- updateAsSiblingsChild,
- moveChildUp,
- mergeBlocks
- };
|