使用Slate.js创建富文本编辑器

首先,

作为前端工程师,我首次参与了一个丰富的文本编辑器开发项目,我在这个项目中开始接触了一个叫做Slate.js的库。
我将会谈谈在开发过程中的一些经验以及对Slate.js的感受。

Slate.js 是什么?

官方 GitHub 请访问 https://github.com/ianstormtaylor/slate
官方文档请访问 https://docs.slatejs.org

Slate是一种基于React和Immutable构建的框架,可以使用React来开发WYSIWYG编辑器和Markdown编辑器等富文本编辑器。

由于 Slate.js 目前仍处于测试版阶段,存在可能进行破坏性更改的可能性。

斯莱特的优点

    • 公式のexamplesが豊富

 

    • history 管理がラク

 

    • Markdown エディタも作れる

 

    型を定義して開発するから安全に作れる(TypeScript の場合)

WYSIWYG(即所见即所得)是什么意思?

在计算机用户界面术语中,指的是通过在显示器上呈现与实际处理内容(特别是打印结果)相一致的技术。它是从What You See Is What You Get(所见即所得)的首字母缩写中衍生而来,也有时被称为去掉了”is”的WYSWYG(或称为“表示为所见”)。

与Markdown编辑器的对比

与在 Qiita 的编辑器和 README.md 文件中使用的 Markdown 格式不同,直接将输入的值显示在屏幕上的编辑器被称为所见即所得(WYSIWYG)编辑器。

使用Markdown语法给文档添加样式需要一定的学习成本,但一旦熟悉了使用方法,它易上手且受到许多工程师的喜爱。

与此相反,WYSIWYG 是一个直观易用的功能,它可以将输入的内容直接作为预览显示,并且还可以使用工具栏和快捷键进行装饰。不仅在Microsoft Word和Mac的备忘应用程序中使用,非技术人员也可以直观地使用它。

我特意进行了比较,发现许多所谓的所见即所得编辑器也使用了一些类似于Markdown的语法。

我能做什么

在HTML的textarea标签中,我们可以使用键盘输入将内容显示在屏幕上。然而,由于基本上是将其作为字符串处理,所以很难对其进行部分装饰。

如果是WYSIWYG编辑器,则可以使用工具栏或快捷键来部分加粗、斜体、用代码块包围文本、创建超链接等进行装饰。

此外,还可以根据输入的特定字符添加功能,例如添加#标签或@提及等。

此文稿中的装饰遵循了Qiita的Markdown语法进行记录。

可编辑的内容

如果在HTML的属性中添加contenteditable=”true”,无论是在div标签还是在p标签中,都可以进行编辑。在Slate.js中,可以基于contenteditable开发所见即所得的编辑器。

实践部分

我們將根據公式教程,用Slate.js開發一個編輯器。

完成状态

image.png

具有以下功能。

Ctrl + b のショートカットキーで文字を太くする
ツールバーの b(太字)をクリックすると文字を太くする

Ctrl + バッククウォート のショートカットキーでコードブロックを作成する
ツールバーの <>(コードブロック)をクリックするとコードブロックを作成する

Slate的版本

石板: 0.65.x
石板反应: 0.65.x

步骤一:你好世界

软件包安装

 yarn add slate slate-react
 yarn add -D @types/slate @types/slate-react

创建类型定义文件

如果使用TypeScript编写,必须创建类型定义文件。

 touch src/custom-types.d.ts
import { BaseEditor } from "slate";
import { ReactEditor } from "slate-react";

type CustomElement = { type: "paragraph"; children: CustomText[] };
type CustomText = { text: string };

declare module "slate" {
  interface CustomTypes {
    Editor: BaseEditor & ReactEditor;
    Element: CustomElement;
    Text: CustomText;
  }
}

创建Editor组件

创建一个用于编辑器用户界面的组件。

import React, { useMemo, useState } from "react";
import { createEditor, Descendant } from "slate";
import { Slate, Editable, withReact } from "slate-react";

