From d4d45d3c0f17b944187dd046a5e1cc09dc3d84c8 Mon Sep 17 00:00:00 2001
From: chris <kokosias@yahoo.gr>
Date: Mon, 11 Oct 2021 21:32:46 +0300
Subject: [PATCH] add gap component

---
 editors/demo/src/Editors.js                   |   2 +-
 .../CreateGapService/CreateGap.js             |  30 +++-
 .../FillTheGapNodeView.js                     |  28 +++
 .../FillTheGapQuestionService.js              |  12 ++
 .../components/EditorComponent.js             | 161 ++++++++++++++++++
 .../components/GapComponent.js                |  12 ++
 .../plugins/placeholder.js                    |  32 ++++
 .../schema/fillTheGapNode.js                  |  25 ++-
 .../components/EditorComponent.js             |  10 +-
 .../components/QuestionComponent.js           |   9 +-
 .../src/NoteService/Note.js                   |   2 +-
 11 files changed, 306 insertions(+), 17 deletions(-)
 create mode 100644 editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapNodeView.js
 create mode 100644 editors/demo/src/HHMI/FillTheGapQuestionService/components/EditorComponent.js
 create mode 100644 editors/demo/src/HHMI/FillTheGapQuestionService/components/GapComponent.js
 create mode 100644 editors/demo/src/HHMI/FillTheGapQuestionService/plugins/placeholder.js

diff --git a/editors/demo/src/Editors.js b/editors/demo/src/Editors.js
index abd0628fb..d19991a2e 100644
--- a/editors/demo/src/Editors.js
+++ b/editors/demo/src/Editors.js
@@ -70,7 +70,7 @@ const Editors = () => {
       case 'ncbi':
         return <NCBI />;
       default:
-        return <Editoria />;
+        return <HHMI />;
     }
   };
 
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/CreateGapService/CreateGap.js b/editors/demo/src/HHMI/FillTheGapQuestionService/CreateGapService/CreateGap.js
index 35d40e082..74b6d44a9 100644
--- a/editors/demo/src/HHMI/FillTheGapQuestionService/CreateGapService/CreateGap.js
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/CreateGapService/CreateGap.js
@@ -1,4 +1,6 @@
 import { injectable } from 'inversify';
+import { Fragment } from 'prosemirror-model';
+import { v4 as uuidv4 } from 'uuid';
 import { Tools } from 'wax-prosemirror-services';
 
 @injectable()
