Skip to content
Snippets Groups Projects
Commit 6f5ca853 authored by Christos's avatar Christos
Browse files

Merge branch 'essay-question' into 'master'

Essay question

See merge request !351
parents f037db17 80be2c0a
No related branches found
No related tags found
1 merge request!351Essay question
Showing
with 444 additions and 111 deletions
......@@ -70,7 +70,7 @@ const Editors = () => {
case 'ncbi':
return <NCBI />;
default:
return <Editoria />;
return <HHMI />;
}
};
......
......@@ -25,6 +25,7 @@ import {
FillTheGapQuestionService,
FillTheGapToolGroupService,
MultipleDropDownToolGroupService,
EssayService,
} from 'wax-prosemirror-services';
import { DefaultSchema } from 'wax-prosemirror-utilities';
......@@ -51,6 +52,7 @@ export default {
'Images',
'Tables',
'MultipleDropDown',
'MultipleChoice',
'FillTheGap',
'FullScreen',
],
......@@ -73,6 +75,7 @@ export default {
new MultipleChoiceQuestionService(),
new MultipleChoiceToolGroupService(),
new MultipleDropDownToolGroupService(),
new EssayService(),
new ListsService(),
new LinkService(),
new InlineAnnotationsService(),
......
......@@ -392,4 +392,24 @@ export default css`
width: 30px;
}
}
/* -- Essay ---------------------------------- */
.essay {
border: 3px solid #f5f5f7;
margin-bottom: 30px;
margin-top: 30px;
padding: 3px;
&:before {
background-color: #fff;
bottom: 22px;
color: #535e76;
content: 'Essay';
height: 10px;
left: -1px;
position: relative;
width: 30px;
}
}
`;
......@@ -126,7 +126,6 @@ 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')) {
......@@ -137,11 +136,12 @@ const WaxView = forwardRef((props, ref) => {
'main',
);
}
if (targetFormat === 'JSON') {
if (view.state.doc !== previousDoc || tr.getMeta('forceUpdate'))
props.onChange(state.doc.toJSON());
} else if (view.state.doc !== previousDoc || tr.getMeta('forceUpdate'))
props.onChange(state.doc.content);
const docContent =
targetFormat === 'JSON' ? state.doc.toJSON() : state.doc.content;
if (!previousDoc.eq(view.state.doc) || tr.getMeta('forceUpdate'))
props.onChange(docContent);
};
const editor = (
......
......@@ -48,7 +48,7 @@ export { default as MultipleChoiceQuestionService } from './src/MultipleChoiceQu
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';
export { default as EssayService } from './src/EssayService/EssayService';
/*
ToolGroups
*/
......
import AbstractNodeView from '../PortalService/AbstractNodeView';
export default class EssayNodeView 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 'essay';
}
update(node) {
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);
}
}
import { injectable } from 'inversify';
import { wrapIn } from 'prosemirror-commands';
import Tools from '../lib/Tools';
@injectable()
class EssayQuestion extends Tools {
title = 'Add Essay Question';
icon = '';
name = 'Essay';
label = 'Essay';
get run() {
return (state, dispatch) => {
wrapIn(state.config.schema.nodes.essay)(state, dispatch);
};
}
get active() {
return state => {};
}
select = (state, activeView) => {
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 => {};
}
}
export default EssayQuestion;
import Service from '../Service';
import EssayQuestion from './EssayQuestion';
import essayNode from './schema/essayNode';
import EssayComponent from './components/EssayComponent';
import EssayNodeView from './EssayNodeView';
class EssayService extends Service {
register() {
this.container.bind('EssayQuestion').to(EssayQuestion);
const createNode = this.container.get('CreateNode');
const addPortal = this.container.get('AddPortal');
createNode({
essay: essayNode,
});
addPortal({
nodeView: EssayNodeView,
component: EssayComponent,
context: this.app,
});
}
}
export default EssayService;
/* 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 essay'),
...plugins,
]);
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.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
// 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;
import React from 'react';
export default ({ node, view, getPos }) => {
return <span>Essay</span>;
};
const essayNode = {
attrs: {
class: { default: 'essay' },
},
group: 'block questions',
atom: true,
selectable: true,
draggable: true,
content: 'block+',
parseDOM: [
{
tag: 'div.essay',
getAttrs(dom) {
return {
id: dom.dataset.id,
class: dom.getAttribute('class'),
};
},
},
],
toDOM(node) {
return ['div', node.attrs, 0];
},
};
export default essayNode;
......@@ -8,9 +8,11 @@ class MultipleChoice extends ToolGroup {
@inject('MultipleChoiceQuestion') multipleChoiceQuestion,
@inject('MultipleChoiceSingleCorrectQuestion')
multipleChoiceSingleCorrectQuestion,
@inject('EssayQuestion')
essayQuestion,
) {
super();
this.tools = [multipleChoiceQuestion, multipleChoiceSingleCorrectQuestion];
this.tools = [multipleChoiceQuestion, essayQuestion];
}
}
......
/* eslint-disable no-underscore-dangle */
import React, { useContext, useMemo, useEffect, useState } from 'react';
import styled from 'styled-components';
import { WaxContext } from 'wax-prosemirror-core';
import { ReactDropDownStyles } from 'wax-prosemirror-components';
import Dropdown from 'react-dropdown';
import { v4 as uuidv4 } from 'uuid';
const Wrapper = styled.div`
${ReactDropDownStyles};
`;
const DropdownStyled = styled(Dropdown)`
display: inline-flex;
cursor: not-allowed;
opacity: ${props => (props.select ? 1 : 0.4)};
pointer-events: ${props => (props.select ? 'default' : 'none')};
.Dropdown-control {
border: none;
padding-top: 12px;
&:hover {
box-shadow: none;
}
}
.Dropdown-arrow {
top: 17px;
}
.Dropdown-menu {
width: 102%;
display: flex;
flex-direction: column;
align-items: flex-start;
.Dropdown-option {
width: 100%;
}
}
`;
const DropComponent = ({ title, view, tools }) => {
const context = useContext(WaxContext);
const {
activeView,
activeViewId,
view: { main },
} = context;
const { state } = view;
const [label, setLabel] = useState(null);
const dropDownOptions = [
{
label: 'Multiple Choice',
value: '0',
item: tools[0],
},
{
label: 'Multiple Choice (single correct)',
value: '1',
item: tools[1],
},
{
label: 'True/False',
value: '2',
item: tools[2],
},
{
label: 'True/False (single correct)',
value: '3',
item: tools[3],
},
];
useEffect(() => {
dropDownOptions.forEach((option, i) => {
if (option.item.active(main.state)) {
setLabel(option.label);
}
});
}, [activeViewId]);
const isDisabled = tools[0].select(state, activeView);
const onChange = option => {
tools[option.value].run(main, context);
};
const MultipleDropDown = useMemo(
() => (
<Wrapper key={uuidv4()}>
<DropdownStyled
key={uuidv4()}
onChange={option => onChange(option)}
options={dropDownOptions}
placeholder="Multiple Question Types"
select={isDisabled}
value={label}
/>
</Wrapper>
),
[isDisabled, label],
);
return MultipleDropDown;
};
export default DropComponent;
import React, { useContext, useMemo, useEffect, useState } from 'react';
import React from 'react';
import { injectable, inject } from 'inversify';
import { isEmpty } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import styled from 'styled-components';
import { WaxContext } from 'wax-prosemirror-core';
import { ReactDropDownStyles } from 'wax-prosemirror-components';
import Dropdown from 'react-dropdown';
import ToolGroup from '../../lib/ToolGroup';
import DropComponent from './DropComponent';
@injectable()
class MultipleDropDown extends ToolGroup {
......@@ -29,104 +26,9 @@ class MultipleDropDown extends ToolGroup {
renderTools(view) {
if (isEmpty(view)) return null;
const Wrapper = styled.div`
${ReactDropDownStyles};
`;
const DropdownStyled = styled(Dropdown)`
display: inline-flex;
cursor: not-allowed;
opacity: ${props => (props.select ? 1 : 0.4)};
pointer-events: ${props => (props.select ? 'default' : 'none')};
.Dropdown-control {
border: none;
padding-top: 12px;
&:hover {
box-shadow: none;
}
}
.Dropdown-arrow {
top: 17px;
}
.Dropdown-menu {
width: 102%;
display: flex;
flex-direction: column;
align-items: flex-start;
.Dropdown-option {
width: 100%;
}
}
`;
const context = useContext(WaxContext);
const {
activeView,
activeViewId,
view: { main },
} = context;
const { state } = view;
const [label, setLabel] = useState(null);
const dropDownOptions = [
{
label: 'Multiple Choice',
value: '0',
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[3],
},
];
useEffect(() => {
dropDownOptions.forEach((option, i) => {
if (option.item.active(main.state)) {
setLabel(option.label);
}
});
}, [activeViewId]);
const isDisabled = this._tools[0].select(state, activeView);
const onChange = option => {
this._tools[option.value].run(main, context);
};
const MultipleDropDown = useMemo(
() => (
<Wrapper key={uuidv4()}>
<DropdownStyled
value={label}
key={uuidv4()}
options={dropDownOptions}
onChange={option => onChange(option)}
placeholder="Multiple Question Types"
select={isDisabled}
/>
</Wrapper>
),
[isDisabled, label],
return (
<DropComponent key="Multipe Drop Down" view={view} tools={this._tools} />
);
return MultipleDropDown;
}
}
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment