# slate

文档编辑器

在 Slate 编辑器中,renderElement 函数接收的 props 是由 Slate 编辑器根据当前的编辑状态(包括初始值或用户操作后的状态)生成的。这些 props 包含了 Slate 编辑器内部表示的元素的属性和子元素信息,这些信息被封装成 React 组件可以理解的格式。

具体来说,renderElement 函数接收到的 props 对象通常包含以下关键属性:

  1. attributes: 包含当前元素的 HTML 属性(如 style、className 等)。这些属性由 Slate 编辑器管理,并根据元素的类型和状态动态更新。
  2. children: 表示当前元素内部的文本或其他嵌套元素。在 Slate 中,文本和元素都被视为节点(Node),而 children 属性通常是一个数组,包含了当前元素下的所有子节点。
  3. element: 代表当前正在渲染的 Slate 元素节点。这个对象包含了元素的类型(type)和其他可能的属性,这些属性由您定义或根据编辑器的状态动态生成。 在您的代码中,renderElement 函数根据 element.type 来决定渲染哪种类型的元素。对于类型为 code 的元素,它使用 CodeElement 组件来渲染;对于其他类型的元素,则使用 DefaultElement 组件。

此外,initialValue 您提供的初始值是一个 Slate 可以理解的节点数组,当 Slate 编辑器初始化时,它会根据这个初始值构建内部的文档表示,并生成相应的 React 组件树来渲染这个文档。用户进行编辑操作时,Slate 编辑器会更新其内部状态,并触发 React 的重新渲染机制来更新用户界面。

总之,renderElement 函数中的 props 是由 Slate 编辑器根据当前的编辑状态(包括初始值和用户操作)生成的,它封装了 Slate 编辑器内部表示的元素的属性和子元素信息,以便在 React 组件中进行渲染。

// import logo from './logo.svg';
// import './App.css';

// Import React dependencies.
import React, { useState,useCallback } from 'react'
// Import the Slate editor factory.
import { createEditor,Transforms,Editor,Element } from 'slate'

// Import the Slate components and React plugin.
import { Slate, Editable, withReact } from 'slate-react'

const initialValue = [
  {
    type: 'paragraph',
    children: [{ text: 'A line of text in a paragraph.' },{ text: '测试用例' }],
  },
]

// Define a React component renderer for our code blocks.
const CodeElement = props => {
  return (
    <pre {...props.attributes}>
      <code>{props.children}</code>
    </pre>
  )
}

const DefaultElement = props => {
  console.log(props)
  console.log(props.children)
  return <p {...props.attributes}>{props.children}</p>
}

function App() {
  const [editor] = useState(() => withReact(createEditor()))

  const renderElement = useCallback(props => {
    console.log(props)
    switch (props.element.type) {
      case 'code':
        return <CodeElement {...props} />
      default:
        return <DefaultElement {...props} />
    }
  }, [])

  return (<Slate editor={editor} initialValue={initialValue} >
    <Editable 
      renderElement = {renderElement}
      onKeyDown={event => {
        console.log(event.key)
        if (event.key === '&') {
          // Prevent the ampersand character from being inserted.
          event.preventDefault()
          // Execute the `insertText` method when the event occurs.
          editor.insertText('and')
        }
         
     
        if (event.key === '`' && event.ctrlKey) {
          // Prevent the "`" from being inserted by default.
          event.preventDefault()
          // Otherwise, set the currently selected blocks type to "code".
          Transforms.setNodes(
            editor,
            { type: 'code' },
            { match: n => {
             console.log(n)
             console.log('isElement',Element.isElement(n))
             console.log('block',Editor.isBlock(editor, n))
             return  Element.isElement(n) && Editor.isBlock(editor, n)
            } }
          )
        }
      }}
    />
  </Slate>)
}

export default App;

# 更多的配置

// import logo from './logo.svg';
// import './App.css';

