NABI NOTE
开源 WYSIWYG 编辑器
NABI NOTE 是一款开源 WYSIWYG 编辑器,它把格式、对齐、表格、上传等功能从核心中分离出来,作为名为 wing 的独立模块组成。你可以只选择需要的 wing 来组装编辑器,也可以连接自己制作的 wing。它用纯 JavaScript 编写,因此可以在 React 或 Vue 等框架中使用。没有构建环境的页面也可以通过 CDN 库 立即开始。它自己的文档格式 NABI TREE 即使在没有浏览器 DOM 的 Node.js 环境中也能转换为 HTML。输入的文档会按照已注册规则整理;生成 HTML 时会转义文本和属性,并检查链接地址。不过,自定义 wing 和从服务器接收的数据仍然需要按照应用的安全策略另行检查。CSS 变量和基于 rem 的布局可以调整主题和大小,也提供适合深色模式和多语言的字体设置。表格排序、本地历史以及 AI 辅助的文档工作,也都延续在同一个文档模型之上。
字体
无衬线(默认)、衬线、等宽、手写体都准备了按语言回退的字体。若没有适合所选字体类别的字体,就会显示浏览器默认字体。编辑器的默认字体由宿主页面决定。
下面可以查看各种字体在多种语言中如何显示。
산세리프 · ゴシック体 · 无衬线 · Serifenlos · Sans empattement · Sin serifa · Sem serifa · Без засечек · بلا زخارف · सैन्स सेरिफ़ · স্যান্স সেরিফ · سانس سیرف · Tanpa serif · بدون سریف · सॅन्स सेरिफ · Không chân · శాన్స్ సెరిఫ్ · Mara seref · Sans serif · Bila serif · சான்ஸ் செரிஃப் · ไร้เชิง · Senza grazie
세리프 · 明朝体 · 衬线 · Serif · Avec empattement · Con serifa · Com serifa · С засечками · بزخارف · सेरिफ़ · সেরিফ · سیرف · Berserif · سریف · सेरिफ · Có chân · సెరిఫ్ · Mai seref · Yenye serif · செரிஃப் · มีเชิง · Con grazie
고정폭 · 等幅 · 等宽 · Dicktengleich · Chasse fixe · Monoespaciada · Monoespaçada · Моноширинный · ثابت العرض · मोनोस्पेस · মনোস্পেস · یکساں چوڑائی · Lebar tetap · تکفاصله · Đơn cách · మోనోస్పేస్ · Tazara ɗaya · Eş aralıklı · Nafasi moja · ஒற்றையகலம் · ความกว้างคงที่ · Monospaziato · Monospace
필기체 · 筆記体 · 手写体 · Schreibschrift · Cursive · Cursiva · Рукописный · خط اليد · हस्तलिपि · হস্তলিপি · رواں خط · Tulisan tangan · دستنویس · हस्ताक्षर · Chữ viết tay · చేతిరాత · Rubutun hannu · El yazısı · Mwandiko · கையெழுத்து · ลายมือ · Corsivo
文字大小
非常小
小
大
非常大
标题
在空行输入 # 并按 Space,就会变成标题。
H1
H2
H3
H4
H5
H6
加粗 · 斜体 · 下划线 · 删除线
加粗 斜体 下划线 删除线可以一起应用在同一个字符上。
也可以叠加使用。
上下标
面积是 3.5m2,脚注可以这样添加。1
水是 H2O。
文字颜色 · 荧光笔
颜色经过配置,在浅色模式和深色模式下都易读。
文字颜色 绿色 · 珊瑚色 · 紫色 · 琥珀色 · 蓝色
荧光笔 黄色 · 浅绿 · 天蓝 · 粉色 · 紫色 · 橙色
链接
输入地址后会变成链接。
除了 http:// 或 https:// 地址,也可以使用以 . 或 / 开头的同站路径。javascript: 这样的地址不允许使用。
例如输入 https://nabi.saro.me 后按 Space 或 Enter,就会自动变成链接。
链接打开方式
文档只保存链接地址,链接如何打开由显示文档的页面决定。
附件链接
图片以外的文件会以下面的附件链接形式保留下来。
附件文件链接会保存到文档中。
对齐
左对齐
居中对齐
右对齐
标题不支持对齐设置。
列表
项目符号列表
在空行输入 - 并按 Space,就会变成项目符号列表。
这是项目符号列表。
可以用 Tab / Shift+Tab 缩进和取消缩进。
编号列表
在空行输入 1. 并按 Space,就会变成编号列表。
第一
第二
第三
任务列表
在空行输入 [ ] 或 [x] 并按 Space,就会变成任务列表。
已勾选的项目。
尚未勾选的项目。
表格
单元格可以容纳多个段落和块,并用背景色区分内容。试着直接编辑下表中的文字和图片。
资料 | 工作笔记 |
|---|---|
图片和说明放在同一个单元格中。 | 自由分段按Enter分段,或按Shift+Enter在同一段落内换行。 |
| 点击查看编辑提示单元格中也可以放入折叠块、引用、代码和视频。 合并单元格后,各单元格的内容仍按顺序保留。 |
点击单元格内部,会显示背景色和行列工具。选中多个单元格后,可以一起更改颜色和对齐方式。要调整整张表的位置,请先选择“选择整个表格”,再使用对齐工具。
包含颜色或多个块的表格,即使保存为Markdown,也会以HTML形式保留。
单元格背景色
这里列出了所有支持的单元格背景色。点击任意单元格更改颜色,或在属性工具中选择“默认”来清除背景色。
黄色 yellow | 绿色 green | 天蓝 cyan |
粉色 pink | 紫色 purple | 橙色 orange |
表格排序
点击 预览,再依次点击 库存 和 价格 表头。
型号 | 库存 | 价格 | 重量 |
|---|---|---|---|
NB-7 | 1,200 | 349 | 1.2 kg |
NB-9 | 20,000 | 99 | 0.9 kg |
NB-12 | 3,500 | 1,299 | 1.4 kg |
NB-80 | 900 | 8,900 | 2.1 kg |
NB-100 | 待定 | 12,999 | 2.4 kg |
价格列的值全部是数字,因此按数字排序。
库存列混有数字和文字,因此按字符串排序。
分隔线
输入 --- 后按 Enter,会变成分隔线。
图片
图片大小可从 30% 调整到 100%,也可以左对齐、居中或右对齐。
浮动区域
一目了然
可容纳文字、图片和表格的小型辅助区域。
位置 | 右侧 |
最大宽度 | 40% |
想在正文旁放置补充说明或小份资料时,可以使用浮动区域。在较宽的正文中,这段文字会绕排在区域旁边。点击区域内部,即可在同一个编辑器中修改文字,或插入图片和表格。
将光标放在区域内即可打开属性工具。切换左右位置,并从20%、30%、40%、50%中选择最大宽度,找到适合正文的比例。粘贴的表格和图片也会留在区域内。
也可以将选中的段落包在区域中。点击宽度旁的解除按钮,区域中的内容会保留在正文中。预览时隐藏编辑用虚线,在较窄的正文或打印时按单列排列。
YouTube
上传
试着把图片或文件拖到编辑器里。
此演示不会将文件发送到真实服务器。在应用中连接上传器后,即可将文件保存到服务器。
打开图片选择器或开始上传后,只移动光标仍会插入到最初的位置。如果等待期间编辑文档,会取消插入并显示提示。在单元格内开始的上传,其进度和结果也会留在该单元格内。
引用
在空行输入 > 并按 Space,就会变成引用。
可以使用多行。
代码
在空行输入 ``` 并按 Space 或 Enter,就会变成代码框。
像 ```java 这样输入语言名称并按 Space 或 Enter,就会变成指定了该语言的代码框。
import { createNabiWith, defaultWings } from 'nabi-note'
const { nabi } = createNabiWith(defaultWings)
const html = nabi.getHtml()折叠
折叠块由标题和内容组成。
可以按需要保存为折起或展开状态。
工具栏
标题、文字格式、对齐、列表、插入和文档工具按组排列。电脑端展开工具,移动端提供快捷工具和完整工具面板。所选表格或浮动区域的属性显示在工具栏下方。
快捷键
快速按两次Shift即可使用键盘选择工具。电脑端操作展开的工具栏,移动端操作完整工具面板。按Tab切换分组,用方向键选择工具,再按Enter执行。
在引用、代码、折叠块等块的末尾快速按两次Enter即可退出。默认连按间隔为180ms。
自动格式
示例 | 操作键 | 说明 |
|---|---|---|
# | 空格 | 标题 |
- | 空格 | 项目符号列表 |
1. | 空格 | 编号列表 |
[ ] · [x] | 空格 | 任务列表 |
> | 空格 | 引用 |
``` · ```ts | 空格 · Enter | 代码框 |
--- | Enter | 分隔线 |
https://… | 空格 · Enter | 链接 |
输出函数
函数 | 结果 |
|---|---|
getHtml() | HTML |
getJson() | JSON |
支持无 DOM 环境
将 NABI TREE 转换为 HTML 时,不需要 DOM。
服务器(Node.js)也可以读取保存的 NABI TREE,并按照相同规则生成 HTML。
移动环境
移动端 UI — 根据屏幕宽度排列编辑工具。
移动键盘调整 — 键盘打开时调整编辑区域的高度。
灵活尺寸 — 主要布局使用 rem 单位,适应放大和缩小。
多语言 — 可根据语言和文字方向设置区域语言和字体。
自定义
自定义 wing — 如果需要某项功能,可以自己制作并接入。
自定义 CSS — 可通过 --nabi-* 标记调整主要颜色、圆角和间距,以适配浅色和深色模式。
开源 — 在 GitHub 上公开源代码并共同开发。
查看文档 → nabi.saro.me
npm install nabi-noteimport {
createLocale, createNabiWith, mountSurface, mountToolbar,
mountContextToolbar, mountHints, watchSettle, makeTranslator,
defaultWings, mountUpload, mountUploadView, browserFileStore,
mountFile, mountViewTools, browserHistoryStorage, mountLocalHistory,
openHistoryPanel,
} from 'nabi-note'
import { mountDiffWing } from 'nabi-note/diff'
import { mountOutline } from 'nabi-note/viewer'
// The box that asks — without it the core answers "no" to everything (plug in your own)
const ask = { message: (t: string) => alert(t), confirm: (t: string) => confirm(t) }
const selected = [
...defaultWings,
]
const locale = createLocale('zh')
const { nabi, registry } = createNabiWith(selected, { ask, locale })
// Bring your own highlighter (Prism, highlight.js, Shiki, …) — the wing stays as it is
// and only the attach is swapped (`makeCodeAttach` ships from the same door)
// { ...codeWing, attach: makeCodeAttach({ highlight: (source, lang) => [{ text: source }] }) }
const root = document.querySelector('.nabi')!
const content = document.querySelector('.nabi-content')!
const view = mountUploadView({ nabi, surface: content, locale })
const upload = mountUpload({
nabi, root: content, locale,
// your upload goes here — report progress with task.onProgress(0–100)
uploader: async (task) => ({ uri: 'https://cdn.example/uploaded' }),
extensions: ['png', 'jpg', 'pdf'], maxFileSize: 10 * 1024 * 1024,
onStart: (tasks) => view.start(tasks),
onProgress: (id, percent) => view.progress(id, percent),
onSettle: () => view.settle(),
onDone: () => view.done(),
})
const file = mountFile({ nabi, registry, store: browserFileStore(document), locale, name: () => 'note' })
const history = mountLocalHistory({ nabi, storage: browserHistoryStorage(window) })
const diff = mountDiffWing({ nabi, registry, surface: content, locale })
// The locale also sets the direction — Arabic and Urdu run right to left
mountSurface({ nabi, registry, root: content, locale, fileSink: upload.take })
const trigger = document.createElement('button')
trigger.type = 'button'
trigger.textContent = makeTranslator(locale).t('outline')
trigger.style.cssText = 'position:fixed;right:1rem;bottom:1rem;z-index:1000'
root.append(trigger)
const outline = mountOutline(content, {
trigger, locale,
scrollContainer: () => root.classList.contains('is-fullscreen') ? root : window,
onChange: ({ items, activeIndex, available, open }) => {
trigger.hidden = !available
trigger.setAttribute("aria-expanded", String(open))
},
})
trigger.onclick = () => outline.toggle()
// Call outline.unmount() when removing the editor
const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale }
// A tool that needs a panel comes back to the host — without this the button is dead
const toolbar = mountToolbar({
...shared, root: document.querySelector('#toolbar')!,
file,
onFiles: upload.take,
onHost: (w) => {
if (w === 'diff') { diff.open(); return }
if (w !== 'localHistory') return
openHistoryPanel({
history, surface: content, locale, sessionId: history.sessionId,
render: (record) => createNabiWith(selected, { doc: JSON.parse(record.body) }).nabi.getHtml(),
})
},
})
const context = mountContextToolbar({ ...shared, root: document.querySelector('#context')! })
mountHints({ toolbar, context, root, surface: content })
// The preview and fullscreen buttons — they stand their own box at the end of the row
mountViewTools({ ...shared, root, container: document.querySelector('#toolbar')! })
// on every change — hook up your own code here
// nabi.onChange(() => user_callback(nabi.getHtml()))
// Call from your language selector. Content and editing history stay intact.
// locale.setLocale('ko')