@@ -8,15 +10,37 @@ class CreateGap extends Tools {
   name = 'Create Gap';
 
   get run() {
-    return (state, dispatch) => {};
+    return (state, dispatch) => {
+      const { empty, $from, $to } = state.selection;
+      let content = Fragment.empty;
+      if (!empty && $from.sameParent($to) && $from.parent.inlineContent)
+        content = $from.parent.content.cut(
+          $from.parentOffset,
+          $to.parentOffset,
+        );
+      const createGap = state.config.schema.nodes.fill_the_gap.create(
+        { id: uuidv4() },
+        content,
+      );
+      dispatch(state.tr.replaceSelectionWith(createGap));
+    };
   }
 
+  select = (state, activeViewId) => {
+    let status = false;
+    const { from, to } = state.selection;
+    state.doc.nodesBetween(from, to, (node, pos) => {
+      if (node.type.name === 'fill_the_gap_container') {
+        status = true;
+      }
+    });
+    return status;
+  };
+
   get active() {
     return state => {};
   }
 
-  select = (state, activeViewId) => {};
-
   get enable() {
     return state => {};
   }
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapNodeView.js b/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapNodeView.js
new file mode 100644
index 000000000..5b61c652d
--- /dev/null
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapNodeView.js
@@ -0,0 +1,28 @@
+import { AbstractNodeView } from 'wax-prosemirror-services';
+
+export default class MultipleChoiceNodeView extends AbstractNodeView {
+  constructor(
+    node,
+    view,
+    getPos,
+    decorations,
+    createPortal,
+    Component,
+    context,
+  ) {
+    super(node, view, getPos, decorations, createPortal, Component, context);
+
+    this.node = node;
+    this.outerView = view;
+    this.getPos = getPos;
+    this.context = context;
+  }
+
+  static name() {
+    return 'fill_the_gap';
+  }
+
+  update(node) {
+    return true;
+  }
+}
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapQuestionService.js b/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapQuestionService.js
index 60612d55d..8863042c8 100644
--- a/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapQuestionService.js
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/FillTheGapQuestionService.js
@@ -3,6 +3,8 @@ import FillTheGapQuestion from './FillTheGapQuestion';
 import fillTheGapContainerNode from './schema/fillTheGapContainerNode';
 import fillTheGapNode from './schema/fillTheGapNode';
 import CreateGapService from './CreateGapService/CreateGapService';
+import FillTheGapNodeView from './FillTheGapNodeView';
+import GapComponent from './components/GapComponent';
 
 class FillTheGapQuestionService extends Service {
   register() {
@@ -13,6 +15,16 @@ class FillTheGapQuestionService extends Service {
     createNode({
       fill_the_gap_container: fillTheGapContainerNode,
     });
+
+    createNode({
+      fill_the_gap: fillTheGapNode,
+    });
+
+    addPortal({
+      nodeView: FillTheGapNodeView,
+      component: GapComponent,
+      context: this.app,
+    });
   }
   dependencies = [new CreateGapService()];
 }
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/components/EditorComponent.js b/editors/demo/src/HHMI/FillTheGapQuestionService/components/EditorComponent.js
new file mode 100644
index 000000000..68a2ee3f7
--- /dev/null
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/components/EditorComponent.js
@@ -0,0 +1,161 @@
+/* eslint-disable react/destructuring-assignment */
+/* eslint-disable react/prop-types */
+/* eslint-disable react-hooks/exhaustive-deps */
+import React, { useContext, useRef, useEffect } from 'react';
+import styled from 'styled-components';
+import { EditorView } from 'prosemirror-view';
+import { EditorState, TextSelection } from 'prosemirror-state';
+import { StepMap } from 'prosemirror-transform';
+import { keymap } from 'prosemirror-keymap';
+import { baseKeymap } from 'prosemirror-commands';
+import { undo, redo } from 'prosemirror-history';
+import { WaxContext } from 'wax-prosemirror-core';
+import Placeholder from '../plugins/placeholder';
+
+const EditorWrapper = styled.div`
+  border: none;
+  display: flex;
+  flex: 2 1 auto;
+  justify-content: left;
+  margin-right: 15px;
+
+  .ProseMirror {
+    white-space: break-spaces;
+    width: 100%;
+    word-wrap: break-word;
+
+    &:focus {
+      outline: none;
+    }
+
+    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;
+    }
+  }
+`;
+
+const EditorComponent = ({ node, view, getPos }) => {
+  const editorRef = useRef();
+
+  const context = useContext(WaxContext);
+  let questionView;
+  const questionId = node.attrs.id;
+  const isEditable = context.view.main.props.editable(editable => {
+    return editable;
+  });
+
+  let finalPlugins = [];
+
+  const createKeyBindings = () => {
+    const keys = getKeys();
+    Object.keys(baseKeymap).forEach(key => {
+      keys[key] = baseKeymap[key];
+    });
+    return keys;
+  };
+
+  const getKeys = () => {
+    return {
+      'Mod-z': () => undo(view.state, view.dispatch),
+      'Mod-y': () => redo(view.state, view.dispatch),
+    };
+  };
+
+  const plugins = [keymap(createKeyBindings()), ...context.app.getPlugins()];
+
+  // eslint-disable-next-line no-shadow
+  const createPlaceholder = placeholder => {
+    return Placeholder({
+      content: placeholder,
+    });
+  };
+
+  finalPlugins = finalPlugins.concat([
+    createPlaceholder('Type your answer'),
+    ...plugins,
+  ]);
+
+  const { activeViewId } = context;
+
+  useEffect(() => {
+    questionView = new EditorView(
+      {
+        mount: editorRef.current,
+      },
+      {
+        editable: () => isEditable,
+        state: EditorState.create({
+          doc: node,
+          plugins: finalPlugins,
+        }),
+        // This is the magic part
+        dispatchTransaction,
+        disallowedTools: ['images', 'lists', 'lift'],
+        handleDOMEvents: {
+          mousedown: () => {
+            context.view[activeViewId].dispatch(
+              context.view[activeViewId].state.tr.setSelection(
+                TextSelection.between(
+                  context.view[activeViewId].state.selection.$anchor,
+                  context.view[activeViewId].state.selection.$head,
+                ),
+              ),
+            );
+            context.updateView({}, questionId);
+            // Kludge to prevent issues due to the fact that the whole
+            // footnote is node-selected (and thus DOM-selected) when
+            // the parent editor is focused.
+            if (questionView.hasFocus()) questionView.focus();
+          },
+        },
+
+        attributes: {
+          spellcheck: 'false',
+        },
+      },
+    );
+
+    //Set Each note into Wax's Context
+    context.updateView(
+      {
+        [questionId]: questionView,
+      },
+      questionId,
+    );
+    if (questionView.hasFocus()) questionView.focus();
+  }, []);
+
+  const dispatchTransaction = tr => {
+    let { state, transactions } = questionView.state.applyTransaction(tr);
+    questionView.updateState(state);
+    context.updateView({}, questionId);
+
+    if (!tr.getMeta('fromOutside')) {
+      let outerTr = view.state.tr,
+        offsetMap = StepMap.offset(getPos() + 1);
+      for (let i = 0; i < transactions.length; i++) {
+        let steps = transactions[i].steps;
+        for (let j = 0; j < steps.length; j++)
+          outerTr.step(steps[j].map(offsetMap));
+      }
+      if (outerTr.docChanged)
+        view.dispatch(outerTr.setMeta('outsideView', questionId));
+    }
+  };
+
+  return (
+    <EditorWrapper>
+      <div ref={editorRef} />
+    </EditorWrapper>
+  );
+};
+
+export default EditorComponent;
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/components/GapComponent.js b/editors/demo/src/HHMI/FillTheGapQuestionService/components/GapComponent.js
new file mode 100644
index 000000000..91c6cca11
--- /dev/null
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/components/GapComponent.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import styled from 'styled-components';
+import EditorComponent from './EditorComponent';
+
+const Gap = styled.span`
+  color: red;
+  text-decoration: underline;
+`;
+
+export default ({ node, view, getPos }) => {
+  return <Gap> Gap</Gap>;
+};
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/plugins/placeholder.js b/editors/demo/src/HHMI/FillTheGapQuestionService/plugins/placeholder.js
new file mode 100644
index 000000000..de3fd8058
--- /dev/null
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/plugins/placeholder.js
@@ -0,0 +1,32 @@
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { Decoration, DecorationSet } from 'prosemirror-view';
+
+const placeHolderText = new PluginKey('placeHolderText');
+
+export default props => {
+  return new Plugin({
+    key: placeHolderText,
+    props: {
+      decorations: state => {
+        const decorations = [];
+        const decorate = (node, pos) => {
+          if (
+            node.type.isBlock &&
+            node.childCount === 0 &&
+            state.doc.content.childCount === 1
+          ) {
+            decorations.push(
+              Decoration.node(pos, pos + node.nodeSize, {
+                class: 'empty-node',
+                'data-content': props.content,
+              }),
+            );
+          }
+        };
+        state.doc.descendants(decorate);
+
+        return DecorationSet.create(state.doc, decorations);
+      },
+    },
+  });
+};
diff --git a/editors/demo/src/HHMI/FillTheGapQuestionService/schema/fillTheGapNode.js b/editors/demo/src/HHMI/FillTheGapQuestionService/schema/fillTheGapNode.js
index 5443c358e..26de347d1 100644
--- a/editors/demo/src/HHMI/FillTheGapQuestionService/schema/fillTheGapNode.js
+++ b/editors/demo/src/HHMI/FillTheGapQuestionService/schema/fillTheGapNode.js
@@ -1,3 +1,26 @@
-const fillTheGapNode = {};
+const fillTheGapNode = {
+  group: 'inline',
+  content: 'inline*',
+  inline: true,
+  atom: true,
+  attrs: {
+    id: { default: '' },
+    class: { default: 'fill-the-gap' },
+  },
+  parseDOM: [
+    {
+      tag: 'span',
+      getAttrs(dom) {
+        return {
+          id: dom.getAttribute('id'),
+          class: dom.getAttribute('class'),
+        };
+      },
+    },
+  ],
+  toDOM: node => {
+    return ['span', node.attrs, 0];
+  },
+};
 
 export default fillTheGapNode;
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js
index 4f6f72187..68a2ee3f7 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js
@@ -1,3 +1,5 @@
+/* eslint-disable react/destructuring-assignment */
+/* eslint-disable react/prop-types */
 /* eslint-disable react-hooks/exhaustive-deps */
 import React, { useContext, useRef, useEffect } from 'react';
 import styled from 'styled-components';
@@ -81,13 +83,7 @@ const EditorComponent = ({ node, view, getPos }) => {
     ...plugins,
   ]);
 
-  const {
-    view: { main },
-    activeViewId,
-  } = context;
-
-  if (activeViewId === node.attrs.id && context.view[activeViewId].focused) {
-  }
+  const { activeViewId } = context;
 
   useEffect(() => {
     questionView = new EditorView(
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js
index 1171dac4e..0cfa34aec 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js
@@ -1,3 +1,4 @@
+/* eslint-disable react/prop-types */
 import React, { useContext } from 'react';
 import styled from 'styled-components';
 import { TextSelection } from 'prosemirror-state';
@@ -124,16 +125,16 @@ export default ({ node, view, getPos }) => {
   return (
     <Wrapper>
       <InfoRow>
-        <QuestionNunber></QuestionNunber>
+        <QuestionNunber />
       </InfoRow>
       <QuestionControlsWrapper>
         <QuestionWrapper>
           <QuestionData>
-            <EditorComponent node={node} view={view} getPos={getPos} />
+            <EditorComponent getPos={getPos} node={node} view={view} />
 
-            <SwitchComponent node={node} getPos={getPos} />
+            <SwitchComponent getPos={getPos} node={node} />
           </QuestionData>
-          <FeedbackComponent node={node} view={view} getPos={getPos} />
+          <FeedbackComponent getPos={getPos} node={node} view={view} />
         </QuestionWrapper>
         <IconsWrapper>
           {showAddIcon && !readOnly && (
diff --git a/wax-prosemirror-services/src/NoteService/Note.js b/wax-prosemirror-services/src/NoteService/Note.js
index 17e97dc21..76395e692 100644
--- a/wax-prosemirror-services/src/NoteService/Note.js
+++ b/wax-prosemirror-services/src/NoteService/Note.js
@@ -1,7 +1,7 @@
-import Tools from '../lib/Tools';
 import { injectable } from 'inversify';
 import { Fragment } from 'prosemirror-model';
 import { v4 as uuidv4 } from 'uuid';
+import Tools from '../lib/Tools';
 
 export default
 @injectable()
-- 
GitLab