From f9ba7a9bf4db4c534752f113163bcf9ab3f2acda Mon Sep 17 00:00:00 2001
From: chris <kokosias@yahoo.gr>
Date: Sat, 18 Dec 2021 12:49:22 +0200
Subject: [PATCH] add question nodeview

---
 .../src/EssayService/components/ToolBarBtn.js | 102 +++++++++++
 .../MultipleChoiceQuestion.js                 |   3 +-
 .../MultipleChoiceQuestionService.js          |  17 +-
 .../QuestionNodeView.js                       |  56 +++++++
 .../components/AnswerComponent.js             | 158 ++++++++++++++++++
 .../components/QuestionComponent.js           | 157 +----------------
 .../helpers/helpers.js                        |  50 ++++--
 .../schema/questionNode.js                    |  27 +++
 8 files changed, 398 insertions(+), 172 deletions(-)
 create mode 100644 wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js

diff --git a/wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js b/wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js
new file mode 100644
index 000000000..4e6920cd4
--- /dev/null
+++ b/wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js
@@ -0,0 +1,102 @@
+/* eslint react/prop-types: 0 */
+import React, { useContext, useMemo } from 'react';
+import styled, { css } from 'styled-components';
+import { v4 as uuidv4 } from 'uuid';
+import { WaxContext } from 'wax-prosemirror-core';
+import { MenuButton } from 'wax-prosemirror-components';
+import { findWrapping } from 'prosemirror-transform';
+import { Selection } from 'prosemirror-state';
+
+const activeStyles = css`
+  pointer-events: none;
+`;
+
+const StyledButton = styled(MenuButton)`
+  ${props => props.active && activeStyles}
+`;
+
+const ToolBarBtn = ({ view = {}, item }) => {
+  console.log('hrtr?');
+  const { icon, label, select, title } = item;
+  const context = useContext(WaxContext);
+  console.log(context);
+  const {
+    view: { main },
+    activeView,
+  } = useContext(WaxContext);
+
+  const isEditable = main.props.editable(editable => {
+    return editable;
+  });
+
+  const { state } = view;
+
+  let isDisabled = !select(state, activeView);
+  if (!isEditable) isDisabled = true;
+
+  const onMouseDown = () => {
+    const { $from, $to } = main.state.selection;
+
+    const range = $from.blockRange($to);
+
+    const { tr } = main.state;
+    const { dispatch } = main;
+    const wrapping1 =
+      range &&
+      findWrapping(range, main.state.config.schema.nodes.essay, {
+        id: uuidv4(),
+      });
+
+    tr.wrap(range, wrapping1).scrollIntoView();
+    dispatch(tr);
+  };
+
+  const onMouseDown2 = () => {
+    const { $from, $to } = main.state.selection;
+
+    const range = $from.blockRange($to);
+
+    const { tr } = main.state;
+    const { dispatch } = main;
+    const wrapping1 =
+      range &&
+      findWrapping(range, main.state.config.schema.nodes.essay_feedBack, {
+        id: uuidv4(),
+      });
+
+    tr.wrap(range, wrapping1).scrollIntoView();
+    dispatch(tr);
+  };
+
+  const ToolBarBtnComponent = useMemo(
+    () => (
+      <StyledButton
+        active={false}
+        disabled={isDisabled}
+        iconName={icon}
+        label={label}
+        onMouseDown={e => {
+          e.preventDefault();
+          //   onMouseDown();
+          //   const { tr } = main.state;
+          //   const { dispatch } = main;
+          //   const map = context.transaction.mapping.maps[0];
+          //   let a = 0;
+          //   map.forEach((_from, _to, _newFrom, newTo) => {
+          //     a = newTo;
+          //   });
+          //   dispatch(
+          //     tr.setSelection(Selection.near(main.state.doc.resolve(a + 1), 0)),
+          //   );
+          onMouseDown2();
+        }}
+        title={title}
+      />
+    ),
+    [isDisabled],
+  );
+
+  return ToolBarBtnComponent;
+};
+
+export default ToolBarBtn;
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js
index 632e739f2..bb219daa6 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js
@@ -19,8 +19,9 @@ class MultipleChoiceQuestion extends Tools {
       helpers.createOptions(
         view,
         context,
-        view.state.config.schema.nodes.multiple_choice,
         view.state.config.schema.nodes.multiple_choice_container,
+        view.state.config.schema.nodes.question_node_multiple,
+        view.state.config.schema.nodes.multiple_choice,
       );
     };
   }
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js
index 32b9cd531..26274f70d 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js
@@ -2,8 +2,11 @@ import Service from '../Service';
 import MultipleChoiceQuestion from './MultipleChoiceQuestion';
 import multipleChoiceNode from './schema/multipleChoiceNode';
 import multipleChoiceContainerNode from './schema/multipleChoiceContainerNode';
