翅膀
默认字体高度


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,就会变成编号列表。

  1. 第一

  2. 第二

  3. 第三

任务列表

在空行输入 [ ] 或 [x] 并按 Space,就会变成任务列表。

  • 已勾选的项目。

  • 尚未勾选的项目。



表格

单元格可以容纳多个段落和块,并用背景色区分内容。试着直接编辑下表中的文字和图片。

资料

工作笔记

图片和说明放在同一个单元格中。

自由分段

按Enter分段,或按Shift+Enter在同一段落内换行。

  • 更改单元格背景色

  • 在列表中按Tab缩进

  • 选择多个单元格并应用相同颜色

点击查看编辑提示

单元格中也可以放入折叠块、引用、代码和视频。

合并单元格后,各单元格的内容仍按顺序保留。

点击单元格内部,会显示背景色和行列工具。选中多个单元格后,可以一起更改颜色和对齐方式。要调整整张表的位置,请先选择“选择整个表格”,再使用对齐工具。

包含颜色或多个块的表格,即使保存为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

HTML正在加载编辑器…
JSONnabi-tree正在加载编辑器…
安装
npm install nabi-note
代码
import {
  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')