diff --git a/editors/demo/package.json b/editors/demo/package.json index c157e23df92f750b69da8d431b7764de1706cb06..2a6658f798252adc9c8a82a1e9df27a854ee5154 100644 --- a/editors/demo/package.json +++ b/editors/demo/package.json @@ -1,12 +1,10 @@ { - "name": "editoria", + "name": "demo", "version": "0.2.0", "private": true, "dependencies": { - "@ant-design/icons": "^4.6.2", "@guardian/prosemirror-invisibles": "^1.2.0", "@pubsweet/ui-toolkit": "^2.3.1", - "antd": "^4.15.4", "fontsource-merriweather": "^3.0.9", "prosemirror-tables": "^1.1.1", "react": "^16.13.1", diff --git a/editors/demo/public/favicon.ico b/editors/demo/public/favicon.ico index a11777cc471a4344702741ab1c8a588998b1311a..cdc74943283f7298c17ca69d8ad9ab913692340d 100644 Binary files a/editors/demo/public/favicon.ico and b/editors/demo/public/favicon.ico differ diff --git a/editors/demo/public/index.html b/editors/demo/public/index.html index dd1ccfd4cd30a29aaa08b295d99be29cdeb29cf9..d5fdeb6cf17e7df9bf8bc82c33e35f5d699cc1d8 100644 --- a/editors/demo/public/index.html +++ b/editors/demo/public/index.html @@ -19,7 +19,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>React App</title> + <title>Wax Editor Demo</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> diff --git a/editors/demo/src/Editors.js b/editors/demo/src/Editors.js index abd0628fb70695fdd171c22a2956784070ef3345..d19991a2ed8613a7f85ed0fed95a7dc6ef294736 100644 --- a/editors/demo/src/Editors.js +++ b/editors/demo/src/Editors.js @@ -70,7 +70,7 @@ const Editors = () => { case 'ncbi': return <NCBI />; default: - return <Editoria />; + return <HHMI />; } }; diff --git a/editors/demo/src/HHMI/HHMI.js b/editors/demo/src/HHMI/HHMI.js index a9a1f565a5a0f238a17a178b2bd1b2ad63c07964..77c52f59b717a961acacc022526d75577c6238e7 100644 --- a/editors/demo/src/HHMI/HHMI.js +++ b/editors/demo/src/HHMI/HHMI.js @@ -15,7 +15,16 @@ const renderImage = file => { }); }; -const t = `<p class="paragraph">Based on the equation below</p><math-display class="math-node">x + y = 5</math-display><p class="paragraph">Which ones are correct?</p><p class="paragraph"></p><div id="" class="multiple-choice"><div class="multiple-choice-option" id="d7b65415-ff82-446f-afa4-accaa3837f4a" correct="false" feedback=""><p class="paragraph">answer 1</p><p class="paragraph"><math-inline class="math-node">x+y=1</math-inline></p></div><div class="multiple-choice-option" id="e7d6bb2f-7cd7-44f1-92a0-281e72157538" correct="true" feedback=""><p class="paragraph">answer 2</p></div><div class="multiple-choice-option" id="d6fc749f-afae-4203-9562-d68c380a86e5" correct="false" feedback="1111111"><p class="paragraph">answer 3</p></div></div><div id="" class="fill-the-gap"><p class="paragraph">A <span id="bfd4376c-4424-455e-9187-f53282fa1024" class="fill-the-gap">DNA</span> molecule is very long and usually consists of hundreds or thousands of genes.</p><p class="paragraph">An electron having a certain discrete amount of <span id="14dedf44-728f-4384-835f-e3af82b25623" class="fill-the-gap">energy</span> is something like a ball on a staircase.</p></div><p class="paragraph"></p>`; +const t = `<p class="paragraph">Based on the equation below</p> +<math-display class="math-node">x + y = 5</math-display><p class="paragraph">Which ones are correct?</p> +<p class="paragraph"></p> +<div id="" class="multiple-choice"><div class="multiple-choice-option" id="d7b65415-ff82-446f-afa4-accaa3837f4a" correct="false" feedback=""> +<p class="paragraph">answer 1</p><p class="paragraph"><math-inline class="math-node">x+y=1</math-inline></p></div> +<div class="multiple-choice-option" id="e7d6bb2f-7cd7-44f1-92a0-281e72157538" correct="true" feedback=""> +<p class="paragraph">answer 2</p></div><div class="multiple-choice-option" id="d6fc749f-afae-4203-9562-d68c380a86e5" correct="false" feedback="1111111"> +<p class="paragraph">answer 3</p></div></div> + +<div id="" class="fill-the-gap"><p class="paragraph">A <span id="bfd4376c-4424-455e-9187-f53282fa1024" class="fill-the-gap">DNA</span> molecule is very long and usually consists of hundreds or thousands of genes.</p><p class="paragraph">An electron having a certain discrete amount of <span id="14dedf44-728f-4384-835f-e3af82b25623" class="fill-the-gap">energy</span> is something like a ball on a staircase.</p></div><p class="paragraph"></p>`; const Hhmi = () => { return ( diff --git a/editors/demo/src/HHMI/layout/EditorElements.js b/editors/demo/src/HHMI/layout/EditorElements.js index 4d3b79fc0c922fe76bca10d0c36807d780af3101..9e50d1c478584ab846f4dc7fb463619280391613 100644 --- a/editors/demo/src/HHMI/layout/EditorElements.js +++ b/editors/demo/src/HHMI/layout/EditorElements.js @@ -324,7 +324,9 @@ export default css` /* -- Multiple Choice ---------------------------------- */ - .multiple-choice { + .multiple-choice, + .multiple-choice-single-correct, + .true-false { border: 3px solid #f5f5f7; counter-reset: question-item-multiple; margin: 38px; @@ -333,7 +335,6 @@ export default css` &:before { bottom: 25px; - content: 'Answer Group ' counter(multiple-question) '.'; counter-increment: multiple-question; position: relative; right: 20px; @@ -345,6 +346,32 @@ export default css` } } + .multiple-choice { + &:before { + content: 'Answer Group ' counter(multiple-question) ' (multiple choice)'; + } + } + + .multiple-choice-single-correct { + &:before { + content: 'Answer Group ' counter(multiple-question) + ' (multiple choice single correct)'; + } + } + + .true-false { + &:before { + content: 'Answer Group ' counter(multiple-question) ' (true/false)'; + } + } + + .true-false-single-correct { + &:before { + content: 'Answer Group ' counter(multiple-question) + ' (true/false single correct)'; + } + } + /* -- Fill The Gap ---------------------------------- */ .fill-the-gap { diff --git a/wax-prosemirror-components/index.js b/wax-prosemirror-components/index.js index 6b8a47e799e03e47d805fc978d278ee760cfeac8..7b19bfe9f1327858e92167fb76f6dda11112b42b 100644 --- a/wax-prosemirror-components/index.js +++ b/wax-prosemirror-components/index.js @@ -1,5 +1,6 @@ export { default as Overlay } from './src/components/Overlay'; export { default as Button } from './src/components/Button'; +export { default as UndoRedoButton } from './src/components/UndoRedoButton'; export { default as MenuButton } from './src/ui/buttons/MenuButton'; export { default as icons } from './src/icons/icons'; export { default as TableDropDown } from './src/components/tables/TableDropDown'; diff --git a/wax-prosemirror-components/src/components/Button.js b/wax-prosemirror-components/src/components/Button.js index 14f7c44e6d8772075e4f4b76d4c7e6b4190377b3..992be55df97356fd3ee82b1c747f7bb701a1fe37 100644 --- a/wax-prosemirror-components/src/components/Button.js +++ b/wax-prosemirror-components/src/components/Button.js @@ -4,7 +4,7 @@ import { WaxContext } from 'wax-prosemirror-core'; import MenuButton from '../ui/buttons/MenuButton'; const Button = ({ view = {}, item }) => { - const { active, icon, label, onlyOnMain, run, select, title } = item; + const { active, icon, label, run, select, title } = item; const { view: { main }, @@ -12,8 +12,6 @@ const Button = ({ view = {}, item }) => { activeView, } = useContext(WaxContext); - if (onlyOnMain) view = main; - const isEditable = main.props.editable(editable => { return editable; }); diff --git a/wax-prosemirror-components/src/components/SaveButton.js b/wax-prosemirror-components/src/components/SaveButton.js index 2d91b820f7f6013a89740e4cd1571d61a7474e9f..fcbcdb459a570ecca38fa5b77bef65d2d3a90968 100644 --- a/wax-prosemirror-components/src/components/SaveButton.js +++ b/wax-prosemirror-components/src/components/SaveButton.js @@ -4,7 +4,7 @@ import { WaxContext } from 'wax-prosemirror-core'; import MenuButton from '../ui/buttons/MenuButton'; const SaveButton = ({ view = {}, item }) => { - const { icon, label, onlyOnMain, select, title } = item; + const { icon, label, select, title } = item; const { view: { main }, @@ -12,8 +12,6 @@ const SaveButton = ({ view = {}, item }) => { activeView, } = useContext(WaxContext); - if (onlyOnMain) view = main; - const { state } = view; const [isSaving, setIsSaving] = useState(false); @@ -57,7 +55,9 @@ const SaveButton = ({ view = {}, item }) => { disabled={isDisabled} iconName={iconTodisplay} label={label} - onMouseDown={e => handleMouseDown(e, view.state, view.dispatch)} + onMouseDown={e => + handleMouseDown(e, main.view.state, main.view.dispatch) + } title={title} /> ), diff --git a/wax-prosemirror-components/src/components/TitleButton.js b/wax-prosemirror-components/src/components/TitleButton.js index 1477ed41f912985ff94ba4ac477f1237cfdf724c..4e6ada5e7beff42e2e825690e6de06e1ab9ba10f 100644 --- a/wax-prosemirror-components/src/components/TitleButton.js +++ b/wax-prosemirror-components/src/components/TitleButton.js @@ -5,7 +5,7 @@ import { DocumentHelpers } from 'wax-prosemirror-utilities'; import MenuButton from '../ui/buttons/MenuButton'; const TitleButton = ({ view = {}, item }) => { - const { active, icon, label, onlyOnMain, run, select, title } = item; + const { active, icon, label, run, select, title } = item; const { app, @@ -14,8 +14,6 @@ const TitleButton = ({ view = {}, item }) => { activeView, } = useContext(WaxContext); - if (onlyOnMain) view = main; - const { dispatch, state } = view; const titleNode = DocumentHelpers.findChildrenByType( diff --git a/wax-prosemirror-components/src/components/UndoRedoButton.js b/wax-prosemirror-components/src/components/UndoRedoButton.js new file mode 100644 index 0000000000000000000000000000000000000000..834b053ff3cacc0b0b0cd1ddf8eed7ca5ad88388 --- /dev/null +++ b/wax-prosemirror-components/src/components/UndoRedoButton.js @@ -0,0 +1,51 @@ +/* eslint react/prop-types: 0 */ +import React, { useContext, useMemo } from 'react'; +import { WaxContext } from 'wax-prosemirror-core'; +import MenuButton from '../ui/buttons/MenuButton'; + +const UndoRedoButton = ({ view = {}, item }) => { + const { active, icon, label, run, select, title } = item; + + const { + view: { main }, + activeViewId, + activeView, + } = useContext(WaxContext); + + const isEditable = main.props.editable(editable => { + return editable; + }); + + const { state } = view; + + const handleMouseDown = (e, editorState, editorDispatch) => { + e.preventDefault(); + run(editorState, editorDispatch); + }; + + const isActive = !!( + active(activeView.state, activeViewId) && + select(state, activeViewId, activeView) + ); + + let isDisabled = !select(state, activeViewId, activeView); + if (!isEditable) isDisabled = true; + + const UndoRedoButtonComponent = useMemo( + () => ( + <MenuButton + active={isActive || false} + disabled={isDisabled} + iconName={icon} + label={label} + onMouseDown={e => handleMouseDown(e, main.state, main.dispatch)} + title={title} + /> + ), + [isActive, isDisabled], + ); + + return UndoRedoButtonComponent; +}; + +export default UndoRedoButton; diff --git a/wax-prosemirror-components/src/components/various/LeftSideButton.js b/wax-prosemirror-components/src/components/various/LeftSideButton.js index 61b65e239a4c3fa3084a07d68c3b0507a0960c8a..9df3369d954320d25d919ae9820774655451d5d2 100644 --- a/wax-prosemirror-components/src/components/various/LeftSideButton.js +++ b/wax-prosemirror-components/src/components/various/LeftSideButton.js @@ -13,7 +13,7 @@ const StyledButton = styled(MenuButton)` `; const LeftSideButton = ({ view = {}, item }) => { - const { active, icon, label, onlyOnMain, run, select, title } = item; + const { active, icon, label, run, select, title } = item; const { view: { main }, @@ -21,8 +21,6 @@ const LeftSideButton = ({ view = {}, item }) => { activeView, } = useContext(WaxContext); - if (onlyOnMain) view = main; - const isEditable = main.props.editable(editable => { return editable; }); diff --git a/wax-prosemirror-core/src/WaxContext.js b/wax-prosemirror-core/src/WaxContext.js index bbfe41a5389e982713f8d1b58c507641882059dc..7437cdd24582dbb9b05ef65b875075c24f1f666f 100644 --- a/wax-prosemirror-core/src/WaxContext.js +++ b/wax-prosemirror-core/src/WaxContext.js @@ -19,6 +19,10 @@ export default props => { activeView: props.activeView || {}, activeViewId: props.activeViewId || {}, options: { fullScreen: false }, + transaction: {}, + setTransaction: tr => { + Object.assign(context.transaction, tr); + }, setOption: option => { Object.assign(context.options, option); }, diff --git a/wax-prosemirror-core/src/WaxView.js b/wax-prosemirror-core/src/WaxView.js index 0ad8492495012ad46d505a6c9f69913862b90ef0..f3648974f61c29315be126b8c90b1913ea70f62f 100644 --- a/wax-prosemirror-core/src/WaxView.js +++ b/wax-prosemirror-core/src/WaxView.js @@ -126,6 +126,9 @@ const WaxView = forwardRef((props, ref) => { main don't keep updating the view ,as this is the central point of each transaction */ + + context.setTransaction(transaction); + if (!transaction.getMeta('outsideView')) { context.updateView( { @@ -137,11 +140,8 @@ const WaxView = forwardRef((props, ref) => { if (targetFormat === 'JSON') { if (view.state.doc !== previousDoc || tr.getMeta('forceUpdate')) props.onChange(state.doc.toJSON()); - } else { - // eslint-disable-next-line no-lonely-if - if (view.state.doc !== previousDoc || tr.getMeta('forceUpdate')) - props.onChange(state.doc.content); - } + } else if (view.state.doc !== previousDoc || tr.getMeta('forceUpdate')) + props.onChange(state.doc.content); }; const editor = ( diff --git a/wax-prosemirror-services/index.js b/wax-prosemirror-services/index.js index 3023e4f4d4d7d5ae97b44c75a42cf8179f498376..9273ee11149d370e5e3e5b60e02b665b8d3769d7 100644 --- a/wax-prosemirror-services/index.js +++ b/wax-prosemirror-services/index.js @@ -45,6 +45,8 @@ 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/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService'; +export { default as TrueFalseQuestionService } from './src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseQuestionService'; export { default as FillTheGapQuestionService } from './src/FillTheGapQuestionService/FillTheGapQuestionService'; /* diff --git a/wax-prosemirror-services/package.json b/wax-prosemirror-services/package.json index 0fe6fce7a9e64f37f83e7a78ba7c415768385d56..bf2f48de17d178a735d7f444fb5a8f334787a056 100644 --- a/wax-prosemirror-services/package.json +++ b/wax-prosemirror-services/package.json @@ -14,6 +14,8 @@ "build": "BABEL_ENV=production rollup -c" }, "dependencies": { + "@ant-design/icons": "^4.6.2", + "antd": "^4.15.4", "inversify": "^5.0.1", "inversify-inject-decorators": "^3.1.0", "lodash": "^4.17.4", diff --git a/wax-prosemirror-services/src/BaseService/RedoService/Redo.js b/wax-prosemirror-services/src/BaseService/RedoService/Redo.js index 2c090f96ec1c86b1b236199808397ae71f8949ef..eb1f230b2263141e3fa72b58d9242d74d78bae49 100644 --- a/wax-prosemirror-services/src/BaseService/RedoService/Redo.js +++ b/wax-prosemirror-services/src/BaseService/RedoService/Redo.js @@ -1,5 +1,8 @@ +import React from 'react'; +import { isEmpty } from 'lodash'; import { redo } from 'prosemirror-history'; import { injectable } from 'inversify'; +import { UndoRedoButton } from 'wax-prosemirror-components'; import Tools from '../../lib/Tools'; export default @@ -7,7 +10,6 @@ export default class Redo extends Tools { title = 'Redo'; icon = 'redo'; - onlyOnMain = true; name = 'Redo'; get run() { @@ -23,4 +25,12 @@ class Redo extends Tools { select(state) { return redo(state); } + + renderTool(view) { + if (isEmpty(view)) return null; + // eslint-disable-next-line no-underscore-dangle + return this._isDisplayed ? ( + <UndoRedoButton item={this.toJSON()} key="Redo" view={view} /> + ) : null; + } } diff --git a/wax-prosemirror-services/src/BaseService/SaveService/Save.js b/wax-prosemirror-services/src/BaseService/SaveService/Save.js index f13d6e90139e4aa9997f36a8ff723e4c478d4215..cd72a2f265fe214602062d2bd1478991570b101d 100644 --- a/wax-prosemirror-services/src/BaseService/SaveService/Save.js +++ b/wax-prosemirror-services/src/BaseService/SaveService/Save.js @@ -1,7 +1,6 @@ import React from 'react'; import { isEmpty } from 'lodash'; import { injectable } from 'inversify'; - import { SaveButton, icons } from 'wax-prosemirror-components'; import Tools from '../../lib/Tools'; @@ -10,7 +9,6 @@ export default class Save extends Tools { title = 'Save changes'; icon = 'save'; - onlyOnMain = true; name = 'Save'; content = icons.save; name = 'Save'; @@ -21,8 +19,6 @@ class Save extends Tools { }; } - get enable() {} - renderTool(view) { if (isEmpty(view)) return null; // eslint-disable-next-line no-underscore-dangle diff --git a/wax-prosemirror-services/src/BaseService/UndoService/Undo.js b/wax-prosemirror-services/src/BaseService/UndoService/Undo.js index 50aa7e657a0a9707db09790da3391f6397f0a057..2781908a212d88b10a4f1421545a9228166069ba 100644 --- a/wax-prosemirror-services/src/BaseService/UndoService/Undo.js +++ b/wax-prosemirror-services/src/BaseService/UndoService/Undo.js @@ -1,5 +1,8 @@ +import React from 'react'; +import { isEmpty } from 'lodash'; import { undo } from 'prosemirror-history'; import { injectable } from 'inversify'; +import { UndoRedoButton } from 'wax-prosemirror-components'; import Tools from '../../lib/Tools'; export default @@ -7,7 +10,6 @@ export default class Undo extends Tools { title = 'Undo'; icon = 'undo'; - onlyOnMain = true; name = 'Undo'; get run() { @@ -23,4 +25,12 @@ class Undo extends Tools { select(state) { return undo(state); } + + renderTool(view) { + if (isEmpty(view)) return null; + // eslint-disable-next-line no-underscore-dangle + return this._isDisplayed ? ( + <UndoRedoButton item={this.toJSON()} key="Undo" view={view} /> + ) : null; + } } diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceNodeView.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceNodeView.js index 742da845512a1469c261615dbcb1626ca245a228..2cb264791805081a9edb21042510ec4b2404a04a 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceNodeView.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceNodeView.js @@ -45,4 +45,12 @@ export default class MultipleChoiceNodeView extends AbstractNodeView { 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/MultipleChoiceQuestion.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js index e5b708061c4b059cb0cc8dfcd2e2e81a8562c41f..632e739f2c1880e2d87f4a4925749b8f365d5cd1 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js @@ -3,49 +3,9 @@ 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 './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_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.create( - { id: newAnswerId }, - Fragment.empty, - ); - dispatch(main.state.tr.replaceSelectionWith(answerOption)); - setTimeout(() => { - helpers.createEmptyParagraph(context, newAnswerId); - }, 50); -}; +import Tools from '../lib/Tools'; @injectable() class MultipleChoiceQuestion extends Tools { @@ -55,14 +15,23 @@ class MultipleChoiceQuestion extends Tools { label = 'Multiple Choice'; get run() { - return (view, main, context) => { - checkifEmpty(view); - createOption(main, context); + return (view, context) => { + helpers.createOptions( + view, + context, + view.state.config.schema.nodes.multiple_choice, + view.state.config.schema.nodes.multiple_choice_container, + ); }; } get active() { - return state => {}; + return state => { + return Commands.isParentOfType( + state, + state.config.schema.nodes.multiple_choice, + ); + }; } select = (state, activeView) => { diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js index 4df5be64aec344cd2f9d908946b21811bf7282cf..29a78d46fca77f1eb14777d2ecf1ab8e553394c0 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js @@ -4,6 +4,8 @@ import multipleChoiceNode from './schema/multipleChoiceNode'; import multipleChoiceContainerNode from './schema/multipleChoiceContainerNode'; import QuestionComponent from './components/QuestionComponent'; import MultipleChoiceNodeView from './MultipleChoiceNodeView'; +import MultipleChoiceSingleCorrectQuestionService from './MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService'; +import TrueFalseQuestionService from './TrueFalseQuestionService/TrueFalseQuestionService'; class MultipleChoiceQuestionService extends Service { register() { @@ -25,6 +27,11 @@ class MultipleChoiceQuestionService extends Service { context: this.app, }); } + + dependencies = [ + new MultipleChoiceSingleCorrectQuestionService(), + new TrueFalseQuestionService(), + ]; } export default MultipleChoiceQuestionService; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js new file mode 100644 index 0000000000000000000000000000000000000000..87956baad8530b370453458a75681225e406f03c --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js @@ -0,0 +1,56 @@ +import AbstractNodeView from '../../PortalService/AbstractNodeView'; + +export default class MultipleChoiceSingleCorrectNodeView 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 'multiple_choice_single_correct'; + } + + 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/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js new file mode 100644 index 0000000000000000000000000000000000000000..b194925133c5d69eedfbf738eab8e9eda81d4fc4 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js @@ -0,0 +1,66 @@ +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 ToolBarBtn from '../components/ToolBarBtn'; +import helpers from '../helpers/helpers'; +import Tools from '../../lib/Tools'; + +@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, context) => { + helpers.createOptions( + view, + context, + view.state.config.schema.nodes.multiple_choice_single_correct, + view.state.config.schema.nodes.multiple_choice_single_correct_container, + ); + }; + } + + get active() { + return state => { + return Commands.isParentOfType( + state, + state.config.schema.nodes.multiple_choice_single_correct, + ); + }; + } + + 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/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js new file mode 100644 index 0000000000000000000000000000000000000000..69f28c6d51b22e1f1b9e07aaff2655756886673d --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/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/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..1fb3cca3d7489a1a3c00529244e13dc237922a05 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/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 '../../helpers/helpers'; +import EditorComponent from '../../components/EditorComponent'; +import FeedbackComponent from '../../components/FeedbackComponent'; +import Button from '../../components/Button'; +import SwitchComponent from './SwitchComponent'; + +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_single_correct') { + 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_single_correct.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/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..c6ff6bdbab825cb8b5174e2b201dbb8fbb86cfea --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js @@ -0,0 +1,117 @@ +/* eslint-disable react/prop-types */ + +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 '../../components/Switch'; +import { NodeSelection } from 'prosemirror-state'; + +const StyledSwitch = styled(Switch)` + display: flex; + margin-left: auto; + + .ant-switch-checked { + background-color: green; + } +`; + +const CustomSwitch = ({ node, getPos }) => { + const context = useContext(WaxContext); + const [checked, setChecked] = useState(false); + const { + view: { main }, + } = context; + + useEffect(() => { + const allNodes = getNodes(main); + allNodes.forEach(singNode => { + if (singNode.node.attrs.id === node.attrs.id) { + setChecked(singNode.node.attrs.correct); + } + }); + }, [getNodes(main)]); + + const handleChange = () => { + setChecked(!checked); + main.dispatch( + main.state.tr.setSelection( + NodeSelection.create(main.state.doc, getPos()), + ), + ); + const parentContainer = findParentOfType( + main.state, + main.state.config.schema.nodes.multiple_choice_single_correct_container, + ); + let parentPosition = 0; + + main.state.doc.descendants((parentNode, parentPos) => { + if ( + parentNode.type.name === 'multiple_choice_single_correct_container' && + parentNode.attrs.id === parentContainer.attrs.id + ) { + parentPosition = parentPos; + } + }); + + const { tr } = main.state; + + parentContainer.descendants((element, position) => { + if ( + element.type.name === 'multiple_choice_single_correct' && + element.attrs.id === node.attrs.id + ) { + tr.setNodeMarkup(getPos(), undefined, { + ...element.attrs, + correct: !checked, + }); + } else if ( + element.type.name === 'multiple_choice_single_correct' && + element.attrs.correct + ) { + tr.setNodeMarkup(parentPosition + position + 1, undefined, { + ...element.attrs, + correct: false, + }); + } + }); + + main.dispatch(tr); + }; + + 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_single_correct') { + multipleChoiceNodes.push(node); + } + }); + return multipleChoiceNodes; +}; + +export default CustomSwitch; + +const findParentOfType = (state, nodeType) => { + let nodeFound = ''; + const predicate = node => node.type === nodeType; + for (let i = state.selection.$from.depth; i > 0; i -= 1) { + const node = state.selection.$from.node(i); + if (predicate(node)) { + nodeFound = node; + } + } + return nodeFound; +}; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js new file mode 100644 index 0000000000000000000000000000000000000000..8113386e89f13067cafb21beab1bc9529664c751 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js @@ -0,0 +1,31 @@ +import { v4 as uuidv4 } from 'uuid'; + +const multipleChoiceSingleCorrectContainerNode = { + attrs: { + id: { default: uuidv4() }, + class: { default: 'multiple-choice-single-correct' }, + correctId: { default: '' }, + }, + group: 'block questions', + atom: true, + selectable: true, + draggable: true, + content: 'block+', + parseDOM: [ + { + tag: 'div.multiple-choice-single-correct', + getAttrs(dom) { + return { + id: dom.dataset.id, + class: dom.getAttribute('class'), + correctId: dom.getAttribute('correctId'), + }; + }, + }, + ], + toDOM(node) { + return ['div', node.attrs, 0]; + }, +}; + +export default multipleChoiceSingleCorrectContainerNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js new file mode 100644 index 0000000000000000000000000000000000000000..719d7e99452cc759919b6efa8cc3429bf77cfbc9 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js @@ -0,0 +1,30 @@ +import { v4 as uuidv4 } from 'uuid'; + +const multipleChoiceSingleCorrectNode = { + attrs: { + class: { default: 'multiple-choice-option-single-correct' }, + id: { default: uuidv4() }, + correct: { default: false }, + feedback: { default: '' }, + }, + group: 'block questions', + content: 'block*', + defining: 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'), + }; + }, + }, + ], + toDOM: node => ['div', node.attrs, 0], +}; + +export default multipleChoiceSingleCorrectNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseNodeView.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseNodeView.js new file mode 100644 index 0000000000000000000000000000000000000000..ad6eb44d84f0bb4cdee64a43c96fd813422556de --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseNodeView.js @@ -0,0 +1,55 @@ +import AbstractNodeView from '../../PortalService/AbstractNodeView'; + +export default class TrueFalseNodeView 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 'true_false'; + } + + update(node) { + 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/TrueFalseQuestionService/TrueFalseQuestion.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseQuestion.js new file mode 100644 index 0000000000000000000000000000000000000000..403b2455100fb8c68e21db121c0d8d0074e14c58 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseQuestion.js @@ -0,0 +1,69 @@ +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 ToolBarBtn from '../components/ToolBarBtn'; +import helpers from '../helpers/helpers'; +import Tools from '../../lib/Tools'; + +@injectable() +class MultipleChoiceQuestion extends Tools { + title = 'Add True False Question'; + icon = 'multipleChoice'; + name = 'TrueFalse'; + label = 'True False'; + + get run() { + return (view, context) => { + helpers.createOptions( + view, + context, + view.state.config.schema.nodes.true_false, + view.state.config.schema.nodes.true_false_container, + ); + }; + } + + get active() { + return state => { + return Commands.isParentOfType( + state, + state.config.schema.nodes.true_false, + ); + }; + } + + 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 MultipleChoiceQuestion; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseQuestionService.js new file mode 100644 index 0000000000000000000000000000000000000000..6455c52b11fdee27a6a288c8d338ac0e8f9474ee --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/TrueFalseQuestionService.js @@ -0,0 +1,30 @@ +import Service from '../../Service'; +import TrueFalseQuestion from './TrueFalseQuestion'; +import trueFalseNode from './schema/trueFalseNode'; +import trueFalseContainerNode from './schema/trueFalseContainerNode'; +import QuestionComponent from './components/QuestionComponent'; +import TrueFalseNodeView from './TrueFalseNodeView'; + +class TrueFalseQuestionService extends Service { + register() { + this.container.bind('TrueFalseQuestion').to(TrueFalseQuestion); + const createNode = this.container.get('CreateNode'); + const addPortal = this.container.get('AddPortal'); + + createNode({ + true_false_container: trueFalseContainerNode, + }); + + createNode({ + true_false: trueFalseNode, + }); + + addPortal({ + nodeView: TrueFalseNodeView, + component: QuestionComponent, + context: this.app, + }); + } +} + +export default TrueFalseQuestionService; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/QuestionComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..e2be5c890e1876d28c38af8fbad3a1ac9a54d440 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/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 '../../helpers/helpers'; +import FeedbackComponent from '../../components/FeedbackComponent'; +import EditorComponent from '../../components/EditorComponent'; +import Button from '../../components/Button'; +import SwitchComponent from './SwitchComponent'; + +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 === 'true_false') { + 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.true_false.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/TrueFalseQuestionService/components/SwitchComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/SwitchComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..8b3de43187bdc8de9c88d78d7440d14955abc689 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/components/SwitchComponent.js @@ -0,0 +1,80 @@ +/* eslint-disable react/prop-types */ + +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 '../../components/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); + const { + view: { main }, + } = context; + + useEffect(() => { + const allNodes = getNodes(main); + allNodes.forEach(singNode => { + if (singNode.node.attrs.id === node.attrs.id) { + setChecked(singNode.node.attrs.correct); + } + }); + }, [getNodes(main)]); + + const handleChange = () => { + setChecked(!checked); + const allNodes = getNodes(main); + allNodes.forEach(singleNode => { + if (singleNode.node.attrs.id === node.attrs.id) { + main.dispatch( + main.state.tr.setNodeMarkup(getPos(), undefined, { + ...singleNode.node.attrs, + correct: !checked, + }), + ); + } + }); + }; + + return ( + <StyledSwitch + checked={checked} + checkedChildren="True" + label="True/false?" + labelPosition="left" + onChange={handleChange} + unCheckedChildren="False" + /> + ); +}; + +const getNodes = view => { + const allNodes = DocumentHelpers.findBlockNodes(view.state.doc); + const multipleChoiceNodes = []; + allNodes.forEach(node => { + if (node.node.type.name === 'true_false') { + multipleChoiceNodes.push(node); + } + }); + return multipleChoiceNodes; +}; + +export default CustomSwitch; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseContainerNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseContainerNode.js new file mode 100644 index 0000000000000000000000000000000000000000..c71293a24bddbbed00856b25d677d06699c49634 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseContainerNode.js @@ -0,0 +1,27 @@ +const trueFalseContainerNode = { + attrs: { + id: { default: '' }, + class: { default: 'true-false' }, + }, + group: 'block questions', + atom: true, + selectable: true, + draggable: true, + content: 'block+', + parseDOM: [ + { + tag: 'div.true-false', + getAttrs(dom) { + return { + id: dom.dataset.id, + class: dom.getAttribute('class'), + }; + }, + }, + ], + toDOM(node) { + return ['div', node.attrs, 0]; + }, +}; + +export default trueFalseContainerNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js new file mode 100644 index 0000000000000000000000000000000000000000..2d9c0de706a3aadc1e0b7f118e12588bf70d5a11 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseQuestionService/schema/trueFalseNode.js @@ -0,0 +1,30 @@ +import { v4 as uuidv4 } from 'uuid'; + +const trueFalseNode = { + attrs: { + class: { default: 'true-false-option' }, + id: { default: uuidv4() }, + correct: { default: false }, + feedback: { default: '' }, + }, + group: 'block questions', + content: 'block*', + defining: true, + + parseDOM: [ + { + tag: 'div.true-false-option', + getAttrs(dom) { + return { + id: dom.getAttribute('id'), + class: dom.getAttribute('class'), + correct: JSON.parse(dom.getAttribute('correct').toLowerCase()), + feedback: dom.getAttribute('feedback'), + }; + }, + }, + ], + toDOM: node => ['div', node.attrs, 0], +}; + +export default trueFalseNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectNodeView.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectNodeView.js new file mode 100644 index 0000000000000000000000000000000000000000..327937384a6fcd686c6ae87a4faf854223029f37 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectNodeView.js @@ -0,0 +1,56 @@ +import AbstractNodeView from '../../PortalService/AbstractNodeView'; + +export default class TrueFalseSingleCorrectNodeView 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 'true_false_single_correct'; + } + + 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/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestion.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestion.js new file mode 100644 index 0000000000000000000000000000000000000000..0098426d830a01dfdf76e819dde8d60219252550 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestion.js @@ -0,0 +1,66 @@ +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 ToolBarBtn from '../components/ToolBarBtn'; +import helpers from '../helpers/helpers'; +import Tools from '../../lib/Tools'; + +@injectable() +class TrueFalseSingleCorrectQuestion extends Tools { + title = 'Add True False Single Correct Question'; + icon = 'multipleChoice'; + name = 'True False Single Correct'; + label = 'True False Single Correct'; + + get run() { + return (view, context) => { + helpers.createOptions( + view, + context, + view.state.config.schema.nodes.true_false_single_correct, + view.state.config.schema.nodes.true_false_single_correct_container, + ); + }; + } + + get active() { + return state => { + return Commands.isParentOfType( + state, + state.config.schema.nodes.true_false_single_correct, + ); + }; + } + + 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 TrueFalseSingleCorrectQuestion; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestionService.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/QuestionComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..1fb3cca3d7489a1a3c00529244e13dc237922a05 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/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 '../../helpers/helpers'; +import EditorComponent from '../../components/EditorComponent'; +import FeedbackComponent from '../../components/FeedbackComponent'; +import Button from '../../components/Button'; +import SwitchComponent from './SwitchComponent'; + +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_single_correct') { + 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_single_correct.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/TrueFalseSingleCorrectQuestionService/components/SwitchComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/SwitchComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..e9110f8f34ca4c96aa609c5ba83983de21910798 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/components/SwitchComponent.js @@ -0,0 +1,117 @@ +/* eslint-disable react/prop-types */ + +import React, { useState, useContext, useEffect } from 'react'; +import { WaxContext } from 'wax-prosemirror-core'; +import { DocumentHelpers } from 'wax-prosemirror-utilities'; +import { NodeSelection } from 'prosemirror-state'; +import styled from 'styled-components'; +import Switch from '../../components/Switch'; + +const StyledSwitch = styled(Switch)` + display: flex; + margin-left: auto; + + .ant-switch-checked { + background-color: green; + } +`; + +const CustomSwitch = ({ node, getPos }) => { + const context = useContext(WaxContext); + const [checked, setChecked] = useState(false); + const { + view: { main }, + } = context; + + useEffect(() => { + const allNodes = getNodes(main); + allNodes.forEach(singNode => { + if (singNode.node.attrs.id === node.attrs.id) { + setChecked(singNode.node.attrs.correct); + } + }); + }, [getNodes(main)]); + + const handleChange = () => { + setChecked(!checked); + main.dispatch( + main.state.tr.setSelection( + NodeSelection.create(main.state.doc, getPos()), + ), + ); + const parentContainer = findParentOfType( + main.state, + main.state.config.schema.nodes.multiple_choice_single_correct_container, + ); + let parentPosition = 0; + + main.state.doc.descendants((parentNode, parentPos) => { + if ( + parentNode.type.name === 'multiple_choice_single_correct_container' && + parentNode.attrs.id === parentContainer.attrs.id + ) { + parentPosition = parentPos; + } + }); + + const { tr } = main.state; + + parentContainer.descendants((element, position) => { + if ( + element.type.name === 'multiple_choice_single_correct' && + element.attrs.id === node.attrs.id + ) { + tr.setNodeMarkup(getPos(), undefined, { + ...element.attrs, + correct: !checked, + }); + } else if ( + element.type.name === 'multiple_choice_single_correct' && + element.attrs.correct + ) { + tr.setNodeMarkup(parentPosition + position + 1, undefined, { + ...element.attrs, + correct: false, + }); + } + }); + + main.dispatch(tr); + }; + + 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_single_correct') { + multipleChoiceNodes.push(node); + } + }); + return multipleChoiceNodes; +}; + +export default CustomSwitch; + +const findParentOfType = (state, nodeType) => { + let nodeFound = ''; + const predicate = node => node.type === nodeType; + for (let i = state.selection.$from.depth; i > 0; i -= 1) { + const node = state.selection.$from.node(i); + if (predicate(node)) { + nodeFound = node; + } + } + return nodeFound; +}; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectContainerNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectContainerNode.js new file mode 100644 index 0000000000000000000000000000000000000000..fa5c5fd1a42ee7db7b35bb2da4b2bb5d8eb19710 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectContainerNode.js @@ -0,0 +1,27 @@ +const trueFalseSingleCorrectContainerNode = { + attrs: { + id: { default: '' }, + class: { default: 'true-false-single-correct' }, + }, + group: 'block questions', + atom: true, + selectable: true, + draggable: true, + content: 'block+', + parseDOM: [ + { + tag: 'div.true-false-single-correct', + getAttrs(dom) { + return { + id: dom.dataset.id, + class: dom.getAttribute('class'), + }; + }, + }, + ], + toDOM(node) { + return ['div', node.attrs, 0]; + }, +}; + +export default trueFalseSingleCorrectContainerNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js new file mode 100644 index 0000000000000000000000000000000000000000..b0720142709739573eb649ef169613ee0f3d6d90 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/TrueFalseSingleCorrectQuestionService/schema/trueFalseSingleCorrectNode.js @@ -0,0 +1,30 @@ +import { v4 as uuidv4 } from 'uuid'; + +const trueFalseSingleCorrectNode = { + attrs: { + class: { default: 'true-false-single-correct-option' }, + id: { default: uuidv4() }, + correct: { default: false }, + feedback: { default: '' }, + }, + group: 'block questions', + content: 'block*', + defining: true, + + parseDOM: [ + { + tag: 'true-false-single-correct-option', + getAttrs(dom) { + return { + id: dom.getAttribute('id'), + class: dom.getAttribute('class'), + correct: JSON.parse(dom.getAttribute('correct').toLowerCase()), + feedback: dom.getAttribute('feedback'), + }; + }, + }, + ], + toDOM: node => ['div', node.attrs, 0], +}; + +export default trueFalseSingleCorrectNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js index ca16b28ec6758d630d907ec81e6a93c08209cebe..82b25b619ce8a037240276e3410c6e420262dbbf 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/EditorComponent.js @@ -82,8 +82,6 @@ const EditorComponent = ({ node, view, getPos }) => { ...plugins, ]); - const { activeViewId } = context; - useEffect(() => { questionView = new EditorView( { @@ -100,14 +98,21 @@ const EditorComponent = ({ node, view, getPos }) => { 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.view.main.dispatch( + context.view.main.state.tr.setSelection( + new TextSelection( + context.view.main.state.tr.doc.resolve(getPos() + 2), ), ), ); + // 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 diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/FeedbackComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/FeedbackComponent.js index 0fa7d157c701c499a128e98776835af53f5db5b7..ce6248364514b317fce896059b5f98aac8a67c17 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/FeedbackComponent.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/FeedbackComponent.js @@ -31,12 +31,14 @@ export default ({ node, view, getPos }) => { 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); + allNodes.forEach(singleNode => { + if (singleNode.node.attrs.id === node.attrs.id) { + if (!typing || context.transaction.meta.inputType === 'Redo') { + setFeedBack(singleNode.node.attrs.feedback); + } if (!isFirstRun) { - if (singNode.node.attrs.feedback === '') - setFeedBack(singNode.node.attrs.feedback); + if (singleNode.node.attrs.feedback === '') + setFeedBack(singleNode.node.attrs.feedback); } } }); @@ -47,7 +49,7 @@ export default ({ node, view, getPos }) => { if (e.key === 'Backspace') { context.view.main.dispatch( context.view.main.state.tr.setSelection( - TextSelection.create(context.view.main.state.tr.doc, 0), + TextSelection.create(context.view.main.state.tr.doc, null), ), ); } @@ -102,7 +104,12 @@ const getNodes = view => { const allNodes = DocumentHelpers.findBlockNodes(view.state.doc); const multipleChoiceNodes = []; allNodes.forEach(node => { - if (node.node.type.name === 'multiple_choice') { + if ( + node.node.type.name === 'multiple_choice' || + node.node.type.name === 'multiple_choice_single_correct' || + node.node.type.name === 'true_false' || + node.node.type.name === 'true_false_single_correct' + ) { multipleChoiceNodes.push(node); } }); diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/SwitchComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/SwitchComponent.js index 0d2b5c97b286e17f9ece73fef804a5c17397934c..df7f64fb2f49f4ed022321889259d0c25365d32d 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/SwitchComponent.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/SwitchComponent.js @@ -26,23 +26,26 @@ const StyledSwitch = styled(Switch)` const CustomSwitch = ({ node, getPos }) => { const context = useContext(WaxContext); const [checked, setChecked] = useState(false); + const { + view: { main }, + } = context; useEffect(() => { - const allNodes = getNodes(context.view.main); + const allNodes = getNodes(main); allNodes.forEach(singNode => { if (singNode.node.attrs.id === node.attrs.id) { setChecked(singNode.node.attrs.correct); } }); - }, [getNodes(context.view.main)]); + }, [getNodes(main)]); const handleChange = () => { setChecked(!checked); - const allNodes = getNodes(context.view.main); + const allNodes = getNodes(main); allNodes.forEach(singleNode => { if (singleNode.node.attrs.id === node.attrs.id) { - context.view.main.dispatch( - context.view.main.state.tr.setNodeMarkup(getPos(), undefined, { + main.dispatch( + main.state.tr.setNodeMarkup(getPos(), undefined, { ...singleNode.node.attrs, correct: !checked, }), diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/ToolBarBtn.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/ToolBarBtn.js index d583f3b18d6f40f4303a1a7e1c73be7330d8d320..84090be501c7a846357f7f570d52e7d327f4356a 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/ToolBarBtn.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/ToolBarBtn.js @@ -13,44 +13,37 @@ const StyledButton = styled(MenuButton)` `; const ToolBarBtn = ({ view = {}, item }) => { - const { active, icon, label, onlyOnMain, run, select, title } = item; + const { icon, label, 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} + active={false} disabled={isDisabled} iconName={icon} label={label} onMouseDown={e => { e.preventDefault(); - item.run(view, main, context); + item.run(context.view.main, context); }} title={title} /> ), - [isActive, isDisabled], + [isDisabled], ); return ToolBarBtnComponent; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js index 537c87cab809865caebe35a5ea90724b7e88352b..dec693bc7ff51704433ececd4ae2ea694a830718 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js @@ -1,4 +1,8 @@ +import { v4 as uuidv4 } from 'uuid'; import { TextSelection } from 'prosemirror-state'; +import { Commands } from 'wax-prosemirror-utilities'; +import { Fragment } from 'prosemirror-model'; +import { wrapIn } from 'prosemirror-commands'; const createEmptyParagraph = (context, newAnswerId) => { if (context.view[newAnswerId]) { @@ -11,7 +15,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()), ); @@ -28,6 +32,54 @@ const createEmptyParagraph = (context, newAnswerId) => { } }; +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'); + }); + if (state.selection.constructor.name === 'GapCursor') { + Commands.simulateKey(view, 13, 'Enter'); + setTimeout(() => { + view.focus(); + }); + } +}; + +const createOptions = (main, context, type, parentType) => { + checkifEmpty(main); + const { state, dispatch } = main; + /* Create Wrapping */ + const { $from, $to } = state.selection; + const range = $from.blockRange($to); + + wrapIn(parentType, { + id: uuidv4(), + })(state, dispatch); + + /* set New Selection */ + dispatch( + main.state.tr.setSelection( + new TextSelection(main.state.tr.doc.resolve(range.$to.pos)), + ), + ); + + /* create First Option */ + const firstOption = type.create({ id: uuidv4() }, Fragment.empty); + dispatch(main.state.tr.replaceSelectionWith(firstOption)); + + /* create Second Option */ + const secondOption = type.create({ id: uuidv4() }, Fragment.empty); + dispatch(main.state.tr.replaceSelectionWith(secondOption)); + + setTimeout(() => { + createEmptyParagraph(context, secondOption.attrs.id); + createEmptyParagraph(context, firstOption.attrs.id); + }, 50); +}; + export default { createEmptyParagraph, + checkifEmpty, + createOptions, }; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/multipleChoiceContainerNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/multipleChoiceContainerNode.js index 0ac8dba15570f6946c7c536fbeacf7cf6f83c732..b3d00706b9c1b181145b669dfb74f6ae172f8b02 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/multipleChoiceContainerNode.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/multipleChoiceContainerNode.js @@ -7,7 +7,7 @@ const multipleChoiceContainerNode = { atom: true, selectable: true, draggable: true, - content: 'multiple_choice+', + content: 'block+', parseDOM: [ { tag: 'div.multiple-choice', diff --git a/wax-prosemirror-services/src/NoteService/Editor.js b/wax-prosemirror-services/src/NoteService/Editor.js index 30d736d6ea2c535bb715318b0539c37ee65379fd..fea500af3b59fd6db8b9e7ea0b982340a8e7ca05 100644 --- a/wax-prosemirror-services/src/NoteService/Editor.js +++ b/wax-prosemirror-services/src/NoteService/Editor.js @@ -56,7 +56,7 @@ export default ({ node, view }) => { // 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 (noteView.hasFocus()) noteView.focus(); + // if (noteView.hasFocus()) noteView.focus(); }, }, handleTextInput: (editorView, from, to, text) => { @@ -108,6 +108,10 @@ export default ({ node, view }) => { setTimeout(() => { if (clickInNote) context.updateView({}, noteId); clickInNote = false; + if (typing) { + context.updateView({}, noteId); + typing = false; + } if (noteView.state.selection.from !== noteView.state.selection.to) context.updateView({}, noteId); }, 20); diff --git a/wax-prosemirror-services/src/WaxToolGroups/MultipleChoiceToolGroupService/MultipleChoice.js b/wax-prosemirror-services/src/WaxToolGroups/MultipleChoiceToolGroupService/MultipleChoice.js index ec4920997bf8fcaa72037b70950b89e583ee3d30..cffe0cf6b7b7ce23bb7ab21a05f0c70e1198a0c6 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/MultipleChoiceToolGroupService/MultipleChoice.js +++ b/wax-prosemirror-services/src/WaxToolGroups/MultipleChoiceToolGroupService/MultipleChoice.js @@ -4,9 +4,13 @@ import ToolGroup from '../../lib/ToolGroup'; @injectable() class MultipleChoice extends ToolGroup { tools = []; - constructor(@inject('MultipleChoiceQuestion') multipleChoiceQuestion) { + constructor( + @inject('MultipleChoiceQuestion') multipleChoiceQuestion, + @inject('MultipleChoiceSingleCorrectQuestion') + multipleChoiceSingleCorrectQuestion, + ) { super(); - this.tools = [multipleChoiceQuestion]; + this.tools = [multipleChoiceQuestion, multipleChoiceSingleCorrectQuestion]; } } diff --git a/wax-prosemirror-services/src/WaxToolGroups/MultipleDropDownToolGroupService/MultipleDropDown.js b/wax-prosemirror-services/src/WaxToolGroups/MultipleDropDownToolGroupService/MultipleDropDown.js index ddee8bbbd00368db3052a91ce0d6c1e61f26ae17..75f1b2359b9b7246af15978d2b4604373a71b6b7 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/MultipleDropDownToolGroupService/MultipleDropDown.js +++ b/wax-prosemirror-services/src/WaxToolGroups/MultipleDropDownToolGroupService/MultipleDropDown.js @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { injectable, inject } from 'inversify'; import { isEmpty } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; @@ -11,9 +11,18 @@ import ToolGroup from '../../lib/ToolGroup'; @injectable() class MultipleDropDown extends ToolGroup { tools = []; - constructor(@inject('MultipleChoiceQuestion') multipleChoiceQuestion) { + constructor( + @inject('MultipleChoiceQuestion') multipleChoiceQuestion, + @inject('MultipleChoiceSingleCorrectQuestion') + multipleChoiceSingleCorrectQuestion, + @inject('TrueFalseQuestion') trueFalseQuestion, + ) { super(); - this.tools = [multipleChoiceQuestion]; + this.tools = [ + multipleChoiceQuestion, + multipleChoiceSingleCorrectQuestion, + trueFalseQuestion, + ]; } renderTools(view) { @@ -60,42 +69,57 @@ class MultipleDropDown extends ToolGroup { const { state } = view; const dropDownOptions = [ - { label: 'Multiple Choice ', value: '0', item: this._tools[0] }, { - label: 'Multiple Choice (single correct) ', - value: '1', + label: 'Multiple Choice', + value: '0', item: this._tools[0], }, - { label: 'True/False ', value: '2', item: this._tools[0] }, { - label: 'True/False (single correct) ', - value: '3', - item: this._tools[0], + label: 'Multiple Choice (single correct)', + value: '1', + item: this._tools[1], }, + { + label: 'True/False', + value: '2', + item: this._tools[2], + }, + // { + // label: 'True/False (single correct)', + // value: '3', + // item: this._tools[0], + // }, ]; const isDisabled = this._tools[0].select(state, activeView); let found = ''; - dropDownOptions.forEach((item, i) => { - if (item.item.select(state, activeView) === false) { - found = item.item.label; + dropDownOptions.forEach((option, i) => { + if (option.item.active(main.state)) { + found = option.label; } }); - return ( - <Wrapper key={uuidv4()}> - <DropdownStyled - value={found} - key={uuidv4()} - options={dropDownOptions} - onChange={option => { - this._tools[option.value].run(view, main, context); - }} - placeholder="Multiple Question Types" - select={isDisabled} - /> - </Wrapper> + const onChange = option => { + this._tools[option.value].run(main, context); + }; + + const MultipleDropDown = useMemo( + () => ( + <Wrapper key={uuidv4()}> + <DropdownStyled + value={found} + key={uuidv4()} + options={dropDownOptions} + onChange={option => onChange(option)} + placeholder="Multiple Question Types" + select={isDisabled} + /> + </Wrapper> + ), + [isDisabled], ); + + return MultipleDropDown; } } diff --git a/wax-prosemirror-services/src/lib/Tools.js b/wax-prosemirror-services/src/lib/Tools.js index 573530f898baeb16638a8b135c077491feb3b050..f6a92e4883432a844e3380e150f7f41d5611eabf 100644 --- a/wax-prosemirror-services/src/lib/Tools.js +++ b/wax-prosemirror-services/src/lib/Tools.js @@ -10,7 +10,6 @@ class Tools { title = 'title'; _isDisplayed = true; _isHiddenInToolGroup = false; - onlyOnMain = false; config = {}; pmplugins = {}; name = 'name'; @@ -48,7 +47,6 @@ class Tools { run: this.run, enable: this.enable, select: this.select, - onlyOnMain: this.onlyOnMain, id: this.id, }; } diff --git a/wax-prosemirror-utilities/package.json b/wax-prosemirror-utilities/package.json index d7a7daed6cd16560e90d40a453572673afdc58d5..6931c3ba3d8139c7d9dceead2accdc4cd8139bf9 100644 --- a/wax-prosemirror-utilities/package.json +++ b/wax-prosemirror-utilities/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "prosemirror-commands": "1.1.10", + "prosemirror-transform": "1.2.6", "prosemirror-utils": "^0.9.6", "uuid": "^7.0.3" } diff --git a/wax-prosemirror-utilities/src/commands/Commands.js b/wax-prosemirror-utilities/src/commands/Commands.js index 479fc0cacb6f5346b16643693a918e11dcd4fbba..99c34a84eb224e16eb38666c2cecb4321974da10 100644 --- a/wax-prosemirror-utilities/src/commands/Commands.js +++ b/wax-prosemirror-utilities/src/commands/Commands.js @@ -240,7 +240,7 @@ const createCommentOnFootnote = (state, dispatch, group, viewid) => { const isInTable = state => { const { $head } = state.selection; - for (let d = $head.depth; d > 0; d--) + for (let d = $head.depth; d > 0; d -= 1) if ($head.node(d).type.spec.tableRole === 'row') return true; return false; }; @@ -253,6 +253,18 @@ const simulateKey = (view, keyCode, key) => { return view.someProp('handleKeyDown', f => f(view, event)); }; +const isParentOfType = (state, nodeType) => { + let status = false; + const predicate = node => node.type === nodeType; + for (let i = state.selection.$from.depth; i > 0; i -= 1) { + const node = state.selection.$from.node(i); + if (predicate(node)) { + status = true; + } + } + return status; +}; + export default { isInTable, setBlockType, @@ -265,4 +277,5 @@ export default { markActive, isOnSameTextBlock, simulateKey, + isParentOfType, };