From 4f27a9deb4dd0162047666db501dcba84319299b Mon Sep 17 00:00:00 2001
From: chris <kokosias@yahoo.gr>
Date: Mon, 22 Nov 2021 12:29:31 +0200
Subject: [PATCH] new service

---
 editors/demo/src/Editors.js                   |   2 +-
 wax-prosemirror-services/index.js             |   1 +
 .../helpers/helpers.js                        |   2 +-
 .../MultipleChoiceSingleCorrectNodeView.js    |   0
 .../MultipleChoiceSingleCorrectQuestion.js    |  97 +++++++++++
 ...tipleChoiceSingleCorrectQuestionService.js |  32 ++++
 .../components/Button.js                      |  88 ++++++++++
 .../components/EditorComponent.js             | 160 ++++++++++++++++++
 .../components/FeedbackComponent.js           | 110 ++++++++++++
 .../components/QuestionComponent.js           | 158 +++++++++++++++++
 .../components/Switch.js                      |  52 ++++++
 .../components/SwitchComponent.js             |  77 +++++++++
 .../components/ToolBarBtn.js                  |  59 +++++++
 ...ultipleChoiceSingleCorrectContainerNode.js |  29 ++++
 .../schema/multipleChoiceSingleCorrectNode.js |  33 ++++
 15 files changed, 898 insertions(+), 2 deletions(-)
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js
 create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.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/wax-prosemirror-services/index.js b/wax-prosemirror-services/index.js
index 3023e4f4d..0f587973f 100644
--- a/wax-prosemirror-services/index.js
+++ b/wax-prosemirror-services/index.js
@@ -45,6 +45,7 @@ export { default as CustomTagInlineService } from './src/CustomTagService/Custom
 export { default as CustomTagBlockService } from './src/CustomTagService/CustomTagBlockService/CustomTagBlockService';
 export { default as CustomTagService } from './src/CustomTagService/CustomTagService';
 export { default as MultipleChoiceQuestionService } from './src/MultipleChoiceQuestionService/MultipleChoiceQuestionService';