export const Editor: React.FC = () => {
  const editor = useMemo(() => withReact(createEditor()), []);
  const [value, setValue] = useState<Descendant[]>(initialValue);

  const handleChange = (value: Descendant[]) => {
    setValue(value);
  };

  return (
    <div>
      <Slate value={value} editor={editor} onChange={handleChange}>
        <Editable />
      </Slate>
    </div>
  );
};

const initialValue: Descendant[] = [
  {
    type: "paragraph",
    children: [{ text: "Hello World!" }],
  },
];

在Slate组件中,需要三个props,分别为value、editor和onChange。
value中存储的值将在屏幕上显示,并且可以通过onChange进行更改。

我会建立一个开发服务器进行确认。

image.png

步骤2. 创建“代码块”。

在类型定义文件中添加一个名为“代码块”的内容

我会添加纯文本元素和代码块元素的类型。

export type ParagraphElement = { type: "paragraph"; children: CustomText[] };
export type CodeElement = { type: "code"; children: CustomText[] };

type CustomElement = ParagraphElement | CodeElement;
type CustomText = { text: string };

declare module "slate" {
  interface CustomTypes {
    Editor: BaseEditor & ReactEditor;
    Element: CustomElement;
    Text: CustomText;
  }
}

创建“普通文本”和“代码块”元素

创建一个组件来判断是明文还是代码块,并返回结果。

import { RenderElementProps } from "slate-react";

const ParagraphElement: React.FC<RenderElementProps> = (props) => {
  return <p {...props.attributes}>{props.children}</p>;
};

const CodeElement: React.FC<RenderElementProps> = (props) => {
  return (
    <pre {...props.attributes}>
      <code>{props.children}</code>
    </pre>
  );
};

export const Element: React.FC<RenderElementProps> = (props) => {
  switch (props.element.type) {
    case "code": {
      return <CodeElement {...props} />;
    }
    default: {
      return <ParagraphElement {...props} />;
    }
  }
};

从编辑器组件中调用元素

将 使用 useCallback 进行 memo 化的 Element 传递给可编辑的 renderElement。

export const Editor: React.FC = () => {
  const editor = useMemo(() => withReact(createEditor()), []);
  const renderElement = useCallback(
    (props: RenderElementProps) => <Element {...props} />,
    []
  );
  const [value, setValue] = useState<Descendant[]>(initialValue);

  const handleChange = (value: Descendant[]) => {
    setValue(value);
  };

  return (
    <div>
      <Slate value={value} editor={editor} onChange={handleChange}>
        <Editable renderElement={renderElement} />
      </Slate>
    </div>
  );
};
// 省略

我会检查代码块是否可行。
我将将 Editor.tsx 中 initialState 的 ‘paragraph’ 更改为 ‘code’。

image.png

你的代码已经被封装起来了,看起来很顺利。

步骤三:设置快捷键。

用 ctrl + 反引号键对代码进行块状化处理。

创建一个将函数块化的函数。

import { Editor, Transforms, Element } from "slate";

export function toggleCodeElement(editor: Editor): void {
  const [match] = Editor.nodes(editor, {
    match: (n) =>
      !Editor.isEditor(n) && Element.isElement(n) && n.type === "code",
  });
  Transforms.setNodes(
    editor,
    { type: match ? "paragraph" : "code" },
    { match: (n) => Editor.isBlock(editor, n) }
  );
}

我们将在可编辑的 onKeyDown 元素中编写根据键盘输入的处理。

export const Editor: React.FC = () => {
  // 省略

  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (event.ctrlKey) {
      switch (event.key) {
        // コードブロック
        case "`": {
          event.preventDefault();
          toggleCodeElement(editor);
          return;
        }
      }
    }
  };

  return (
    <div>
      <Slate value={value} editor={editor} onChange={handleChange}>
        <Editable renderElement={renderElement} onKeyDown={handleKeyDown} />
      </Slate>
    </div>
  );
};

现在可以通过按下ctrl + 反引号键来创建一个代码块了。

第四步:建立工具栏。

创建工具栏并创建代码块化按钮。由于代码块化的处理与按下 ctrl + 反引号时相同,因此将 editor 作为 props 传递。

