draft-js
Version:
A React framework for building text editors.
345 lines (275 loc) • 11.9 kB
JavaScript
/**
* 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
*
* @emails oncall+draft_js
*/
;
var ContentBlockNode = require("./ContentBlockNode");
var getNextDelimiterBlockKey = require("./getNextDelimiterBlockKey");
var Immutable = require("immutable");
var List = Immutable.List,
Map = Immutable.Map;
var transformBlock = function transformBlock(key, blockMap, func) {
if (!key) {
return;
}
var block = blockMap.get(key);
if (!block) {
return;
}
blockMap.set(key, func(block));
};
/**
* Ancestors needs to be preserved when there are non selected
* children to make sure we do not leave any orphans behind
*/
var getAncestorsKeys = function getAncestorsKeys(blockKey, blockMap) {
var parents = [];
if (!blockKey) {
return parents;
}
var blockNode = blockMap.get(blockKey);
while (blockNode && blockNode.getParentKey()) {
var parentKey = blockNode.getParentKey();
if (parentKey) {
parents.push(parentKey);
}
blockNode = parentKey ? blockMap.get(parentKey) : null;
}
return parents;
};
/**
* Get all next delimiter keys until we hit a root delimiter and return
* an array of key references
*/
var getNextDelimitersBlockKeys = function getNextDelimitersBlockKeys(block, blockMap) {
var nextDelimiters = [];
if (!block) {
return nextDelimiters;
}
var nextDelimiter = getNextDelimiterBlockKey(block, blockMap);
while (nextDelimiter && blockMap.get(nextDelimiter)) {
var _block = blockMap.get(nextDelimiter);
nextDelimiters.push(nextDelimiter); // we do not need to keep checking all root node siblings, just the first occurance
nextDelimiter = _block.getParentKey() ? getNextDelimiterBlockKey(_block, blockMap) : null;
}
return nextDelimiters;
};
var getNextValidSibling = function getNextValidSibling(block, blockMap, originalBlockMap) {
if (!block) {
return null;
} // note that we need to make sure we refer to the original block since this
// function is called within a withMutations
var nextValidSiblingKey = originalBlockMap.get(block.getKey()).getNextSiblingKey();
while (nextValidSiblingKey && !blockMap.get(nextValidSiblingKey)) {
nextValidSiblingKey = originalBlockMap.get(nextValidSiblingKey).getNextSiblingKey() || null;
}
return nextValidSiblingKey;
};
var getPrevValidSibling = function getPrevValidSibling(block, blockMap, originalBlockMap) {
if (!block) {
return null;
} // note that we need to make sure we refer to the original block since this
// function is called within a withMutations
var prevValidSiblingKey = originalBlockMap.get(block.getKey()).getPrevSiblingKey();
while (prevValidSiblingKey && !blockMap.get(prevValidSiblingKey)) {
prevValidSiblingKey = originalBlockMap.get(prevValidSiblingKey).getPrevSiblingKey() || null;
}
return prevValidSiblingKey;
};
var updateBlockMapLinks = function updateBlockMapLinks(blockMap, startBlock, endBlock, originalBlockMap) {
return blockMap.withMutations(function (blocks) {
// update start block if its retained
transformBlock(startBlock.getKey(), blocks, function (block) {
return block.merge({
nextSibling: getNextValidSibling(block, blocks, originalBlockMap),
prevSibling: getPrevValidSibling(block, blocks, originalBlockMap)
});
}); // update endblock if its retained
transformBlock(endBlock.getKey(), blocks, function (block) {
return block.merge({
nextSibling: getNextValidSibling(block, blocks, originalBlockMap),
prevSibling: getPrevValidSibling(block, blocks, originalBlockMap)
});
}); // update start block parent ancestors
getAncestorsKeys(startBlock.getKey(), originalBlockMap).forEach(function (parentKey) {
return transformBlock(parentKey, blocks, function (block) {
return block.merge({
children: block.getChildKeys().filter(function (key) {
return blocks.get(key);
}),
nextSibling: getNextValidSibling(block, blocks, originalBlockMap),
prevSibling: getPrevValidSibling(block, blocks, originalBlockMap)
});
});
}); // update start block next - can only happen if startBlock == endBlock
transformBlock(startBlock.getNextSiblingKey(), blocks, function (block) {
return block.merge({
prevSibling: startBlock.getPrevSiblingKey()
});
}); // update start block prev
transformBlock(startBlock.getPrevSiblingKey(), blocks, function (block) {
return block.merge({
nextSibling: getNextValidSibling(block, blocks, originalBlockMap)
});
}); // update end block next
transformBlock(endBlock.getNextSiblingKey(), blocks, function (block) {
return block.merge({
prevSibling: getPrevValidSibling(block, blocks, originalBlockMap)
});
}); // update end block prev
transformBlock(endBlock.getPrevSiblingKey(), blocks, function (block) {
return block.merge({
nextSibling: endBlock.getNextSiblingKey()
});
}); // update end block parent ancestors
getAncestorsKeys(endBlock.getKey(), originalBlockMap).forEach(function (parentKey) {
transformBlock(parentKey, blocks, function (block) {
return block.merge({
children: block.getChildKeys().filter(function (key) {
return blocks.get(key);
}),
nextSibling: getNextValidSibling(block, blocks, originalBlockMap),
prevSibling: getPrevValidSibling(block, blocks, originalBlockMap)
});
});
}); // update next delimiters all the way to a root delimiter
getNextDelimitersBlockKeys(endBlock, originalBlockMap).forEach(function (delimiterKey) {
return transformBlock(delimiterKey, blocks, function (block) {
return block.merge({
nextSibling: getNextValidSibling(block, blocks, originalBlockMap),
prevSibling: getPrevValidSibling(block, blocks, originalBlockMap)
});
});
}); // if parent (startBlock) was deleted
if (blockMap.get(startBlock.getKey()) == null && blockMap.get(endBlock.getKey()) != null && endBlock.getParentKey() === startBlock.getKey() && endBlock.getPrevSiblingKey() == null) {
var prevSiblingKey = startBlock.getPrevSiblingKey(); // endBlock becomes next sibling of parent's prevSibling
transformBlock(endBlock.getKey(), blocks, function (block) {
return block.merge({
prevSibling: prevSiblingKey
});
});
transformBlock(prevSiblingKey, blocks, function (block) {
return block.merge({
nextSibling: endBlock.getKey()
});
}); // Update parent for previous parent's children, and children for that parent
var prevSibling = prevSiblingKey ? blockMap.get(prevSiblingKey) : null;
var newParentKey = prevSibling ? prevSibling.getParentKey() : null;
startBlock.getChildKeys().forEach(function (childKey) {
transformBlock(childKey, blocks, function (block) {
return block.merge({
parent: newParentKey // set to null if there is no parent
});
});
});
if (newParentKey != null) {
var newParent = blockMap.get(newParentKey);
transformBlock(newParentKey, blocks, function (block) {
return block.merge({
children: newParent.getChildKeys().concat(startBlock.getChildKeys())
});
});
} // last child of deleted parent should point to next sibling
transformBlock(startBlock.getChildKeys().find(function (key) {
var block = blockMap.get(key);
return block.getNextSiblingKey() === null;
}), blocks, function (block) {
return block.merge({
nextSibling: startBlock.getNextSiblingKey()
});
});
}
});
};
var removeRangeFromContentState = function removeRangeFromContentState(contentState, selectionState) {
if (selectionState.isCollapsed()) {
return contentState;
}
var blockMap = contentState.getBlockMap();
var startKey = selectionState.getStartKey();
var startOffset = selectionState.getStartOffset();
var endKey = selectionState.getEndKey();
var endOffset = selectionState.getEndOffset();
var startBlock = blockMap.get(startKey);
var endBlock = blockMap.get(endKey); // we assume that ContentBlockNode and ContentBlocks are not mixed together
var isExperimentalTreeBlock = startBlock instanceof ContentBlockNode; // used to retain blocks that should not be deleted to avoid orphan children
var parentAncestors = [];
if (isExperimentalTreeBlock) {
var endBlockchildrenKeys = endBlock.getChildKeys();
var endBlockAncestors = getAncestorsKeys(endKey, blockMap); // endBlock has unselected siblings so we can not remove its ancestors parents
if (endBlock.getNextSiblingKey()) {
parentAncestors = parentAncestors.concat(endBlockAncestors);
} // endBlock has children so can not remove this block or any of its ancestors
if (!endBlockchildrenKeys.isEmpty()) {
parentAncestors = parentAncestors.concat(endBlockAncestors.concat([endKey]));
} // we need to retain all ancestors of the next delimiter block
parentAncestors = parentAncestors.concat(getAncestorsKeys(getNextDelimiterBlockKey(endBlock, blockMap), blockMap));
}
var characterList;
if (startBlock === endBlock) {
characterList = removeFromList(startBlock.getCharacterList(), startOffset, endOffset);
} else {
characterList = startBlock.getCharacterList().slice(0, startOffset).concat(endBlock.getCharacterList().slice(endOffset));
}
var modifiedStart = startBlock.merge({
text: startBlock.getText().slice(0, startOffset) + endBlock.getText().slice(endOffset),
characterList: characterList
}); // If cursor (collapsed) is at the start of the first child, delete parent
// instead of child
var shouldDeleteParent = isExperimentalTreeBlock && startOffset === 0 && endOffset === 0 && endBlock.getParentKey() === startKey && endBlock.getPrevSiblingKey() == null;
var newBlocks = shouldDeleteParent ? Map([[startKey, null]]) : blockMap.toSeq().skipUntil(function (_, k) {
return k === startKey;
}).takeUntil(function (_, k) {
return k === endKey;
}).filter(function (_, k) {
return parentAncestors.indexOf(k) === -1;
}).concat(Map([[endKey, null]])).map(function (_, k) {
return k === startKey ? modifiedStart : null;
});
var updatedBlockMap = blockMap.merge(newBlocks).filter(function (block) {
return !!block;
}); // Only update tree block pointers if the range is across blocks
if (isExperimentalTreeBlock && startBlock !== endBlock) {
updatedBlockMap = updateBlockMapLinks(updatedBlockMap, startBlock, endBlock, blockMap);
}
return contentState.merge({
blockMap: updatedBlockMap,
selectionBefore: selectionState,
selectionAfter: selectionState.merge({
anchorKey: startKey,
anchorOffset: startOffset,
focusKey: startKey,
focusOffset: startOffset,
isBackward: false
})
});
};
/**
* Maintain persistence for target list when removing characters on the
* head and tail of the character list.
*/
var removeFromList = function removeFromList(targetList, startOffset, endOffset) {
if (startOffset === 0) {
while (startOffset < endOffset) {
targetList = targetList.shift();
startOffset++;
}
} else if (endOffset === targetList.count()) {
while (endOffset > startOffset) {
targetList = targetList.pop();
endOffset--;
}
} else {
var head = targetList.slice(0, startOffset);
var tail = targetList.slice(endOffset);
targetList = head.concat(tail).toList();
}
return targetList;
};
module.exports = removeRangeFromContentState;