diff --git a/wax-questions-service/package.json b/wax-questions-service/package.json index c08eba148691d9d0cb93f4fa7921d594530f6698..1fb84f909110aa234167b38cb8fd9435df9b2832 100644 --- a/wax-questions-service/package.json +++ b/wax-questions-service/package.json @@ -23,6 +23,7 @@ "prosemirror-keymap": "1.2.1", "prosemirror-schema-list": "1.3.0", "prosemirror-state": "1.4.2", + "prosemirror-model": "1.19.0", "prosemirror-transform": "1.7.1", "prosemirror-view": "1.30.2", "rc-switch": "^3.2.2", diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/AnswerComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/AnswerComponent.js index 352a37d3b223669f09d5f4c31a80346ea07b9e19..5108d752fb49020ad4ce4f7b10c125414a80d6a6 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/AnswerComponent.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/AnswerComponent.js @@ -5,7 +5,7 @@ import { WaxContext, DocumentHelpers, Icon } from 'wax-prosemirror-core'; import { Fragment } from 'prosemirror-model'; import { v4 as uuidv4 } from 'uuid'; import helpers from '../../helpers/helpers'; -import EditorComponent from '../../components/EditorComponent'; +import EditorAnswerComponent from '../../components/EditorAnswerComponent'; import FeedbackComponent from '../../components/FeedbackComponent'; import SwitchComponent from './SwitchComponent'; @@ -226,7 +226,7 @@ export default ({ node, view, getPos }) => { </InfoRow> <QuestionWrapper> <QuestionData> - <EditorComponent getPos={getPos} node={node} view={view} /> + <EditorAnswerComponent getPos={getPos} node={node} view={view} /> </QuestionData> {!testMode && !(readOnly && feedback === '') && ( <FeedbackComponent diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/AnswerComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/AnswerComponent.js index d6f2bf08d9441d396f69e5cea5567a82c80fc9bb..c2e6c35f048071b6ab0febbbdf52173beba78ed0 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/AnswerComponent.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/AnswerComponent.js @@ -6,7 +6,7 @@ import { Fragment } from 'prosemirror-model'; import { v4 as uuidv4 } from 'uuid'; import helpers from '../../helpers/helpers'; import FeedbackComponent from '../../components/FeedbackComponent'; -import EditorComponent from '../../components/EditorComponent'; +import EditorAnswerComponent from '../../components/EditorAnswerComponent'; import SwitchComponent from './SwitchComponent'; const Wrapper = styled.div` @@ -226,7 +226,7 @@ export default ({ node, view, getPos }) => { </InfoRow> <QuestionWrapper> <QuestionData> - <EditorComponent getPos={getPos} node={node} view={view} /> + <EditorAnswerComponent getPos={getPos} node={node} view={view} /> </QuestionData> {!testMode && !(readOnly && feedback === '') && ( <FeedbackComponent diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/AnswerComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/AnswerComponent.js index 23aeae7c18240662ae28ebe9eb8f48c1441b16c0..bf4fec586349720e9744eba016e66d98c459928e 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/AnswerComponent.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/AnswerComponent.js @@ -5,7 +5,7 @@ import { WaxContext, DocumentHelpers, Icon } from 'wax-prosemirror-core'; import { Fragment } from 'prosemirror-model'; import { v4 as uuidv4 } from 'uuid'; import helpers from '../../helpers/helpers'; -import EditorComponent from '../../components/EditorComponent'; +import EditorAnswerComponent from '../../components/EditorAnswerComponent'; import FeedbackComponent from '../../components/FeedbackComponent'; import SwitchComponent from './SwitchComponent'; @@ -226,7 +226,7 @@ export default ({ node, view, getPos }) => { </InfoRow> <QuestionWrapper> <QuestionData> - <EditorComponent getPos={getPos} node={node} view={view} /> + <EditorAnswerComponent getPos={getPos} node={node} view={view} /> </QuestionData> {!testMode && !(readOnly && feedback === '') && ( <FeedbackComponent diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/components/AnswerComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/components/AnswerComponent.js index 0c3a92ae10f0438ca9257499e24d36dcfbdac027..a8beee3cdf422f646f509bb90a882b6f73847758 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/components/AnswerComponent.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/components/AnswerComponent.js @@ -4,7 +4,7 @@ import { TextSelection, NodeSelection } from 'prosemirror-state'; import { WaxContext, DocumentHelpers, Icon } from 'wax-prosemirror-core'; import { Fragment } from 'prosemirror-model'; import { v4 as uuidv4 } from 'uuid'; -import EditorComponent from './EditorComponent'; +import EditorAnswerComponent from './EditorAnswerComponent'; import SwitchComponent from './SwitchComponent'; import FeedbackComponent from './FeedbackComponent'; import helpers from '../helpers/helpers'; @@ -224,7 +224,7 @@ export default ({ node, view, getPos }) => { </InfoRow> <QuestionWrapper> <QuestionData> - <EditorComponent getPos={getPos} node={node} view={view} /> + <EditorAnswerComponent getPos={getPos} node={node} view={view} /> </QuestionData> {!testMode && !(readOnly && feedback === '') && ( <FeedbackComponent diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorAnswerComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorAnswerComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..8d43e90ec531a42d7ef2354167bceea4a41d921c --- /dev/null +++ b/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorAnswerComponent.js @@ -0,0 +1,228 @@ +import React, { useContext, useRef, useEffect } from 'react'; +import styled from 'styled-components'; +import { EditorView } from 'prosemirror-view'; +import { EditorState, TextSelection, NodeSelection } from 'prosemirror-state'; +import { StepMap } from 'prosemirror-transform'; +import { keymap } from 'prosemirror-keymap'; +import { baseKeymap, chainCommands } from 'prosemirror-commands'; +import { undo, redo } from 'prosemirror-history'; +import { WaxContext, ComponentPlugin } from 'wax-prosemirror-core'; +import { + splitListItem, + liftListItem, + sinkListItem, +} from 'prosemirror-schema-list'; +import Placeholder from '../plugins/placeholder'; +import FakeCursorPlugin from '../../MultipleDropDownService/plugins/FakeCursorPlugin'; + +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; + } + + :empty::before { + content: 'Type your answer'; + color: #aaa; + float: left; + font-style: italic; + pointer-events: none; + } + + p:first-child { + margin: 0; + } + + 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; + } + } +`; + +let WaxOverlays = () => true; + +const EditorAnswerComponent = ({ node, view, getPos }) => { + const editorRef = useRef(); + + const context = useContext(WaxContext); + const { + app, + pmViews: { main }, + } = context; + let answerView; + const questionId = node.attrs.id; + const isEditable = main.props.editable(editable => { + return editable; + }); + + let finalPlugins = [FakeCursorPlugin()]; + + const createKeyBindings = () => { + const keys = getKeys(); + Object.keys(baseKeymap).forEach(key => { + if (keys[key]) { + keys[key] = chainCommands(keys[key], baseKeymap[key]); + } else { + keys[key] = baseKeymap[key]; + } + }); + return keys; + }; + + 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; + }; + + const getKeys = () => { + return { + 'Mod-z': () => undo(view.state, view.dispatch), + 'Mod-y': () => redo(view.state, view.dispatch), + 'Mod-[': liftListItem(view.state.schema.nodes.list_item), + 'Mod-]': sinkListItem(view.state.schema.nodes.list_item), + // Enter: () => + // splitListItem(answerView.state.schema.nodes.list_item)( + // answerView.state, + // answerView.dispatch, + // ), + Enter: pressEnter, + }; + }; + + const plugins = [keymap(createKeyBindings()), ...app.getPlugins()]; + + const createPlaceholder = placeholder => { + return Placeholder({ + content: placeholder, + }); + }; + + finalPlugins = finalPlugins.concat([ + createPlaceholder('Type your answer'), + ...plugins, + ]); + + useEffect(() => { + WaxOverlays = ComponentPlugin('waxOverlays'); + answerView = new EditorView( + { + mount: editorRef.current, + }, + { + editable: () => isEditable, + state: EditorState.create({ + doc: node, + plugins: finalPlugins, + }), + dispatchTransaction, + disallowedTools: ['MultipleChoice'], + handleDOMEvents: { + mousedown: () => { + context.updateView({}, questionId); + main.dispatch( + main.state.tr + .setMeta('outsideView', questionId) + .setSelection( + new TextSelection( + main.state.tr.doc.resolve( + getPos() + + 1 + + context.pmViews[questionId].state.selection.to, + ), + ), + ), + ); + // context.pmViews[activeViewId].dispatch( + // context.pmViews[activeViewId].state.tr.setSelection( + // TextSelection.between( + // context.pmViews[activeViewId].state.selection.$anchor, + // context.pmViews[activeViewId].state.selection.$head, + // ), + // ), + // ); + + context.updateView({}, questionId); + + if (answerView.hasFocus()) answerView.focus(); + }, + blur: (editorView, event) => { + if (answerView && event.relatedTarget === null) { + answerView.focus(); + } + }, + }, + + attributes: { + spellcheck: 'false', + }, + }, + ); + + // Set Each note into Wax's Context + context.updateView( + { + [questionId]: answerView, + }, + questionId, + ); + if (answerView.hasFocus()) answerView.focus(); + }, []); + + const dispatchTransaction = tr => { + const { state, transactions } = answerView.state.applyTransaction(tr); + answerView.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} /> + <WaxOverlays activeViewId={questionId} /> + </EditorWrapper> + ); +}; + +export default EditorAnswerComponent;