Skip to content
prosemirror.js 4.34 KiB
Newer Older
import 'katex/dist/katex.min.css';

// prosemirror imports
import { Schema, Node } from 'prosemirror-model';
import { EditorView } from 'prosemirror-view';
import { EditorState, Plugin, PluginKey } from 'prosemirror-state';
import { schema as basicSchema } from 'prosemirror-schema-basic';
import { addListNodes } from 'prosemirror-schema-list';
import {
  chainCommands, deleteSelection, selectNodeBackward, joinBackward,
} from 'prosemirror-commands';
import { keymap } from 'prosemirror-keymap';
import { inputRules } from 'prosemirror-inputrules';
import 'prosemirror-view/style/prosemirror.css';
import 'prosemirror-menu/style/menu.css';

import { exampleSetup } from 'prosemirror-example-setup';
import 'prosemirror-example-setup/style/style.css';

import {
  mathPlugin, mathBackspaceCmd, insertMathCmd, mathSerializer,
  makeBlockMathInputRule, makeInlineMathInputRule,
  REGEX_INLINE_MATH_DOLLARS, REGEX_BLOCK_MATH_DOLLARS,
// I've no idea why eslint can't find the module; rollup can.
// eslint-disable-next-line import/no-unresolved
} from '@benrbray/prosemirror-math';
import '@benrbray/prosemirror-math/dist/prosemirror-math.css';

let anyEditorChangedFlag = false;

export function anyEditorChanged() {
  return anyEditorChangedFlag;
}

export function resetAnyEditorChanged() {
  anyEditorChangedFlag = false;
}

const schema = new Schema({
  nodes: addListNodes(basicSchema.spec.nodes, 'paragraph block*', 'block')
    .remove('image')
    .addToEnd('math_inline', {
      group: 'inline math',
      content: 'text*', // important!
      inline: true, // important!
      atom: true, // important!
      toDOM: () => ['math-inline', { class: 'math-node' }, 0],
      parseDOM: [{
        tag: 'math-inline', // important!
      }],
    })
    .addToEnd('math_display', {
      group: 'block math',
      content: 'text*', // important!
      atom: true, // important!
      code: true, // important!
      toDOM: () => ['math-display', { class: 'math-node' }, 0],
      parseDOM: [{
        tag: 'math-display', // important!
      }],
    }),
  marks: basicSchema.spec.marks,
});

const inlineMathInputRule = makeInlineMathInputRule(
  REGEX_INLINE_MATH_DOLLARS,
  schema.nodes.math_inline,
);
const blockMathInputRule = makeBlockMathInputRule(
  REGEX_BLOCK_MATH_DOLLARS,
  schema.nodes.math_display,
);

const readonlyPlugin = new Plugin({
  key: new PluginKey('readonly'),
  // Allow selections but prevent any other changes
  filterTransaction: (transaction) => transaction.docChanged === false,
});

const changeListenerPlugin = new Plugin({
  key: new PluginKey('readonly'),
  filterTransaction: (transaction) => {
    if (transaction.docChanged) {
      anyEditorChangedFlag = true;
    }
    return true;
  },
});

// eslint-disable-next-line import/prefer-default-export
export function editorFromTextArea(textarea, autofocus) {
  const plugins = [
    ...exampleSetup({ schema }),
    mathPlugin,
    keymap({
      'Mod-Space': insertMathCmd(schema.nodes.math_inline),

      Backspace: chainCommands(
        deleteSelection,
        mathBackspaceCmd,
        joinBackward,
        selectNodeBackward,
      ),
    }),
    inputRules({ rules: [inlineMathInputRule, blockMathInputRule] }),
  ];

  if (textarea.disabled || textarea.readOnly) {
    plugins.push(readonlyPlugin);
  }
  // Change listener should be after readonly.
  plugins.push(changeListenerPlugin);

  let docJson = null;
  if (textarea.value) {
    docJson = JSON.parse(textarea.value);
  }

  let doc;
  if (docJson) {
    doc = Node.fromJSON(schema, docJson);
  } else {
    doc = schema.topNodeType.createAndFill();
  }

  const state = EditorState.create({ schema, plugins, doc });

  const editorElt = document.createElement('div');
  editorElt.classList.add('rl-prosemirror-container');
  const view = new EditorView(editorElt, {
    state,
    clipboardTextSerializer: mathSerializer.serializeSlice,
  });

  if (autofocus) {
    document.addEventListener('DOMContentLoaded', () => {
      view.focus();
    });
  }

  textarea.parentNode.insertBefore(editorElt, textarea);

  // eslint-disable-next-line no-param-reassign
  textarea.style.display = 'none';
  if (textarea.form) {
    textarea.form.addEventListener('submit', () => {
      // eslint-disable-next-line no-param-reassign
      textarea.value = JSON.stringify(view.state.doc.toJSON());
    });
  }
  textarea.classList.add('rl-managed-by-prosemirror');

  return view;
}