// Import React dependencies.
import React, { useState,useCallback } from 'react'
// Import the Slate editor factory.
import { createEditor,Transforms,Editor,Element } from 'slate'

// Import the Slate components and React plugin.
import { Slate, Editable, withReact } from 'slate-react'

const initialValue = [
  {
    type: 'paragraph',
    children: [{ text: 'A line of text in a paragraph.' },{ text: '测试用例' }],
  },
  {
    type: 'code',
    children: [{ text: 'code' }],
  },
  {
    type: 'custom-type',
    children: [{ text: 'A line of text in a paragraph.' },{ text: '测试用例' }],
  },
]



// Define a React component renderer for our code blocks.
const CodeElement = props => {
  return (
    <pre {...props.attributes}>
      <code>{props.children}</code>
    </pre>
  )
}

const DefaultElement = props => {
  // console.log(props)
  // console.log(props.children)
  return <p {...props.attributes}>{props.children}</p>
}

// Define a React component to render leaves with bold text.
const Leaf = props => {
  return (
    <span
      {...props.attributes}
      style={{ fontWeight: props.leaf.bold ? 'bold' : 'normal' }}
    >
      {props.children}
    </span>
  )
}

function App() {
  const [editor] = useState(() => withReact(createEditor()))
  const renderElement = useCallback(props => {
    // console.log(props)
    switch (props.element.type) {
      case 'code':
        return <CodeElement {...props} />
      case 'custom-type':
        return <span {...props.attributes}>{props.children}</span>
      default:
        return <DefaultElement {...props} />
    }
  }, [])

  // Define a leaf rendering function that is memoized with `useCallback`.
  const renderLeaf = useCallback(props => {
    return <Leaf {...props} />
  }, [])

  return (<Slate editor={editor} initialValue={initialValue} >
     <Editable
        renderElement={renderElement}
        renderLeaf={renderLeaf}
        onKeyDown={event => {
          if (!event.ctrlKey) {
            return
          }

          switch (event.key) {
            // When "`" is pressed, keep our existing code block logic.
            case '`': {
              event.preventDefault()
              const [match] = Editor.nodes(editor, {
                match: n => n.type === 'code',
              })
              Transforms.setNodes(
                editor,
                { type: match ? 'paragraph' : 'code' },
                { match: n => Editor.isBlock(editor, n) }
              )
              break
            }

            // When "B" is pressed, bold the text in the selection.
            case 'b': {
              event.preventDefault()
              Editor.addMark(editor, 'bold', true)
              break
            }
          }
        }}
      />
  </Slate>)
}

export default App;

# 自定义封装

// import logo from './logo.svg';
// import './App.css';

// Import React dependencies.
import React, { useState,useCallback } from 'react'
// Import the Slate editor factory.
import { createEditor,Transforms,Editor,Element } from 'slate'

// Import the Slate components and React plugin.
import { Slate, Editable, withReact } from 'slate-react'


const CustomEditor = {
  isBoldMarkActive(editor) {
    const marks = Editor.marks(editor)
    return marks ? marks.bold === true : false
  },

  isCodeBlockActive(editor) {
    const [match] = Editor.nodes(editor, {
      match: n => n.type === 'code',
    })

    return !!match
  },

  toggleBoldMark(editor) {
    const isActive = CustomEditor.isBoldMarkActive(editor)
    if (isActive) {
      Editor.removeMark(editor, 'bold')
    } else {
      Editor.addMark(editor, 'bold', true)
    }
  },

  toggleCodeBlock(editor) {
    const isActive = CustomEditor.isCodeBlockActive(editor)
    Transforms.setNodes(
      editor,
      { type: isActive ? null : 'code' },
      { match: n => Editor.isBlock(editor, n) }
    )
  },
}

const initialValue = [
  {
    type: 'paragraph',
    children: [{ text: 'A line of text in a paragraph.' },{ text: '测试用例' }],
  },
  {
    type: 'code',
    children: [{ text: 'code' }],
  },
  {
    type: 'custom-type',
    children: [{ text: 'A line of text in a paragraph.' },{ text: '测试用例' }],
  },
]