+import questionNode from './schema/questionNode';
+import AnswerComponent from './components/AnswerComponent';
 import QuestionComponent from './components/QuestionComponent';
 import MultipleChoiceNodeView from './MultipleChoiceNodeView';
+import QuestionNodeView from './QuestionNodeView';
 import MultipleChoiceSingleCorrectQuestionService from './MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService';
 import TrueFalseQuestionService from './TrueFalseQuestionService/TrueFalseQuestionService';
 import TrueFalseSingleCorrectQuestionService from './TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestionService';
@@ -14,19 +17,29 @@ class MultipleChoiceQuestionService extends Service {
     const createNode = this.container.get('CreateNode');
     const addPortal = this.container.get('AddPortal');
 
+    createNode({
+      multiple_choice_container: multipleChoiceContainerNode,
+    });
+
     createNode({
       multiple_choice: multipleChoiceNode,
     });
 
     createNode({
-      multiple_choice_container: multipleChoiceContainerNode,
+      question_node_multiple: questionNode,
     });
 
     addPortal({
-      nodeView: MultipleChoiceNodeView,
+      nodeView: QuestionNodeView,
       component: QuestionComponent,
       context: this.app,
     });
+
+    addPortal({
+      nodeView: MultipleChoiceNodeView,
+      component: AnswerComponent,
+      context: this.app,
+    });
   }
 
   dependencies = [
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js
new file mode 100644
index 000000000..cbacc21ed
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js
@@ -0,0 +1,56 @@
+import AbstractNodeView from '../PortalService/AbstractNodeView';
+
+export default class QuestionNodeView 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 'question_node_multiple';
+  }
+
+  update(node) {
+    // if (!node.sameMarkup(this.node)) return false;
+    this.node = node;
+    if (this.context.view[node.attrs.id]) {
+      const { state } = this.context.view[node.attrs.id];
+      const start = node.content.findDiffStart(state.doc.content);
+      if (start != null) {
+        let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+        const overlap = start - Math.min(endA, endB);
+        if (overlap > 0) {
+          endA += overlap;
+          endB += overlap;
+        }
+        this.context.view[node.attrs.id].dispatch(
+          state.tr
+            .replace(start, endB, node.slice(start, endA))
+            .setMeta('fromOutside', true),
+        );
+      }
+    }
+
+    return true;
+  }
+
+  stopEvent(event) {
+    if (event.target.type === 'text') {
+      return true;
+    }
+    const innerView = this.context.view[this.node.attrs.id];
+    return innerView && innerView.dom.contains(event.target);
+  }
+}
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.js
new file mode 100644
index 000000000..1e5f8cf98
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.js
@@ -0,0 +1,158 @@
+/* eslint-disable react/prop-types */
+import React, { useContext } from 'react';
+import styled from 'styled-components';
+import { TextSelection } from 'prosemirror-state';
+import { WaxContext } from 'wax-prosemirror-core';
+import { PlusSquareOutlined, DeleteOutlined } from '@ant-design/icons';
+import { Fragment } from 'prosemirror-model';
+import { v4 as uuidv4 } from 'uuid';
+import helpers from '../helpers/helpers';
+import EditorComponent from './EditorComponent';
+import SwitchComponent from './SwitchComponent';
+import FeedbackComponent from './FeedbackComponent';
+import Button from './Button';
+
+const Wrapper = styled.div`
+  display: flex;
+  flex-direction: row;
+  width: 100%;
+`;
+
+const InfoRow = styled.div`
+  color: black;
+  display: flex;
+  flex-direction: row;
+  padding: 10px 0px 4px 0px;
+`;
+
+const QuestionNunber = styled.span`
+  &:before {
+    content: 'Answer ' counter(question-item-multiple);
+    counter-increment: question-item-multiple;
+  }
+`;
+
+const QuestionControlsWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+`;
+
+const QuestionWrapper = styled.div`
+  border: 1px solid #a5a1a2;
+  border-radius: 4px;
+  color: black;
+  display: flex;
+  flex: 2 1 auto;
+  flex-direction: column;
+  padding: 10px;
+`;
+
+const IconsWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+
+  button {
+    border: none;
+    box-shadow: none;
+  }
+
+  span {
+    cursor: pointer;
+  }
+`;
+
+const QuestionData = styled.div`
+  align-items: normal;
+  display: flex;
+  flex-direction: row;
+`;
+
+export default ({ node, view, getPos }) => {
+  const context = useContext(WaxContext);
+  const {
+    view: { main },
+  } = context;
+
+  const isEditable = main.props.editable(editable => {
+    return editable;
+  });
+
+  const removeOption = () => {
+    main.state.doc.nodesBetween(getPos(), getPos() + 1, (sinlgeNode, pos) => {
+      if (sinlgeNode.attrs.id === node.attrs.id) {
+        main.dispatch(
+          main.state.tr.deleteRange(getPos(), getPos() + sinlgeNode.nodeSize),
+        );
+      }
+    });
+  };
+
+  const addOption = nodeId => {
+    const newAnswerId = uuidv4();
+    context.view.main.state.doc.descendants((editorNode, index) => {
+      if (editorNode.type.name === 'multiple_choice') {
+        if (editorNode.attrs.id === nodeId) {
+          context.view.main.dispatch(
+            context.view.main.state.tr.setSelection(
+              new TextSelection(
+                context.view.main.state.tr.doc.resolve(
+                  editorNode.nodeSize + index,
+                ),
+              ),
+            ),
+          );
+
+          const answerOption = context.view.main.state.config.schema.nodes.multiple_choice.create(
+            { id: newAnswerId },
+            Fragment.empty,
+          );
+          context.view.main.dispatch(
+            context.view.main.state.tr.replaceSelectionWith(answerOption),
+          );
+          // create Empty Paragraph
+          setTimeout(() => {
+            helpers.createEmptyParagraph(context, newAnswerId);
+          }, 120);
+        }
+      }
+    });
+  };
+
+  const readOnly = !isEditable;
+  const showAddIcon = true;
+  const showRemoveIcon = true;
+
+  return (
+    <Wrapper>
+      <QuestionControlsWrapper>
+        <InfoRow>
+          <QuestionNunber />
+          <SwitchComponent getPos={getPos} node={node} />
+        </InfoRow>
+        <QuestionWrapper>
+          <QuestionData>
+            <EditorComponent getPos={getPos} node={node} view={view} />
+          </QuestionData>
+          <FeedbackComponent getPos={getPos} node={node} view={view} />
+        </QuestionWrapper>
+      </QuestionControlsWrapper>
+      <IconsWrapper>
+        {showAddIcon && !readOnly && (
+          <Button
+            icon={<PlusSquareOutlined title="Add Option" />}
+            onClick={() => addOption(node.attrs.id)}
+          />
+        )}
+        {showRemoveIcon && !readOnly && (
+          <Button
+            icon={
+              <DeleteOutlined onClick={removeOption} title="Delete Option" />
+            }
+          />
+        )}
+      </IconsWrapper>
+    </Wrapper>
+  );
+};
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js
index 1e5f8cf98..934ecd963 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js
@@ -1,158 +1,5 @@
-/* eslint-disable react/prop-types */
-import React, { useContext } from 'react';
-import styled from 'styled-components';
-import { TextSelection } from 'prosemirror-state';
-import { WaxContext } from 'wax-prosemirror-core';
-import { PlusSquareOutlined, DeleteOutlined } from '@ant-design/icons';
-import { Fragment } from 'prosemirror-model';
-import { v4 as uuidv4 } from 'uuid';
-import helpers from '../helpers/helpers';
-import EditorComponent from './EditorComponent';
-import SwitchComponent from './SwitchComponent';
-import FeedbackComponent from './FeedbackComponent';
-import Button from './Button';
-
-const Wrapper = styled.div`
-  display: flex;
-  flex-direction: row;
-  width: 100%;
-`;
-
-const InfoRow = styled.div`
-  color: black;
-  display: flex;
-  flex-direction: row;
-  padding: 10px 0px 4px 0px;
-`;
-
-const QuestionNunber = styled.span`
-  &:before {
-    content: 'Answer ' counter(question-item-multiple);
-    counter-increment: question-item-multiple;
-  }
-`;
-
-const QuestionControlsWrapper = styled.div`
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-`;
-
-const QuestionWrapper = styled.div`
-  border: 1px solid #a5a1a2;
-  border-radius: 4px;
-  color: black;
-  display: flex;
-  flex: 2 1 auto;
-  flex-direction: column;
-  padding: 10px;
-`;
-
-const IconsWrapper = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-
-  button {
-    border: none;
-    box-shadow: none;
-  }
-
-  span {
-    cursor: pointer;
-  }
-`;
-
-const QuestionData = styled.div`
-  align-items: normal;
-  display: flex;
-  flex-direction: row;
-`;
+import React from 'react';
 
 export default ({ node, view, getPos }) => {
-  const context = useContext(WaxContext);
-  const {
-    view: { main },
-  } = context;
-
-  const isEditable = main.props.editable(editable => {
-    return editable;
-  });
-
-  const removeOption = () => {
-    main.state.doc.nodesBetween(getPos(), getPos() + 1, (sinlgeNode, pos) => {
-      if (sinlgeNode.attrs.id === node.attrs.id) {
-        main.dispatch(
-          main.state.tr.deleteRange(getPos(), getPos() + sinlgeNode.nodeSize),
-        );
-      }
-    });
-  };
-
-  const addOption = nodeId => {
-    const newAnswerId = uuidv4();
-    context.view.main.state.doc.descendants((editorNode, index) => {
-      if (editorNode.type.name === 'multiple_choice') {
-        if (editorNode.attrs.id === nodeId) {
-          context.view.main.dispatch(
-            context.view.main.state.tr.setSelection(
-              new TextSelection(
-                context.view.main.state.tr.doc.resolve(
-                  editorNode.nodeSize + index,
-                ),
-              ),
-            ),
-          );
-
-          const answerOption = context.view.main.state.config.schema.nodes.multiple_choice.create(
-            { id: newAnswerId },
-            Fragment.empty,
-          );
-          context.view.main.dispatch(
-            context.view.main.state.tr.replaceSelectionWith(answerOption),
-          );
-          // create Empty Paragraph
-          setTimeout(() => {
-            helpers.createEmptyParagraph(context, newAnswerId);
-          }, 120);
-        }
-      }
-    });
-  };
-
-  const readOnly = !isEditable;
-  const showAddIcon = true;
-  const showRemoveIcon = true;
-
-  return (
-    <Wrapper>
-      <QuestionControlsWrapper>
-        <InfoRow>
-          <QuestionNunber />
-          <SwitchComponent getPos={getPos} node={node} />
-        </InfoRow>
-        <QuestionWrapper>
-          <QuestionData>
-            <EditorComponent getPos={getPos} node={node} view={view} />
-          </QuestionData>
-          <FeedbackComponent getPos={getPos} node={node} view={view} />
-        </QuestionWrapper>
-      </QuestionControlsWrapper>
-      <IconsWrapper>
-        {showAddIcon && !readOnly && (
-          <Button
-            icon={<PlusSquareOutlined title="Add Option" />}
-            onClick={() => addOption(node.attrs.id)}
-          />
-        )}
-        {showRemoveIcon && !readOnly && (
-          <Button
-            icon={
-              <DeleteOutlined onClick={removeOption} title="Delete Option" />
-            }
-          />
-        )}
-      </IconsWrapper>
-    </Wrapper>
-  );
+  return <span>Question</span>;
 };
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js
index dec693bc7..702247163 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js
@@ -3,6 +3,15 @@ import { TextSelection } from 'prosemirror-state';
 import { Commands } from 'wax-prosemirror-utilities';
 import { Fragment } from 'prosemirror-model';
 import { wrapIn } from 'prosemirror-commands';
+import {
+  joinPoint,
+  canJoin,
+  findWrapping,
+  liftTarget,
+  canSplit,
+  ReplaceAroundStep,
+} from 'prosemirror-transform';
+import { Selection } from 'prosemirror-state';
 
 const createEmptyParagraph = (context, newAnswerId) => {
   if (context.view[newAnswerId]) {
@@ -46,31 +55,44 @@ const checkifEmpty = view => {
   }
 };
 
-const createOptions = (main, context, type, parentType) => {
+const createOptions = (
+  main,
+  context,
+  parentType,
+  questionType,
+  answerTtype,
+) => {
   checkifEmpty(main);
   const { state, dispatch } = main;
   /* Create Wrapping */
   const { $from, $to } = state.selection;
   const range = $from.blockRange($to);
+  const { tr } = main.state;
 
-  wrapIn(parentType, {
-    id: uuidv4(),
-  })(state, dispatch);
+  const wrapping = range && findWrapping(range, parentType, { id: uuidv4 });
+  if (!wrapping) return false;
+  tr.wrap(range, wrapping);
 
-  /* set New Selection */
-  dispatch(
-    main.state.tr.setSelection(
-      new TextSelection(main.state.tr.doc.resolve(range.$to.pos)),
-    ),
-  );
+  const map = tr.mapping.maps[0];
+  let newPos = 0;
+  map.forEach((_from, _to, _newFrom, newTo) => {
+    newPos = newTo;
+  });
+
+  tr.setSelection(TextSelection.create(tr.doc, range.$to.pos));
+
+  const question = questionType.create({ id: uuidv4() }, Fragment.empty);
 
   /* create First Option */
-  const firstOption = type.create({ id: uuidv4() }, Fragment.empty);
-  dispatch(main.state.tr.replaceSelectionWith(firstOption));
+  const firstOption = answerTtype.create({ id: uuidv4() }, Fragment.empty);
 
   /* create Second Option */
-  const secondOption = type.create({ id: uuidv4() }, Fragment.empty);
-  dispatch(main.state.tr.replaceSelectionWith(secondOption));
+  const secondOption = answerTtype.create({ id: uuidv4() }, Fragment.empty);
+  tr.replaceSelectionWith(question);
+  tr.replaceSelectionWith(firstOption);
+  tr.setSelection(TextSelection.create(tr.doc, newPos + 1));
+  tr.replaceSelectionWith(secondOption);
+  dispatch(tr);
 
   setTimeout(() => {
     createEmptyParagraph(context, secondOption.attrs.id);
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js
new file mode 100644
index 000000000..ca2378579
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js
@@ -0,0 +1,27 @@
+import { v4 as uuidv4 } from 'uuid';
+
+const questionNode = {
+  attrs: {
+    class: { default: 'multiple-choice-question' },
+    id: { default: uuidv4() },
+  },
+  group: 'block questions',
+  content: 'block*',
+  defining: true,
+
+  // atom: true,
+  parseDOM: [
+    {
+      tag: 'div.multiple-choice-question',
+      getAttrs(dom) {
+        return {
+          id: dom.getAttribute('id'),
+          class: dom.getAttribute('class'),
+        };
+      },
+    },
+  ],
+  toDOM: node => ['div', node.attrs, 0],
+};
+
+export default questionNode;
-- 
GitLab