From 4f27a9deb4dd0162047666db501dcba84319299b Mon Sep 17 00:00:00 2001 From: chris <kokosias@yahoo.gr> Date: Mon, 22 Nov 2021 12:29:31 +0200 Subject: [PATCH] new service --- editors/demo/src/Editors.js | 2 +- wax-prosemirror-services/index.js | 1 + .../helpers/helpers.js | 2 +- .../MultipleChoiceSingleCorrectNodeView.js | 0 .../MultipleChoiceSingleCorrectQuestion.js | 97 +++++++++++ ...tipleChoiceSingleCorrectQuestionService.js | 32 ++++ .../components/Button.js | 88 ++++++++++ .../components/EditorComponent.js | 160 ++++++++++++++++++ .../components/FeedbackComponent.js | 110 ++++++++++++ .../components/QuestionComponent.js | 158 +++++++++++++++++ .../components/Switch.js | 52 ++++++ .../components/SwitchComponent.js | 77 +++++++++ .../components/ToolBarBtn.js | 59 +++++++ ...ultipleChoiceSingleCorrectContainerNode.js | 29 ++++ .../schema/multipleChoiceSingleCorrectNode.js | 33 ++++ 15 files changed, 898 insertions(+), 2 deletions(-) create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js diff --git a/editors/demo/src/Editors.js b/editors/demo/src/Editors.js index abd0628fb..d19991a2e 100644 --- a/editors/demo/src/Editors.js +++ b/editors/demo/src/Editors.js @@ -70,7 +70,7 @@ const Editors = () => { case 'ncbi': return <NCBI />; default: - return <Editoria />; + return <HHMI />; } }; diff --git a/wax-prosemirror-services/index.js b/wax-prosemirror-services/index.js index 3023e4f4d..0f587973f 100644 --- a/wax-prosemirror-services/index.js +++ b/wax-prosemirror-services/index.js @@ -45,6 +45,7 @@ export { default as CustomTagInlineService } from './src/CustomTagService/Custom export { default as CustomTagBlockService } from './src/CustomTagService/CustomTagBlockService/CustomTagBlockService'; export { default as CustomTagService } from './src/CustomTagService/CustomTagService'; export { default as MultipleChoiceQuestionService } from './src/MultipleChoiceQuestionService/MultipleChoiceQuestionService'; +export { default as MultipleChoiceSingleCorrectQuestionService } from './src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService'; export { default as FillTheGapQuestionService } from './src/FillTheGapQuestionService/FillTheGapQuestionService'; /* diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js index 537c87cab..f8cd0b5cc 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js @@ -11,7 +11,7 @@ const createEmptyParagraph = (context, newAnswerId) => { ), ); if (context.view[newAnswerId].dispatch) { - let type = context.view.main.state.schema.nodes.paragraph; + const type = context.view.main.state.schema.nodes.paragraph; context.view[newAnswerId].dispatch( context.view[newAnswerId].state.tr.insert(0, type.create()), ); diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectNodeView.js new file mode 100644 index 000000000..e69de29bb diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js new file mode 100644 index 000000000..d53b66ed1 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestion.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { isEmpty } from 'lodash'; +import { injectable } from 'inversify'; +import { Commands } from 'wax-prosemirror-utilities'; +import { v4 as uuidv4 } from 'uuid'; +import { Fragment } from 'prosemirror-model'; +import { TextSelection } from 'prosemirror-state'; +import { wrapIn } from 'prosemirror-commands'; +import helpers from '../MultipleChoiceQuestionService/helpers/helpers'; +import Tools from '../lib/Tools'; +import ToolBarBtn from './components/ToolBarBtn'; + +const checkifEmpty = view => { + const { state } = view; + const { from, to } = state.selection; + state.doc.nodesBetween(from, to, (node, pos) => { + if (node.textContent !== ' ') Commands.simulateKey(view, 13, 'Enter'); + }); +}; + +const createOption = (main, context) => { + const { state, dispatch } = main; + /* Create Wrapping */ + const { $from, $to } = state.selection; + const range = $from.blockRange($to); + + wrapIn(state.config.schema.nodes.multiple_choice_single_correct_container, { + id: uuidv4(), + })(state, dispatch); + + /* set New Selection */ + dispatch( + main.state.tr.setSelection( + new TextSelection(main.state.tr.doc.resolve(range.$to.pos)), + ), + ); + + /* create Second Option */ + const newAnswerId = uuidv4(); + const answerOption = main.state.config.schema.nodes.multiple_choice_single_correct.create( + { id: newAnswerId }, + Fragment.empty, + ); + dispatch(main.state.tr.replaceSelectionWith(answerOption)); + setTimeout(() => { + helpers.createEmptyParagraph(context, newAnswerId); + }, 50); +}; + +@injectable() +class MultipleChoiceSingleCorrectQuestion extends Tools { + title = 'Add Multiple Choice Single Correct Question'; + icon = 'multipleChoice'; + name = 'Multiple Choice Single Correct'; + label = 'Multiple Choice Single Correct'; + + get run() { + return (view, main, context) => { + checkifEmpty(view); + createOption(main, context); + }; + } + + get active() { + return state => {}; + } + + select = (state, activeView) => { + const { disallowedTools } = activeView.props; + if (disallowedTools.includes('MultipleChoice')) return false; + let status = true; + const { from, to } = state.selection; + + if (from === null) return false; + + state.doc.nodesBetween(from, to, (node, pos) => { + if (node.type.groups.includes('questions')) { + status = false; + } + }); + return status; + }; + + get enable() { + return state => {}; + } + + renderTool(view) { + if (isEmpty(view)) return null; + // eslint-disable-next-line no-underscore-dangle + return this._isDisplayed ? ( + <ToolBarBtn item={this.toJSON()} key={uuidv4()} view={view} /> + ) : null; + } +} + +export default MultipleChoiceSingleCorrectQuestion; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js new file mode 100644 index 000000000..871c0f06c --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService.js @@ -0,0 +1,32 @@ +import Service from '../Service'; +import MultipleChoiceSingleCorrectQuestion from './MultipleChoiceSingleCorrectQuestion'; +import multipleChoiceSingleCorrectNode from './schema/multipleChoiceSingleCorrectNode'; +import multipleChoiceSingleCorrectContainerNode from './schema/multipleChoiceSingleCorrectContainerNode'; +import QuestionComponent from './components/QuestionComponent'; +import MultipleChoiceSingleCorrectNodeView from './MultipleChoiceSingleCorrectNodeView'; + +class MultipleChoiceSingleCorrectQuestionService extends Service { + register() { + this.container + .bind('MultipleChoiceSingleCorrectQuestion') + .to(MultipleChoiceSingleCorrectQuestion); + const createNode = this.container.get('CreateNode'); + const addPortal = this.container.get('AddPortal'); + + createNode({ + multiple_choice_single_correct: multipleChoiceSingleCorrectNode, + }); + + createNode({ + multiple_choice_single_correct_container: multipleChoiceSingleCorrectContainerNode, + }); + + addPortal({ + nodeView: MultipleChoiceSingleCorrectNodeView, + component: QuestionComponent, + context: this.app, + }); + } +} + +export default MultipleChoiceSingleCorrectQuestionService; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js new file mode 100644 index 000000000..7d1619944 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Button.js @@ -0,0 +1,88 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import PropTypes from 'prop-types'; +import { Button as AntButton } from 'antd'; +import { omit } from 'lodash'; + +import { darken, lighten } from '@pubsweet/ui-toolkit'; + +const colors = { + danger: 'colorError', + error: 'colorError', + success: 'colorSuccess', + // warn: 'colorWarning', +}; + +const StyledButton = styled(AntButton)` + ${props => { + const { status, theme, type } = props; + if (!Object.keys(colors).includes(status)) return null; + const color = theme[colors[status]]; + + // primary + if (type === 'primary') + return css` + background-color: ${color}; + border-color: ${color}; + color: ${theme.colorTextReverse}; + + &:hover, + &:focus, + &:active { + border-color: ${color}; + color: ${theme.colorTextReverse}; + } + + &:hover, + &:focus { + background-color: ${lighten(color, 0.25)}; + } + + &:active { + background-color: ${darken(color, 0.25)}; + } + `; + + // non-primary + return css` + color: ${color}; + border-color: ${color}; + &:hover, + &:focus { + color: ${lighten(color, 0.25)}; + border-color: ${lighten(color, 0.25)}; + } + + &:active { + color: ${darken(color, 0.25)}; + border-color: ${darken(color, 0.25)}; + } + `; + }} +`; +/** + * API is the same as https://ant.design/components/button/#API, except for the + * `danger` prop, which is ommited in favour of `status`, described below. + */ + +const Button = props => { + const { children, className, ...rest } = props; + const passProps = omit(rest, 'danger'); + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + <StyledButton className={className} {...passProps}> + {children} + </StyledButton> + ); +}; + +Button.propTypes = { + status: PropTypes.oneOf(['error', 'danger', 'success']), +}; + +Button.defaultProps = { + status: null, +}; + +export default Button; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js new file mode 100644 index 000000000..8a80dbd4a --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/EditorComponent.js @@ -0,0 +1,160 @@ +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable react/prop-types */ + +import React, { useContext, useRef, useEffect } from 'react'; +import styled from 'styled-components'; +import { EditorView } from 'prosemirror-view'; +import { EditorState, TextSelection } from 'prosemirror-state'; +import { StepMap } from 'prosemirror-transform'; +import { keymap } from 'prosemirror-keymap'; +import { baseKeymap } from 'prosemirror-commands'; +import { undo, redo } from 'prosemirror-history'; +import { WaxContext } from 'wax-prosemirror-core'; +import Placeholder from '../../MultipleChoiceQuestionService/plugins/placeholder'; + +const EditorWrapper = styled.div` + border: none; + display: flex; + flex: 2 1 auto; + justify-content: left; + + .ProseMirror { + white-space: break-spaces; + width: 100%; + word-wrap: break-word; + + &:focus { + outline: none; + } + + p.empty-node:first-child::before { + content: attr(data-content); + } + + .empty-node::before { + color: rgb(170, 170, 170); + float: left; + font-style: italic; + height: 0px; + pointer-events: none; + } + } +`; + +const EditorComponent = ({ node, view, getPos }) => { + const editorRef = useRef(); + + const context = useContext(WaxContext); + let questionView; + const questionId = node.attrs.id; + const isEditable = context.view.main.props.editable(editable => { + return editable; + }); + + let finalPlugins = []; + + const createKeyBindings = () => { + const keys = getKeys(); + Object.keys(baseKeymap).forEach(key => { + keys[key] = baseKeymap[key]; + }); + return keys; + }; + + const getKeys = () => { + return { + 'Mod-z': () => undo(view.state, view.dispatch), + 'Mod-y': () => redo(view.state, view.dispatch), + }; + }; + + const plugins = [keymap(createKeyBindings()), ...context.app.getPlugins()]; + + // eslint-disable-next-line no-shadow + const createPlaceholder = placeholder => { + return Placeholder({ + content: placeholder, + }); + }; + + finalPlugins = finalPlugins.concat([ + createPlaceholder('Type your answer'), + ...plugins, + ]); + + const { activeViewId } = context; + + useEffect(() => { + questionView = new EditorView( + { + mount: editorRef.current, + }, + { + editable: () => isEditable, + state: EditorState.create({ + doc: node, + plugins: finalPlugins, + }), + // This is the magic part + dispatchTransaction, + disallowedTools: ['Images', 'Lists', 'lift', 'MultipleChoice'], + handleDOMEvents: { + mousedown: () => { + context.view[activeViewId].dispatch( + context.view[activeViewId].state.tr.setSelection( + TextSelection.between( + context.view[activeViewId].state.selection.$anchor, + context.view[activeViewId].state.selection.$head, + ), + ), + ); + context.updateView({}, questionId); + // Kludge to prevent issues due to the fact that the whole + // footnote is node-selected (and thus DOM-selected) when + // the parent editor is focused. + if (questionView.hasFocus()) questionView.focus(); + }, + }, + + attributes: { + spellcheck: 'false', + }, + }, + ); + + // Set Each note into Wax's Context + context.updateView( + { + [questionId]: questionView, + }, + questionId, + ); + if (questionView.hasFocus()) questionView.focus(); + }, []); + + const dispatchTransaction = tr => { + const { state, transactions } = questionView.state.applyTransaction(tr); + questionView.updateState(state); + context.updateView({}, questionId); + + if (!tr.getMeta('fromOutside')) { + const outerTr = view.state.tr; + const offsetMap = StepMap.offset(getPos() + 1); + for (let i = 0; i < transactions.length; i++) { + const { steps } = transactions[i]; + for (let j = 0; j < steps.length; j++) + outerTr.step(steps[j].map(offsetMap)); + } + if (outerTr.docChanged) + view.dispatch(outerTr.setMeta('outsideView', questionId)); + } + }; + + return ( + <EditorWrapper> + <div ref={editorRef} /> + </EditorWrapper> + ); +}; + +export default EditorComponent; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js new file mode 100644 index 000000000..0fa7d157c --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/FeedbackComponent.js @@ -0,0 +1,110 @@ +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable react/prop-types */ + +import React, { useContext, useRef, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { TextSelection } from 'prosemirror-state'; +import { WaxContext } from 'wax-prosemirror-core'; +import { DocumentHelpers } from 'wax-prosemirror-utilities'; + +const FeedBack = styled.div` + color: black; + margin-top: 10px; +`; + +const FeedBackLabel = styled.span` + font-weight: 700; +`; + +const FeedBackInput = styled.input` + border: none; + display: flex; + width: 100%; +`; + +export default ({ node, view, getPos }) => { + const context = useContext(WaxContext); + const [feedBack, setFeedBack] = useState(''); + const [isFirstRun, setFirstRun] = useState(true); + const [typing, setTyping] = useState(false); + const feedBackRef = useRef(null); + + useEffect(() => { + const allNodes = getNodes(context.view.main); + allNodes.forEach(singNode => { + if (singNode.node.attrs.id === node.attrs.id) { + if (!typing) setFeedBack(singNode.node.attrs.feedback); + if (!isFirstRun) { + if (singNode.node.attrs.feedback === '') + setFeedBack(singNode.node.attrs.feedback); + } + } + }); + }, [getNodes(context.view.main)]); + + const handleKeyDown = e => { + setTyping(true); + if (e.key === 'Backspace') { + context.view.main.dispatch( + context.view.main.state.tr.setSelection( + TextSelection.create(context.view.main.state.tr.doc, 0), + ), + ); + } + }; + + const feedBackInput = () => { + setFeedBack(feedBackRef.current.value); + }; + + const saveFeedBack = () => { + const allNodes = getNodes(context.view.main); + allNodes.forEach(singleNode => { + if (singleNode.node.attrs.id === node.attrs.id) { + context.view.main.dispatch( + context.view.main.state.tr.setNodeMarkup(getPos(), undefined, { + ...singleNode.node.attrs, + feedback: feedBack, + }), + ); + setFirstRun(false); + } + }); + return false; + }; + + const onFocus = () => { + context.view.main.dispatch( + context.view.main.state.tr.setSelection( + TextSelection.create(context.view.main.state.tr.doc, null), + ), + ); + }; + + return ( + <FeedBack> + <FeedBackLabel>Feedback</FeedBackLabel> + <FeedBackInput + onBlur={saveFeedBack} + onChange={feedBackInput} + onFocus={onFocus} + onKeyDown={handleKeyDown} + placeholder="Insert feedback" + ref={feedBackRef} + type="text" + value={feedBack} + /> + </FeedBack> + ); +}; + +const getNodes = view => { + const allNodes = DocumentHelpers.findBlockNodes(view.state.doc); + const multipleChoiceNodes = []; + allNodes.forEach(node => { + if (node.node.type.name === 'multiple_choice') { + multipleChoiceNodes.push(node); + } + }); + return multipleChoiceNodes; +}; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js new file mode 100644 index 000000000..cc00ad244 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/QuestionComponent.js @@ -0,0 +1,158 @@ +/* eslint-disable react/prop-types */ +import React, { useContext } from 'react'; +import styled from 'styled-components'; +import { TextSelection } from 'prosemirror-state'; +import { WaxContext } from 'wax-prosemirror-core'; +import { PlusSquareOutlined, DeleteOutlined } from '@ant-design/icons'; +import { Fragment } from 'prosemirror-model'; +import { v4 as uuidv4 } from 'uuid'; +import helpers from '../../MultipleChoiceQuestionService/helpers/helpers'; +import EditorComponent from './EditorComponent'; +import SwitchComponent from './SwitchComponent'; +import FeedbackComponent from './FeedbackComponent'; +import Button from './Button'; + +const Wrapper = styled.div` + display: flex; + flex-direction: row; + width: 100%; +`; + +const InfoRow = styled.div` + color: black; + display: flex; + flex-direction: row; + padding: 10px 0px 4px 0px; +`; + +const QuestionNunber = styled.span` + &:before { + content: 'Answer ' counter(question-item-multiple); + counter-increment: question-item-multiple; + } +`; + +const QuestionControlsWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const QuestionWrapper = styled.div` + border: 1px solid #a5a1a2; + border-radius: 4px; + color: black; + display: flex; + flex: 2 1 auto; + flex-direction: column; + padding: 10px; +`; + +const IconsWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + + button { + border: none; + box-shadow: none; + } + + span { + cursor: pointer; + } +`; + +const QuestionData = styled.div` + align-items: normal; + display: flex; + flex-direction: row; +`; + +export default ({ node, view, getPos }) => { + const context = useContext(WaxContext); + const { + view: { main }, + } = context; + + const isEditable = main.props.editable(editable => { + return editable; + }); + + const removeOption = () => { + main.state.doc.nodesBetween(getPos(), getPos() + 1, (sinlgeNode, pos) => { + if (sinlgeNode.attrs.id === node.attrs.id) { + main.dispatch( + main.state.tr.deleteRange(getPos(), getPos() + sinlgeNode.nodeSize), + ); + } + }); + }; + + const addOption = nodeId => { + const newAnswerId = uuidv4(); + context.view.main.state.doc.descendants((editorNode, index) => { + if (editorNode.type.name === 'multiple_choice') { + if (editorNode.attrs.id === nodeId) { + context.view.main.dispatch( + context.view.main.state.tr.setSelection( + new TextSelection( + context.view.main.state.tr.doc.resolve( + editorNode.nodeSize + index, + ), + ), + ), + ); + + const answerOption = context.view.main.state.config.schema.nodes.multiple_choice.create( + { id: newAnswerId }, + Fragment.empty, + ); + context.view.main.dispatch( + context.view.main.state.tr.replaceSelectionWith(answerOption), + ); + // create Empty Paragraph + setTimeout(() => { + helpers.createEmptyParagraph(context, newAnswerId); + }, 120); + } + } + }); + }; + + const readOnly = !isEditable; + const showAddIcon = true; + const showRemoveIcon = true; + + return ( + <Wrapper> + <QuestionControlsWrapper> + <InfoRow> + <QuestionNunber /> + <SwitchComponent getPos={getPos} node={node} /> + </InfoRow> + <QuestionWrapper> + <QuestionData> + <EditorComponent getPos={getPos} node={node} view={view} /> + </QuestionData> + <FeedbackComponent getPos={getPos} node={node} view={view} /> + </QuestionWrapper> + </QuestionControlsWrapper> + <IconsWrapper> + {showAddIcon && !readOnly && ( + <Button + icon={<PlusSquareOutlined title="Add Option" />} + onClick={() => addOption(node.attrs.id)} + /> + )} + {showRemoveIcon && !readOnly && ( + <Button + icon={ + <DeleteOutlined onClick={removeOption} title="Delete Option" /> + } + /> + )} + </IconsWrapper> + </Wrapper> + ); +}; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js new file mode 100644 index 000000000..6cad91dad --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/Switch.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled, { css } from 'styled-components'; +import { Switch as AntSwitch } from 'antd'; + +import { grid } from '@pubsweet/ui-toolkit'; + +const Wrapper = styled.span``; + +const Label = styled.span` + ${props => + props.labelPosition === 'left' && + css` + margin-right: ${grid(2)}; + `} + + ${props => + props.labelPosition === 'right' && + css` + margin-left: ${grid(2)}; + `} +`; + +const Switch = props => { + const { className, label, labelPosition, ...rest } = props; + + return ( + <Wrapper className={className}> + {label && labelPosition === 'left' && ( + <Label labelPosition={labelPosition}>{label}</Label> + )} + + <AntSwitch {...rest} /> + + {label && labelPosition === 'right' && ( + <Label labelPosition={labelPosition}>{label}</Label> + )} + </Wrapper> + ); +}; + +Switch.propTypes = { + label: PropTypes.string, + labelPosition: PropTypes.string, +}; + +Switch.defaultProps = { + label: null, + labelPosition: 'right', +}; + +export default Switch; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js new file mode 100644 index 000000000..0d2b5c97b --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/SwitchComponent.js @@ -0,0 +1,77 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useState, useContext, useEffect } from 'react'; +import { WaxContext } from 'wax-prosemirror-core'; +import { DocumentHelpers } from 'wax-prosemirror-utilities'; +import styled from 'styled-components'; +import Switch from './Switch'; + +const StyledSwitch = styled(Switch)` + display: flex; + margin-left: auto; + + span:nth-child(1) { + // bottom: 36px; + // display: flex; + // left: 4px; + // position: relative; + // width: 0px; + } + + .ant-switch-checked { + background-color: green; + } +`; + +const CustomSwitch = ({ node, getPos }) => { + const context = useContext(WaxContext); + const [checked, setChecked] = useState(false); + + useEffect(() => { + const allNodes = getNodes(context.view.main); + allNodes.forEach(singNode => { + if (singNode.node.attrs.id === node.attrs.id) { + setChecked(singNode.node.attrs.correct); + } + }); + }, [getNodes(context.view.main)]); + + const handleChange = () => { + setChecked(!checked); + const allNodes = getNodes(context.view.main); + allNodes.forEach(singleNode => { + if (singleNode.node.attrs.id === node.attrs.id) { + context.view.main.dispatch( + context.view.main.state.tr.setNodeMarkup(getPos(), undefined, { + ...singleNode.node.attrs, + correct: !checked, + }), + ); + } + }); + }; + + return ( + <StyledSwitch + checked={checked} + checkedChildren="YES" + label="Correct?" + labelPosition="left" + onChange={handleChange} + unCheckedChildren="NO" + /> + ); +}; + +const getNodes = view => { + const allNodes = DocumentHelpers.findBlockNodes(view.state.doc); + const multipleChoiceNodes = []; + allNodes.forEach(node => { + if (node.node.type.name === 'multiple_choice') { + multipleChoiceNodes.push(node); + } + }); + return multipleChoiceNodes; +}; + +export default CustomSwitch; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js new file mode 100644 index 000000000..d583f3b18 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/components/ToolBarBtn.js @@ -0,0 +1,59 @@ +/* eslint react/prop-types: 0 */ +import React, { useContext, useMemo } from 'react'; +import { WaxContext } from 'wax-prosemirror-core'; +import styled, { css } from 'styled-components'; +import { MenuButton } from 'wax-prosemirror-components'; + +const activeStyles = css` + pointer-events: none; +`; + +const StyledButton = styled(MenuButton)` + ${props => props.active && activeStyles} +`; + +const ToolBarBtn = ({ view = {}, item }) => { + const { active, icon, label, onlyOnMain, run, select, title } = item; + const context = useContext(WaxContext); + const { + view: { main }, + activeViewId, + activeView, + } = useContext(WaxContext); + + if (onlyOnMain) view = main; + + const isEditable = main.props.editable(editable => { + return editable; + }); + + const { state } = view; + + const isActive = !!( + active(state, activeViewId) && select(state, activeViewId) + ); + + let isDisabled = !select(state, activeView); + if (!isEditable) isDisabled = true; + + const ToolBarBtnComponent = useMemo( + () => ( + <StyledButton + active={isActive || false} + disabled={isDisabled} + iconName={icon} + label={label} + onMouseDown={e => { + e.preventDefault(); + item.run(view, main, context); + }} + title={title} + /> + ), + [isActive, isDisabled], + ); + + return ToolBarBtnComponent; +}; + +export default ToolBarBtn; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js new file mode 100644 index 000000000..c45138788 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectContainerNode.js @@ -0,0 +1,29 @@ +const multipleChoiceSingleCorrectContainerNode = { + attrs: { + id: { default: '' }, + class: { default: 'multiple-choice-single-correct' }, + singleCorrect: { default: true }, + }, + group: 'block questions', + atom: true, + selectable: true, + draggable: true, + content: 'multiple_choice_single_correct+', + parseDOM: [ + { + tag: 'div.multiple-choice-single-correct', + getAttrs(dom) { + return { + id: dom.dataset.id, + class: dom.getAttribute('class'), + singleCorrect: dom.getAttribute('singleCorrect'), + }; + }, + }, + ], + toDOM(node) { + return ['div', node.attrs, 0]; + }, +}; + +export default multipleChoiceSingleCorrectContainerNode; diff --git a/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js new file mode 100644 index 000000000..3936d4632 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceSingleCorrectQuestionService/schema/multipleChoiceSingleCorrectNode.js @@ -0,0 +1,33 @@ +import { v4 as uuidv4 } from 'uuid'; + +const multipleChoiceSingleCorrectNode = { + attrs: { + class: { default: 'multiple-choice-option-single-correct' }, + id: { default: uuidv4() }, + correct: { default: false }, + feedback: { default: '' }, + singleCorrect: { default: true }, + }, + group: 'block questions', + content: 'block*', + defining: true, + + // atom: true, + parseDOM: [ + { + tag: 'div.multiple-choice-option-single-correct', + getAttrs(dom) { + return { + id: dom.getAttribute('id'), + class: dom.getAttribute('class'), + correct: JSON.parse(dom.getAttribute('correct').toLowerCase()), + feedback: dom.getAttribute('feedback'), + singleCorrect: dom.getAttribute('singleCorrect'), + }; + }, + }, + ], + toDOM: node => ['div', node.attrs, 0], +}; + +export default multipleChoiceSingleCorrectNode; -- GitLab