安装
把下面的代码贴进页面即可。单个 82KB 文件、零依赖运行。
<div id="typdit-editor"></div>
<script src="https://typdit.com/embed/v1.js"></script>
<script>
const editor = Typdit.create(document.getElementById('typdit-editor'), {
placeholder: 'Write here',
licenseKey: 'YOUR_KEY',
onChange: (snapshot) => console.log(snapshot),
});
</script>直接从编辑器页面带走
不必手写安装代码。在编辑器页面把模式和主题调成你想要的样子,往下滚动,就能看到照抄这些设置的安装代码。与默认值相同的选项不会写出来,所以一眼就能看清哪里做了特别设置。
点击连文档一起复制,屏幕上的内容会作为 snapshot 选项一并带上。适合打开模板看中了骨架、想连骨架一起带走的时候。
全部选项
全部通过 Typdit.create(element, options) 的第二个参数传入。所有字段均可省略,默认值见表。
| 选项 | 说明 |
|---|---|
snapshot | 初始文档(JSON 快照, 见下方格式一节)。省略则从空文档开始 |
theme: 'light' | 'dark' | 主题(默认 light) |
mode: 'notion' | Notion 风格预设: 隐藏工具栏并开启块手柄(见下文)。默认 classic |
handles: true | 单独开关块手柄(优先于 mode 预设) |
writingMode: 'paper' | 写作模式。可取 plain、focus、book、paper、essay、script、techdoc,或传入预设对象。排版、计量、目录、脚注与剧本流程会一起改变 |
writingMode: { id, extends } | 在内置模式之上只改几项,就成了自己的模式。不写 extends 则从什么都没打开的底座开始 |
writingLabels | 替换模式 UI 文案(计量名称、目录、脚注、目标)。可只写一部分,其余用英文默认值 |
writingSlashLabels | 模式专属斜杠条目的文案(脚注、剧本块) |
handleLabels | 替换块手柄文字(可只传部分键) |
toolbar: false | 隐藏顶部工具栏(默认显示) |
menus: false | 关闭斜杠命令与选中气泡菜单(默认开启) |
markdown: false | 关闭 # 标题、- 列表等 Markdown 快捷输入(默认开启) |
typography: false | 关闭弯引号、省略号自动替换(默认开启) |
placeholder | 空文档中显示的提示文字 |
autofocus: true | 创建后立即聚焦编辑器(默认关闭) |
toolbarLabels | 替换工具栏文字(可只传部分键, 见下文) |
licenseKey | Plus 许可密钥, 域名校验通过后去除水印 |
onChange(snapshot) | 内容每次变化时回调最新快照 |
onRequestImage() | 图片上传钩子。resolve {src, alt?} 即插入, null 为取消。未提供时工具栏回退为 URL 输入 |
onRequestFile() | 文件附件钩子, 返回 {src, name?, size?}。仅在提供此钩子时才渲染文件按钮 |
onRequestVideo() | 视频上传钩子, 返回 {src}。提供后视频链接输入行会多一个上传按钮(链接输入始终可用) |
Notion 风格模式
mode: 'notion' 会隐藏工具栏并开启块手柄。鼠标悬停在块上时, 左侧会出现添加(+)和菜单(⋮⋮)按钮; 菜单可上下移动、复制、删除块或转换块类型。排版仍可通过斜杠命令和选中气泡菜单完成。
没有工具栏时, 图片、视频、文件插入项也会从斜杠菜单中移除(其输入界面属于工具栏)。若既要手柄又要附件, 可同时传 toolbar: true, 手柄与工具栏可以共存。
Typdit.create(el, {
mode: 'notion',
handleLabels: { moveUp: '위로 이동', moveDown: '아래로 이동', delete: '삭제' },
});实例方法
用 Typdit.create() 返回的句柄读写文档, 存储完全由宿主站点负责。
| 方法 | 说明 |
|---|---|
getSnapshot() | 以 JSON 快照返回当前文档 |
setSnapshot(json) | 用快照整体替换文档(加载) |
getText() | 返回无格式纯文本(用于搜索索引、字数统计) |
focus() | 将焦点移入编辑器 |
destroy() | 移除编辑器并解除监听, SPA 卸载组件时调用 |
version | SDK 版本字符串(当前 1.0.0) |
快照格式
快照是可以原样存储、原样传回的扁平 JSON。text 的每一行与 blocks 的每一项一一对应, marks 是基于 text 的偏移区间。加载时会跳过未知的块和标记类型, 因此新版本写出的快照在旧版本中也能打开。
{
"version": 1,
"text": "제목\n첫 문단입니다.",
"blocks": [
{ "type": "heading", "attrs": { "level": 1 } },
{ "type": "paragraph" }
],
"marks": [
{ "type": "bold", "from": 3, "to": 7 }
]
}接上自动保存
onChange 每次击键都会触发。常见做法是加防抖, 停止输入后只向服务器发送一次。
let timer;
const editor = Typdit.create(el, {
onChange: (snapshot) => {
clearTimeout(timer);
timer = setTimeout(() => {
fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(snapshot),
});
}, 800);
},
});接入图片与文件上传
编辑器本身不上传文件。钩子负责选择与上传并返回 URL, 插入和渲染由编辑器完成。不提供钩子时图片仍可通过 URL 输入使用, 文件按钮仅在提供 onRequestFile 时出现。
Typdit.create(el, {
onRequestImage: async () => {
const file = await pickFile('image/*'); // 파일 선택 UI는 호스트 구현
if (!file) return null; // null = 취소
const src = await uploadToMyServer(file); // 업로드도 호스트 구현
return { src, alt: file.name };
},
onRequestFile: async () => {
const file = await pickFile('*/*');
if (!file) return null;
const src = await uploadToMyServer(file);
return { src, name: file.name, size: file.size };
},
onRequestVideo: async () => {
const file = await pickFile('video/*');
return file ? { src: await uploadToMyServer(file) } : null;
},
});修改工具栏文字
默认文字为英文。在 toolbarLabels 中只传要修改的键, 其余保持默认。全部键: text, quote, code, divider, table, image, video, file, upload, font, fontDefault, color, highlight, none, alignLeft, alignCenter, alignRight, imageUrl, videoUrl, link, linkApply, linkRemove, undo, redo。
Typdit.create(el, {
toolbarLabels: {
text: '본문', quote: '인용', code: '코드', divider: '구분선',
image: '이미지', video: '영상', link: '링크',
undo: '되돌리기', redo: '다시 실행',
},
});在 React 中使用
用 ref 拿到容器, 挂载时创建, 卸载时调用 destroy()。
import { useEffect, useRef } from 'react';
function TypditEditor({ onChange }) {
const ref = useRef(null);
useEffect(() => {
const editor = window.Typdit.create(ref.current, { onChange });
return () => editor.destroy();
}, []);
return <div ref={ref} />;
}在 Vue 中使用
在 onMounted 中创建, 在 onBeforeUnmount 中调用 destroy()。
<template><div ref="host"></div></template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
const host = ref(null);
let editor;
onMounted(() => { editor = window.Typdit.create(host.value, {}); });
onBeforeUnmount(() => editor?.destroy());
</script>在 SSR 框架(如 Next.js)中使用
脚本只在浏览器中运行。服务端渲染时没有 window, 请在仅客户端的组件里加载脚本后再创建。
'use client';
import { useEffect, useRef } from 'react';
export default function Editor() {
const ref = useRef(null);
useEffect(() => {
let editor;
const s = document.createElement('script');
s.src = 'https://typdit.com/embed/v1.js';
s.onload = () => { editor = window.Typdit.create(ref.current, {}); };
document.head.appendChild(s);
return () => editor?.destroy();
}, []);
return <div ref={ref} />;
}一页多个编辑器
每次调用 Typdit.create() 都会创建独立实例, 一页放多少个都可以。样式只在首次创建时注入一个 style 标签, 每个实例通过各自句柄的 destroy() 单独移除。
许可与水印
- 没有密钥时编辑器功能完整,底部会有小水印。
- Plus 订户在编辑器页的嵌入卡片按域名签发密钥。密钥在注册域名及其子域名生效(写成 *.example.com 含义相同)。
- 密钥有效期与签发时的订阅期一致,续订后请重新签发。
- 校验失败或网络不通都不会禁用编辑器, 只是水印保留, 也不会抛错。
网络与 CSP
- 文档内容不会被发送到任何地方。编辑器不为内容发起网络请求, 存储完全由宿主站点完成。
- 唯一的请求是设置 licenseKey 时的一次校验(只读, 目标 firestore.googleapis.com)。
- 使用 CSP 的站点在 script-src 放行 typdit.com, 若使用密钥再在 connect-src 放行 firestore.googleapis.com。正文中插入的图片和视频另算: 需在 img-src、frame-src 放行其来源(YouTube 嵌入用 youtube-nocookie.com)。
- 样式通过一个 style 标签注入, 类名以 td- 为前缀作用域隔离, 不会与宿主 CSS 混淆。
版本策略
/embed/v1.js 是锁定主版本的 URL。v1 URL 内保持向后兼容, 破坏兼容的变更会以新 URL(v2)发布。运行时可通过 Typdit.version 查看已加载版本。