// Define a React component renderer for our code blocks.
const CodeElement = props => {
  return (
    <pre {...props.attributes}>
      <code>{props.children}</code>
    </pre>
  )
}

const DefaultElement = props => {
  // console.log(props)
  // console.log(props.children)
  return <p {...props.attributes}>{props.children}</p>
}

// Define a React component to render leaves with bold text.
const Leaf = props => {
  return (
    <span
      {...props.attributes}
      style={{ fontWeight: props.leaf.bold ? 'bold' : 'normal' }}
    >
      {props.children}
    </span>
  )
}

function App() {
  const [editor] = useState(() => withReact(createEditor()))
  const renderElement = useCallback(props => {
    // console.log(props)
    switch (props.element.type) {
      case 'code':
        return <CodeElement {...props} />
      case 'custom-type':
        return <span {...props.attributes}>{props.children}</span>
      default:
        return <DefaultElement {...props} />
    }
  }, [])

  // Define a leaf rendering function that is memoized with `useCallback`.
  const renderLeaf = useCallback(props => {
    return <Leaf {...props} />
  }, [])

  return (<Slate editor={editor} initialValue={initialValue} >
     <Editable
        renderElement={renderElement}
        renderLeaf={renderLeaf}
        onKeyDown={event => {
          if (!event.ctrlKey) {
            return
          }

          // Replace the `onKeyDown` logic with our new commands.
          switch (event.key) {
            case '`': {
              event.preventDefault()
              CustomEditor.toggleCodeBlock(editor)
              break
            }

            case 'b': {
              event.preventDefault()
              CustomEditor.toggleBoldMark(editor)
              break
            }
          }
        }}
      />
  </Slate>)
}

export default App;

# Editor

const pointBefore = Editor.before(editor, {
  path: [0, 0], // 例如,文档的第一个文本节点的路径
  offset: 5,    // 例如,想要获取该文本节点中第 5 个字符之前的位置
});

// pointBefore 应该表示第 5 个字符之前的位置,即第 4 个字符后的位置
// 因此,offset 应该是 4
console.log(pointBefore);
// 输出可能类似于:{ path: [0, 0], offset: 4 }

# Transforms

setTimeout(() => {
    Transforms.setSelection(editor, {
        anchor: { path: [editor.selection.focus.path[0] + 1, 0], offset: 0 },
        focus: { path: [editor.selection.focus.path[0] + 1, 0], offset: 1 },
    })
}, 1000);

# 插件扩展

为了避免手写A(B(C(D(E(F(someResult()))))))对插件的增强,可以采取以下代码去处理

type PluginProcessor<T = any> = (consumer: T) => T

const applyMiddleware = (processor: any, middleware: PluginProcessor) => {
  return middleware(processor)
}

export const createProcessorChain = (
  initialProcessor: any,
  middlewares: PluginProcessor[]
): any => {
  let processed = initialProcessor

  // 使用中间件链式处理初始处理器
  for (const middleware of middlewares) {
    processed = applyMiddleware(processed, middleware)
  }

  return processed
}

# slatejs自定义某个类型的插件如加载image



// Import React dependencies.
import React, { useState, useCallback } from "react";
// Import the Slate editor factory.
import { createEditor, Transforms, Editor, Element } from "slate";

// Import the Slate components and React plugin.
import { Slate, Editable, withReact } from "slate-react";

const initialValue = [
  {
    type: "paragraph",
    children: [
      { text: "A line of text in a paragraph." },
      { text: "测试用例" },
    ],
  },
  {
    type: "code",
    children: [{ text: "code" }],
  },
  {
    type: "custom-type",
    url: "https://weishell.github.io/img/logo.jpg",
    children: [
      { text: "A line of text in a paragraph.", type: "zero" },
      { text: "" },
      { text: "测试用例2", type: "zero" },
    ],
  },
];

