【文档编辑器专栏】5-ProseMirror Plugin插件系统

364 阅读4分钟

前言

prosemirror的插件系统,提供了丰富的能力,让开发者可以轻松地开发出各种插件,是prosemirror变得强大的一个重要模块

相信大家都了解或知道webpack的插件原理,webpack在构建的各个阶段会提供出钩子,方便插件模块介入去做一些代码逻辑。例如 clean-webpack-html(清理构建物), html-webpack-plugin(构建后处理html)等webpack插件

prosemirror的插件系统也是类似的设计,但是prosemirror和webpack不一样的地方是,prosemirror提供的plugin钩子更多帮助开发者去实现更丰富的功能和视图操作

plugin

我们先使用官方提供的plugin,感受下plugin的作用

// 安装三个包
// npm install prosemirror-keymap prosemirror-commands prosemirror-history

import { keymap } from 'prosemirror-keymap'
import { baseKeymap } from 'prosemirror-commands'
import { history, undo, redo } from 'prosemirror-history';

const editorState = EditorState.create({
// schema
    doc: DOMParser.fromSchema(schema).parse(content),
    plugins: [
      // 支持enter delete backspace等基本键盘操作
      keymap(baseKeymap),
      // 支持操作历史
      history(),
      // 支持键盘undo和redo
      keymap({ "Mod-z": undo, "Mod-y": redo }),
    ]
});

可以看到prosemirror的插件,提供监听dom event事件的钩子

通过plugin的注册,可以看到编辑器已经支持了基本的enter delete backspace redo undo的操作了

下面我们具体讲下plugin的几个重要模块

plugin key

在给定状态下,只能有一个插件,可以通过这个key访问插件的配置和状态

import { Plugin, PluginKey } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';

// new PluginKey得到
const pluginKey = new PluginKey('Test-Plugin');

export const testPlugin = () => {
    return new Plugin({
        // 传入key
        key: pluginKey,
        props: {
            handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
                // 获取plugin state对象
                const pluginState = pluginKey.getState(view.state);
                return false;
            }
        },
    });
}

image.png

pluginKey对象有getState和get方法,其实主要是用来获取plugin state

plugin props

官方文档 plugin props

ProseMirror 对可编辑 DOM 元素上触发的事件进行处理之前,会调用这些plugin props上注册的函数

如果return true, 则会阻止后续的监听响应,有点类似preventDefault

handleDONEvents为对象,其它为事件,支持的evnet比较多,大家可以查看源码

image.png

实现一个handleKeyDown的监听

可以看到每次keydownDown都会收到响应,就类似我们的dom.addEventListener, 只是这个是基于prosemirror编辑器的每一次keydown

import { Plugin, PluginKey } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';

const pluginKey = new PluginKey('Test-Plugin');

export const testPlugin = () => {

    return new Plugin({
        key: pluginKey,
        props: {
            handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
                console.log(view, event);
                return false;
            }
        }
    });
}

plugin state

定义plugin内部使用的state,用于记录一些内部状态,方便内部和外部使用

  • init: 初始化一个state对象
  • apply: 每次有事务变更,都会触发apply。有点像react的didupdate
  • toJSON
  • fromJSON
 return new Plugin({
    key: pluginKey,
    state:{
      init: (config: EditorStateConfig, instance: EditorState) => {
        return {
          isShowXXX: false,
        }
      },
      apply(tr: Transaction, value: T, oldState: EditorState, newState: EditorState) {
        let isShowXXX: boolean = !!value.isShowXXX;
        
        // do something else
        if (false) {
            isShowXXX = true;
        }

        return { isShowXXX };
      },
    },
    props: {
        handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
            const pluginState = pluginKey.getState(view.state);
            if (pluginState.isShowXXX) {
                console.log('do something');
            }
            return false;
        }
    },
});

plugin view

当插件需要与编辑器视图交互或在 DOM 中设置某些内容时,请使用此字段。

  • update: 编辑器view有更新时
  • destory: view destory时
declare type PluginView = {
    /**
    Called whenever the view's state is updated.
    */
    update?: (view: EditorView, prevState: EditorState) => void;
    /**
    Called when the view is destroyed or receives a state
    with different plugins.
    */
    destroy?: () => void;
};

实际例子

PluginView定义了这个plugin视图相关的逻辑,获取到prosemirror的view并添加一个div。

结合Plugin State属性isShowXXX, 去显示不同的内容

import { Plugin, PluginKey, PluginView } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { EditorState } from 'prosemirror-state';

const pluginKey = new PluginKey('Test-Plugin');

class TestPluginView implements PluginView {
    container: HTMLElement;
    view: EditorView;
    constructor(view: EditorView) {
        // 初始化添加一个div
        this.container = document.createElement('div');
        this.view = view;
        this.view.dom.parentNode?.appendChild(this.container);
    }
    
    // view update时,更改innerText
    update(view: EditorView, state: EditorState) {
        const { isShowXXX } = pluginKey.getState(state);
        this.container.innerText = isShowXXX ? 'xxxx' : 'yyyy';
    }

    destroy() {
        this.container.remove();
    }
}

export const testPlugin = () => {

    return new Plugin({
        key: pluginKey,
        state: {
            init: () => {
                return {
                    isShowXXX: false,
                }
            },
            apply(tr, value) {
                let isShowXXX: boolean = !!value.isShowXXX;
                
                // 有事务更新就会将isShowXXX设置为true
                if (true) {
                    isShowXXX = true;
                }

                return { isShowXXX };
            },
        },
        props: {
            handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
                const pluginState = pluginKey.getState(view.state);

                if (pluginState.isShowXXX) {
                    console.log('do something');
                }

                return false;
            }
        },
        view: (view) => {
            return new TestPluginView(view);
        }
    });
}

1721701093095.gif

上面例子讲述的是plugin提供的plugin view的实现方式,可以拓展出的场景是非常多的,例如字数统计/工具栏/右键菜单等

filterTransaction

filterTransaction?: (tr: Transaction, state: EditorState) => boolean;

在事务之前调用,允许插件取消事务

retrun true 则取消后续的事务

appendTransaction

在调用该事务后追加了一个事务

代码

github.com/pm-editor/d…

总结

本文主要讲述了plugin的key/props/state/view的基础用法,以及几个对象之间的联动和应用场景

实际开发过程中,并不是plugin的props/state/view都是全部都一起用上,需要大家看需要选择

另外,Plugin nodeViews/markViews/Decorations,这些都是可以修改编辑器视图,后续会重点讲述这部分的内容