gzkx-editor
v0.0.3
Published
Structured WYSIWYM editor
Readme
GZKX Editor 使用指南
简介
GZKX Editor 是一个基于 ProseMirror 的富文本编辑器,提供了模块化的编辑组件,支持协同编辑、自定义文档结构等功能。
安装
npm install gzkx-editor-model gzkx-editor-state gzkx-editor-view gzkx-editor-commands gzkx-editor-schema-basic gzkx-editor-schema-list gzkx-editor-example-setup或使用完整套装:
npm install gzkx-editor-example-setup核心模块
1. gzkx-editor-model
文档模型层,定义了文档的数据结构。
主要导出:
Node- 文档节点Fragment- 文档片段Slice- 文档切片(用于跨编辑器传递内容)Mark- 标记(如加粗、斜体等)Schema- 文档模式,定义可用的节点和标记类型DOMParser- 将 DOM 解析为文档DOMSerializer- 将文档序列化为 DOM
2. gzkx-editor-state
编辑器状态管理层。
主要导出:
EditorState- 编辑器状态Transaction- 状态变更事务Plugin- 编辑器插件
3. gzkx-editor-view
编辑器视图层,负责渲染和交互。
主要导出:
EditorView- 编辑器视图组件
4. gzkx-editor-commands
编辑命令集。
主要导出:
toggleMark- 切换标记setBlockType- 设置块类型wrapIn- 包裹节点baseKeymap- 基础快捷键绑定
5. gzkx-editor-schema-basic
基础文档模式,提供常用节点和标记。
包含:
- 段落 (paragraph)
- 标题 (heading, 1-6级)
- 代码块 (code_block)
- 引用 (blockquote)
- 水平线 (horizontal_rule)
- 图片 (image)
- 链接 (link)
- 加粗 (strong)
- 斜体 (em)
- 代码 (code)
6. gzkx-editor-schema-list
列表相关节点和命令。
包含:
- 有序列表 (ordered_list)
- 无序列表 (bullet_list)
- 列表项 (list_item)
快速开始
方式一:使用 exampleSetup(推荐)
import { Schema, DOMParser } from 'gzkx-editor-model';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { schema } from 'gzkx-editor-schema-basic';
import { addListNodes } from 'gzkx-editor-schema-list';
import { exampleSetup } from 'gzkx-editor-example-setup';
// 创建带列表支持的模式
const demoSchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
});
// 创建初始状态
const state = EditorState.create({
doc: DOMParser.fromSchema(demoSchema).parse(document.querySelector("#content")),
plugins: exampleSetup({ schema: demoSchema })
});
// 创建编辑器视图
const view = new EditorView(document.querySelector(".editor"), {
state
});方式二:手动配置
import { keymap } from 'gzkx-editor-keymap';
import { history } from 'gzkx-editor-history';
import { dropCursor } from 'gzkx-editor-dropcursor';
import { gapCursor } from 'gzkx-editor-gapcursor';
import { baseKeymap, toggleMark, setBlockType } from 'gzkx-editor-commands';
import { buildInputRules } from 'gzkx-editor-inputrules';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { Schema, DOMParser } from 'gzkx-editor-model';
const mySchema = new Schema({
nodes: {
doc: { content: 'block+' },
paragraph: { group: 'block', parseDOM: [{ tag: 'p' }] },
text: { group: 'inline' },
strong: { parseDOM: [{ tag: 'strong' }, { tag: 'b' }] },
em: { parseDOM: [{ tag: 'em' }, { tag: 'i' }] }
}
});
const state = EditorState.create({
doc: DOMParser.fromSchema(mySchema).parse(document.getElementById('content')),
plugins: [
buildInputRules(mySchema),
keymap(buildKeymap(mySchema)),
keymap(baseKeymap),
dropCursor(),
gapCursor(),
history()
]
});
const view = new EditorView(document.querySelector('.editor'), { state });常用功能
获取编辑器内容
// 获取 JSON 格式的文档
const doc = view.state.doc.toJSON();
// 获取 HTML 字符串
import { DOMSerializer } from 'gzkx-editor-model';
const div = document.createElement('div');
const fragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(view.state.doc.content);
div.appendChild(fragment);
const html = div.innerHTML;修改编辑器内容
// 通过事务修改
const tr = view.state.tr;
tr.insertText('Hello, GZKX Editor!', 10);
view.dispatch(tr);监听变化
const view = new EditorView(document.querySelector('.editor'), {
state: initialState,
dispatchTransaction(transaction) {
const newState = this.state.apply(transaction);
this.updateState(newState);
// 检测文档变化
if (transaction.docChanged) {
console.log('文档已更新');
}
}
});快捷键
| 快捷键 | 功能 | |--------|------| | Ctrl+B / Cmd+B | 加粗 | | Ctrl+I / Cmd+I | 斜体 | | Ctrl+Shift+X | 行内代码 | | Ctrl+Z | 撤销 | | Ctrl+Shift+Z | 重做 | | Tab | 缩进 | | Shift+Tab | 取消缩进 |
自定义菜单
import { buildMenuItems } from 'gzkx-editor-example-setup';
const menuItems = buildMenuItems(schema);
// 自定义菜单内容
const plugins = exampleSetup({
schema,
menuContent: menuItems.fullMenu
});自定义样式
引入 CSS:
<link rel="stylesheet" href="gzkx-editor-view/style/prosemirror.css">
<link rel="stylesheet" href="gzkx-editor-menu/style/menu.css">
<link rel="stylesheet" href="gzkx-editor-gapcursor/style/gapcursor.css">React 使用方法
GZKX Editor 可以通过 React 封装组件来使用,下面介绍两种封装方式。
方式一:使用 ref 和 useEffect(类组件风格)
import React, { useEffect, useRef, useState } from 'react';
import { Schema, DOMParser, DOMSerializer } from 'gzkx-editor-model';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { schema } from 'gzkx-editor-schema-basic';
import { addListNodes } from 'gzkx-editor-schema-list';
import { exampleSetup } from 'gzkx-editor-example-setup';
import 'gzkx-editor-view/style/prosemirror.css';
import 'gzkx-editor-menu/style/menu.css';
const Editor = ({ initialContent = '', onChange }) => {
const editorRef = useRef(null);
const viewRef = useRef(null);
useEffect(() => {
if (!editorRef.current) return;
// 创建 Schema
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
});
// 解析初始内容
const content = document.createElement('div');
content.innerHTML = initialContent || '<p></p>';
// 创建初始状态
const state = EditorState.create({
doc: DOMParser.fromSchema(mySchema).parse(content),
plugins: exampleSetup({ schema: mySchema })
});
// 创建编辑器视图
const view = new EditorView(editorRef.current, {
state,
dispatchTransaction(transaction) {
const newState = view.state.apply(transaction);
view.updateState(newState);
// 触发内容变化回调
if (transaction.docChanged && onChange) {
const serializer = DOMSerializer.fromSchema(mySchema);
const div = document.createElement('div');
serializer.serializeFragment(newState.doc.content, { document: div });
onChange(div.innerHTML, newState.doc.toJSON());
}
}
});
viewRef.current = view;
// 清理函数
return () => {
view.destroy();
};
}, []);
return <div ref={editorRef} className="gzkx-editor" />;
};
// 使用示例
const App = () => {
const handleChange = (html, json) => {
console.log('HTML:', html);
console.log('JSON:', json);
};
return (
<div>
<h1>我的编辑器</h1>
<Editor
initialContent="<p>初始内容</p>"
onChange={handleChange}
/>
</div>
);
};
export default App;方式二:封装为 React 组件(Hooks 版本)
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import { Schema, DOMParser, DOMSerializer } from 'gzkx-editor-model';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { schema } from 'gzkx-editor-schema-basic';
import { addListNodes } from 'gzkx-editor-schema-list';
import { exampleSetup } from 'gzkx-editor-example-setup';
import 'gzkx-editor-view/style/prosemirror.css';
import 'gzkx-editor-menu/style/menu.css';
const GZKXEditor = ({
value = '',
onChange,
placeholder = '请输入内容...',
readOnly = false,
className = ''
}) => {
const editorRef = useRef(null);
const viewRef = useRef(null);
// 创建 Schema
const editorSchema = useMemo(() => {
return new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
});
}, []);
// 获取 HTML 内容
const getContentHTML = useCallback((doc) => {
const div = document.createElement('div');
const serializer = DOMSerializer.fromSchema(editorSchema);
serializer.serializeFragment(doc.content, { document: div });
return div.innerHTML;
}, [editorSchema]);
// 获取 JSON 内容
const getContentJSON = useCallback((doc) => {
return doc.toJSON();
}, []);
useEffect(() => {
if (!editorRef.current) return;
// 解析初始内容
const content = document.createElement('div');
content.innerHTML = value || '<p></p>';
// 创建初始状态
const state = EditorState.create({
doc: DOMParser.fromSchema(editorSchema).parse(content),
plugins: exampleSetup({ schema: editorSchema })
});
// 创建编辑器视图
const view = new EditorView(editorRef.current, {
state,
editable() {
return !readOnly;
},
dispatchTransaction(transaction) {
const newState = view.state.apply(transaction);
view.updateState(newState);
if (transaction.docChanged && onChange) {
onChange({
html: getContentHTML(newState.doc),
json: getContentJSON(newState.doc)
});
}
}
});
viewRef.current = view;
return () => {
view.destroy();
};
}, [editorSchema]);
// 更新内容(外部控制)
useEffect(() => {
if (!viewRef.current) return;
const currentHTML = getContentHTML(viewRef.current.state.doc);
if (value !== currentHTML && value !== undefined) {
const content = document.createElement('div');
content.innerHTML = value;
const newDoc = DOMParser.fromSchema(editorSchema).parse(content);
const tr = viewRef.current.state.tr.replaceWith(0, viewRef.current.state.doc.content.size, newDoc.content);
viewRef.current.dispatch(tr);
}
}, [value, editorSchema, getContentHTML]);
return (
<div
ref={editorRef}
className={`gzkx-editor ${className}`}
data-placeholder={placeholder}
/>
);
};
export default GZKXEditor;使用示例
import React, { useState } from 'react';
import GZKXEditor from './GZKXEditor';
const App = () => {
const [content, setContent] = useState({
html: '<p>初始内容</p>',
json: null
});
return (
<div className="app">
<h1>GZKX Editor in React</h1>
<GZKXEditor
value={content.html}
onChange={setContent}
placeholder="输入文章内容..."
/>
<div className="preview">
<h2>预览</h2>
<div dangerouslySetInnerHTML={{ __html: content.html }} />
</div>
<div className="json-view">
<h2>JSON 数据</h2>
<pre>{JSON.stringify(content.json, null, 2)}</pre>
</div>
</div>
);
};
export default App;样式调整
添加自定义样式:
/* Editor.css */
.gzkx-editor {
border: 1px solid #d1d5db;
border-radius: 8px;
min-height: 300px;
padding: 16px;
}
.gzkx-editor:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* 占位符样式 */
.gzkx-editor:empty::before {
content: attr(data-placeholder);
color: #9ca3af;
pointer-events: none;
}
/* ProseMirror 样式覆盖 */
.ProseMirror {
outline: none;
min-height: 200px;
}
.ProseMirror p.is-empty::before {
content: attr(data-placeholder);
color: #9ca3af;
float: left;
height: 0;
pointer-events: none;
}完整 React 项目示例
// EditorComponent.jsx
import React, { useEffect, useRef, useState } from 'react';
import {
Schema,
DOMParser,
DOMSerializer
} from 'gzkx-editor-model';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { schema } from 'gzkx-editor-schema-basic';
import { addListNodes } from 'gzkx-editor-schema-list';
import { exampleSetup } from 'gzkx-editor-example-setup';
import 'gzkx-editor-view/style/prosemirror.css';
import 'gzkx-editor-menu/style/menu.css';
import './EditorComponent.css';
const EditorComponent = ({
initialValue = '<p></p>',
onChange,
editable = true
}) => {
const containerRef = useRef(null);
const viewRef = useRef(null);
const schemaRef = useRef(null);
// 初始化 Schema
if (!schemaRef.current) {
schemaRef.current = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
});
}
useEffect(() => {
if (!containerRef.current) return;
// 解析初始内容
const tempDiv = document.createElement('div');
tempDiv.innerHTML = initialValue;
// 创建状态
const state = EditorState.create({
doc: DOMParser.fromSchema(schemaRef.current).parse(tempDiv),
plugins: exampleSetup({ schema: schemaRef.current })
});
// 创建视图
viewRef.current = new EditorView(containerRef.current, {
state,
editable() {
return editable;
},
dispatchTransaction(transaction) {
const newState = viewRef.current.state.apply(transaction);
viewRef.current.updateState(newState);
if (transaction.docChanged && onChange) {
const div = document.createElement('div');
const serializer = DOMSerializer.fromSchema(schemaRef.current);
serializer.serializeFragment(newState.doc.content, {
document: div
});
onChange({
html: div.innerHTML,
json: newState.doc.toJSON()
});
}
}
});
return () => {
if (viewRef.current) {
viewRef.current.destroy();
}
};
}, [initialValue, editable]);
return <div ref={containerRef} className="gzkx-editor-container" />;
};
export default EditorComponent;Next.js中使用
在 Next.js 中使用时,需要注意动态导入避免 SSR 问题:
// components/Editor.jsx
import dynamic from 'next/dynamic';
const EditorComponent = dynamic(() => import('./EditorComponent'), {
ssr: false,
loading: () => <div>加载中...</div>
});
export default function Editor({ initialValue, onChange }) {
return (
<EditorComponent
initialValue={initialValue}
onChange={onChange}
/>
);
}在 TypeScript 中使用
// types/editor.ts
import { Node as ProseMirrorNode, Schema } from 'gzkx-editor-model';
export interface EditorContent {
html: string;
json: ProseMirrorNode;
}
export interface EditorProps {
value?: string;
onChange?: (content: EditorContent) => void;
placeholder?: string;
readOnly?: boolean;
className?: string;
}
// Editor.tsx
import React, { useEffect, useRef } from 'react';
import { Schema, DOMParser, DOMSerializer, Node } from 'gzkx-editor-model';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { schema } from 'gzkx-editor-schema-basic';
import { addListNodes } from 'gzkx-editor-schema-list';
import { exampleSetup } from 'gzkx-editor-example-setup';
import { EditorProps, EditorContent } from '../types/editor';
export const GZKXEditor: React.FC<EditorProps> = ({
value = '<p></p>',
onChange,
placeholder = '请输入内容...',
readOnly = false,
className = ''
}) => {
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
const editorSchema = React.useMemo(() => {
return new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
});
}, []);
useEffect(() => {
if (!editorRef.current) return;
const content = document.createElement('div');
content.innerHTML = value;
const state = EditorState.create({
doc: DOMParser.fromSchema(editorSchema).parse(content),
plugins: exampleSetup({ schema: editorSchema })
});
viewRef.current = new EditorView(editorRef.current, {
state,
editable() {
return !readOnly;
},
dispatchTransaction(transaction) {
const newState = viewRef.current!.state.apply(transaction);
viewRef.current!.updateState(newState);
if (transaction.docChanged && onChange) {
const div = document.createElement('div');
const serializer = DOMSerializer.fromSchema(editorSchema);
serializer.serializeFragment(newState.doc.content, {
document: div
});
onChange({
html: div.innerHTML,
json: newState.doc.toJSON()
});
}
}
});
return () => {
viewRef.current?.destroy();
};
}, [editorSchema, value, readOnly]);
return (
<div
ref={editorRef}
className={`gzkx-editor ${className}`}
data-placeholder={placeholder}
/>
);
};完整示例(原生 JS)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>GZKX Editor 示例</title>
<link rel="stylesheet" href="node_modules/gzkx-editor-view/style/prosemirror.css">
<link rel="stylesheet" href="node_modules/gzkx-editor-menu/style/menu.css">
<style>
.editor { border: 1px solid #ccc; min-height: 300px; }
</style>
</head>
<body>
<div id="editor" class="editor"></div>
<script type="module">
import { Schema, DOMParser } from 'gzkx-editor-model';
import { EditorView } from 'gzkx-editor-view';
import { EditorState } from 'gzkx-editor-state';
import { schema } from 'gzkx-editor-schema-basic';
import { addListNodes } from 'gzkx-editor-schema-list';
import { exampleSetup } from 'gzkx-editor-example-setup';
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
});
const content = document.createElement('div');
content.innerHTML = '<p>欢迎使用 GZKX Editor!</p>';
const state = EditorState.create({
doc: DOMParser.fromSchema(mySchema).parse(content),
plugins: exampleSetup({ schema: mySchema })
});
const view = new EditorView(document.getElementById('editor'), { state });
// 获取内容
function getContent() {
const div = document.createElement('div');
const serializer = DOMSerializer.fromSchema(mySchema);
serializer.serializeFragment(view.state.doc.content, { document: div });
return div.innerHTML;
}
</script>
</body>
</html>模块列表
| 包名 | 说明 | |------|------| | gzkx-editor-model | 文档模型 | | gzkx-editor-state | 状态管理 | | gzkx-editor-view | 视图渲染 | | gzkx-editor-commands | 编辑命令 | | gzkx-editor-keymap | 快捷键绑定 | | gzkx-editor-inputrules | 输入规则 | | gzkx-editor-history | 历史记录(撤销/重做) | | gzkx-editor-collab | 协同编辑 | | gzkx-editor-gapcursor | 空隙光标 | | gzkx-editor-schema-basic | 基础模式 | | gzkx-editor-schema-list | 列表模式 | | gzkx-editor-menu | 菜单组件 | | gzkx-editor-example-setup | 快速设置 | | gzkx-editor-markdown | Markdown 转换 | | gzkx-editor-dropcursor | 拖拽光标 | | gzkx-editor-search | 搜索功能 | | gzkx-editor-changeset | 变更集 |
注意事项
- 事务处理:每次内容修改都需要通过
dispatch方法提交事务 - Schema 定义:确保所有节点和标记在 Schema 中正确定义
- CSS 样式:需要引入对应的 CSS 文件才能正常显示
- 依赖顺序:部分模块依赖其他模块,安装时注意包之间的依赖关系
