@lexical/mark
Version:
This package contains helpers and nodes for wrapping content in marks for Lexical.
262 lines (250 loc) • 8.47 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
;
var lexical = require('lexical');
var utils = require('@lexical/utils');
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const NO_IDS = [];
/** @noInheritDoc */
class MarkNode extends lexical.ElementNode {
/** @internal */
static getType() {
return 'mark';
}
static clone(node) {
return new MarkNode(node.__ids, node.__key);
}
static importDOM() {
return null;
}
static importJSON(serializedNode) {
return $createMarkNode().updateFromJSON(serializedNode);
}
updateFromJSON(serializedNode) {
return super.updateFromJSON(serializedNode).setIDs(serializedNode.ids);
}
exportJSON() {
return {
...super.exportJSON(),
ids: this.getIDs()
};
}
constructor(ids = NO_IDS, key) {
super(key);
this.__ids = ids;
}
createDOM(config) {
const element = document.createElement('mark');
utils.addClassNamesToElement(element, config.theme.mark);
if (this.__ids.length > 1) {
utils.addClassNamesToElement(element, config.theme.markOverlap);
}
return element;
}
updateDOM(prevNode, element, config) {
const prevIDs = prevNode.__ids;
const nextIDs = this.__ids;
const prevIDsCount = prevIDs.length;
const nextIDsCount = nextIDs.length;
const overlapTheme = config.theme.markOverlap;
if (prevIDsCount !== nextIDsCount) {
if (prevIDsCount === 1) {
if (nextIDsCount === 2) {
utils.addClassNamesToElement(element, overlapTheme);
}
} else if (nextIDsCount === 1) {
utils.removeClassNamesFromElement(element, overlapTheme);
}
}
return false;
}
hasID(id) {
return this.getIDs().includes(id);
}
getIDs() {
return Array.from(this.getLatest().__ids);
}
setIDs(ids) {
const self = this.getWritable();
self.__ids = ids;
return self;
}
addID(id) {
const self = this.getWritable();
return self.__ids.includes(id) ? self : self.setIDs([...self.__ids, id]);
}
deleteID(id) {
const self = this.getWritable();
const idx = self.__ids.indexOf(id);
if (idx === -1) {
return self;
}
const ids = Array.from(self.__ids);
ids.splice(idx, 1);
return self.setIDs(ids);
}
insertNewAfter(selection, restoreSelection = true) {
const markNode = $createMarkNode(this.__ids);
this.insertAfter(markNode, restoreSelection);
return markNode;
}
canInsertTextBefore() {
return false;
}
canInsertTextAfter() {
return false;
}
canBeEmpty() {
return false;
}
isInline() {
return true;
}
extractWithChild(child, selection, destination) {
if (!lexical.$isRangeSelection(selection) || destination === 'html') {
return false;
}
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isBackward = selection.isBackward();
const selectionLength = isBackward ? anchor.offset - focus.offset : focus.offset - anchor.offset;
return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && this.getTextContent().length === selectionLength;
}
excludeFromCopy(destination) {
return destination !== 'clone';
}
}
function $createMarkNode(ids = NO_IDS) {
return lexical.$applyNodeReplacement(new MarkNode(ids));
}
function $isMarkNode(node) {
return node instanceof MarkNode;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
function $unwrapMarkNode(node) {
const children = node.getChildren();
let target = null;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (target === null) {
node.insertBefore(child);
} else {
target.insertAfter(child);
}
target = child;
}
node.remove();
}
function $wrapSelectionInMarkNode(selection, isBackward, id, createNode) {
// Force a forwards selection since append is used, ignore the argument.
// A new selection is used to avoid side-effects of flipping the given
// selection
const forwardSelection = lexical.$createRangeSelection();
const [startPoint, endPoint] = selection.isBackward() ? [selection.focus, selection.anchor] : [selection.anchor, selection.focus];
forwardSelection.anchor.set(startPoint.key, startPoint.offset, startPoint.type);
forwardSelection.focus.set(endPoint.key, endPoint.offset, endPoint.type);
let currentNodeParent;
let lastCreatedMarkNode;
// Note that extract will split text nodes at the boundaries
const nodes = forwardSelection.extract();
// We only want wrap adjacent text nodes, line break nodes
// and inline element nodes. For decorator nodes and block
// element nodes, we step out of their boundary and start
// again after, if there are more nodes.
for (const node of nodes) {
if (lexical.$isElementNode(lastCreatedMarkNode) && lastCreatedMarkNode.isParentOf(node)) {
// If the current node is a child of the last created mark node, there is nothing to do here
continue;
}
let targetNode = null;
if (lexical.$isTextNode(node)) {
// Case 1: The node is a text node and we can include it
targetNode = node;
} else if ($isMarkNode(node)) {
// Case 2: the node is a mark node and we can ignore it as a target,
// moving on to its children. Note that when we make a mark inside
// another mark, it may ultimately be unnested by a call to
// `registerNestedElementResolver<MarkNode>` somewhere else in the
// codebase.
continue;
} else if ((lexical.$isElementNode(node) || lexical.$isDecoratorNode(node)) && node.isInline()) {
// Case 3: inline element/decorator nodes can be added in their entirety
// to the new mark
targetNode = node;
}
if (targetNode !== null) {
// Now that we have a target node for wrapping with a mark, we can run
// through special cases.
if (targetNode && targetNode.is(currentNodeParent)) {
// The current node is a child of the target node to be wrapped, there
// is nothing to do here.
continue;
}
const parentNode = targetNode.getParent();
if (parentNode == null || !parentNode.is(currentNodeParent)) {
// If the parent node is not the current node's parent node, we can
// clear the last created mark node.
lastCreatedMarkNode = undefined;
}
currentNodeParent = parentNode;
if (lastCreatedMarkNode === undefined) {
// If we don't have a created mark node, we can make one
const createMarkNode = createNode || $createMarkNode;
lastCreatedMarkNode = createMarkNode([id]);
targetNode.insertBefore(lastCreatedMarkNode);
}
// Add the target node to be wrapped in the latest created mark node
lastCreatedMarkNode.append(targetNode);
} else {
// If we don't have a target node to wrap we can clear our state and
// continue on with the next node
currentNodeParent = undefined;
lastCreatedMarkNode = undefined;
}
}
// Make selection collapsed at the end
if (lexical.$isElementNode(lastCreatedMarkNode)) {
// eslint-disable-next-line no-unused-expressions
isBackward ? lastCreatedMarkNode.selectStart() : lastCreatedMarkNode.selectEnd();
}
}
function $getMarkIDs(node, offset) {
let currentNode = node;
while (currentNode !== null) {
if ($isMarkNode(currentNode)) {
return currentNode.getIDs();
} else if (lexical.$isTextNode(currentNode) && offset === currentNode.getTextContentSize()) {
const nextSibling = currentNode.getNextSibling();
if ($isMarkNode(nextSibling)) {
return nextSibling.getIDs();
}
}
currentNode = currentNode.getParent();
}
return null;
}
exports.$createMarkNode = $createMarkNode;
exports.$getMarkIDs = $getMarkIDs;
exports.$isMarkNode = $isMarkNode;
exports.$unwrapMarkNode = $unwrapMarkNode;
exports.$wrapSelectionInMarkNode = $wrapSelectionInMarkNode;
exports.MarkNode = MarkNode;