Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
262 lines
8.3 KiB
Plaintext
262 lines
8.3 KiB
Plaintext
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
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;
|