+
{choices?.length > 0 &&
choices.map((choice) => (
-
+
))}
-
+
)}
{showRationale && (
@@ -125,45 +161,9 @@ export class InlineDropdown extends React.Component {
)}
-
+
);
}
}
-const styles = (theme) => ({
- mainContainer: {
- color: color.text(),
- backgroundColor: color.background(),
- },
- collapsible: {
- marginBottom: theme.spacing.unit * 2,
- },
- choiceRationaleWrapper: {
- '&:not(:last-child)': {
- marginBottom: theme.spacing.unit * 2,
- },
- },
- choiceRationale: {
- display: 'flex',
- whiteSpace: 'break-spaces',
- },
- choiceRationaleLabel: {
- display: 'flex',
- '&.correct': {
- color: color.correct(),
- },
- '&.incorrect': {
- color: color.incorrectWithIcon(),
- },
- },
- srOnly: {
- position: 'absolute',
- left: '-10000px',
- top: 'auto',
- width: '1px',
- height: '1px',
- overflow: 'hidden',
- },
-});
-
-export default withStyles(styles)(InlineDropdown);
+export default InlineDropdown;
diff --git a/packages/likert/configure/package.json b/packages/likert/configure/package.json
index a0c24828fc..82f8a8637b 100644
--- a/packages/likert/configure/package.json
+++ b/packages/likert/configure/package.json
@@ -7,15 +7,18 @@
"module": "src/index.js",
"author": "",
"dependencies": {
- "@material-ui/core": "^3.9.2",
+ "@emotion/react": "^11.14.0",
+ "@emotion/style": "^0.8.0",
+ "@mui/icons-material": "^7.3.4",
+ "@mui/material": "^7.3.4",
"@pie-framework/pie-configure-events": "^1.3.0",
- "@pie-lib/config-ui": "11.30.2",
- "@pie-lib/editable-html": "11.21.2",
- "@pie-lib/render-ui": "4.35.2",
+ "@pie-lib/config-ui": "11.30.4-next.0",
+ "@pie-lib/editable-html": "11.21.4-next.0",
+ "@pie-lib/render-ui": "4.35.4-next.0",
"debug": "^4.1.1",
"prop-types": "^15.6.2",
- "react": "^16.8.1",
- "react-dom": "^16.8.1"
+ "react": "18.2.0",
+ "react-dom": "18.2.0"
},
"scripts": {
"test": "./node_modules/.bin/jest"
diff --git a/packages/likert/configure/src/index.js b/packages/likert/configure/src/index.js
index 2f7995edc3..5f1722a940 100644
--- a/packages/likert/configure/src/index.js
+++ b/packages/likert/configure/src/index.js
@@ -1,5 +1,5 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client';
import debug from 'debug';
import { ModelUpdatedEvent, InsertSoundEvent, DeleteSoundEvent } from '@pie-framework/pie-configure-events';
@@ -28,6 +28,7 @@ export default class Likert extends HTMLElement {
constructor() {
super();
+ this._root = null;
this._model = Likert.createDefaultModel();
this._configuration = sensibleDefaults.configuration;
this.onModelChanged = this.onModelChanged.bind(this);
@@ -99,6 +100,15 @@ export default class Likert extends HTMLElement {
delete: this.onDeleteSound.bind(this),
},
});
- ReactDOM.render(element, this);
+ if (!this._root) {
+ this._root = createRoot(this);
+ }
+ this._root.render(element);
+ }
+
+ disconnectedCallback() {
+ if (this._root) {
+ this._root.unmount();
+ }
}
}
diff --git a/packages/likert/configure/src/main.jsx b/packages/likert/configure/src/main.jsx
index bbb075100e..977d3c17e0 100644
--- a/packages/likert/configure/src/main.jsx
+++ b/packages/likert/configure/src/main.jsx
@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import EditableHtml from '@pie-lib/editable-html';
-import Radio from '@material-ui/core/Radio';
-import RadioGroup from '@material-ui/core/RadioGroup';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
+import Radio from '@mui/material/Radio';
+import RadioGroup from '@mui/material/RadioGroup';
+import FormControlLabel from '@mui/material/FormControlLabel';
import { InputContainer, settings, layout, NumberTextField } from '@pie-lib/config-ui';
-import { withStyles } from '@material-ui/core/styles';
+import { styled } from '@mui/material/styles';
import merge from 'lodash/merge';
import { LIKERT_TYPE, LIKERT_SCALE, LIKERT_ORIENTATION } from './likertEntities';
import generateChoices from './choiceGenerator';
@@ -13,70 +13,81 @@ import { color } from '@pie-lib/render-ui';
const { Panel, toggle, radio } = settings;
-const styles = (theme) => ({
- promptHolder: {
- width: '100%',
- paddingTop: theme.spacing.unit * 2,
- marginBottom: theme.spacing.unit * 2,
- },
- radioButtonsWrapper: {
- display: 'flex',
- flexDirection: 'column',
- },
- radioButtonsColumnHeader: {
- color: theme.palette.grey[400],
- fontSize: theme.typography.fontSize - 2,
- },
- likertLabelHolder: {
- display: 'flex',
- width: '100%',
- alignItems: 'flex-start',
- marginBottom: theme.spacing.unit * 2,
- },
- likertOptionsHolder: {
- display: 'flex',
- width: '100%',
- justifyContent: 'space-around',
- marginBottom: theme.spacing.unit * 2.5,
- },
- likertLabelInput: {
- width: 'calc(100% - 200px)',
- marginRight: 0,
- },
- errorMessage: {
- color: theme.palette.error.main,
- fontSize: theme.typography.fontSize - 2,
- },
- width100: {
- width: '100%',
- },
- flexRow: {
- display: 'flex',
- flexDirection: 'row',
- },
- likertValueHolder: {
- paddingLeft: theme.spacing.unit * 2.5,
- width: '150px',
- },
- likertLabelEditableHtml: {
- paddingTop: theme.spacing.unit * 2,
- },
- inputFormGroupIndex: {
- width: '30px',
- paddingTop: theme.spacing.unit * 4,
- },
- errorText: {
- fontSize: theme.typography.fontSize - 2,
- color: theme.palette.error.main,
- paddingTop: theme.spacing.unit,
- },
- customColor: {
- color: `${color.tertiary()} !important`,
- },
+const PromptHolder = styled(InputContainer)(({ theme }) => ({
+ width: '100%',
+ paddingTop: theme.spacing(2),
+ marginBottom: theme.spacing(2),
+}));
+
+const RadioButtonsWrapper = styled('div')({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+const RadioButtonsColumnHeader = styled('p')(({ theme }) => ({
+ color: theme.palette.grey[400],
+ fontSize: theme.typography.fontSize - 2,
+}));
+
+const LikertLabelHolder = styled('div')(({ theme }) => ({
+ display: 'flex',
+ width: '100%',
+ alignItems: 'flex-start',
+ marginBottom: theme.spacing(2),
+}));
+
+const LikertOptionsHolder = styled('div')(({ theme }) => ({
+ display: 'flex',
+ width: '100%',
+ justifyContent: 'space-around',
+ marginBottom: theme.spacing(2.5),
+}));
+
+const LikertLabelInput = styled(InputContainer)({
+ width: 'calc(100% - 200px)',
+ marginRight: 0,
+});
+
+const ErrorMessage = styled('p')(({ theme }) => ({
+ color: theme.palette.error.main,
+ fontSize: theme.typography.fontSize - 2,
+}));
+
+const StyledNumberTextField = styled(NumberTextField)({
+ width: '100%',
+});
+
+const FlexRow = styled('div')({
+ display: 'flex',
+ flexDirection: 'row',
});
-const LikertOrientation = withStyles(styles)((props) => {
- const { classes, model, onChangeModel } = props;
+const LikertValueHolder = styled('div')(({ theme }) => ({
+ paddingLeft: theme.spacing(2.5),
+ width: '150px',
+}));
+
+const StyledEditableHtml = styled(EditableHtml)(({ theme }) => ({
+ paddingTop: theme.spacing(2),
+}));
+
+const InputFormGroupIndex = styled('span')(({ theme }) => ({
+ width: '30px',
+ paddingTop: theme.spacing(4),
+}));
+
+const ErrorText = styled('div')(({ theme }) => ({
+ fontSize: theme.typography.fontSize - 2,
+ color: theme.palette.error.main,
+ paddingTop: theme.spacing(1),
+}));
+
+const CustomColorRadio = styled(Radio)({
+ color: `${color.tertiary()} !important`,
+});
+
+const LikertOrientation = (props) => {
+ const { model, onChangeModel } = props;
const onChangeLikertOrientation = (e) => {
const likertOrientation = e.target.value;
@@ -85,8 +96,8 @@ const LikertOrientation = withStyles(styles)((props) => {
};
return (
-
-
Likert Orientation
+
+ Likert Orientation
{
>
}
+ control={}
label="Horizontal"
/>
}
+ control={}
label="Vertical"
/>
-
+
);
-});
+};
-const LikertScale = withStyles(styles)((props) => {
- const { classes, model, onChangeModel } = props;
+const LikertScale = (props) => {
+ const { model, onChangeModel } = props;
const onChangeLikertScale = (e) => {
const likertScale = e.target.value;
@@ -121,31 +132,31 @@ const LikertScale = withStyles(styles)((props) => {
};
return (
-
-
Likert Scale
+
+ Likert Scale
}
+ control={}
label="Likert 3"
/>
}
+ control={}
label="Likert 5"
/>
}
+ control={}
label="Likert 7"
/>
-
+
);
-});
+};
-const LikertType = withStyles(styles)((props) => {
- const { classes, model, onChangeModel } = props;
+const LikertType = (props) => {
+ const { model, onChangeModel } = props;
const onChangeLikertType = (e) => {
const likertType = e.target.value;
@@ -157,10 +168,10 @@ const LikertType = withStyles(styles)((props) => {
};
return (
-
-
Label Type
+
+ Label Type
-
+
{
>
}
+ control={}
label="Agreement"
/>
}
+ control={}
label="Frequency"
/>
}
+ control={}
label="Yes/No"
/>
@@ -192,20 +203,20 @@ const LikertType = withStyles(styles)((props) => {
>
}
+ control={}
label="Importance"
/>
}
+ control={}
label="Likelihood"
/>
- } label="Like" />
+ } label="Like" />
-
-
+
+
);
-});
+};
const buildValuesMap = (model) =>
model.choices.reduce((acc, choice) => {
@@ -219,9 +230,8 @@ const buildValuesMap = (model) =>
return { ...accClone, [choiceValue]: accClone[choiceValue] + 1 };
}, {});
-const Design = withStyles(styles)((props) => {
+const Design = (props) => {
const {
- classes,
configuration,
imageSupport,
model,
@@ -279,9 +289,8 @@ const Design = withStyles(styles)((props) => {
}
>
{teacherInstructionsEnabled && (
-
+
{
uploadSoundSupport={uploadSoundSupport}
pluginProps={getPluginProps(teacherInstructions?.inputConfiguration)}
/>
- {teacherInstructionsError && {teacherInstructionsError}
}
-
+ {teacherInstructionsError &&
{teacherInstructionsError}}
+
)}
-
+
{
uploadSoundSupport={uploadSoundSupport}
pluginProps={getPluginProps(prompt?.inputConfiguration)}
/>
- {promptError && {promptError}
}
-
+ {promptError &&
{promptError}}
+
-
+
-
+
{model.choices.map((choice, index) => (
-
-
{index + 1}.
-
-
+ {index + 1}.
+
+ onChoiceChanged(index, { ...choice, label: c })}
imageSupport={imageSupport}
@@ -330,13 +337,12 @@ const Design = withStyles(styles)((props) => {
uploadSoundSupport={uploadSoundSupport}
pluginProps={getPluginProps(likertChoice?.inputConfiguration)}
/>
-
+
-
-
+ {
@@ -345,14 +351,14 @@ const Design = withStyles(styles)((props) => {
imageSupport={imageSupport}
/>
{valuesMap[choice.value] && valuesMap[choice.value] > 1 && (
- Value should be unique
+ Value should be unique
)}
-
-
+
+
))}
);
-});
+};
export class Main extends React.Component {
static propTypes = {
@@ -360,7 +366,6 @@ export class Main extends React.Component {
disableSidePanel: PropTypes.bool,
onModelChanged: PropTypes.func.isRequired,
onConfigurationChanged: PropTypes.func.isRequired,
- classes: PropTypes.object.isRequired,
imageSupport: PropTypes.shape({
add: PropTypes.func.isRequired,
delete: PropTypes.func.isRequired,
@@ -397,6 +402,4 @@ export class Main extends React.Component {
}
}
-const Styled = withStyles(styles)(Main);
-
-export default Styled;
+export default Main;
diff --git a/packages/likert/package.json b/packages/likert/package.json
index c9b57e5876..e8799f337e 100644
--- a/packages/likert/package.json
+++ b/packages/likert/package.json
@@ -10,17 +10,20 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
- "@material-ui/core": "^3.9.2",
+ "@emotion/react": "^11.14.0",
+ "@emotion/style": "^0.8.0",
+ "@mui/icons-material": "^7.3.4",
+ "@mui/material": "^7.3.4",
"@pie-framework/pie-player-events": "^0.1.0",
- "@pie-lib/math-rendering": "3.22.1",
- "@pie-lib/render-ui": "4.35.2",
+ "@pie-lib/math-rendering": "3.22.3-next.0",
+ "@pie-lib/render-ui": "4.35.4-next.0",
"classnames": "^2.2.5",
"debug": "^4.1.1",
"lodash": "^4.17.10",
"prop-types": "^15.6.1",
- "react": "^16.8.1",
- "react-dom": "^16.8.1",
- "react-test-renderer": "^16.3.2",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-test-renderer": "18.2.0",
"react-transition-group": "^2.3.1"
},
"author": "",
diff --git a/packages/likert/src/choice-input.jsx b/packages/likert/src/choice-input.jsx
index e3d7757c36..2be02fd90a 100644
--- a/packages/likert/src/choice-input.jsx
+++ b/packages/likert/src/choice-input.jsx
@@ -1,57 +1,38 @@
import React from 'react';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
+import FormControlLabel from '@mui/material/FormControlLabel';
import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
+import { styled } from '@mui/material/styles';
import { color } from '@pie-lib/render-ui';
-import Radio from '@material-ui/core/Radio';
+import Radio from '@mui/material/Radio';
import { LIKERT_ORIENTATION } from './likertEntities';
-const radioStyles = {
- root: {
- color: `var(--choice-input-color, ${color.text()}) !important`,
+export const RadioStyled = styled(Radio)({
+ color: `var(--choice-input-color, ${color.text()})`,
+ '&.Mui-checked': {
+ color: `var(--choice-input-selected-color, ${color.primary()})`,
},
- checked: {
- color: `var(--choice-input-selected-color, ${color.primary()}) !important`,
+ '&.Mui-disabled': {
+ color: `var(--choice-input-disabled-color, ${color.defaults.DISABLED})`,
},
- disabled: {
- color: `var(--choice-input-disabled-color, ${color.defaults.DISABLED}) !important`,
- }
-};
+});
-export const RadioStyled = withStyles(radioStyles)((props) => {
- const { classes, checked, onChange, disabled } = props;
+const LabelRoot = styled('p')({
+ color: color.text(),
+ textAlign: 'center',
+ cursor: 'pointer',
+});
- return (
-
- );
+const CheckboxHolderRoot = styled('div')({
+ display: 'flex',
+ alignItems: 'center',
+ flex: 1,
+ padding: '0 5px',
+ '& label': {},
});
-const choiceInputStyles = () => ({
- labelRoot: {
- color: color.text(),
- textAlign: 'center',
- cursor: 'pointer',
- },
- checkboxHolderRoot: {
- display: 'flex',
- alignItems: 'center',
- flex: 1,
- padding: '0 5px',
- '& label': {},
- },
- formControlLabelRoot: {
- margin: 0,
- },
+const StyledFormControlLabel = styled(FormControlLabel)({
+ margin: 0,
});
export class ChoiceInput extends React.Component {
@@ -62,7 +43,6 @@ export class ChoiceInput extends React.Component {
likertOrientation: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.number.isRequired,
- classes: PropTypes.object,
};
static defaultProps = {
@@ -77,20 +57,19 @@ export class ChoiceInput extends React.Component {
};
render() {
- const { disabled, label, checked, likertOrientation, classes } = this.props;
+ const { disabled, label, checked, likertOrientation } = this.props;
const flexDirection = likertOrientation === LIKERT_ORIENTATION.vertical ? 'row' : 'column';
return (
-
-
+ }
+ control={}
/>
-
-
+
+
);
}
}
-export default withStyles(choiceInputStyles)(ChoiceInput);
+export default ChoiceInput;
diff --git a/packages/likert/src/index.js b/packages/likert/src/index.js
index 5ee1670eb8..2f5a670071 100644
--- a/packages/likert/src/index.js
+++ b/packages/likert/src/index.js
@@ -1,6 +1,6 @@
import Main from './main';
import React from 'react';
-import ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client';
import { SessionChangedEvent } from '@pie-framework/pie-player-events';
import { renderMath } from '@pie-lib/math-rendering';
@@ -9,6 +9,7 @@ export const isComplete = (session) => !!(session && session.value && session.va
export default class Likert extends HTMLElement {
constructor() {
super();
+ this._root = null;
}
set model(m) {
@@ -47,8 +48,18 @@ export default class Likert extends HTMLElement {
onSessionChange: this.sessionChanged.bind(this),
});
- ReactDOM.render(el, this, () => {
+ if (!this._root) {
+ this._root = createRoot(this);
+ }
+ this._root.render(el);
+ queueMicrotask(() => {
renderMath(this);
});
}
+
+ disconnectedCallback() {
+ if (this._root) {
+ this._root.unmount();
+ }
+ }
}
diff --git a/packages/likert/src/likert.jsx b/packages/likert/src/likert.jsx
index 0e8108b0a4..fa1adcd733 100644
--- a/packages/likert/src/likert.jsx
+++ b/packages/likert/src/likert.jsx
@@ -1,32 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import ChoiceInput from './choice-input';
-import { withStyles } from '@material-ui/core/styles';
+import { styled } from '@mui/material/styles';
import { color, Collapsible, PreviewPrompt } from '@pie-lib/render-ui';
import { LIKERT_ORIENTATION } from './likertEntities';
-import classNames from 'classnames';
-const styles = (theme) => ({
- main: {
- color: color.text(),
- backgroundColor: color.background(),
- '& *': {
- '-webkit-font-smoothing': 'antialiased',
- },
- },
- teacherInstructions: {
- marginBottom: theme.spacing.unit * 2,
- },
- prompt: {
- verticalAlign: 'middle',
- color: 'var(--pie-primary-text, var(--pie-text, #000000))',
- paddingBottom: theme.spacing.unit * 2,
- },
- choicesWrapper: {
- display: 'flex',
+const Main = styled('div')({
+ color: color.text(),
+ backgroundColor: color.background(),
+ '& *': {
+ '-webkit-font-smoothing': 'antialiased',
},
});
+const StyledCollapsible = styled(Collapsible)(({ theme }) => ({
+ marginBottom: theme.spacing(2),
+}));
+
+const Prompt = styled('div')(({ theme }) => ({
+ verticalAlign: 'middle',
+ color: 'var(--pie-primary-text, var(--pie-text, #000000))',
+ paddingBottom: theme.spacing(2),
+}));
+
+const ChoicesWrapper = styled('div')({
+ display: 'flex',
+});
+
export class Likert extends React.Component {
static propTypes = {
className: PropTypes.string,
@@ -37,7 +37,6 @@ export class Likert extends React.Component {
disabled: PropTypes.bool.isRequired,
onSessionChange: PropTypes.func.isRequired,
likertOrientation: PropTypes.string.isRequired,
- classes: PropTypes.object.isRequired,
};
UNSAFE_componentWillReceiveProps() {}
@@ -53,7 +52,6 @@ export class Likert extends React.Component {
prompt,
onSessionChange,
teacherInstructions,
- classes,
className,
likertOrientation,
} = this.props;
@@ -61,26 +59,25 @@ export class Likert extends React.Component {
const flexDirection = likertOrientation === LIKERT_ORIENTATION.vertical ? 'column' : 'row';
return (
-
+
{teacherInstructions && (
-
-
+
)}
{prompt && (
-
+
)}
-
+
{choices.map((choice, index) => (
))}
-
-
+
+
);
}
}
@@ -105,4 +102,4 @@ Likert.defaultProps = {
},
};
-export default withStyles(styles)(Likert);
+export default Likert;
diff --git a/packages/likert/src/main.jsx b/packages/likert/src/main.jsx
index fb37beb5ee..81415d96c0 100644
--- a/packages/likert/src/main.jsx
+++ b/packages/likert/src/main.jsx
@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
import { PreviewLayout } from '@pie-lib/render-ui';
import Likert from './likert';
@@ -9,7 +8,6 @@ class Main extends React.Component {
model: PropTypes.object,
session: PropTypes.object,
onSessionChange: PropTypes.func,
- classes: PropTypes.object.isRequired,
};
static defaultProps = {
@@ -29,10 +27,4 @@ class Main extends React.Component {
}
}
-const Styled = withStyles({}, { name: 'Main' })(Main);
-
-const Root = (props) => (
-
-);
-
-export default Root;
+export default Main;
diff --git a/packages/match-list/controller/package.json b/packages/match-list/controller/package.json
index 472889b5ae..4580e565ad 100644
--- a/packages/match-list/controller/package.json
+++ b/packages/match-list/controller/package.json
@@ -9,16 +9,16 @@
"test": "./node_modules/.bin/jest"
},
"dependencies": {
- "@pie-lib/controller-utils": "0.22.2",
- "@pie-lib/feedback": "0.24.1",
+ "@pie-lib/controller-utils": "0.22.4-next.0",
+ "@pie-lib/feedback": "0.24.3-next.0",
"debug": "^3.1.0",
"lodash": "^4.17.15"
},
"devDependencies": {
- "babel-jest": "^22.4.0",
+ "babel-jest": "^29.7.0",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-0": "^6.24.1",
- "jest": "^22.4.0"
+ "jest": "^29.7.0"
},
"author": "",
"license": "ISC"
diff --git a/packages/match-list/controller/src/index.js b/packages/match-list/controller/src/index.js
index 56a1effa51..8d456862eb 100644
--- a/packages/match-list/controller/src/index.js
+++ b/packages/match-list/controller/src/index.js
@@ -100,93 +100,89 @@ export const normalize = (model) => ({ ...defaults, ...model });
* @param {*} env
* @param {*} updateSession - optional - a function that will set the properties passed into it on the session.
*/
-export function model(question, session, env, updateSession) {
- return new Promise(async (resolve) => {
- question = normalize(question);
- const correctness = getCorrectness(question, env, session && session.value);
- const correctResponse = {};
- const score = `${getOutComeScore(question, env, session && session.value) * 100}%`;
- const correctInfo = {
- score,
- correctness,
- };
-
- const shuffledValues = {};
- let prompts = question.prompts;
- let answers = question.answers;
-
- const us = (part) => (id, element, update) => {
- return new Promise((resolve) => {
- shuffledValues[part] = update.shuffledValues;
- resolve();
- });
- };
-
- const lockChoiceOrder = lockChoices(question, session, env);
-
- if (!lockChoiceOrder) {
- prompts = await getShuffledChoices(
- prompts,
- { shuffledValues: ((session && session.shuffledValues) || {}).prompts },
- us('prompts'),
- 'id',
- );
- answers = await getShuffledChoices(
- answers,
- { shuffledValues: ((session && session.shuffledValues) || {}).answers },
- us('answers'),
- 'id',
- );
- }
-
- if (!isEmpty(shuffledValues)) {
- if (updateSession && typeof updateSession === 'function') {
- updateSession(session.id, session.element, {
- shuffledValues,
- }).catch((e) => {
- // eslint-disable-next-line no-console
- console.error('update session failed', e);
- });
- }
- }
+export async function model(question, session, env, updateSession) {
+ question = normalize(question);
+ const correctness = getCorrectness(question, env, session && session.value);
+ const correctResponse = {};
+ const score = `${getOutComeScore(question, env, session && session.value) * 100}%`;
+ const correctInfo = {
+ score,
+ correctness,
+ };
+
+ const shuffledValues = {};
+ let prompts = question.prompts;
+ let answers = question.answers;
+
+ const us = (part) => (id, element, update) => {
+ return new Promise((resolve) => {
+ shuffledValues[part] = update.shuffledValues;
+ resolve();
+ });
+ };
+
+ const lockChoiceOrder = lockChoices(question, session, env);
+
+ if (!lockChoiceOrder) {
+ prompts = await getShuffledChoices(
+ prompts,
+ { shuffledValues: ((session && session.shuffledValues) || {}).prompts },
+ us('prompts'),
+ 'id',
+ );
+ answers = await getShuffledChoices(
+ answers,
+ { shuffledValues: ((session && session.shuffledValues) || {}).answers },
+ us('answers'),
+ 'id',
+ );
+ }
- if (question && prompts) {
- prompts.forEach((prompt) => {
- correctResponse[prompt.id] = prompt.relatedAnswer;
+ if (!isEmpty(shuffledValues)) {
+ if (updateSession && typeof updateSession === 'function') {
+ updateSession(session.id, session.element, {
+ shuffledValues,
+ }).catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error('update session failed', e);
});
}
+ }
- const fb =
- env.mode === 'evaluate'
- ? getFeedbackForCorrectness(correctInfo.correctness, question.feedback)
- : Promise.resolve(undefined);
-
- fb.then((feedback) => {
- const base = {
- config: {
- ...question,
- prompts,
- answers,
- },
- correctness: correctInfo,
- feedback,
- mode: env.mode,
- };
-
- if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {
- base.rationale = question.rationale;
- } else {
- base.rationale = null;
- }
+ if (question && prompts) {
+ prompts.forEach((prompt) => {
+ correctResponse[prompt.id] = prompt.relatedAnswer;
+ });
+ }
- const out = Object.assign(base, {
- correctResponse,
- });
+ const feedback =
+ env.mode === 'evaluate'
+ ? await getFeedbackForCorrectness(correctInfo.correctness, question.feedback)
+ : undefined;
+
+ const base = {
+ config: {
+ ...question,
+ prompts,
+ answers,
+ },
+ correctness: correctInfo,
+ feedback,
+ mode: env.mode,
+ };
+
+ if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {
+ base.rationale = question.rationale;
+ } else {
+ base.rationale = null;
+ }
- log('out: ', out);
- resolve(out);
- });
+ const out = Object.assign(base, {
+ correctResponse,
});
+
+ log('out: ', out);
+ return out;
}
export const createCorrectResponseSession = (question, env) => {
diff --git a/packages/match-list/package.json b/packages/match-list/package.json
index 2c45541f73..87cad99c21 100644
--- a/packages/match-list/package.json
+++ b/packages/match-list/package.json
@@ -11,20 +11,21 @@
"postpublish": "../../scripts/postpublish"
},
"dependencies": {
- "@material-ui/core": "^3.9.2",
- "@material-ui/icons": "^3.0.2",
+ "@emotion/react": "^11.14.0",
+ "@emotion/style": "^0.8.0",
+ "@mui/icons-material": "^7.3.4",
+ "@mui/material": "^7.3.4",
"@pie-framework/pie-player-events": "^0.1.0",
- "@pie-lib/correct-answer-toggle": "2.25.2",
- "@pie-lib/drag": "2.22.2",
- "@pie-lib/math-rendering": "3.22.1",
- "@pie-lib/render-ui": "4.35.2",
- "classnames": "^2.2.5",
+ "@pie-lib/correct-answer-toggle": "2.25.4-next.0",
+ "@pie-lib/drag": "2.22.4-next.0",
+ "@pie-lib/math-rendering": "3.22.3-next.0",
+ "@pie-lib/render-ui": "4.35.4-next.0",
"debug": "^4.1.1",
"lodash": "^4.17.10",
"prop-types": "^15.6.1",
- "react": "^16.8.1",
- "react-dnd": "^14.0.5",
- "react-dom": "^16.8.1"
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "@dnd-kit/core": "6.1.0"
},
"author": "",
"license": "ISC",
diff --git a/packages/match-list/src/__tests__/__snapshots__/answer-area.test.js.snap b/packages/match-list/src/__tests__/__snapshots__/answer-area.test.js.snap
deleted file mode 100644
index 003d5b0a9d..0000000000
--- a/packages/match-list/src/__tests__/__snapshots__/answer-area.test.js.snap
+++ /dev/null
@@ -1,110 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AnswerArea render renders correctly 1`] = `
-
-`;
diff --git a/packages/match-list/src/__tests__/__snapshots__/answer.test.js.snap b/packages/match-list/src/__tests__/__snapshots__/answer.test.js.snap
deleted file mode 100644
index 53c46884c3..0000000000
--- a/packages/match-list/src/__tests__/__snapshots__/answer.test.js.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Answer render renders correctly 1`] = `
-
-
-
-`;
diff --git a/packages/match-list/src/__tests__/__snapshots__/arrow.test.js.snap b/packages/match-list/src/__tests__/__snapshots__/arrow.test.js.snap
deleted file mode 100644
index 103bf2ddc4..0000000000
--- a/packages/match-list/src/__tests__/__snapshots__/arrow.test.js.snap
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Arrow render renders correctly for default direction 1`] = `
-
-`;
-
-exports[`Arrow render renders correctly for direction left 1`] = `
-
-`;
diff --git a/packages/match-list/src/__tests__/__snapshots__/choices-list.test.js.snap b/packages/match-list/src/__tests__/__snapshots__/choices-list.test.js.snap
deleted file mode 100644
index ecbdd8c17f..0000000000
--- a/packages/match-list/src/__tests__/__snapshots__/choices-list.test.js.snap
+++ /dev/null
@@ -1,56 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ChoicesList render renders correctly 1`] = `
-
-`;
diff --git a/packages/match-list/src/__tests__/__snapshots__/main.test.js.snap b/packages/match-list/src/__tests__/__snapshots__/main.test.js.snap
deleted file mode 100644
index cd310f7eeb..0000000000
--- a/packages/match-list/src/__tests__/__snapshots__/main.test.js.snap
+++ /dev/null
@@ -1,163 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Main render renders correctly 1`] = `
-
-
-
-
-
-
-`;
diff --git a/packages/match-list/src/__tests__/answer-area.test.js b/packages/match-list/src/__tests__/answer-area.test.js
index 22a29c161e..44ebd97232 100644
--- a/packages/match-list/src/__tests__/answer-area.test.js
+++ b/packages/match-list/src/__tests__/answer-area.test.js
@@ -1,39 +1,41 @@
import * as React from 'react';
-import { shallow } from 'enzyme';
+import { render } from '@testing-library/react';
import isObject from 'lodash/isObject';
import isArray from 'lodash/isArray';
import { AnswerArea } from '../answer-area';
import { model, answer } from '../../docs/demo/config';
+jest.mock('../arrow', () => (props) =>
);
+jest.mock('../answer', () => (props) =>
);
+
describe('AnswerArea', () => {
const defaultProps = {
model: model('1'),
session: {},
classes: {},
+ instanceId: '1',
+ disabled: false,
+ showCorrect: false,
};
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallow(
);
- });
+ const wrapper = (props = {}) => {
+ return render(
);
+ };
- describe('render', () => {
- it('renders correctly', () => {
- expect(wrapper).toMatchSnapshot();
- });
- });
+ const createInstance = (props = {}) => {
+ const instanceProps = {
+ ...defaultProps,
+ ...props,
+ };
+ return new AnswerArea(instanceProps);
+ };
describe('logic', () => {
describe('getCorrectOrIncorrectArray', () => {
const mkTestForFn = (title, extraProps, val) => {
it(title, () => {
- wrapper.setProps({
- ...defaultProps,
- ...extraProps,
- });
-
- const value = wrapper.instance().getCorrectOrIncorrectMap();
+ const instance = createInstance(extraProps);
+ const value = instance.getCorrectOrIncorrectMap();
expect(isObject(value)).toBe(true);
expect(value).toEqual(val);
@@ -100,13 +102,10 @@ describe('AnswerArea', () => {
const values = isArray(val) ? val : [val];
it(title, () => {
- wrapper.setProps({
- ...defaultProps,
- ...extraProps,
- });
+ const instance = createInstance(extraProps);
indexes.forEach((i, key) => {
- const value = wrapper.instance().getAnswerFromSession(i);
+ const value = instance.getAnswerFromSession(i);
expect(value).toEqual(values[key]);
});
diff --git a/packages/match-list/src/__tests__/answer.test.js b/packages/match-list/src/__tests__/answer.test.js
deleted file mode 100644
index 2da11d8c34..0000000000
--- a/packages/match-list/src/__tests__/answer.test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import { Answer } from '../answer';
-import { model } from '../../docs/demo/config';
-
-describe('Answer', () => {
- const defaultProps = {
- model: model('1'),
- session: {},
- classes: {},
- };
-
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallow(
);
- });
-
- describe('render', () => {
- it('renders correctly', () => {
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/packages/match-list/src/__tests__/arrow.test.js b/packages/match-list/src/__tests__/arrow.test.js
deleted file mode 100644
index 32a26fbd63..0000000000
--- a/packages/match-list/src/__tests__/arrow.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import { Arrow } from '../arrow';
-
-describe('Arrow', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallow(
);
- });
-
- describe('render', () => {
- it('renders correctly for default direction', () => {
- expect(wrapper).toMatchSnapshot();
- });
-
- it('renders correctly for direction left', () => {
- wrapper.setProps({ direction: 'left' });
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/packages/match-list/src/__tests__/choices-list.test.js b/packages/match-list/src/__tests__/choices-list.test.js
deleted file mode 100644
index bb2e4e85ac..0000000000
--- a/packages/match-list/src/__tests__/choices-list.test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import { ChoicesList } from '../choices-list';
-import { model } from '../../docs/demo/config';
-
-describe('ChoicesList', () => {
- const defaultProps = {
- model: model('1'),
- session: {},
- classes: {},
- };
-
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallow(
);
- });
-
- describe('render', () => {
- it('renders correctly', () => {
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/packages/match-list/src/__tests__/main.test.js b/packages/match-list/src/__tests__/main.test.js
index 16ddcacb4d..fe8168f4d9 100644
--- a/packages/match-list/src/__tests__/main.test.js
+++ b/packages/match-list/src/__tests__/main.test.js
@@ -1,8 +1,29 @@
import * as React from 'react';
-import { shallow } from 'enzyme';
+import { render } from '@testing-library/react';
import { Main } from '../main';
import { model } from '../../docs/demo/config';
+jest.mock('../answer-area', () => (props) =>
);
+jest.mock('../choices-list', () => (props) =>
);
+jest.mock('@pie-lib/correct-answer-toggle', () => (props) =>
);
+jest.mock('@pie-lib/render-ui', () => ({
+ color: {
+ text: () => '#000',
+ background: () => '#fff',
+ },
+ Feedback: (props) =>
,
+ PreviewPrompt: (props) =>
{props.children}
,
+}));
+jest.mock('@pie-lib/drag', () => ({
+ swap: (value, from, to) => {
+ const newValue = { ...value };
+ const temp = newValue[from];
+ newValue[from] = newValue[to];
+ newValue[to] = temp;
+ return newValue;
+ },
+}));
+
describe('Main', () => {
const onSessionChange = jest.fn();
const defaultProps = {
@@ -14,22 +35,27 @@ describe('Main', () => {
onSessionChange,
};
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallow(
);
- });
+ const wrapper = (props = {}) => {
+ return render(
);
+ };
- describe('render', () => {
- it('renders correctly', () => {
- expect(wrapper).toMatchSnapshot();
+ const createInstance = (props = {}) => {
+ const instanceProps = {
+ ...defaultProps,
+ ...props,
+ };
+ const instance = new Main(instanceProps);
+ instance.setState = jest.fn((state) => {
+ Object.assign(instance.state, typeof state === 'function' ? state(instance.state) : state);
});
- });
+ return instance;
+ };
describe('logic', () => {
describe('onRemoveAnswer', () => {
it('should call onSessionChange with appropriate values', () => {
- wrapper.instance().onRemoveAnswer(0);
+ const instance = createInstance();
+ instance.onRemoveAnswer(0);
expect(onSessionChange).toHaveBeenCalledWith({
value: [undefined, 4, 3, 2],
});
@@ -38,17 +64,28 @@ describe('Main', () => {
describe('onPlaceAnswer', () => {
it('should call onSessionChange with appropriate values', () => {
- wrapper.instance().onPlaceAnswer(0, 5);
- expect(onSessionChange).toHaveBeenCalledWith({
- value: [5, 4, 3, 2],
+ const instance = createInstance();
+ instance.onPlaceAnswer({
+ active: {
+ data: {
+ current: { type: 'choice', id: 5 },
+ },
+ },
+ over: {
+ data: {
+ current: { type: 'drop-zone', promptId: 0 },
+ },
+ },
});
+ expect(onSessionChange).toHaveBeenCalled();
});
});
describe('toggleShowCorrect', () => {
it('should change state the value for showCorrectAnswer to true', () => {
- wrapper.instance().toggleShowCorrect();
- expect(wrapper.state('showCorrectAnswer')).toBe(true);
+ const instance = createInstance();
+ instance.toggleShowCorrect();
+ expect(instance.state.showCorrectAnswer).toBe(true);
});
});
});
diff --git a/packages/match-list/src/answer-area.jsx b/packages/match-list/src/answer-area.jsx
index 22a01435d3..b7919807ff 100644
--- a/packages/match-list/src/answer-area.jsx
+++ b/packages/match-list/src/answer-area.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
+import { styled } from '@mui/material/styles';
import isEmpty from 'lodash/isEmpty';
import isUndefined from 'lodash/isUndefined';
import reduce from 'lodash/reduce';
@@ -8,15 +8,50 @@ import reduce from 'lodash/reduce';
import Arrow from './arrow';
import DragAndDropAnswer from './answer';
+const ArrowEntry = styled('div')({
+ alignItems: 'normal',
+ display: 'flex',
+ height: 40,
+ margin: '10px 20px',
+});
+
+const ItemList = styled('div')(({ theme }) => ({
+ alignItems: 'flex-start',
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ marginTop: theme.spacing(2),
+ marginBottom: theme.spacing(2),
+}));
+
+const PromptEntry = styled('div')(({ theme }) => ({
+ border: `1px solid ${theme.palette.grey[400]}`,
+ boxSizing: 'border-box',
+ flex: 1,
+ margin: '10px 0',
+ minHeight: 40,
+ overflow: 'hidden',
+ padding: 10,
+ textAlign: 'center',
+ width: '100%',
+ wordBreak: 'break-word',
+}));
+
+const Row = styled('div')({
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'space-between',
+ width: '100%',
+});
+
export class AnswerArea extends React.Component {
static propTypes = {
- classes: PropTypes.object,
session: PropTypes.object.isRequired,
showCorrect: PropTypes.bool.isRequired,
disabled: PropTypes.bool.isRequired,
onSessionChange: PropTypes.func,
onRemoveAnswer: PropTypes.func,
- onPlaceAnswer: PropTypes.func.isRequired,
instanceId: PropTypes.string.isRequired,
model: PropTypes.object.isRequired,
prompt: PropTypes.string,
@@ -90,25 +125,25 @@ export class AnswerArea extends React.Component {
};
render() {
- const { classes, disabled, onPlaceAnswer, instanceId, onRemoveAnswer } = this.props;
+ const { disabled, instanceId, onRemoveAnswer } = this.props;
const rows = this.buildRows();
const correctnessMap = this.getCorrectOrIncorrectMap();
return (
-
+
{rows.map(({ sessionAnswer, title, id }, index) => {
return (
-
-
+
+
-
+
onPlaceAnswer(place, id)}
title={sessionAnswer.title}
type={'target'}
onRemoveChoice={() => onRemoveAnswer(id)}
/>
-
+
);
})}
-
+
);
}
}
-const styles = (theme) => ({
- answer: {
- flex: 1,
- },
- arrowEntry: {
- alignItems: 'normal',
- display: 'flex',
- height: 40,
- margin: '10px 20px',
- },
- itemList: {
- alignItems: 'flex-start',
- display: 'flex',
- flex: 1,
- flexDirection: 'column',
- justifyContent: 'space-between',
- marginTop: theme.spacing.unit * 2,
- marginBottom: theme.spacing.unit * 2,
- },
- promptEntry: {
- border: `1px solid ${theme.palette.grey[400]}`,
- boxSizing: 'border-box',
- flex: 1,
- margin: '10px 0',
- minHeight: 40,
- overflow: 'hidden',
- padding: 10,
- textAlign: 'center',
- width: '100%',
- wordBreak: 'break-word',
- },
- row: {
- alignItems: 'center',
- display: 'flex',
- justifyContent: 'space-between',
- width: '100%',
- },
-});
-
-export default withStyles(styles)(AnswerArea);
+export default AnswerArea;
diff --git a/packages/match-list/src/answer.js b/packages/match-list/src/answer.js
deleted file mode 100644
index 38a2ccff49..0000000000
--- a/packages/match-list/src/answer.js
+++ /dev/null
@@ -1,233 +0,0 @@
-import { DragSource, DropTarget } from 'react-dnd';
-import PropTypes from 'prop-types';
-import React from 'react';
-import classNames from 'classnames';
-import debug from 'debug';
-import { withStyles } from '@material-ui/core/styles';
-import { PlaceHolder } from '@pie-lib/drag';
-import isEmpty from 'lodash/isEmpty';
-import { color } from '@pie-lib/render-ui';
-
-const log = debug('pie-elements:match-title:answer');
-
-const Holder = withStyles((theme) => ({
- number: {
- width: '100%',
- fontSize: '18px',
- textAlign: 'center',
- color: `rgba(${theme.palette.common.black}, 0.6)`,
- },
- placeholder: {
- display: 'flex',
- padding: '0',
- alignItems: 'center',
- justifyContent: 'center',
- height: '40px',
- },
-}))(({ classes, index, isOver, disabled }) => (
-
- {index !== undefined && {index}
}
-
-));
-
-Holder.propTypes = {
- index: PropTypes.number,
- isOver: PropTypes.bool,
- disabled: PropTypes.bool,
-};
-
-const AnswerContent = withStyles((theme) => ({
- over: {
- opacity: 0.2,
- },
- answerContent: {
- color: color.text(),
- backgroundColor: color.white(),
- border: `1px solid ${theme.palette.grey[400]}`,
- cursor: 'pointer',
- width: '100%',
- padding: '10px',
- boxSizing: 'border-box',
- overflow: 'hidden',
- transition: 'opacity 200ms linear',
- wordBreak: 'break-word',
- // Added for touch devices, for image content.
- // This will prevent the context menu from appearing and not allowing other interactions with the image.
- // If interactions with the image in the token will be requested we should handle only the context Menu.
- pointerEvents: 'none',
- },
- dragging: {
- opacity: 0.5,
- },
- disabled: {
- backgroundColor: color.white(),
- cursor: 'not-allowed',
- },
- incorrect: {
- border: `1px solid ${color.incorrect()}`,
- },
- correct: {
- border: `1px solid ${color.correct()}`,
- },
-}))((props) => {
- const { classes, isDragging, isOver, title, disabled, empty, outcome, guideIndex, type } = props;
-
- if (empty) {
- return
;
- } else {
- const names = classNames(
- classes.answerContent,
- isDragging && !disabled && classes.dragging,
- isOver && !disabled && classes.over,
- disabled && classes.disabled,
- outcome && classes[outcome],
- );
-
- return
;
- }
-});
-
-export class Answer extends React.Component {
- static propTypes = {
- className: PropTypes.string,
- connectDragSource: PropTypes.func,
- connectDropTarget: PropTypes.func,
- isDragging: PropTypes.bool.isRequired,
- id: PropTypes.any,
- title: PropTypes.string,
- isOver: PropTypes.bool,
- classes: PropTypes.object.isRequired,
- empty: PropTypes.bool,
- type: PropTypes.string,
- disabled: PropTypes.bool,
- correct: PropTypes.bool,
- };
-
- componentDidMount() {
- if (this.ref) {
- this.ref.addEventListener('touchstart', this.handleTouchStart, { passive: false });
- }
- }
-
- componentWillUnmount() {
- if (this.ref) {
- this.ref.removeEventListener('touchstart', this.handleTouchStart);
- }
- }
-
- handleTouchStart = (e) => {
- e.preventDefault();
- };
-
- render() {
- const {
- id,
- title,
- isDragging,
- className,
- connectDragSource,
- connectDropTarget,
- disabled,
- classes,
- isOver,
- type,
- correct,
- } = this.props;
-
- log('[render], props: ', this.props);
-
- const name = classNames(className, classes.answer, {
- [classes.correct]: correct === true,
- [classes.incorrect]: correct === false,
- });
-
- const content = (
-
- );
- const droppable = connectDropTarget ? connectDropTarget(content) : content;
-
- return connectDragSource ? connectDragSource(droppable) : droppable;
- }
-}
-
-const StyledAnswer = withStyles({
- answer: {
- boxSizing: 'border-box',
- minHeight: 40,
- minWidth: '100px',
- overflow: 'hidden',
- margin: '10px 0',
- padding: '0px',
- textAlign: 'center',
- },
- incorrect: {
- border: `1px solid var(--feedback-incorrect-bg-color, ${color.incorrect()})`,
- },
- correct: {
- border: `1px solid var(--feedback-correct-bg-color, ${color.correct()})`,
- },
-})(Answer);
-
-const answerTarget = {
- drop(props, monitor) {
- const draggedItem = monitor.getItem();
-
- if (draggedItem.instanceId === props.instanceId) {
- props.onPlaceAnswer(props.promptId, draggedItem.id);
- }
- },
- canDrop(props, monitor) {
- const draggedItem = monitor.getItem();
-
- return draggedItem.instanceId === props.instanceId;
- },
-};
-
-export const DropAnswer = DropTarget('Answer', answerTarget, (connect, monitor) => ({
- connectDropTarget: connect.dropTarget(),
- isOver: monitor.isOver(),
-}))(StyledAnswer);
-
-const answerSource = {
- canDrag(props) {
- return props.draggable && !props.disabled;
- },
- beginDrag(props) {
- return {
- id: props.id,
- type: props.type,
- instanceId: props.instanceId,
- value: props.title,
- promptId: props.promptId,
- };
- },
- endDrag(props, monitor) {
- if (!monitor.didDrop()) {
- if (props.type === 'target') {
- props.onRemoveChoice(monitor.getItem());
- }
- }
- },
-};
-
-export const DragAnswer = DragSource('Answer', answerSource, (connect, monitor) => ({
- connectDragSource: connect.dragSource(),
- isDragging: monitor.isDragging(),
-}))(StyledAnswer);
-
-const DragAndDropAnswer = DragSource('Answer', answerSource, (connect, monitor) => ({
- connectDragSource: connect.dragSource(),
- isDragging: monitor.isDragging(),
-}))(DropAnswer);
-
-export default DragAndDropAnswer;
diff --git a/packages/match-list/src/answer.jsx b/packages/match-list/src/answer.jsx
new file mode 100644
index 0000000000..c9517f29a5
--- /dev/null
+++ b/packages/match-list/src/answer.jsx
@@ -0,0 +1,251 @@
+import { useDraggable, useDroppable } from '@dnd-kit/core';
+import PropTypes from 'prop-types';
+import React from 'react';
+import debug from 'debug';
+import { styled } from '@mui/material/styles';
+import { PlaceHolder } from '@pie-lib/drag';
+import isEmpty from 'lodash/isEmpty';
+import { color } from '@pie-lib/render-ui';
+
+const log = debug('pie-elements:match-title:answer');
+
+const HolderNumber = styled('div')(({ theme }) => ({
+ width: '100%',
+ fontSize: '18px',
+ textAlign: 'center',
+ color: `rgba(${theme.palette.common.black}, 0.6)`,
+}));
+
+const Holder = ({ index, isOver, disabled, type }) => (
+
+ {index !== undefined && {index}}
+
+);
+
+Holder.propTypes = {
+ index: PropTypes.number,
+ isOver: PropTypes.bool,
+ disabled: PropTypes.bool,
+ type: PropTypes.string,
+};
+
+const AnswerContentContainer = styled('div')(({ theme, isDragging, isOver, disabled, outcome }) => ({
+ color: color.text(),
+ backgroundColor: color.white(),
+ border: `1px solid ${outcome === 'correct' ? color.correct() :
+ outcome === 'incorrect' ? color.incorrect() :
+ theme.palette.grey[400]
+ }`,
+ cursor: disabled ? 'not-allowed' : 'pointer',
+ width: '100%',
+ padding: '10px',
+ boxSizing: 'border-box',
+ overflow: 'hidden',
+ transition: 'opacity 200ms linear',
+ wordBreak: 'break-word',
+ opacity: isDragging && !disabled ? 0.5 : isOver && !disabled ? 0.2 : 1,
+}));
+
+const AnswerContent = (props) => {
+ const { isDragging, isOver, title, disabled, empty, outcome, guideIndex, type } = props;
+
+ if (empty) {
+ return
;
+ } else {
+ return (
+
+ );
+ }
+};
+
+const AnswerContainer = styled('div')(({ correct, theme }) => ({
+ boxSizing: 'border-box',
+ minHeight: 40,
+ minWidth: '200px',
+ overflow: 'hidden',
+ margin: theme.spacing(0.5),
+ padding: '0px',
+ textAlign: 'center',
+ height: 'initial',
+ border: correct === true ? `1px solid var(--feedback-correct-bg-color, ${color.correct()})` :
+ correct === false ? `1px solid var(--feedback-incorrect-bg-color, ${color.incorrect()})` :
+ 'none',
+}));
+
+export class Answer extends React.Component {
+ static propTypes = {
+ className: PropTypes.string,
+ isDragging: PropTypes.bool,
+ id: PropTypes.any,
+ title: PropTypes.string,
+ isOver: PropTypes.bool,
+ empty: PropTypes.bool,
+ type: PropTypes.string,
+ disabled: PropTypes.bool,
+ correct: PropTypes.bool,
+ };
+
+ componentDidMount() {
+ if (this.ref) {
+ // NOTE: preventing default on touchstart can block dnd-kit pointer handling on some devices.
+ // Consider removing this if you have issues on touch devices.
+ this.ref.addEventListener('touchstart', this.handleTouchStart, { passive: true });
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.ref) {
+ this.ref.removeEventListener('touchstart', this.handleTouchStart);
+ }
+ }
+
+ handleTouchStart = (e) => {
+ // do NOT call e.preventDefault() here — it prevents pointer events necessary for dnd-kit.
+ // Keep this handler empty or remove it if you don't need it.
+ // e.preventDefault();
+ };
+
+ render() {
+ const {
+ id,
+ title,
+ isDragging = false,
+ className,
+ disabled,
+ isOver = false,
+ type,
+ correct,
+ } = this.props;
+
+ log('[render], props: ', this.props);
+
+ return (
+
(this.ref = ref)}>
+
+
+ );
+ }
+}
+
+function DragAndDropAnswer(props) {
+ const { id, instanceId, promptId, draggable = true, disabled = false, type } = props;
+
+ const dragId = `${type || 'answer'}-${id}`;
+ // droppable only if promptId exists
+ const dropId = promptId ? `drop-${promptId}` : undefined;
+
+ const {
+ attributes,
+ listeners,
+ setNodeRef: setDragRef,
+ transform,
+ transition,
+ isDragging,
+ } = useDraggable({
+ id: dragId,
+ data: {
+ type: type || 'answer',
+ id,
+ instanceId,
+ value: props.title,
+ promptId,
+ },
+ disabled: !draggable || disabled,
+ });
+
+ const droppable = useDroppable({
+ id: dropId,
+ data: dropId ? { type: 'drop-zone', promptId, instanceId } : undefined,
+ disabled: disabled || !dropId,
+ });
+
+ const setDropRef = droppable.setNodeRef;
+ const isOver = droppable.isOver;
+
+ // compute style: apply transform to the element that actually moves
+ const transformStyle = transform
+ ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
+ : undefined;
+
+ // If this item is a drop-zone (prompt slot), we render an outer droppable wrapper.
+ // For droppable wrapper we apply style to the outer wrapper
+ if (dropId) {
+ return (
+
+ );
+ }
+
+ // if there is NO dropId (this is a choice / draggable-only), render only draggable node and apply transform to it.
+ return (
+
+ );
+}
+
+DragAndDropAnswer.propTypes = {
+ id: PropTypes.any,
+ instanceId: PropTypes.string,
+ promptId: PropTypes.any,
+ title: PropTypes.string,
+ draggable: PropTypes.bool,
+ disabled: PropTypes.bool,
+ type: PropTypes.string,
+};
+
+export default DragAndDropAnswer;
diff --git a/packages/match-list/src/arrow.js b/packages/match-list/src/arrow.js
deleted file mode 100644
index 284de133a1..0000000000
--- a/packages/match-list/src/arrow.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classnames from 'classnames';
-import ArrowHead from '@material-ui/icons/ArrowDropDown';
-import { withStyles } from '@material-ui/core/styles';
-
-export class Arrow extends React.Component {
- static propTypes = {
- direction: PropTypes.string,
- classes: PropTypes.object.isRequired,
- };
-
- render() {
- const { direction, classes } = this.props;
-
- const extraStyle =
- direction === 'left'
- ? {}
- : {
- transform: 'rotate(180deg)',
- };
-
- return (
-
- );
- }
-}
-
-const styledArrow = withStyles((theme) => ({
- arrow: {
- display: 'inline-block',
- position: 'relative',
- width: '100%',
- },
- line: {
- backgroundColor: theme.palette.grey[500],
- bottom: 19,
- content: '""',
- display: 'block',
- height: 1,
- left: 20,
- position: 'absolute',
- width: '100%',
- },
- right: {
- bottom: 20,
- },
-}))(Arrow);
-
-export default styledArrow;
diff --git a/packages/match-list/src/arrow.jsx b/packages/match-list/src/arrow.jsx
new file mode 100644
index 0000000000..0cc016f6ec
--- /dev/null
+++ b/packages/match-list/src/arrow.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ArrowHead from '@mui/icons-material/ArrowDropDown';
+import { styled } from '@mui/material/styles';
+
+const ArrowContainer = styled('div')({
+ display: 'inline-block',
+ position: 'relative',
+ width: '100%',
+});
+
+const Line = styled('span')(({ theme, isRight }) => ({
+ backgroundColor: theme.palette.grey[500],
+ bottom: isRight ? 20 : 19,
+ content: '""',
+ display: 'block',
+ height: 1,
+ left: 20,
+ position: 'absolute',
+ width: '100%',
+}));
+
+export class Arrow extends React.Component {
+ static propTypes = {
+ direction: PropTypes.string,
+ };
+
+ render() {
+ const { direction } = this.props;
+
+ const extraStyle =
+ direction === 'left'
+ ? {}
+ : {
+ transform: 'rotate(180deg)',
+ };
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default Arrow;
diff --git a/packages/match-list/src/choices-list.jsx b/packages/match-list/src/choices-list.jsx
index dabb781aef..04d6ee0539 100644
--- a/packages/match-list/src/choices-list.jsx
+++ b/packages/match-list/src/choices-list.jsx
@@ -1,15 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
+import { styled } from '@mui/material/styles';
import isEmpty from 'lodash/isEmpty';
import isUndefined from 'lodash/isUndefined';
import find from 'lodash/find';
-import { DragAnswer } from './answer';
+import DragAndDropAnswer from './answer';
import { MatchDroppablePlaceholder } from '@pie-lib/drag';
+const ChoicesContainer = styled('div')(({ theme }) => ({
+ marginBottom: theme.spacing(2),
+}));
+
+const AnswersContainer = styled('div')(({ theme }) => ({
+ alignItems: 'center',
+ display: 'flex',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'space-between',
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+}));
+
export class ChoicesList extends React.Component {
static propTypes = {
- classes: PropTypes.object,
session: PropTypes.object.isRequired,
instanceId: PropTypes.string.isRequired,
model: PropTypes.object.isRequired,
@@ -18,7 +31,7 @@ export class ChoicesList extends React.Component {
};
render() {
- const { model, classes, disabled, session, instanceId, onRemoveAnswer } = this.props;
+ const { model, disabled, session, instanceId, onRemoveAnswer } = this.props;
const { config } = model;
const { duplicates } = config;
@@ -31,10 +44,9 @@ export class ChoicesList extends React.Component {
isUndefined(find(session.value, (val) => val === answer.id)),
)
.map((answer) => (
-
- {MatchDroppablePlaceholder ? (
+
+ {MatchDroppablePlaceholder ? (
{filteredAnswers}
) : (
- {filteredAnswers}
+ {filteredAnswers}
)}
-
+
);
}
}
-const styles = (theme) => ({
- choicesContainer: {
- marginBottom: theme.spacing.unit * 2,
- },
- answersContainer: {
- alignItems: 'center',
- display: 'flex',
- flexDirection: 'row',
- flexWrap: 'wrap',
- justifyContent: 'space-between',
- marginTop: theme.spacing.unit,
- marginBottom: theme.spacing.unit,
- },
- choice: {
- minHeight: '40px',
- minWidth: '200px',
- height: 'initial',
- margin: theme.spacing.unit / 2,
- },
-});
-
-export default withStyles(styles)(ChoicesList);
+export default ChoicesList;
diff --git a/packages/match-list/src/index.js b/packages/match-list/src/index.js
index ea132cd4c2..258210b387 100644
--- a/packages/match-list/src/index.js
+++ b/packages/match-list/src/index.js
@@ -1,5 +1,5 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client';
import debug from 'debug';
import Main from './main';
@@ -32,6 +32,7 @@ export const isComplete = (session, model) => {
export default class MatchList extends HTMLElement {
constructor() {
super();
+ this._root = null;
}
set model(m) {
@@ -72,8 +73,18 @@ export default class MatchList extends HTMLElement {
onSessionChange: this.sessionChanged.bind(this),
});
- ReactDOM.render(el, this, () => {
+ if (!this._root) {
+ this._root = createRoot(this);
+ }
+ this._root.render(el);
+ queueMicrotask(() => {
renderMath(this);
});
}
+
+ disconnectedCallback() {
+ if (this._root) {
+ this._root.unmount();
+ }
+ }
}
diff --git a/packages/match-list/src/main.jsx b/packages/match-list/src/main.jsx
index 3899c6f8bb..c8d6dcb8ec 100644
--- a/packages/match-list/src/main.jsx
+++ b/packages/match-list/src/main.jsx
@@ -1,18 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { withDragContext, swap } from '@pie-lib/drag';
+import { swap } from '@pie-lib/drag';
+import {
+ DndContext,
+} from '@dnd-kit/core';
import CorrectAnswerToggle from '@pie-lib/correct-answer-toggle';
import { color, Feedback, PreviewPrompt } from '@pie-lib/render-ui';
-import { withStyles } from '@material-ui/core/styles';
+import { styled } from '@mui/material/styles';
import uniqueId from 'lodash/uniqueId';
import isUndefined from 'lodash/isUndefined';
import findKey from 'lodash/findKey';
import AnswerArea from './answer-area';
import ChoicesList from './choices-list';
+const MainContainer = styled('div')({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ color: color.text(),
+ backgroundColor: color.background(),
+});
+
export class Main extends React.Component {
static propTypes = {
- classes: PropTypes.object,
session: PropTypes.object.isRequired,
onSessionChange: PropTypes.func,
model: PropTypes.object.isRequired,
@@ -36,26 +46,44 @@ export class Main extends React.Component {
onSessionChange(session);
}
- onPlaceAnswer(place, id) {
- const { session, onSessionChange, model } = this.props;
- const {
- config: { duplicates },
- } = model;
+ onPlaceAnswer = (event) => {
+ const { active, over } = event;
- if (isUndefined(session.value)) {
- session.value = {};
+ if (!over || !active) {
+ return;
}
- const choiceKey = findKey(session.value, (val) => val === id);
+ const activeData = active.data.current;
+ const overData = over.data.current;
- if (choiceKey && !duplicates) {
- session.value = swap(session.value, choiceKey, place);
- } else {
- session.value[place] = id;
- }
+ if (activeData && overData) {
+ const { session, onSessionChange, model } = this.props;
+ const { config: { duplicates } } = model;
- onSessionChange(session);
- }
+ if (isUndefined(session.value)) {
+ session.value = {};
+ }
+
+ const answerId = activeData.id;
+ const targetPromptId = overData.promptId;
+
+ // only allow dropping choices (not already placed answers) onto drop zones
+ if (activeData.type === 'choice' && overData.type === 'drop-zone' && targetPromptId) {
+ // check if this choice is already placed somewhere
+ const existingPlacement = findKey(session.value, (val) => val === answerId);
+
+ if (existingPlacement && !duplicates) {
+ // swap if duplicates not allowed
+ session.value = swap(session.value, existingPlacement, targetPromptId);
+ } else {
+ // place answer
+ session.value[targetPromptId] = answerId;
+ }
+
+ onSessionChange(session);
+ }
+ }
+ };
toggleShowCorrect = () => {
this.setState({ showCorrectAnswer: !this.state.showCorrectAnswer });
@@ -63,59 +91,46 @@ export class Main extends React.Component {
render() {
const { showCorrectAnswer } = this.state;
- const { classes, model, session } = this.props;
+ const { model, session } = this.props;
const { config, mode } = model;
const { prompt, language } = config;
return (
-