npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 | 变更集 |

注意事项

  1. 事务处理:每次内容修改都需要通过 dispatch 方法提交事务
  2. Schema 定义:确保所有节点和标记在 Schema 中正确定义
  3. CSS 样式:需要引入对应的 CSS 文件才能正常显示
  4. 依赖顺序:部分模块依赖其他模块,安装时注意包之间的依赖关系