export const Toolbar: React.FC<Props> = ({ editor }) => {
  const handleClickCodeButton = () => {
    toggleCodeElement(editor);
  };

  return (
    <div>
      <button onClick={handleClickCodeButton}>{"<>"}</button>
    </div>
  );
};

步骤5:添加“太字”。

接下来我们将添加一个将文字变为粗体的功能。基本上,只需要按照第3和4步的方式编写代码即可。在第3步中,我们将表现为块级元素的code标签添加到名为Element的组件中,而将要变为粗体的strong标签是内联元素,所以我们将其添加到名为Leaf的组件中。

创建并调用一个叶子组件。

import { RenderLeafProps } from "slate-react";

export const Leaf: React.FC<RenderLeafProps> = ({
  attributes,
  children,
  leaf,
}) => {
  return <span {...attributes}>{children}</span>;
};

export const Editor: React.FC = () => {
 // 省略
  const renderLeaf = useCallback(
    (props: RenderLeafProps) => <Leaf {...props} />,
    []
  );
// 省略

  return (
    <div>
      <Slate value={value} editor={editor} onChange={handleChange}>
        <Toolbar editor={editor} />
        <Editable
          renderElement={renderElement}
          renderLeaf={renderLeaf}
          onKeyDown={handleKeyDown}
        />
      </Slate>
    </div>
  );
};

接下来,在 custom-types.d.ts 文件中,向 CustomText 类型中添加一个属性来表示文本是否为粗体。

type CustomText = { text: string; bold?: boolean };

下面的代码是为了使 Leaf 组件中的文本加粗而编写的。
在某个处理过程中,根据文本的布尔值进行更改,如果为 true,则在文本中添加 strong 标签。

import { RenderLeafProps } from "slate-react";

export const Leaf: React.FC<RenderLeafProps> = ({
  attributes,
  children,
  leaf,
}) => {
  if (leaf.bold) {
    children = <strong>{children}</strong>;
  }
  return <span {...attributes}>{children}</span>;
};

我会创建一个将文字变为粗体的处理过程。

import { Editor } from "slate";

export function toggleMark(editor: Editor): void {
  const marks = Editor.marks(editor);
  const isActive = marks ? marks["bold"] === true : false;

  if (isActive) {
    Editor.removeMark(editor, "bold");
  } else {
    Editor.addMark(editor, "bold", true);
  }
}

使用快捷键和工具栏调用在上面创建的处理程序。

  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (event.ctrlKey) {
      switch (event.key) {
        // コードブロック
        case "`": {
          event.preventDefault();
          toggleCodeElement(editor);
          return;
        }
        // 太字
        case "b": {
          event.preventDefault();
          toggleMark(editor);
          return;
        }
      }
    }
  };
import React from "react";
import { Editor } from "slate";
import { toggleCodeElement } from "./toggleCodeElement";
import { toggleMark } from "./toggleMark";

type Props = {
  editor: Editor;
};

export const Toolbar: React.FC<Props> = ({ editor }) => {
  const handleClickCodeButton = () => {
    toggleCodeElement(editor);
  };

  const handleClickBoldButton = () => {
    toggleMark(editor);
  };

  return (
    <div>
      <button onClick={handleClickCodeButton}>{"<>"}</button>
      <button onClick={handleClickBoldButton}>b</button>
    </div>
  );
};

以上是以 Hands-on 形式介绍和开发 Slate.js 的结束。
我们创建的演示应用程序的存储库在这里。

最后

透過Slate.js可以在各種應用中輸入@提及、#標籤和URL,即使不使用Slate.js,也能實現連結功能。我了解到這種方法。

这次文章的反省之处是,由于我个人对HTML的知识(诸如DOM和Node等)了解不深,所以无法很好地用语言来描述和解释许多地方,我在撰写文章时一直感到沮丧。

由于我在前端学习方面还有很长的路要走,所以很可能会发布错误的信息。如果您发现了错误,请不吝指出或补充,我将非常感激。

bannerAds