// 这种写法子节点不会走leaf
// const CustomContainer = (props) => {
//   const { attributes, children, element } = props

//   // 处理混合内容
//   const processedChildren = element.children.map((child, index) => {
//     // 如果是zero类型,渲染普通文本
//     if (child.type === 'zero') {
//       return (
//         <span key={index} style={{ color: 'red' }}>
//           {child.text}
//         </span>
//       )
//     }

//     // 没有type且存在url时,渲染图片
//     if (!child.type && element.url) {
//       return (
//         <img
//           key={index}
//           src={element.url}
//           style={{ maxWidth: '100%', display: 'block', margin: '10px 0' }}
//           alt="custom"
//         />
//       )
//     }

//     // 默认情况
//     return <span key={index}>{child.text}</span>
//   })

//   return (
//     <div {...attributes} style={{ border: '2px dashed #ccc', padding: 10 }}>
//       {processedChildren}
//       {/* 保留Slate的children占位 */}
//       {children}
//     </div>
//   )
// }

// 这种写法子节点会走leaf
const CustomContainer = (props) => {
  const { attributes, children, element } = props;

  const childrenArray = React.Children.toArray(children);



  const processedChildren = childrenArray.map((child, index) => {
    // 判断是否是空文本节点

    if (element.children[index].text === "" && !element.children[index].type) {
      return (
        <img
          key={index}
          src={element.url}
          style={{ maxWidth: "100%", display: "block", margin: "10px 0" }}
          alt="custom"
        />
      );
    }

    return child;
  });

  return (
    <div {...attributes} style={{ border: "2px dashed #ccc", padding: 10 }}>
      {processedChildren}
    </div>
  );
};

// Define a React component renderer for our code blocks.
const CodeElement = (props) => {
  return (
    <pre {...props.attributes}>
      <code>{props.children}</code>
    </pre>
  );
};

const DefaultElement = (props) => {
  // console.log(props)
  // console.log(props.children)
  return <p {...props.attributes}>{props.children}</p>;
};

// Define a React component to render leaves with bold text.
const Leaf = (props) => {
  return (
    <span
      {...props.attributes}
      style={{ fontWeight: props.leaf.bold ? "bold" : "normal" }}
    >
      {props.children}--测试走leaf
    </span>
  );
};

function App() {
  const [editor] = useState(() => withReact(createEditor()));
  const renderElement = useCallback((props) => {
    // console.log(props)
    switch (props.element.type) {
      case "code":
        return <CodeElement {...props} />;
      case "custom-type":
        return <CustomContainer {...props} />;
      default:
        return <DefaultElement {...props} />;
    }
  }, []);

  // Define a leaf rendering function that is memoized with `useCallback`.
  const renderLeaf = useCallback((props) => {
    return <Leaf {...props} />;
  }, []);

  return (
    <Slate editor={editor} initialValue={initialValue}>
      <Editable
        renderElement={renderElement}
        renderLeaf={renderLeaf}
        onKeyDown={(event) => {
          if (!event.ctrlKey) {
            return;
          }

          switch (event.key) {
            // When "`" is pressed, keep our existing code block logic.
            case "`": {
              event.preventDefault();
              const [match] = Editor.nodes(editor, {
                match: (n) => n.type === "code",
              });
              Transforms.setNodes(
                editor,
                { type: match ? "paragraph" : "code" },
                { match: (n) => Editor.isBlock(editor, n) }
              );
              break;
            }

            // When "B" is pressed, bold the text in the selection.
            case "b": {
              event.preventDefault();
              Editor.addMark(editor, "bold", true);
              break;
            }
          }
        }}
      />
    </Slate>
  );
}

export default App;

renderElement 的优化很重要,否则会触发很多次的无效调用.

资料1 (opens new window)

资料2 (opens new window)

资料3 (opens new window)

plate (opens new window)

最后更新: 8/24/2025, 12:13:25 PM