Skip to content
Snippets Groups Projects
EditorComponent.js 6.42 KiB
Newer Older
/* eslint-disable react/prop-types */
chris's avatar
chris committed
import React, { useContext, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { EditorView } from 'prosemirror-view';
chris's avatar
chris committed
import { EditorState, TextSelection, NodeSelection } from 'prosemirror-state';
chris's avatar
chris committed
import { dropCursor } from 'prosemirror-dropcursor';
import { gapCursor } from 'prosemirror-gapcursor';
chris's avatar
chris committed
import { StepMap } from 'prosemirror-transform';
import { keymap } from 'prosemirror-keymap';
chris's avatar
chris committed
import { baseKeymap, chainCommands } from 'prosemirror-commands';
chris's avatar
chris committed
import { undo, redo } from 'prosemirror-history';
chris's avatar
chris committed
import { WaxContext, ComponentPlugin } from 'wax-prosemirror-core';
chris's avatar
chris committed
import {
  splitListItem,
  liftListItem,
  sinkListItem,
} from 'prosemirror-schema-list';
chris's avatar
chris committed
import Placeholder from '../plugins/placeholder';
import FakeCursorPlugin from '../../MultipleDropDownService/plugins/FakeCursorPlugin';
chris's avatar
chris committed
const EditorWrapper = styled.div`
  border: none;
  display: flex;
  flex: 2 1 auto;
chris's avatar
chris committed
  width: 100%;
chris's avatar
chris committed
  justify-content: left;

  .ProseMirror {
    white-space: break-spaces;
    width: 100%;
    word-wrap: break-word;

    &:focus {
      outline: none;
    }

chris's avatar
chris committed
    :empty::before {
chris's avatar
chris committed
      content: 'Type your item';
chris's avatar
chris committed
      color: #aaa;
      float: left;
      font-style: italic;
      pointer-events: none;
    }

chris's avatar
chris committed
    p:first-child {
      margin: 0;
    }

chris's avatar
chris committed
    p.empty-node:first-child::before {
      content: attr(data-content);
    }

    .empty-node::before {
      color: rgb(170, 170, 170);
      float: left;
      font-style: italic;
      height: 0px;
      pointer-events: none;
    }
  }
`;
chris's avatar
chris committed

chris's avatar
chris committed
let WaxOverlays = () => true;
const QuestionEditorComponent = ({
  node,
  view,
  getPos,
chris's avatar
chris committed
  placeholderText = 'Type your item',
chris's avatar
chris committed
  const editorRef = useRef();

  const context = useContext(WaxContext);
chris's avatar
chris committed
  const {
    app,
    pmViews: { main },
  } = context;
chris's avatar
chris committed
  let questionView;
  const questionId = node.attrs.id;
chris's avatar
chris committed
  const isEditable = main.props.editable(editable => {
chris's avatar
chris committed
    return editable;
  });
chris's avatar
chris committed

chris's avatar
chris committed
  let finalPlugins = [FakeCursorPlugin(), gapCursor(), dropCursor()];
chris's avatar
chris committed

  const createKeyBindings = () => {
    const keys = getKeys();
    Object.keys(baseKeymap).forEach(key => {
chris's avatar
chris committed
      if (keys[key]) {
        keys[key] = chainCommands(keys[key], baseKeymap[key]);
      } else {
        keys[key] = baseKeymap[key];
      }
chris's avatar
chris committed
    });
    return keys;
  };

chris's avatar
chris committed
  const pressEnter = (state, dispatch) => {
    if (state.selection.node && state.selection.node.type.name === 'image') {
      const { $from, to } = state.selection;

      const same = $from.sharedDepth(to);

      const pos = $from.before(same);
      dispatch(state.tr.setSelection(NodeSelection.create(state.doc, pos)));
      return true;
    }
    // LISTS
    if (splitListItem(state.schema.nodes.list_item)(state)) {
      splitListItem(state.schema.nodes.list_item)(state, dispatch);
      return true;
    }

    return false;
  };

chris's avatar
chris committed
  const getKeys = () => {
    return {
      'Mod-z': () => undo(view.state, view.dispatch),
      'Mod-y': () => redo(view.state, view.dispatch),
chris's avatar
chris committed
      'Mod-[': liftListItem(view.state.schema.nodes.list_item),
      'Mod-]': sinkListItem(view.state.schema.nodes.list_item),
      //   Enter: () =>
      //     splitListItem(questionView.state.schema.nodes.list_item)(
      //       questionView.state,
      //       questionView.dispatch,
      //     ),
      Enter: pressEnter,
chris's avatar
chris committed
  const plugins = [keymap(createKeyBindings()), ...app.getPlugins()];
chris's avatar
chris committed

  const createPlaceholder = placeholder => {
    return Placeholder({
      content: placeholder,
    });
  };

  finalPlugins = finalPlugins.concat([
    createPlaceholder(placeholderText),
chris's avatar
chris committed
    ...plugins,
  ]);

chris's avatar
chris committed
  useEffect(() => {
chris's avatar
chris committed
    WaxOverlays = ComponentPlugin('waxOverlays');
chris's avatar
chris committed
    questionView = new EditorView(
chris's avatar
chris committed
      {
        mount: editorRef.current,
      },
chris's avatar
chris committed
      {
chris's avatar
chris committed
        editable: () => isEditable,
chris's avatar
chris committed
        state: EditorState.create({
          doc: node,
chris's avatar
chris committed
          plugins: finalPlugins,
chris's avatar
chris committed
        }),
        dispatchTransaction,
chris's avatar
chris committed
        disallowedTools: ['MultipleChoice'],
chris's avatar
chris committed
        handleDOMEvents: {
          mousedown: () => {
chris's avatar
chris committed
            context.updateView({}, questionId);
chris's avatar
chris committed
            main.dispatch(
              main.state.tr
chris's avatar
chris committed
                .setMeta('outsideView', questionId)
chris's avatar
chris committed
                .setSelection(
chris's avatar
chris committed
                  new TextSelection(
chris's avatar
chris committed
                    main.state.tr.doc.resolve(
chris's avatar
chris committed
                      getPos() +
                        1 +
                        context.pmViews[questionId].state.selection.to,
chris's avatar
chris committed
                    ),
chris's avatar
chris committed
                  ),
chris's avatar
chris committed
                ),
            );
chris's avatar
chris committed
            // context.pmViews[activeViewId].dispatch(
            //   context.pmViews[activeViewId].state.tr.setSelection(
chris's avatar
chris committed
            //     TextSelection.between(
chris's avatar
chris committed
            //       context.pmViews[activeViewId].state.selection.$anchor,
            //       context.pmViews[activeViewId].state.selection.$head,
chris's avatar
chris committed
            //     ),
            //   ),
            // );
chris's avatar
chris committed

chris's avatar
chris committed
            context.updateView({}, questionId);
chris's avatar
chris committed

chris's avatar
chris committed
            if (questionView.hasFocus()) questionView.focus();
          },
chris's avatar
chris committed
          blur: (editorView, event) => {
            if (questionView && event.relatedTarget === null) {
              questionView.focus();
            }
          },
chris's avatar
chris committed
        },
chris's avatar
chris committed
        scrollMargin: 200,
        scrollThreshold: 200,
chris's avatar
chris committed
        attributes: {
          spellcheck: 'false',
        },
      },
    );

chris's avatar
chris committed
    // Set Each note into Wax's Context
chris's avatar
chris committed
    context.updateView(
      {
        [questionId]: questionView,
      },
      questionId,
    );
chris's avatar
chris committed
    if (questionView.hasFocus()) questionView.focus();
chris's avatar
chris committed
  }, []);

  const dispatchTransaction = tr => {
chris's avatar
chris committed
    const addToHistory = !tr.getMeta('exludeToHistoryFromOutside');
chris's avatar
chris committed
    const { state, transactions } = questionView.state.applyTransaction(tr);
chris's avatar
chris committed
    questionView.updateState(state);
    context.updateView({}, questionId);

    if (!tr.getMeta('fromOutside')) {
chris's avatar
chris committed
      const outerTr = view.state.tr;
      const offsetMap = StepMap.offset(getPos() + 1);
chris's avatar
chris committed
      for (let i = 0; i < transactions.length; i++) {
chris's avatar
chris committed
        const { steps } = transactions[i];
chris's avatar
chris committed
        for (let j = 0; j < steps.length; j++)
          outerTr.step(steps[j].map(offsetMap));
      }
      if (outerTr.docChanged)
chris's avatar
chris committed
        view.dispatch(
          outerTr
            .setMeta('outsideView', questionId)
            .setMeta('addToHistory', addToHistory),
        );
chris's avatar
chris committed
  return (
    <EditorWrapper>
      <div ref={editorRef} />
chris's avatar
chris committed
      <WaxOverlays activeViewId={questionId} />
chris's avatar
chris committed
    </EditorWrapper>
  );
chris's avatar
chris committed
export default QuestionEditorComponent;