diff --git a/editors/demo/src/HHMI/config/config.js b/editors/demo/src/HHMI/config/config.js index 7fc649f31ced9483756acb011a2040eceeaa1626..163f2146245f787854ad18b184bfe9b8764bd928 100644 --- a/editors/demo/src/HHMI/config/config.js +++ b/editors/demo/src/HHMI/config/config.js @@ -13,7 +13,6 @@ import { MathService, FullScreenService, FullScreenToolGroupService, - // ExternalAPIContentService, } from 'wax-prosemirror-services'; import { QuestionsService } from 'wax-questions-service'; @@ -21,71 +20,6 @@ import { TablesService, tableEditing, columnResizing } from 'wax-table-service'; import { DefaultSchema } from 'wax-prosemirror-core'; import invisibles, { hardBreak } from '@guardian/prosemirror-invisibles'; -const API_KEY = ''; - -async function ExternalAPIContentTransformation(prompt) { - const response = await fetch('https://api.openai.com/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${API_KEY}`, - }, - body: JSON.stringify({ - model: 'gpt-3.5-turbo', - messages: [ - { - role: 'user', - content: prompt, - }, - ], - temperature: 0, - // max_tokens: 400, - // n: 1, - // stop: null, - }), - }); - - try { - const data = await response.json(); - console.log(data); - return data.choices[0].message.content; - } catch (e) { - console.error(e); - alert( - 'That model is currently overloaded with other requests. You can retry your request.', - ); - } finally { - } - return prompt; -} - -// async function ExternalAPIContentTransformation(prompt) { -// const response = await fetch('https://api.openai.com/v1/completions', { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// Authorization: `Bearer ${API_KEY}`, -// }, -// body: JSON.stringify({ -// model: 'text-davinci-003', -// prompt: prompt, -// max_tokens: 1400, -// n: 1, -// stop: null, -// temperature: 0.5, -// }), -// }); - -// try { -// const data = await response.json(); -// console.log(data); -// return data.choices[0].text.trim(); -// } catch (e) { -// console.error(e); -// } finally { -// } -// return prompt; -// } export default { MenuService: [ @@ -107,7 +41,6 @@ export default { 'Lists', 'Images', 'Tables', - // 'ExternalAPIContent', 'QuestionsDropDown', 'FullScreen', ], @@ -121,9 +54,6 @@ export default { toolGroups: ['MultipleDropDown'], }, ], - // ExternalAPIContentService: { - // ExternalAPIContentTransformation: ExternalAPIContentTransformation, - // }, SchemaService: DefaultSchema, RulesService: [emDash, ellipsis], @@ -131,7 +61,6 @@ export default { PmPlugins: [invisibles([hardBreak()])], services: [ - // new ExternalAPIContentService(), new QuestionsService(), new ListsService(), new LinkService(), diff --git a/wax-questions-service/package.json b/wax-questions-service/package.json index 7863d3e63215b2e38518bdc0013fb90cab0701b1..721eb3d4e339ca818a996fefce9a1d37a8541a7c 100644 --- a/wax-questions-service/package.json +++ b/wax-questions-service/package.json @@ -18,6 +18,8 @@ "inversify": "^5.0.1", "lodash": "^4.17.4", "prop-types": "^15.7.2", + "prosemirror-dropcursor": "1.7.1", + "prosemirror-gapcursor": "1.3.1", "prosemirror-commands": "1.5.1", "prosemirror-history": "1.3.0", "prosemirror-keymap": "1.2.1", diff --git a/wax-questions-service/src/MatchingService/components/DropDownComponent.js b/wax-questions-service/src/MatchingService/components/DropDownComponent.js index 2ad24b4f52a427de59d90219ff252db5af4683a5..19ffdbd17f56f0d663f15773869261b8859563a5 100644 --- a/wax-questions-service/src/MatchingService/components/DropDownComponent.js +++ b/wax-questions-service/src/MatchingService/components/DropDownComponent.js @@ -54,7 +54,8 @@ const DropDownMenu = styled.div` padding: 8px 10px; } - span:focus { + span:focus, + span:hover { background: #f2f9fc; outline: 2px solid #f2f9fc; } diff --git a/wax-questions-service/src/MatchingService/components/TestModeDropDownComponent.js b/wax-questions-service/src/MatchingService/components/TestModeDropDownComponent.js index 34751cc9442d8462d9264bef11a6a0ab07567d83..1f28cb77a80417a1d8a33cffaafa68ce55df337d 100644 --- a/wax-questions-service/src/MatchingService/components/TestModeDropDownComponent.js +++ b/wax-questions-service/src/MatchingService/components/TestModeDropDownComponent.js @@ -55,7 +55,8 @@ const DropDownMenu = styled.div` padding: 8px 10px; } - span:focus { + span:focus, + span:hover { background: #f2f9fc; outline: 2px solid #f2f9fc; } diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js index 5b3dc591c0db0b9f44012f81678d2a8df0679611..7d4172f03c8907c3c68c575462de925a2f97f847 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js @@ -8,7 +8,7 @@ const multipleChoiceSingleCorrectNode = { }, group: 'block questions', content: 'block*', - defining: true, + // defining: true, parseDOM: [ { diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/questionSingleNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/questionSingleNode.js index 4c1231444efe4cca40564e03d62510b6f583d7b4..4c88fdd391c06883da27a4ef8032f74535d2d327 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/questionSingleNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/questionSingleNode.js @@ -4,8 +4,8 @@ const questionSingleNode = { class: { default: 'multiple-choice-question-single' }, }, group: 'block questions', - content: 'block+', - defining: true, + content: 'block*', + // defining: true, // atom: true, parseDOM: [ diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/questionTrueFalseNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/questionTrueFalseNode.js index fde65c6fdda9a1cd9025dd8f4905ef58ecf2b62c..77b6ce39246bbbef56a31d531299b0e7149bb486 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/questionTrueFalseNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/questionTrueFalseNode.js @@ -5,8 +5,8 @@ const questionTrueFalseNode = { }, group: 'block questions', // content: 'paragraph* bulletlist* orderedlist*', - content: 'block+', - defining: true, + content: 'block*', + // defining: true, // atom: true, parseDOM: [ diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js index 8150946dd1c0dc3e7932157fad4ca68bf96eedc1..22fa87adca3f2e0bd367e6b88ad642152842ac57 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js @@ -8,7 +8,7 @@ const trueFalseNode = { }, group: 'block questions', content: 'block*', - defining: true, + // defining: true, parseDOM: [ { diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/questionTrueFalseSingleNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/questionTrueFalseSingleNode.js index ee82a9170f2edb7067c13a97986d1c9e0435a6de..51da9ff1ed9cca6046aca08dd2e56fb51f561dbb 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/questionTrueFalseSingleNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/questionTrueFalseSingleNode.js @@ -4,10 +4,8 @@ const questionTrueFalseNode = { class: { default: 'true-false-question-single' }, }, group: 'block questions', - content: 'block+', - defining: true, - - // atom: true, + content: 'block*', + // defining: true, parseDOM: [ { tag: 'div.true-false-question-single', diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js index e56c8c88a94e9ca6d7c420ea12fd08ad8c186270..4baaa7b5f5238bda22aad0f5153e5c0a5466ad6f 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js @@ -8,7 +8,7 @@ const trueFalseSingleCorrectNode = { }, group: 'block questions', content: 'block*', - defining: true, + // defining: true, parseDOM: [ { diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorComponent.js index 9a94fd2e19093c698f742953b63e5c46e3ce2303..24d4bfbf4fb4d2fef5dcfca1ffc2f4c42be8ebb8 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorComponent.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/components/EditorComponent.js @@ -67,6 +67,7 @@ const QuestionEditorComponent = ({ view, getPos, placeholderText = 'Type your item', + QuestionType = 'Multiple', }) => { const editorRef = useRef(); @@ -191,7 +192,9 @@ const QuestionEditorComponent = ({ } }, }, - + type: QuestionType, + scrollMargin: 200, + scrollThreshold: 200, attributes: { spellcheck: 'false', }, @@ -209,6 +212,7 @@ const QuestionEditorComponent = ({ }, []); const dispatchTransaction = tr => { + const addToHistory = !tr.getMeta('exludeToHistoryFromOutside'); const { state, transactions } = questionView.state.applyTransaction(tr); questionView.updateState(state); context.updateView({}, questionId); @@ -222,7 +226,11 @@ const QuestionEditorComponent = ({ outerTr.step(steps[j].map(offsetMap)); } if (outerTr.docChanged) - view.dispatch(outerTr.setMeta('outsideView', questionId)); + view.dispatch( + outerTr + .setMeta('outsideView', questionId) + .setMeta('addToHistory', addToHistory), + ); } }; diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/components/FeedbackComponent.js b/wax-questions-service/src/MultipleChoiceQuestionService/components/FeedbackComponent.js index 2cf40ab1e318a9d423175d9bffdec70aae3b1fbb..1634d5bcf22d5a3aae17c3190a6d3d9a78cdebe8 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/components/FeedbackComponent.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/components/FeedbackComponent.js @@ -120,7 +120,8 @@ const getNodes = view => { node.node.type.name === 'true_false_single_correct' || node.node.type.name === 'matching_container' || node.node.type.name === 'fill_the_gap_container' || - node.node.type.name === 'multiple_drop_down_container' + node.node.type.name === 'multiple_drop_down_container' || + node.node.type.name === 'numerical_answer_container' ) { multipleChoiceNodes.push(node); } diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/helpers/helpers.js b/wax-questions-service/src/MultipleChoiceQuestionService/helpers/helpers.js index 221982fc512a9a86c6452ebd95660a00d8ef7deb..60b6c87333da9a59cbfeafc11e356c80de92ef03 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/helpers/helpers.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/helpers/helpers.js @@ -17,7 +17,9 @@ const createEmptyParagraph = (context, newAnswerId) => { if (context.pmViews[newAnswerId].dispatch) { const type = context.pmViews.main.state.schema.nodes.paragraph; context.pmViews[newAnswerId].dispatch( - context.pmViews[newAnswerId].state.tr.insert(0, type.create()), + context.pmViews[newAnswerId].state.tr + .insert(0, type.create()) + .setMeta('exludeToHistoryFromOutside', true), ); } context.pmViews[newAnswerId].dispatch( @@ -80,9 +82,9 @@ const createOptions = (main, context, parentType, questionType, answerType) => { dispatch(tr); setTimeout(() => { context.pmViews[question.attrs.id].focus(); - // createEmptyParagraph(context, firstOption.attrs.id); - // createEmptyParagraph(context, secondOption.attrs.id); - // createEmptyParagraph(context, question.attrs.id); + createEmptyParagraph(context, firstOption.attrs.id); + createEmptyParagraph(context, secondOption.attrs.id); + createEmptyParagraph(context, question.attrs.id); }, 50); return true; diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/schema/multipleChoiceNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/schema/multipleChoiceNode.js index daf0168fab9ba5dfdcb57788072292fa1f4c7ec3..7bd9b6bcf8377320cceaca2d0aff2c79240ffb92 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/schema/multipleChoiceNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/schema/multipleChoiceNode.js @@ -8,7 +8,7 @@ const multipleChoiceNode = { }, group: 'block questions', content: 'block*', - defining: true, + // defining: true, parseDOM: [ { tag: 'div.multiple-choice-option', diff --git a/wax-questions-service/src/MultipleChoiceQuestionService/schema/questionNode.js b/wax-questions-service/src/MultipleChoiceQuestionService/schema/questionNode.js index 05dc969d5c6fb9e79d10ef69f19fbf72b02d0250..06faabc2f53fb4219d0d66dd54aff567af8415e1 100644 --- a/wax-questions-service/src/MultipleChoiceQuestionService/schema/questionNode.js +++ b/wax-questions-service/src/MultipleChoiceQuestionService/schema/questionNode.js @@ -4,8 +4,8 @@ const questionNode = { id: { default: '' }, }, group: 'block questions', - content: 'block+', - defining: true, + content: 'block*', + // defining: true, parseDOM: [ { diff --git a/wax-questions-service/src/MultipleDropDownService/components/ReadOnlyDropDown.js b/wax-questions-service/src/MultipleDropDownService/components/ReadOnlyDropDown.js index 2b7a2dd5c2dd19b5589c4f117c375a2c22132913..0f96256e9ca4b5643531cd5e631e9d05a8b1f797 100644 --- a/wax-questions-service/src/MultipleDropDownService/components/ReadOnlyDropDown.js +++ b/wax-questions-service/src/MultipleDropDownService/components/ReadOnlyDropDown.js @@ -59,7 +59,8 @@ const DropDownMenu = styled.div` padding: 8px 10px; } - span:focus { + span:focus, + span:hover { background: #f2f9fc; outline: 2px solid #f2f9fc; } diff --git a/wax-questions-service/src/NumericalAnswerService/NumericalAnswerContainerNodeView.js b/wax-questions-service/src/NumericalAnswerService/NumericalAnswerContainerNodeView.js new file mode 100644 index 0000000000000000000000000000000000000000..263c2ec4147ab65d85c2562f876cd7fa576b9458 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/NumericalAnswerContainerNodeView.js @@ -0,0 +1,32 @@ +import { QuestionsNodeView } from 'wax-prosemirror-core'; + +export default class NumericalAnswerContainerNodeView extends QuestionsNodeView { + 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 'numerical_answer_container'; + } + + selectNode() { + this.context.pmViews[this.node.attrs.id].focus(); + } + + stopEvent(event) { + return true; + } +} diff --git a/wax-questions-service/src/NumericalAnswerService/NumericalAnswerQuestion.js b/wax-questions-service/src/NumericalAnswerService/NumericalAnswerQuestion.js new file mode 100644 index 0000000000000000000000000000000000000000..fffde564b5ae28c7c09d450b0963cc7193c3c32a --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/NumericalAnswerQuestion.js @@ -0,0 +1,54 @@ +import { injectable } from 'inversify'; +import { findWrapping } from 'prosemirror-transform'; +import { v4 as uuidv4 } from 'uuid'; +import { Commands, Tools } from 'wax-prosemirror-core'; +import helpers from '../MultipleChoiceQuestionService/helpers/helpers'; + +@injectable() +class NumericalAnswerQuestion extends Tools { + title = 'Numerical Answer Question'; + icon = ''; + name = 'Numerical Answer'; + + get run() { + return main => { + const { dispatch } = main; + const { state } = main; + helpers.checkifEmpty(main); + const { $from, $to } = main.state.selection; + const range = $from.blockRange($to); + const { tr } = main.state; + + const wrapping = + range && + findWrapping( + range, + state.config.schema.nodes.numerical_answer_container, + { + id: uuidv4(), + }, + ); + if (!wrapping) return false; + tr.wrap(range, wrapping); + dispatch(tr); + }; + } + + get active() { + return state => { + if ( + Commands.isParentOfType( + state, + state.config.schema.nodes.numerical_answer_container, + ) + ) { + return true; + } + return false; + }; + } + + select = (state, activeViewId, activeView) => {}; +} + +export default NumericalAnswerQuestion; diff --git a/wax-questions-service/src/NumericalAnswerService/NumericalAnswerService.js b/wax-questions-service/src/NumericalAnswerService/NumericalAnswerService.js new file mode 100644 index 0000000000000000000000000000000000000000..bb867dfa2ecd37c446e807dee5d7645456e58b19 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/NumericalAnswerService.js @@ -0,0 +1,26 @@ +import { Service } from 'wax-prosemirror-core'; +import NumericalAnswerContainerNode from './schema/NumericalAnswerContainerNode'; +import NumericalAnswerQuestion from './NumericalAnswerQuestion'; +import NumericalAnswerContainerNodeView from './NumericalAnswerContainerNodeView'; +import NumericalAnswerContainerComponent from './components/NumericalAnswerContainerComponent'; +import './numericalAnswer.css'; + +class NumericalAnswerService extends Service { + register() { + this.container.bind('NumericalAnswerQuestion').to(NumericalAnswerQuestion); + const createNode = this.container.get('CreateNode'); + const addPortal = this.container.get('AddPortal'); + + createNode({ + numerical_answer_container: NumericalAnswerContainerNode, + }); + + addPortal({ + nodeView: NumericalAnswerContainerNodeView, + component: NumericalAnswerContainerComponent, + context: this.app, + }); + } +} + +export default NumericalAnswerService; diff --git a/wax-questions-service/src/NumericalAnswerService/components/ExactAnswerComponent.js b/wax-questions-service/src/NumericalAnswerService/components/ExactAnswerComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..070b6156956e6a8ad04fe804545de613414ce072 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/components/ExactAnswerComponent.js @@ -0,0 +1,77 @@ +import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; + +const AnswerContainer = styled.div` + display: flex; + flex-direction: row; + width: 100%; +`; + +const ValueContainer = styled.div` + display: flex; + flex-direction: column; + margin-right: 25px; + label { + font-size: 12px; + } + + input:focus { + outline: none; + } +`; + +const ValueInnerContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const ExactAnswerComponent = () => { + const [exact, setExact] = useState(''); + const [marginError, setMarginError] = useState(''); + + const exactRef = useRef(null); + const errorRef = useRef(null); + + const onChangeExact = () => { + setExact(exactRef.current.value); + }; + + const onChangeError = () => { + setMarginError(errorRef.current.value); + }; + + return ( + <AnswerContainer> + <ValueContainer> + <label htmlFor="exactAnswer"> + <ValueInnerContainer> + <span>Exact Answer</span> + <input + name="exactAnswer" + onChange={onChangeExact} + ref={exactRef} + type="text" + value={exact} + /> + </ValueInnerContainer> + </label> + </ValueContainer> + <ValueContainer> + <label htmlFor="errorAnswer"> + <ValueInnerContainer> + <span>Margin of error (%)</span> + <input + name="errorAnswer" + onChange={onChangeError} + ref={errorRef} + type="text" + value={marginError} + /> + </ValueInnerContainer> + </label> + </ValueContainer> + </AnswerContainer> + ); +}; + +export default ExactAnswerComponent; diff --git a/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..8d05991f482d633391cdfd437b9caf52893a00bb --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerContainerComponent.js @@ -0,0 +1,140 @@ +import React, { useContext } from 'react'; +import { WaxContext, DocumentHelpers, Icon } from 'wax-prosemirror-core'; +import styled from 'styled-components'; +import EditorComponent from '../../MultipleChoiceQuestionService/components/EditorComponent'; +import FeedbackComponent from '../../MultipleChoiceQuestionService/components/FeedbackComponent'; +import NumericalAnswerDropDownCompontent from './NumericalAnswerDropDownCompontent'; +import ExactAnswerComponent from './ExactAnswerComponent'; +import PreciseAnswerComponent from './PreciseAnswerComponent'; +import RangeAnswerComponent from './RangeAnswerComponent'; + +const NumericalAnswerWrapper = styled.div` + margin: 0px 38px 15px 38px; + margin-top: 10px; +`; + +const NumericalAnswerContainer = styled.div` + border: 3px solid #f5f5f7; + margin-bottom: 30px; +`; + +const NumericalAnswerContainerTool = styled.div` + border: 3px solid #f5f5f7; + border-bottom: none; + height: 33px; + display: flex; + flex-direction: row; + span:first-of-type { + position: relative; + top: 3px; + } +`; + +const NumericalAnswerOption = styled.div` + padding: 8px; +`; + +const ActionButton = styled.button` + background: transparent; + cursor: pointer; + margin-top: 16px; + border: none; + position: relative; + margin-left: auto; + bottom: 13px; +`; + +const StyledIconActionRemove = styled(Icon)` + height: 24px; + width: 24px; +`; + +export default ({ node, view, getPos }) => { + const context = useContext(WaxContext); + const { + options, + pmViews: { main }, + } = context; + + const customProps = main.props.customValues; + const { testMode } = customProps; + + const isEditable = main.props.editable(editable => { + return editable; + }); + + const readOnly = !isEditable; + const { feedback } = node.attrs; + + const removeQuestion = () => { + const allNodes = getNodes(context.pmViews.main); + allNodes.forEach(singleNode => { + if (singleNode.node.attrs.id === node.attrs.id) { + context.pmViews.main.dispatch( + context.pmViews.main.state.tr.delete( + singleNode.pos, + singleNode.pos + singleNode.node.nodeSize, + ), + ); + } + }); + }; + + return ( + <NumericalAnswerWrapper> + <div> + {!testMode && !readOnly && ( + <NumericalAnswerContainerTool> + <NumericalAnswerDropDownCompontent nodeId={node.attrs.id} /> + <ActionButton + aria-label="delete this question" + onClick={removeQuestion} + type="button" + > + <StyledIconActionRemove name="deleteOutlinedQuestion" /> + </ActionButton> + </NumericalAnswerContainerTool> + )} + </div> + <NumericalAnswerContainer className="numerical-answer"> + <EditorComponent + getPos={getPos} + node={node} + type="NumericalAnswer" + view={view} + /> + <NumericalAnswerOption> + {!options[node.attrs.id] && <>No Type Selected</>} + {options[node.attrs.id]?.numericalAnswer === 'exactAnswer' && ( + <ExactAnswerComponent /> + )} + {options[node.attrs.id]?.numericalAnswer === 'rangeAnswer' && ( + <RangeAnswerComponent /> + )} + {options[node.attrs.id]?.numericalAnswer === 'preciseAnswer' && ( + <PreciseAnswerComponent /> + )} + </NumericalAnswerOption> + {!testMode && !(readOnly && feedback === '') && ( + <FeedbackComponent + getPos={getPos} + node={node} + readOnly={readOnly} + view={view} + /> + )} + </NumericalAnswerContainer> + </NumericalAnswerWrapper> + ); +}; + +const getNodes = view => { + const allNodes = DocumentHelpers.findBlockNodes(view.state.doc); + const numericalAnswerpContainerNodes = []; + allNodes.forEach(node => { + if (node.node.type.name === 'numerical_answer_container') { + numericalAnswerpContainerNodes.push(node); + } + }); + return numericalAnswerpContainerNodes; +}; diff --git a/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js new file mode 100644 index 0000000000000000000000000000000000000000..aad0a1c803b8454a53e42549d16b5ac9e7907ca7 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/components/NumericalAnswerDropDownCompontent.js @@ -0,0 +1,236 @@ +/* eslint-disable react/prop-types */ +import React, { + useMemo, + useContext, + useState, + useEffect, + useRef, + createRef, +} from 'react'; +import styled from 'styled-components'; +import { + DocumentHelpers, + WaxContext, + Icon, + useOnClickOutside, +} from 'wax-prosemirror-core'; + +const Wrapper = styled.div` + opacity: ${props => (props.disabled ? '0.4' : '1')}; +`; + +const DropDownButton = styled.button` + background: #fff; + border: 1px solid #f4f4f4; + color: #000; + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; + display: flex; + position: relative; + top: 2px; + left: 3px; + width: 235px; + height: 26px; + + span { + position: relative; + top: 12px; + } +`; + +const DropDownMenu = styled.div` + visibility: ${props => (props.isOpen ? 'visible' : 'hidden')}; + background: #fff; + display: flex; + flex-direction: column; + border: 1px solid #ddd; + border-radius: 0.25rem; + box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%); + margin: 2px auto auto; + position: absolute; + width: 235px; + max-height: 150px; + overflow-y: auto; + z-index: 2; + + span { + cursor: pointer; + border-bottom: 1px solid #f4f4f4; + font-size: 11px; + padding: 8px 10px; + } + + span:focus, + span:hover { + background: #f2f9fc; + outline: 2px solid #f2f9fc; + } +`; + +const StyledIcon = styled(Icon)` + height: 18px; + width: 18px; + margin-left: auto; + position: relative; + top: 1px; +`; + +const NumericalAnswerDropDownCompontent = ({ nodeId }) => { + const dropDownOptions = [ + { + label: 'Exact answer with margin of error', + value: 'exactAnswer', + }, + { + label: 'Answer within a range', + value: 'rangeAnswer', + }, + { + label: 'Precise answer', + value: 'preciseAnswer', + }, + ]; + + const context = useContext(WaxContext); + const { + activeView, + pmViews: { main }, + } = context; + + const itemRefs = useRef([]); + const wrapperRef = useRef(); + const [isOpen, setIsOpen] = useState(false); + useOnClickOutside(wrapperRef, () => setIsOpen(false)); + + const [label, setLabel] = useState('Select Type'); + + const isEditable = main.props.editable(editable => { + return editable; + }); + + useEffect(() => { + setLabel('Select Type'); + dropDownOptions.forEach(option => { + if (context.options?.numericalAnswer === option.value) { + setLabel(option.label); + } + }); + }, []); + + let isDisabled = false; + + useEffect(() => { + if (isDisabled) setIsOpen(false); + }, [isDisabled]); + + const openCloseMenu = () => { + if (!isDisabled) setIsOpen(!isOpen); + if (isOpen) + setTimeout(() => { + activeView.focus(); + }); + }; + + const onKeyDown = (e, index) => { + e.preventDefault(); + // arrow down + if (e.keyCode === 40) { + if (index === itemRefs.current.length - 1) { + itemRefs.current[0].current.focus(); + } else { + itemRefs.current[index + 1].current.focus(); + } + } + + // arrow up + if (e.keyCode === 38) { + if (index === 0) { + itemRefs.current[itemRefs.current.length - 1].current.focus(); + } else { + itemRefs.current[index - 1].current.focus(); + } + } + + // enter + if (e.keyCode === 13) { + itemRefs.current[index].current.click(); + } + + // ESC + if (e.keyCode === 27) { + setIsOpen(false); + } + }; + + const onChange = option => { + context.setOption({ [nodeId]: { numericalAnswer: option.value } }); + main.dispatch(main.state.tr.setMeta('addToHistory', false)); + setLabel(option.label); + openCloseMenu(); + }; + + const NumericalAnswerDropDown = useMemo( + () => ( + <Wrapper disabled={isDisabled} ref={wrapperRef}> + <DropDownButton + aria-controls="numerical-answer-list" + aria-expanded={isOpen} + aria-haspopup + disabled={isDisabled} + onKeyDown={e => { + if (e.keyCode === 40) { + itemRefs.current[0].current.focus(); + } + if (e.keyCode === 27) { + setIsOpen(false); + } + if (e.keyCode === 13 || e.keyCode === 32) { + setIsOpen(true); + } + }} + onMouseDown={openCloseMenu} + type="button" + > + <span>{label}</span> <StyledIcon name="expand" /> + </DropDownButton> + <DropDownMenu + aria-label="Choose an item type" + id="numerical-list" + isOpen={isOpen} + role="menu" + > + {dropDownOptions.map((option, index) => { + itemRefs.current[index] = itemRefs.current[index] || createRef(); + return ( + <span + key={option.value} + onClick={() => onChange(option)} + onKeyDown={e => onKeyDown(e, index)} + ref={itemRefs.current[index]} + role="menuitem" + tabIndex="-1" + > + {option.label} + </span> + ); + })} + </DropDownMenu> + </Wrapper> + ), + [isDisabled, isOpen, label], + ); + + return NumericalAnswerDropDown; +}; + +const getNodes = view => { + const allNodes = DocumentHelpers.findBlockNodes(view.state.doc); + const numericalAnswerpContainerNodes = []; + allNodes.forEach(node => { + if (node.node.type.name === 'numerical_answer_container') { + numericalAnswerpContainerNodes.push(node); + } + }); + return numericalAnswerpContainerNodes; +}; + +export default NumericalAnswerDropDownCompontent; diff --git a/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js b/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..2ab9c1a5ff0467e798aac4b760c333fd2bd9c19d --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/components/PreciseAnswerComponent.js @@ -0,0 +1,57 @@ +import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; + +const AnswerContainer = styled.div` + display: flex; + flex-direction: row; + width: 100%; +`; + +const ValueContainer = styled.div` + display: flex; + flex-direction: column; + margin-right: 25px; + label { + font-size: 12px; + } + + input:focus { + outline: none; + } +`; + +const ValueInnerContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const PreciseAnswerComponent = () => { + const [precise, setPrecise] = useState(''); + + const preciseRef = useRef(null); + + const onChangePrecice = () => { + setPrecise(preciseRef.current.value); + }; + + return ( + <AnswerContainer> + <ValueContainer> + <label htmlFor="preciseAnswer"> + <ValueInnerContainer> + <span>Precise Answer</span> + <input + name="preciseAnswer" + onChange={onChangePrecice} + ref={preciseRef} + type="text" + value={precise} + /> + </ValueInnerContainer> + </label> + </ValueContainer> + </AnswerContainer> + ); +}; + +export default PreciseAnswerComponent; diff --git a/wax-questions-service/src/NumericalAnswerService/components/RangeAnswerComponent.js b/wax-questions-service/src/NumericalAnswerService/components/RangeAnswerComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..8b98b87a1f4e5e00fc6b57fa247bec302df202f8 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/components/RangeAnswerComponent.js @@ -0,0 +1,77 @@ +import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; + +const AnswerContainer = styled.div` + display: flex; + flex-direction: row; + width: 100%; +`; + +const ValueContainer = styled.div` + display: flex; + flex-direction: column; + margin-right: 25px; + label { + font-size: 12px; + } + + input:focus { + outline: none; + } +`; + +const ValueInnerContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const RangeAnswerComponent = () => { + const [minValue, setMinValue] = useState(''); + const [maxValue, setMaxValue] = useState(''); + + const minRef = useRef(null); + const maxRef = useRef(null); + + const onChangeMin = () => { + setMinValue(minRef.current.value); + }; + + const onChangeMax = () => { + setMaxValue(maxRef.current.value); + }; + + return ( + <AnswerContainer> + <ValueContainer> + <label htmlFor="minAnswer"> + <ValueInnerContainer> + <span>Min</span> + <input + name="minAnswer" + onChange={onChangeMin} + ref={minRef} + type="text" + value={minValue} + /> + </ValueInnerContainer> + </label> + </ValueContainer> + <ValueContainer> + <label htmlFor="maxAnswer"> + <ValueInnerContainer> + <span>Max</span> + <input + name="maxAnswer" + onChange={onChangeMax} + ref={maxRef} + type="text" + value={maxValue} + /> + </ValueInnerContainer> + </label> + </ValueContainer> + </AnswerContainer> + ); +}; + +export default RangeAnswerComponent; diff --git a/wax-questions-service/src/NumericalAnswerService/numericalAnswer.css b/wax-questions-service/src/NumericalAnswerService/numericalAnswer.css new file mode 100644 index 0000000000000000000000000000000000000000..237d2b993b2b3f04a9c9e8b7ca8b623b663cf6a4 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/numericalAnswer.css @@ -0,0 +1,15 @@ +/* fill The Gap */ + +.numerical-answer {} + +.ProseMirror .numerical-answer .ProseMirror { + box-shadow: none; + border-bottom: 3px solid #F5F5F7; + line-height: 2.2; + padding: 25px 10px 20px 10px; +} + +.ProseMirror .numerical-answer span>.ProseMirror { + box-shadow: none; + line-height: 1.6; +} \ No newline at end of file diff --git a/wax-questions-service/src/NumericalAnswerService/schema/NumericalAnswerContainerNode.js b/wax-questions-service/src/NumericalAnswerService/schema/NumericalAnswerContainerNode.js new file mode 100644 index 0000000000000000000000000000000000000000..a98ec716b4e15bcdefe16103d1288006e0136d81 --- /dev/null +++ b/wax-questions-service/src/NumericalAnswerService/schema/NumericalAnswerContainerNode.js @@ -0,0 +1,28 @@ +const NumericalAnswerContainerNode = { + attrs: { + id: { default: '' }, + class: { default: 'numerical-answer' }, + feedback: { default: '' }, + answerType: { default: '' }, + }, + group: 'block questions', + atom: true, + content: 'block+', + parseDOM: [ + { + tag: 'div.numerical-answer', + getAttrs(dom) { + return { + id: dom.getAttribute('id'), + class: dom.getAttribute('class'), + feedback: dom.getAttribute('feedback'), + }; + }, + }, + ], + toDOM(node) { + return ['div', node.attrs, 0]; + }, +}; + +export default NumericalAnswerContainerNode; diff --git a/wax-questions-service/src/QuestionsDropDownToolGroupService/DropDownComponent.js b/wax-questions-service/src/QuestionsDropDownToolGroupService/DropDownComponent.js index 1d4ef29d959ce91d5bcbeb6aedfbf2699f99c176..da1b97e76680f408737466898cdd8b68338c47e1 100644 --- a/wax-questions-service/src/QuestionsDropDownToolGroupService/DropDownComponent.js +++ b/wax-questions-service/src/QuestionsDropDownToolGroupService/DropDownComponent.js @@ -51,7 +51,8 @@ const DropDownMenu = styled.div` padding: 8px 10px; } - span:focus { + span:focus, + span:hover { background: #f2f9fc; outline: 2px solid #f2f9fc; } @@ -68,22 +69,22 @@ const StyledIcon = styled(Icon)` const DropDownComponent = ({ view, tools }) => { const dropDownOptions = [ { - label: 'Multiple Answers', + label: 'Multiple Choice', value: '0', item: tools[0], }, { - label: 'Multiple Choice', + label: 'Multiple Choice Single Correct', value: '1', item: tools[1], }, { - label: 'Multiple True/False', + label: 'True/False', value: '2', item: tools[2], }, { - label: 'True/False', + label: 'True/False Single Correct', value: '3', item: tools[3], }, @@ -107,6 +108,11 @@ const DropDownComponent = ({ view, tools }) => { value: '7', item: tools[7], }, + { + label: 'Numerical answer', + value: '8', + item: tools[8], + }, ]; const context = useContext(WaxContext); @@ -122,13 +128,13 @@ const DropDownComponent = ({ view, tools }) => { const [isOpen, setIsOpen] = useState(false); useOnClickOutside(wrapperRef, () => setIsOpen(false)); - const [label, setLabel] = useState('Item Type'); + const [label, setLabel] = useState('Question Type'); const isEditable = main.props.editable(editable => { return editable; }); useEffect(() => { - setLabel('Item Type'); + setLabel('Question Type'); dropDownOptions.forEach(option => { if (option.item.active(main.state)) { setLabel(option.label); diff --git a/wax-questions-service/src/QuestionsDropDownToolGroupService/QuestionsDropDown.js b/wax-questions-service/src/QuestionsDropDownToolGroupService/QuestionsDropDown.js index 395c8bedb5d1d4d89046b79d19a4f56aa8ad7a4e..94e612926602e279ac062a55e4f9c425df6d28a4 100644 --- a/wax-questions-service/src/QuestionsDropDownToolGroupService/QuestionsDropDown.js +++ b/wax-questions-service/src/QuestionsDropDownToolGroupService/QuestionsDropDown.js @@ -18,6 +18,7 @@ class QuestionsDropDown extends ToolGroup { @inject('EssayQuestion') essayQuestion, @inject('MultipleDropDownQuestion') MultipleDropDownQuestion, @inject('FillTheGapQuestion') FillTheGapQuestion, + @inject('NumericalAnswerQuestion') NumericalAnswerQuestion, ) { super(); this.tools = [ @@ -29,6 +30,7 @@ class QuestionsDropDown extends ToolGroup { essayQuestion, MultipleDropDownQuestion, FillTheGapQuestion, + NumericalAnswerQuestion, ]; } diff --git a/wax-questions-service/src/QuestionsService.js b/wax-questions-service/src/QuestionsService.js index 346fc5784d80271d39d0c02210fa6fc13f765f99..a8663f0628bf68d8a563ea44d4491047dd6c7a24 100644 --- a/wax-questions-service/src/QuestionsService.js +++ b/wax-questions-service/src/QuestionsService.js @@ -3,6 +3,7 @@ import EssayService from './EssayService/EssayService'; import FillTheGapQuestionService from './FillTheGapQuestionService/FillTheGapQuestionService'; import MatchingService from './MatchingService/MatchingService'; import MultipleDropDownService from './MultipleDropDownService/MultipleDropDownService'; +import NumericalAnswerService from './NumericalAnswerService/NumericalAnswerService'; import QuestionsDropDownToolGroupService from './QuestionsDropDownToolGroupService/QuestionsDropDownToolGroupService'; import MultipleChoiceQuestionService from './MultipleChoiceQuestionService/MultipleChoiceQuestionService'; @@ -15,6 +16,7 @@ class QuestionsService extends Service { new FillTheGapQuestionService(), new MatchingService(), new MultipleDropDownService(), + new NumericalAnswerService(), new QuestionsDropDownToolGroupService(), ]; } diff --git a/wax-table-service/src/components/TableDropDown.js b/wax-table-service/src/components/TableDropDown.js index 7b5cdf4a04e13acccebde81a0772c1c7141b20d3..9625d2373cc4b64b4242b157e3b86017584f2620 100644 --- a/wax-table-service/src/components/TableDropDown.js +++ b/wax-table-service/src/components/TableDropDown.js @@ -52,7 +52,8 @@ const DropDownMenu = styled.div` padding: 8px 10px; } - span:focus { + span:focus, + span:hover { background: #f2f9fc; outline: 2px solid #f2f9fc; } @@ -71,6 +72,7 @@ const TableDropDown = ({ item }) => { <>{!isEmpty(i18n) && i18n.exists(label) ? t(label) : defaultTrans}</> ); }; + const dropDownOptions = [ { label: (