+export { default as MultipleChoiceSingleCorrectQuestionService } from './src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService';
 export { default as FillTheGapQuestionService } from './src/FillTheGapQuestionService/FillTheGapQuestionService';
 
 /*
diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js
index 537c87cab..f8cd0b5cc 100644
--- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js
+++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js
@@ -11,7 +11,7 @@ const createEmptyParagraph = (context, newAnswerId) => {
       ),
     );
     if (context.view[newAnswerId].dispatch) {
-      let type = context.view.main.state.schema.nodes.paragraph;
+      const type = context.view.main.state.schema.nodes.paragraph;
       context.view[newAnswerId].dispatch(
         context.view[newAnswerId].state.tr.insert(0, type.create()),
       );
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js
new file mode 100644
index 000000000..d53b66ed1
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js
@@ -0,0 +1,97 @@
+import React from 'react';
+import { isEmpty } from 'lodash';
+import { injectable } from 'inversify';
+import { Commands } from 'wax-prosemirror-utilities';
+import { v4 as uuidv4 } from 'uuid';
+import { Fragment } from 'prosemirror-model';
+import { TextSelection } from 'prosemirror-state';
+import { wrapIn } from 'prosemirror-commands';
+import helpers from '../MultipleChoiceQuestionService/helpers/helpers';
+import Tools from '../lib/Tools';
+import ToolBarBtn from './components/ToolBarBtn';
+
+const checkifEmpty = view => {
+  const { state } = view;
+  const { from, to } = state.selection;
+  state.doc.nodesBetween(from, to, (node, pos) => {
+    if (node.textContent !== ' ') Commands.simulateKey(view, 13, 'Enter');
+  });
+};
+
+const createOption = (main, context) => {
+  const { state, dispatch } = main;
+  /* Create Wrapping */
+  const { $from, $to } = state.selection;
+  const range = $from.blockRange($to);
+
+  wrapIn(state.config.schema.nodes.multiple_choice_single_correct_container, {
+    id: uuidv4(),
+  })(state, dispatch);
+
+  /* set New Selection */
+  dispatch(
+    main.state.tr.setSelection(
+      new TextSelection(main.state.tr.doc.resolve(range.$to.pos)),
+    ),
+  );
+
+  /* create Second Option */
+  const newAnswerId = uuidv4();
+  const answerOption = main.state.config.schema.nodes.multiple_choice_single_correct.create(
+    { id: newAnswerId },
+    Fragment.empty,
+  );
+  dispatch(main.state.tr.replaceSelectionWith(answerOption));
+  setTimeout(() => {
+    helpers.createEmptyParagraph(context, newAnswerId);
+  }, 50);
+};
+
+@injectable()
+class MultipleChoiceSingleCorrectQuestion extends Tools {
+  title = 'Add Multiple Choice Single Correct Question';
+  icon = 'multipleChoice';
+  name = 'Multiple Choice Single Correct';
+  label = 'Multiple Choice Single Correct';
+
+  get run() {
+    return (view, main, context) => {
+      checkifEmpty(view);
+      createOption(main, context);
+    };
+  }
+
+  get active() {
+    return state => {};
+  }
+
+  select = (state, activeView) => {
+    const { disallowedTools } = activeView.props;
+    if (disallowedTools.includes('MultipleChoice')) return false;
+    let status = true;
+    const { from, to } = state.selection;
+
+    if (from === null) return false;
+
+    state.doc.nodesBetween(from, to, (node, pos) => {
+      if (node.type.groups.includes('questions')) {
+        status = false;
+      }
+    });
+    return status;
+  };
+
+  get enable() {
+    return state => {};
+  }
+
+  renderTool(view) {
+    if (isEmpty(view)) return null;
+    // eslint-disable-next-line no-underscore-dangle
+    return this._isDisplayed ? (
+      <ToolBarBtn item={this.toJSON()} key={uuidv4()} view={view} />
+    ) : null;
+  }
+}
+
+export default MultipleChoiceSingleCorrectQuestion;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js
new file mode 100644
index 000000000..871c0f06c
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js
@@ -0,0 +1,32 @@
+import Service from '../Service';
+import MultipleChoiceSingleCorrectQuestion from './MultipleChoiceSingleCorrectQuestion';
+import multipleChoiceSingleCorrectNode from './schema/multipleChoiceSingleCorrectNode';
+import multipleChoiceSingleCorrectContainerNode from './schema/multipleChoiceSingleCorrectContainerNode';
+import QuestionComponent from './components/QuestionComponent';
+import MultipleChoiceSingleCorrectNodeView from './MultipleChoiceSingleCorrectNodeView';
+
+class MultipleChoiceSingleCorrectQuestionService extends Service {
+  register() {
+    this.container
+      .bind('MultipleChoiceSingleCorrectQuestion')
+      .to(MultipleChoiceSingleCorrectQuestion);
+    const createNode = this.container.get('CreateNode');
+    const addPortal = this.container.get('AddPortal');
+
+    createNode({
+      multiple_choice_single_correct: multipleChoiceSingleCorrectNode,
+    });
+
+    createNode({
+      multiple_choice_single_correct_container: multipleChoiceSingleCorrectContainerNode,
+    });
+
+    addPortal({
+      nodeView: MultipleChoiceSingleCorrectNodeView,
+      component: QuestionComponent,
+      context: this.app,
+    });
+  }
+}
+
+export default MultipleChoiceSingleCorrectQuestionService;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js
new file mode 100644
index 000000000..7d1619944
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import styled, { css } from 'styled-components';
+import PropTypes from 'prop-types';
+import { Button as AntButton } from 'antd';
+import { omit } from 'lodash';
+
+import { darken, lighten } from '@pubsweet/ui-toolkit';
+
+const colors = {
+  danger: 'colorError',
+  error: 'colorError',
+  success: 'colorSuccess',
+  // warn: 'colorWarning',
+};
+
+const StyledButton = styled(AntButton)`
+  ${props => {
+    const { status, theme, type } = props;
+    if (!Object.keys(colors).includes(status)) return null;
+    const color = theme[colors[status]];
+
+    // primary
+    if (type === 'primary')
+      return css`
+        background-color: ${color};
+        border-color: ${color};
+        color: ${theme.colorTextReverse};
+
+        &:hover,
+        &:focus,
+        &:active {
+          border-color: ${color};
+          color: ${theme.colorTextReverse};
+        }
+
+        &:hover,
+        &:focus {
+          background-color: ${lighten(color, 0.25)};
+        }
+
+        &:active {
+          background-color: ${darken(color, 0.25)};
+        }
+      `;
+
+    // non-primary
+    return css`
+      color: ${color};
+      border-color: ${color};
+      &:hover,
+      &:focus {
+        color: ${lighten(color, 0.25)};
+        border-color: ${lighten(color, 0.25)};
+      }
+
+      &:active {
+        color: ${darken(color, 0.25)};
+        border-color: ${darken(color, 0.25)};
+      }
+    `;
+  }}
+`;
+/**
+ * API is the same as https://ant.design/components/button/#API, except for the
+ * `danger` prop, which is ommited in favour of `status`, described below.
+ */
+
+const Button = props => {
+  const { children, className, ...rest } = props;
+  const passProps = omit(rest, 'danger');
+
+  return (
+    // eslint-disable-next-line react/jsx-props-no-spreading
+    <StyledButton className={className} {...passProps}>
+      {children}
+    </StyledButton>
+  );
+};
+
+Button.propTypes = {
+  status: PropTypes.oneOf(['error', 'danger', 'success']),
+};
+
+Button.defaultProps = {
+  status: null,
+};
+
+export default Button;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js
new file mode 100644
index 000000000..8a80dbd4a
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js
@@ -0,0 +1,160 @@
+/* eslint-disable react/destructuring-assignment */
+/* eslint-disable react/prop-types */
+
+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 '../../MultipleChoiceQuestionService/plugins/placeholder';
+
+const EditorWrapper = styled.div`
+  border: none;
+  display: flex;
+  flex: 2 1 auto;
+  justify-content: left;
+
+  .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', 'MultipleChoice'],
+        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 => {
+    const { state, transactions } = questionView.state.applyTransaction(tr);
+    questionView.updateState(state);
+    context.updateView({}, questionId);
+
+    if (!tr.getMeta('fromOutside')) {
+      const outerTr = view.state.tr;
+      const offsetMap = StepMap.offset(getPos() + 1);
+      for (let i = 0; i < transactions.length; i++) {
+        const { steps } = transactions[i];
+        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/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js
new file mode 100644
index 000000000..0fa7d157c
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js
@@ -0,0 +1,110 @@
+/* eslint-disable react/destructuring-assignment */
+/* eslint-disable react/prop-types */
+
+import React, { useContext, useRef, useState, useEffect } from 'react';
+import styled from 'styled-components';
+import { TextSelection } from 'prosemirror-state';
+import { WaxContext } from 'wax-prosemirror-core';
+import { DocumentHelpers } from 'wax-prosemirror-utilities';
+
+const FeedBack = styled.div`
+  color: black;
+  margin-top: 10px;
+`;
+
+const FeedBackLabel = styled.span`
+  font-weight: 700;
+`;
+
+const FeedBackInput = styled.input`
+  border: none;
+  display: flex;
+  width: 100%;
+`;
+
+export default ({ node, view, getPos }) => {
+  const context = useContext(WaxContext);
+  const [feedBack, setFeedBack] = useState('');
+  const [isFirstRun, setFirstRun] = useState(true);
+  const [typing, setTyping] = useState(false);
+  const feedBackRef = useRef(null);
+
+  useEffect(() => {
+    const allNodes = getNodes(context.view.main);
+    allNodes.forEach(singNode => {
+      if (singNode.node.attrs.id === node.attrs.id) {
+        if (!typing) setFeedBack(singNode.node.attrs.feedback);
+        if (!isFirstRun) {
+          if (singNode.node.attrs.feedback === '')
+            setFeedBack(singNode.node.attrs.feedback);
+        }
+      }
+    });
+  }, [getNodes(context.view.main)]);
+
+  const handleKeyDown = e => {
+    setTyping(true);
+    if (e.key === 'Backspace') {
+      context.view.main.dispatch(
+        context.view.main.state.tr.setSelection(
+          TextSelection.create(context.view.main.state.tr.doc, 0),
+        ),
+      );
+    }
+  };
+
+  const feedBackInput = () => {
+    setFeedBack(feedBackRef.current.value);
+  };
+
+  const saveFeedBack = () => {
+    const allNodes = getNodes(context.view.main);
+    allNodes.forEach(singleNode => {
+      if (singleNode.node.attrs.id === node.attrs.id) {
+        context.view.main.dispatch(
+          context.view.main.state.tr.setNodeMarkup(getPos(), undefined, {
+            ...singleNode.node.attrs,
+            feedback: feedBack,
+          }),
+        );
+        setFirstRun(false);
+      }
+    });
+    return false;
+  };
+
+  const onFocus = () => {
+    context.view.main.dispatch(
+      context.view.main.state.tr.setSelection(
+        TextSelection.create(context.view.main.state.tr.doc, null),
+      ),
+    );
+  };
+
+  return (
+    <FeedBack>
+      <FeedBackLabel>Feedback</FeedBackLabel>
+      <FeedBackInput
+        onBlur={saveFeedBack}
+        onChange={feedBackInput}
+        onFocus={onFocus}
+        onKeyDown={handleKeyDown}
+        placeholder="Insert feedback"
+        ref={feedBackRef}
+        type="text"
+        value={feedBack}
+      />
+    </FeedBack>
+  );
+};
+
+const getNodes = view => {
+  const allNodes = DocumentHelpers.findBlockNodes(view.state.doc);
+  const multipleChoiceNodes = [];
+  allNodes.forEach(node => {
+    if (node.node.type.name === 'multiple_choice') {
+      multipleChoiceNodes.push(node);
+    }
+  });
+  return multipleChoiceNodes;
+};
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js
new file mode 100644
index 000000000..cc00ad244
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.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 '../../MultipleChoiceQuestionService/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/MultipleChoiceSingleCorrectQuestionService/components/Switch.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js
new file mode 100644
index 000000000..6cad91dad
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled, { css } from 'styled-components';
+import { Switch as AntSwitch } from 'antd';
+
+import { grid } from '@pubsweet/ui-toolkit';
+
+const Wrapper = styled.span``;
+
+const Label = styled.span`
+  ${props =>
+    props.labelPosition === 'left' &&
+    css`
+      margin-right: ${grid(2)};
+    `}
+
+  ${props =>
+    props.labelPosition === 'right' &&
+    css`
+      margin-left: ${grid(2)};
+    `}
+`;
+
+const Switch = props => {
+  const { className, label, labelPosition, ...rest } = props;
+
+  return (
+    <Wrapper className={className}>
+      {label && labelPosition === 'left' && (
+        <Label labelPosition={labelPosition}>{label}</Label>
+      )}
+
+      <AntSwitch {...rest} />
+
+      {label && labelPosition === 'right' && (
+        <Label labelPosition={labelPosition}>{label}</Label>
+      )}
+    </Wrapper>
+  );
+};
+
+Switch.propTypes = {
+  label: PropTypes.string,
+  labelPosition: PropTypes.string,
+};
+
+Switch.defaultProps = {
+  label: null,
+  labelPosition: 'right',
+};
+
+export default Switch;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js
new file mode 100644
index 000000000..0d2b5c97b
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js
@@ -0,0 +1,77 @@
+/* eslint-disable react/prop-types */
+/* eslint-disable react-hooks/exhaustive-deps */
+import React, { useState, useContext, useEffect } from 'react';
+import { WaxContext } from 'wax-prosemirror-core';
+import { DocumentHelpers } from 'wax-prosemirror-utilities';
+import styled from 'styled-components';
+import Switch from './Switch';
+
+const StyledSwitch = styled(Switch)`
+  display: flex;
+  margin-left: auto;
+
+  span:nth-child(1) {
+    // bottom: 36px;
+    // display: flex;
+    // left: 4px;
+    // position: relative;
+    // width: 0px;
+  }
+
+  .ant-switch-checked {
+    background-color: green;
+  }
+`;
+
+const CustomSwitch = ({ node, getPos }) => {
+  const context = useContext(WaxContext);
+  const [checked, setChecked] = useState(false);
+
+  useEffect(() => {
+    const allNodes = getNodes(context.view.main);
+    allNodes.forEach(singNode => {
+      if (singNode.node.attrs.id === node.attrs.id) {
+        setChecked(singNode.node.attrs.correct);
+      }
+    });
+  }, [getNodes(context.view.main)]);
+
+  const handleChange = () => {
+    setChecked(!checked);
+    const allNodes = getNodes(context.view.main);
+    allNodes.forEach(singleNode => {
+      if (singleNode.node.attrs.id === node.attrs.id) {
+        context.view.main.dispatch(
+          context.view.main.state.tr.setNodeMarkup(getPos(), undefined, {
+            ...singleNode.node.attrs,
+            correct: !checked,
+          }),
+        );
+      }
+    });
+  };
+
+  return (
+    <StyledSwitch
+      checked={checked}
+      checkedChildren="YES"
+      label="Correct?"
+      labelPosition="left"
+      onChange={handleChange}
+      unCheckedChildren="NO"
+    />
+  );
+};
+
+const getNodes = view => {
+  const allNodes = DocumentHelpers.findBlockNodes(view.state.doc);
+  const multipleChoiceNodes = [];
+  allNodes.forEach(node => {
+    if (node.node.type.name === 'multiple_choice') {
+      multipleChoiceNodes.push(node);
+    }
+  });
+  return multipleChoiceNodes;
+};
+
+export default CustomSwitch;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js
new file mode 100644
index 000000000..d583f3b18
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js
@@ -0,0 +1,59 @@
+/* eslint react/prop-types: 0 */
+import React, { useContext, useMemo } from 'react';
+import { WaxContext } from 'wax-prosemirror-core';
+import styled, { css } from 'styled-components';
+import { MenuButton } from 'wax-prosemirror-components';
+
+const activeStyles = css`
+  pointer-events: none;
+`;
+
+const StyledButton = styled(MenuButton)`
+  ${props => props.active && activeStyles}
+`;
+
+const ToolBarBtn = ({ view = {}, item }) => {
+  const { active, icon, label, onlyOnMain, run, select, title } = item;
+  const context = useContext(WaxContext);
+  const {
+    view: { main },
+    activeViewId,
+    activeView,
+  } = useContext(WaxContext);
+
+  if (onlyOnMain) view = main;
+
+  const isEditable = main.props.editable(editable => {
+    return editable;
+  });
+
+  const { state } = view;
+
+  const isActive = !!(
+    active(state, activeViewId) && select(state, activeViewId)
+  );
+
+  let isDisabled = !select(state, activeView);
+  if (!isEditable) isDisabled = true;
+
+  const ToolBarBtnComponent = useMemo(
+    () => (
+      <StyledButton
+        active={isActive || false}
+        disabled={isDisabled}
+        iconName={icon}
+        label={label}
+        onMouseDown={e => {
+          e.preventDefault();
+          item.run(view, main, context);
+        }}
+        title={title}
+      />
+    ),
+    [isActive, isDisabled],
+  );
+
+  return ToolBarBtnComponent;
+};
+
+export default ToolBarBtn;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js
new file mode 100644
index 000000000..c45138788
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js
@@ -0,0 +1,29 @@
+const multipleChoiceSingleCorrectContainerNode = {
+  attrs: {
+    id: { default: '' },
+    class: { default: 'multiple-choice-single-correct' },
+    singleCorrect: { default: true },
+  },
+  group: 'block questions',
+  atom: true,
+  selectable: true,
+  draggable: true,
+  content: 'multiple_choice_single_correct+',
+  parseDOM: [
+    {
+      tag: 'div.multiple-choice-single-correct',
+      getAttrs(dom) {
+        return {
+          id: dom.dataset.id,
+          class: dom.getAttribute('class'),
+          singleCorrect: dom.getAttribute('singleCorrect'),
+        };
+      },
+    },
+  ],
+  toDOM(node) {
+    return ['div', node.attrs, 0];
+  },
+};
+
+export default multipleChoiceSingleCorrectContainerNode;
diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js
new file mode 100644
index 000000000..3936d4632
--- /dev/null
+++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js
@@ -0,0 +1,33 @@
+import { v4 as uuidv4 } from 'uuid';
+
+const multipleChoiceSingleCorrectNode = {
+  attrs: {
+    class: { default: 'multiple-choice-option-single-correct' },
+    id: { default: uuidv4() },
+    correct: { default: false },
+    feedback: { default: '' },
+    singleCorrect: { default: true },
+  },
+  group: 'block questions',
+  content: 'block*',
+  defining: true,
+
+  // atom: true,
+  parseDOM: [
+    {
+      tag: 'div.multiple-choice-option-single-correct',
+      getAttrs(dom) {
+        return {
+          id: dom.getAttribute('id'),
+          class: dom.getAttribute('class'),
+          correct: JSON.parse(dom.getAttribute('correct').toLowerCase()),
+          feedback: dom.getAttribute('feedback'),
+          singleCorrect: dom.getAttribute('singleCorrect'),
+        };
+      },
+    },
+  ],
+  toDOM: node => ['div', node.attrs, 0],
+};
+
+export default multipleChoiceSingleCorrectNode;
-- 
GitLab