Path: blob/master/src/packages/frontend/editors/slate/slate-react/components/element.tsx
1698 views
import React from "react";1import { useRef } from "react";2const getDirection = require("direction");3import { Editor, Node, Range, Element as SlateElement } from "slate";45import Text from "./text";6import Children from "./children";7import { ReactEditor, useSlateStatic, useReadOnly } from "..";8import { SelectedContext } from "../hooks/use-selected";9import { useIsomorphicLayoutEffect } from "../hooks/use-isomorphic-layout-effect";10import {11NODE_TO_ELEMENT,12ELEMENT_TO_NODE,13NODE_TO_PARENT,14NODE_TO_INDEX,15KEY_TO_ELEMENT,16} from "../utils/weak-maps";17import { RenderElementProps, RenderLeafProps } from "./editable";1819/**20* Element.21*/2223const Element = (props: {24decorations: Range[];25element: SlateElement;26renderElement?: React.FC<RenderElementProps>;27renderLeaf?: React.FC<RenderLeafProps>;28selection: Range | null;29}) => {30const {31decorations,32element,33renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,34renderLeaf,35selection,36} = props;37// console.log("renderElement", element);3839const ref = useRef<HTMLElement>(null);40const editor = useSlateStatic();41const readOnly = useReadOnly();42const key = ReactEditor.findKey(editor, element);4344// Update element-related weak maps with the DOM element ref.45useIsomorphicLayoutEffect(() => {46if (ref.current) {47KEY_TO_ELEMENT.set(key, ref.current);48NODE_TO_ELEMENT.set(element, ref.current);49ELEMENT_TO_NODE.set(ref.current, element);50} else {51KEY_TO_ELEMENT.delete(key);52NODE_TO_ELEMENT.delete(element);53}54});5556const isInline = editor.isInline(element);5758let children: React.JSX.Element | null = (59<Children60decorations={decorations}61node={element}62renderElement={renderElement}63renderLeaf={renderLeaf}64selection={selection}65/>66);6768// Attributes that the developer must mix into the element in their69// custom node renderer component.70const attributes: {71"data-slate-node": "element";72"data-slate-void"?: true;73"data-slate-inline"?: true;74contentEditable?: false;75dir?: "rtl";76ref: any;77} = {78"data-slate-node": "element",79ref,80};8182if (isInline) {83attributes["data-slate-inline"] = true;84}8586// If it's a block node with inline children, add the proper `dir` attribute87// for text direction.88if (!isInline && Editor.hasInlines(editor, element)) {89const text = Node.string(element);90const dir = getDirection(text);9192if (dir === "rtl") {93attributes.dir = dir;94}95}9697// If it's a void node, wrap the children in extra void-specific elements.98if (Editor.isVoid(editor, element)) {99attributes["data-slate-void"] = true;100101if (!readOnly && isInline) {102attributes.contentEditable = false;103}104105const Tag = isInline ? "span" : "div";106const [[text]] = Node.texts(element);107108children = readOnly ? null : (109<Tag110data-slate-spacer111style={{112height: "0",113color: "transparent",114outline: "none",115position: "absolute",116}}117>118<Text decorations={[]} isLast={false} parent={element} text={text} />119</Tag>120);121122NODE_TO_INDEX.set(text, 0);123NODE_TO_PARENT.set(text, element);124}125126return (127<SelectedContext.Provider value={!!selection}>128{React.createElement(renderElement, { attributes, children, element })}129</SelectedContext.Provider>130);131};132133const MemoizedElement = React.memo(Element, (prev, next) => {134const is_equal =135prev.element === next.element &&136prev.renderElement === next.renderElement &&137prev.renderLeaf === next.renderLeaf &&138isRangeListEqual(prev.decorations, next.decorations) &&139(prev.selection === next.selection ||140(!!prev.selection &&141!!next.selection &&142Range.equals(prev.selection, next.selection)));143// console.log("MemoizedElement", { is_equal, prev, next });144return is_equal;145});146147/**148* The default element renderer.149*/150151export const DefaultElement = (props: RenderElementProps) => {152const { attributes, children, element } = props;153const editor = useSlateStatic();154const Tag = editor.isInline(element) ? "span" : "div";155return (156<Tag {...attributes} style={{ position: "relative" }}>157{children}158</Tag>159);160};161162/**163* Check if a list of ranges is equal to another.164*165* PERF: this requires the two lists to also have the ranges inside them in the166* same order, but this is an okay constraint for us since decorations are167* kept in order, and the odd case where they aren't is okay to re-render for.168*/169170const isRangeListEqual = (list?: Range[], another?: Range[]): boolean => {171if (list == null || another == null) {172return list === another;173}174if (list.length !== another.length) {175return false;176}177178for (let i = 0; i < list.length; i++) {179const range = list[i];180const other = another[i];181182if (!Range.equals(range, other)) {183return false;184}185}186187return true;188};189190export default MemoizedElement;191192193