From f9ba7a9bf4db4c534752f113163bcf9ab3f2acda Mon Sep 17 00:00:00 2001 From: chris <kokosias@yahoo.gr> Date: Sat, 18 Dec 2021 12:49:22 +0200 Subject: [PATCH] add question nodeview --- .../src/EssayService/components/ToolBarBtn.js | 102 +++++++++++ .../MultipleChoiceQuestion.js | 3 +- .../MultipleChoiceQuestionService.js | 17 +- .../QuestionNodeView.js | 56 +++++++ .../components/AnswerComponent.js | 158 ++++++++++++++++++ .../components/QuestionComponent.js | 157 +---------------- .../helpers/helpers.js | 50 ++++-- .../schema/questionNode.js | 27 +++ 8 files changed, 398 insertions(+), 172 deletions(-) create mode 100644 wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.js create mode 100644 wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js diff --git a/wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js b/wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js new file mode 100644 index 000000000..4e6920cd4 --- /dev/null +++ b/wax-prosemirror-services/src/EssayService/components/ToolBarBtn.js @@ -0,0 +1,102 @@ +/* eslint react/prop-types: 0 */ +import React, { useContext, useMemo } from 'react'; +import styled, { css } from 'styled-components'; +import { v4 as uuidv4 } from 'uuid'; +import { WaxContext } from 'wax-prosemirror-core'; +import { MenuButton } from 'wax-prosemirror-components'; +import { findWrapping } from 'prosemirror-transform'; +import { Selection } from 'prosemirror-state'; + +const activeStyles = css` + pointer-events: none; +`; + +const StyledButton = styled(MenuButton)` + ${props => props.active && activeStyles} +`; + +const ToolBarBtn = ({ view = {}, item }) => { + console.log('hrtr?'); + const { icon, label, select, title } = item; + const context = useContext(WaxContext); + console.log(context); + const { + view: { main }, + activeView, + } = useContext(WaxContext); + + const isEditable = main.props.editable(editable => { + return editable; + }); + + const { state } = view; + + let isDisabled = !select(state, activeView); + if (!isEditable) isDisabled = true; + + const onMouseDown = () => { + const { $from, $to } = main.state.selection; + + const range = $from.blockRange($to); + + const { tr } = main.state; + const { dispatch } = main; + const wrapping1 = + range && + findWrapping(range, main.state.config.schema.nodes.essay, { + id: uuidv4(), + }); + + tr.wrap(range, wrapping1).scrollIntoView(); + dispatch(tr); + }; + + const onMouseDown2 = () => { + const { $from, $to } = main.state.selection; + + const range = $from.blockRange($to); + + const { tr } = main.state; + const { dispatch } = main; + const wrapping1 = + range && + findWrapping(range, main.state.config.schema.nodes.essay_feedBack, { + id: uuidv4(), + }); + + tr.wrap(range, wrapping1).scrollIntoView(); + dispatch(tr); + }; + + const ToolBarBtnComponent = useMemo( + () => ( + <StyledButton + active={false} + disabled={isDisabled} + iconName={icon} + label={label} + onMouseDown={e => { + e.preventDefault(); + // onMouseDown(); + // const { tr } = main.state; + // const { dispatch } = main; + // const map = context.transaction.mapping.maps[0]; + // let a = 0; + // map.forEach((_from, _to, _newFrom, newTo) => { + // a = newTo; + // }); + // dispatch( + // tr.setSelection(Selection.near(main.state.doc.resolve(a + 1), 0)), + // ); + onMouseDown2(); + }} + title={title} + /> + ), + [isDisabled], + ); + + return ToolBarBtnComponent; +}; + +export default ToolBarBtn; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js index 632e739f2..bb219daa6 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestion.js @@ -19,8 +19,9 @@ class MultipleChoiceQuestion extends Tools { helpers.createOptions( view, context, - view.state.config.schema.nodes.multiple_choice, view.state.config.schema.nodes.multiple_choice_container, + view.state.config.schema.nodes.question_node_multiple, + view.state.config.schema.nodes.multiple_choice, ); }; } diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js index 32b9cd531..26274f70d 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/MultipleChoiceQuestionService.js @@ -2,8 +2,11 @@ import Service from '../Service'; import MultipleChoiceQuestion from './MultipleChoiceQuestion'; import multipleChoiceNode from './schema/multipleChoiceNode'; import multipleChoiceContainerNode from './schema/multipleChoiceContainerNode'; +import questionNode from './schema/questionNode'; +import AnswerComponent from './components/AnswerComponent'; import QuestionComponent from './components/QuestionComponent'; import MultipleChoiceNodeView from './MultipleChoiceNodeView'; +import QuestionNodeView from './QuestionNodeView'; import MultipleChoiceSingleCorrectQuestionService from './MultipleChoiceSingleCorrectQuestionService/MultipleChoiceSingleCorrectQuestionService'; import TrueFalseQuestionService from './TrueFalseQuestionService/TrueFalseQuestionService'; import TrueFalseSingleCorrectQuestionService from './TrueFalseSingleCorrectQuestionService/TrueFalseSingleCorrectQuestionService'; @@ -14,19 +17,29 @@ class MultipleChoiceQuestionService extends Service { const createNode = this.container.get('CreateNode'); const addPortal = this.container.get('AddPortal'); + createNode({ + multiple_choice_container: multipleChoiceContainerNode, + }); + createNode({ multiple_choice: multipleChoiceNode, }); createNode({ - multiple_choice_container: multipleChoiceContainerNode, + question_node_multiple: questionNode, }); addPortal({ - nodeView: MultipleChoiceNodeView, + nodeView: QuestionNodeView, component: QuestionComponent, context: this.app, }); + + addPortal({ + nodeView: MultipleChoiceNodeView, + component: AnswerComponent, + context: this.app, + }); } dependencies = [ diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js new file mode 100644 index 000000000..cbacc21ed --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/QuestionNodeView.js @@ -0,0 +1,56 @@ +import AbstractNodeView from '../PortalService/AbstractNodeView'; + +export default class QuestionNodeView 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 'question_node_multiple'; + } + + 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/components/AnswerComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.js new file mode 100644 index 000000000..1e5f8cf98 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/AnswerComponent.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 './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/MultipleChoiceQuestionService/components/QuestionComponent.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js index 1e5f8cf98..934ecd963 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/components/QuestionComponent.js @@ -1,158 +1,5 @@ -/* 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 './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; -`; +import React from 'react'; 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> - ); + return <span>Question</span>; }; diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js index dec693bc7..702247163 100644 --- a/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/helpers/helpers.js @@ -3,6 +3,15 @@ import { TextSelection } from 'prosemirror-state'; import { Commands } from 'wax-prosemirror-utilities'; import { Fragment } from 'prosemirror-model'; import { wrapIn } from 'prosemirror-commands'; +import { + joinPoint, + canJoin, + findWrapping, + liftTarget, + canSplit, + ReplaceAroundStep, +} from 'prosemirror-transform'; +import { Selection } from 'prosemirror-state'; const createEmptyParagraph = (context, newAnswerId) => { if (context.view[newAnswerId]) { @@ -46,31 +55,44 @@ const checkifEmpty = view => { } }; -const createOptions = (main, context, type, parentType) => { +const createOptions = ( + main, + context, + parentType, + questionType, + answerTtype, +) => { checkifEmpty(main); const { state, dispatch } = main; /* Create Wrapping */ const { $from, $to } = state.selection; const range = $from.blockRange($to); + const { tr } = main.state; - wrapIn(parentType, { - id: uuidv4(), - })(state, dispatch); + const wrapping = range && findWrapping(range, parentType, { id: uuidv4 }); + if (!wrapping) return false; + tr.wrap(range, wrapping); - /* set New Selection */ - dispatch( - main.state.tr.setSelection( - new TextSelection(main.state.tr.doc.resolve(range.$to.pos)), - ), - ); + const map = tr.mapping.maps[0]; + let newPos = 0; + map.forEach((_from, _to, _newFrom, newTo) => { + newPos = newTo; + }); + + tr.setSelection(TextSelection.create(tr.doc, range.$to.pos)); + + const question = questionType.create({ id: uuidv4() }, Fragment.empty); /* create First Option */ - const firstOption = type.create({ id: uuidv4() }, Fragment.empty); - dispatch(main.state.tr.replaceSelectionWith(firstOption)); + const firstOption = answerTtype.create({ id: uuidv4() }, Fragment.empty); /* create Second Option */ - const secondOption = type.create({ id: uuidv4() }, Fragment.empty); - dispatch(main.state.tr.replaceSelectionWith(secondOption)); + const secondOption = answerTtype.create({ id: uuidv4() }, Fragment.empty); + tr.replaceSelectionWith(question); + tr.replaceSelectionWith(firstOption); + tr.setSelection(TextSelection.create(tr.doc, newPos + 1)); + tr.replaceSelectionWith(secondOption); + dispatch(tr); setTimeout(() => { createEmptyParagraph(context, secondOption.attrs.id); diff --git a/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js b/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js new file mode 100644 index 000000000..ca2378579 --- /dev/null +++ b/wax-prosemirror-services/src/MultipleChoiceQuestionService/schema/questionNode.js @@ -0,0 +1,27 @@ +import { v4 as uuidv4 } from 'uuid'; + +const questionNode = { + attrs: { + class: { default: 'multiple-choice-question' }, + id: { default: uuidv4() }, + }, + group: 'block questions', + content: 'block*', + defining: true, + + // atom: true, + parseDOM: [ + { + tag: 'div.multiple-choice-question', + getAttrs(dom) { + return { + id: dom.getAttribute('id'), + class: dom.getAttribute('class'), + }; + }, + }, + ], + toDOM: node => ['div', node.attrs, 0], +}; + +export default questionNode; -- GitLab