Path: blob/master/src/packages/frontend/editors/slate/slate-react/utils/dom.ts
1698 views
/**1* Types.2*/34// COMPAT: This is required to prevent TypeScript aliases from doing some very5// weird things for Slate's types with the same name as globals. (2019/11/27)6// https://github.com/microsoft/TypeScript/issues/350027import DOMNode = globalThis.Node;8import DOMComment = globalThis.Comment;9import DOMElement = globalThis.Element;10import DOMText = globalThis.Text;11import DOMRange = globalThis.Range;12import DOMSelection = globalThis.Selection;13import DOMStaticRange = globalThis.StaticRange;1415export {16DOMNode,17DOMComment,18DOMElement,19DOMText,20DOMRange,21DOMSelection,22DOMStaticRange,23};2425export type DOMPoint = [Node, number];2627/**28* Check if a DOM node is a comment node.29*/3031export const isDOMComment = (value: any): value is DOMComment => {32return isDOMNode(value) && value.nodeType === 8;33};3435/**36* Check if a DOM node is an element node.37*/3839export const isDOMElement = (value: any): value is DOMElement => {40return isDOMNode(value) && value.nodeType === 1;41};4243/**44* Check if a value is a DOM node.45*/4647export const isDOMNode = (value: any): value is DOMNode => {48return value instanceof Node;49};5051/**52* Check if a DOM node is an element node.53*/5455export const isDOMText = (value: any): value is DOMText => {56return isDOMNode(value) && value.nodeType === 3;57};5859/**60* Checks whether a paste event is a plaintext-only event.61*/6263export const isPlainTextOnlyPaste = (event: ClipboardEvent) => {64return (65event.clipboardData &&66event.clipboardData.getData("text/plain") !== "" &&67event.clipboardData.types.length === 168);69};7071/**72* Normalize a DOM point so that it always refers to a text node.73*/7475export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => {76let [node, offset] = domPoint;7778// If it's an element node, its offset refers to the index of its children79// including comment nodes, so try to find the right text child node.80if (isDOMElement(node) && node.childNodes.length) {81const isLast = offset === node.childNodes.length;82const direction = isLast ? "backward" : "forward";83const index = isLast ? offset - 1 : offset;84node = getEditableChild(node, index, direction);8586// If the node has children, traverse until we have a leaf node. Leaf nodes87// can be either text nodes, or other void DOM nodes.88while (isDOMElement(node) && node.childNodes.length) {89const i = isLast ? node.childNodes.length - 1 : 0;90node = getEditableChild(node, i, direction);91}9293// Determine the new offset inside the text node.94offset = isLast && node.textContent != null ? node.textContent.length : 0;95}9697// Return the node and offset.98return [node, offset];99};100101/**102* Get the nearest editable child at `index` in a `parent`, preferring103* `direction`.104*/105106export const getEditableChild = (107parent: DOMElement,108index: number,109direction: "forward" | "backward"110): DOMNode => {111const { childNodes } = parent;112let child = childNodes[index];113let i = index;114let triedForward = false;115let triedBackward = false;116117// While the child is a comment node, or an element node with no children,118// keep iterating to find a sibling non-void, non-comment node.119while (120isDOMComment(child) ||121(isDOMElement(child) && child.childNodes.length === 0) ||122(isDOMElement(child) && child.getAttribute("contenteditable") === "false")123) {124if (triedForward && triedBackward) {125break;126}127128if (i >= childNodes.length) {129triedForward = true;130i = index - 1;131direction = "backward";132continue;133}134135if (i < 0) {136triedBackward = true;137i = index + 1;138direction = "forward";139continue;140}141142child = childNodes[i];143i += direction === "forward" ? 1 : -1;144}145146return child;147};148149/**150* Get a plaintext representation of the content of a node, accounting for block151* elements which get a newline appended.152*153* The domNode must be attached to the DOM.154*/155156export const getPlainText = (domNode: DOMNode) => {157let text = "";158159if (isDOMText(domNode) && domNode.nodeValue) {160return domNode.nodeValue;161}162163if (isDOMElement(domNode)) {164for (const childNode of Array.from(domNode.childNodes)) {165text += getPlainText(childNode);166}167168const display = getComputedStyle(domNode).getPropertyValue("display");169170if (display === "block" || display === "list" || domNode?.tagName === "BR") {171text += "\n";172}173}174175return text;176};177178179