@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
544 lines (539 loc) • 22.3 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck";
import _createClass from "@babel/runtime/helpers/esm/createClass";
import _possibleConstructorReturn from "@babel/runtime/helpers/esm/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/esm/getPrototypeOf";
import _inherits from "@babel/runtime/helpers/esm/inherits";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
var _class, _MatchingEdit;
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _callSuper(_this, derived, args) {
function isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
return !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
} catch (e) {
return false;
}
}
derived = _getPrototypeOf(derived);
return _possibleConstructorReturn(_this, isNativeReflectConstruct() ? Reflect.construct(derived, args || [], _getPrototypeOf(_this).constructor) : derived.apply(_this, args));
}
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import map from 'lodash/fp/map';
import filter from 'lodash/fp/filter';
import set from 'lodash/fp/set';
import fromPairs from 'lodash/fp/fromPairs';
import pullAt from 'lodash/fp/pullAt';
import findIndex from 'lodash/fp/findIndex';
import isEmpty from 'lodash/fp/isEmpty';
import uniq from 'lodash/fp/uniq';
import shuffle from 'lodash/fp/shuffle';
import { v4 as uuid } from 'uuid';
import FocusGroup from '../../common/components/FocusGroup';
import MatchingInteractionType from '../../../records/interactions/matching';
import QuestionContainer from '../../common/edit/components/QuestionContainer';
import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer';
import DistractorList from './DistractorList';
import MatchListEdit from './MatchListEdit';
import withEditTools from '../../../util/withEditTools';
import t from '@instructure/quiz-i18n/es/format-message';
import { RadioInputGroup, RadioInput } from '@instructure/ui-radio-input';
import { Checkbox } from '@instructure/ui-checkbox';
import { View } from '@instructure/ui-view';
import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel';
import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert';
import { ScreenReaderContent } from '@instructure/ui-a11y-content';
import { IconButton } from '@instructure/ui-buttons';
import { IconQuestionLine } from '@instructure/ui-icons';
import { Text } from '@instructure/ui-text';
import { SimpleModal, FormFieldGroup } from '@instructure/quiz-common';
var QUESTION_SELECTOR = '[data-role=questionWrapper] input';
// Scoring Algorithms:
var DEEP_EQUALS = 'DeepEquals';
var PARTIAL_DEEP = 'PartialDeep';
// return editData if supplied, otherwise reconstruct it from props
export var getEditData = function getEditData(_ref) {
var scoringData = _ref.scoringData,
interactionData = _ref.interactionData;
var editData = scoringData.editData;
if (!isEmpty(editData)) {
return {
matches: editData.matches || [],
distractors: editData.distractors || []
};
}
var matches = map(function (_ref2) {
var id = _ref2.id,
itemBody = _ref2.itemBody;
return {
questionId: id,
questionBody: itemBody,
answerBody: scoringData.value[id]
};
}, interactionData.questions);
var matchBodies = map('answerBody', matches);
var distractors = filter(function (answerBody) {
return !matchBodies.includes(answerBody);
}, interactionData.answers);
return {
matches: matches,
distractors: distractors
};
};
// given editData, calculate the rest of the props
export var getPropsFromEditData = function getPropsFromEditData(editData) {
return {
interactionData: {
questions: map(function (match) {
return {
id: match.questionId,
itemBody: match.questionBody
};
}, editData.matches),
answers: shuffle(uniq([].concat(_toConsumableArray(map('answerBody', editData.matches)), _toConsumableArray(editData.distractors))))
},
scoringData: {
value: fromPairs(map(function (match) {
return [match.questionId, match.answerBody];
}, editData.matches)),
editData: editData
}
};
};
/**
---
category: Matching
---
Matching Edit component
```jsx_example
function Example (props) {
const exampleProps = {
itemBody: 'Match the Secretary of State with the President they served under.',
overrideEditableForRegrading: false,
properties: {
shuffleRules: {
questions: { shuffled: true }
}
},
interactionData: {
questions: [
{ id: 'uuid1', itemBody: 'Condi Rice' },
{ id: 'uuid2', itemBody: 'Alexander Haig' },
{ id: 'uuid3', itemBody: 'John Kerry' }
],
answers: [
'Ronald Reagan',
'George W. Bush',
'Barack Obama',
'George H.W. Bush',
'Bill Clinton'
]
},
scoringData: {
value: {
uuid1: 'George W. Bush',
uuid2: 'Ronald Reagan',
uuid3: 'Barack Obama'
},
editData: {}
}
}
return (
<MatchingEdit {...exampleProps} {...props} />
)
}
<SettingsSwitcher locales={LOCALES}>
<EditStateProvider>
<Example />
</EditStateProvider>
</SettingsSwitcher>
```
**/
var MatchingEdit = withEditTools(_class = (_MatchingEdit = /*#__PURE__*/function (_Component) {
function MatchingEdit() {
var _this2;
_classCallCheck(this, MatchingEdit);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this2 = _callSuper(this, MatchingEdit, [].concat(args));
_defineProperty(_this2, "state", {
isModalOpen: false
});
_defineProperty(_this2, "_timeouts", []);
_defineProperty(_this2, "focusMatch", null);
_defineProperty(_this2, "focusDistractor", null);
_defineProperty(_this2, "stemElement", null);
_defineProperty(_this2, "questionErrors", function (index) {
return _this2.props.getErrors("scoringData.editData.matches[".concat(index, "].questionBody")).slice(0, 1);
});
_defineProperty(_this2, "answerErrors", function (index) {
return _this2.props.getErrors("scoringData.editData.matches[".concat(index, "].answerBody")).slice(0, 1);
});
_defineProperty(_this2, "distractorErrors", function (index) {
return _this2.props.getErrors("scoringData.editData.distractors[".concat(index, "]")).slice(0, 1);
});
// =============
// HANDLERS
// =============
_defineProperty(_this2, "handleScoringAlgoChange", function (e, value) {
_this2.props.changeItemState({
scoringAlgorithm: value
}, {
scoringAlgorithm: value
});
});
_defineProperty(_this2, "blurDistractor", function (index, e) {
_this2.editDistractor(index, {
target: {
value: e.target.value.trim()
}
});
});
_defineProperty(_this2, "editDistractor", function (index, e) {
var editData = getEditData(_this2.props);
if (index >= 0 && index < editData.distractors.length) {
_this2.updateEditData(set("distractors[".concat(index, "]"), e.target.value, editData));
}
});
_defineProperty(_this2, "editQuestion", function (questionId, text) {
var editData = getEditData(_this2.props);
var questionIndex = findIndex({
questionId: questionId
}, editData.matches);
if (questionIndex !== -1) {
_this2.updateEditData(set("matches[".concat(questionIndex, "].questionBody"), text, editData));
}
});
_defineProperty(_this2, "editAnswer", function (questionId, text) {
var editData = getEditData(_this2.props);
var questionIndex = findIndex({
questionId: questionId
}, editData.matches);
if (questionIndex !== -1) {
_this2.updateEditData(set("matches[".concat(questionIndex, "].answerBody"), text, editData));
}
});
_defineProperty(_this2, "handleShuffleChange", function (event) {
var properties = set('shuffleRules.questions.shuffled', event.target.checked, _this2.props.properties);
_this2.props.changeItemState({
properties: properties
}, {
properties: properties
});
});
_defineProperty(_this2, "handleRemoveMatch", function (questionId) {
if (document.activeElement !== document.body) {
// In FF clicking on a button doesn't focus it, so don't update focus in such case
_this2.updateFocusOnRemoveMatch();
}
var editData = getEditData(_this2.props);
_this2.updateEditData(set('matches', editData.matches.filter(function (match) {
return match.questionId !== questionId;
}), editData));
});
_defineProperty(_this2, "handleCreateMatch", function () {
_this2._timeouts = [].concat(_toConsumableArray(_this2._timeouts), [setTimeout(function () {
return _this2.focusMatch.focusLast(QUESTION_SELECTOR);
}, 100)]);
var editData = getEditData(_this2.props);
_this2.updateEditData(set('matches', [].concat(_toConsumableArray(editData.matches), [{
questionId: _this2.props.newId(),
questionBody: '',
answerBody: ''
}]), editData));
});
_defineProperty(_this2, "handleRemoveDistractor", function (index) {
if (_this2.focusDistractor.previousExists('button')) {
_this2.focusDistractor.focusPrevious('button');
} else {
_this2.focusMatch.focusLast();
}
var editData = getEditData(_this2.props);
_this2.updateEditData(set('distractors', pullAt([index], editData.distractors), editData));
});
_defineProperty(_this2, "handleCreateDistractor", function () {
_this2._timeouts = [].concat(_toConsumableArray(_this2._timeouts), [setTimeout(function () {
return _this2.focusDistractor.focusLast('input');
}, 100)]);
var editData = getEditData(_this2.props);
_this2.updateEditData(set('distractors', [].concat(_toConsumableArray(editData.distractors), ['']), editData));
});
_defineProperty(_this2, "handleCalculatorTypeChange", function (e, value) {
_this2.props.changeItemState({
calculatorType: value
});
});
_defineProperty(_this2, "handleDescriptionChange", function (itemBody) {
_this2.props.changeItemState({
itemBody: itemBody
});
});
_defineProperty(_this2, "handleFocusMatchRef", function (node) {
_this2.focusMatch = node;
});
_defineProperty(_this2, "handleFocusDistractorRef", function (node) {
_this2.focusDistractor = node;
});
_defineProperty(_this2, "handleStemRef", function (node) {
_this2.stemElement = node;
});
_defineProperty(_this2, "handleCloseModal", function () {
_this2.setState({
isModalOpen: false
});
});
_defineProperty(_this2, "handleOpenModal", function () {
_this2.setState({
isModalOpen: true
});
});
return _this2;
}
_inherits(MatchingEdit, _Component);
return _createClass(MatchingEdit, [{
key: "componentDidMount",
value: function componentDidMount() {
this.setDefaultScoringAlgorithm();
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
this._timeouts.forEach(clearTimeout);
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate() {
this.setDefaultScoringAlgorithm();
}
}, {
key: "setDefaultScoringAlgorithm",
value: function setDefaultScoringAlgorithm() {
if (!this.props.scoringAlgorithm && this.props.partialDeepScoringEnabled) {
this.props.changeItemState({
scoringAlgorithm: PARTIAL_DEEP
});
}
}
}, {
key: "overrideEditable",
value: function overrideEditable() {
return this.props.overrideEditableForItem || this.props.overrideEditableForRegrading;
}
// =============
// HELPERS
// =============
}, {
key: "updateEditData",
value: function updateEditData(newEditData) {
this.props.changeItemState(getPropsFromEditData(newEditData));
}
}, {
key: "updateFocusOnRemoveMatch",
value: function updateFocusOnRemoveMatch() {
var _this3 = this;
if (!this.focusMatch.previousExists('button')) {
// if removing the first choice, focus on stem
this.stemElement.focus();
} else if (this.props.interactionData.questions.length === 2) {
// if removing the second choice out of two choices
this._timeouts = [].concat(_toConsumableArray(this._timeouts), [setTimeout(function () {
return _this3.focusMatch.focusLast('input');
}, 100)]);
} else {
// all the other cases
this.focusMatch.focusPrevious('button');
}
}
}, {
key: "renderOptionsDescription",
value:
// =============
// RENDERING
// =============
function renderOptionsDescription() {
return /*#__PURE__*/React.createElement(ScreenReaderContent, null, t('Matching options'));
}
}, {
key: "renderGradingOptions",
value: function renderGradingOptions() {
return /*#__PURE__*/React.createElement(View, {
as: "div",
margin: "medium 0",
position: "relative"
}, /*#__PURE__*/React.createElement(RadioInputGroup, {
onChange: this.handleScoringAlgoChange,
name: t('Grading'),
value: this.props.scoringAlgorithm,
description: /*#__PURE__*/React.createElement(ScreenReaderContent, null, t('Grading'))
}, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(Text, null, t('Grading')), /*#__PURE__*/React.createElement(IconButton, {
size: "small",
withBackground: false,
withBorder: false,
renderIcon: IconQuestionLine,
onClick: this.handleOpenModal,
screenReaderLabel: t('Open grading option information')
})), /*#__PURE__*/React.createElement(RadioInput, {
value: PARTIAL_DEEP,
label: t('Partial credit'),
"data-automation": "sdk-grading-partial-credit-radio-input"
}), /*#__PURE__*/React.createElement(RadioInput, {
value: DEEP_EQUALS,
label: t('Exact match'),
"data-automation": "sdk-grading-exact-match-radio-input"
})));
}
}, {
key: "renderGradingOptionsModal",
value: function renderGradingOptionsModal() {
return /*#__PURE__*/React.createElement(SimpleModal, {
size: "small",
title: t('Grading'),
label: t('Grading'),
isModalOpen: this.state.isModalOpen,
onModalDismiss: this.handleCloseModal
}, /*#__PURE__*/React.createElement(Text, {
weight: "bold",
lineHeight: "double"
}, t('Partial credit')), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement(Text, null, t('Students are awarded points for every correct answer.')), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement(Text, {
weight: "bold",
lineHeight: "double"
}, t('Exact match')), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement(Text, null, t('Students are awarded full credit if all correct answers are selected and no incorrect answers are selected.')));
}
}, {
key: "render",
value: function render() {
var _this4 = this;
var _getEditData = getEditData(this.props),
matches = _getEditData.matches,
distractors = _getEditData.distractors;
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(QuestionContainer, {
disabled: this.props.overrideEditableForRegrading,
enableRichContentEditor: this.props.enableRichContentEditor,
itemBody: this.props.itemBody,
onDescriptionChange: this.handleDescriptionChange,
onModalClose: this.props.onModalClose,
onModalOpen: this.props.onModalOpen,
openImportModal: this.props.openImportModal,
stemErrors: this.props.getErrors('itemBody'),
textareaRef: this.handleStemRef
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(FocusGroup, {
ref: this.handleFocusMatchRef
}, /*#__PURE__*/React.createElement(MatchListEdit
// Passed in as new function to force re-render
, {
answerErrors: function answerErrors(index) {
return _this4.answerErrors(index);
},
createNewMatch: this.handleCreateMatch,
editAnswer: this.editAnswer,
editQuestion: this.editQuestion
// Passed in as new function to force re-render
,
questionErrors: function questionErrors(index) {
return _this4.questionErrors(index);
},
removeMatch: this.handleRemoveMatch,
disabled: this.props.overrideEditableForRegrading,
matches: matches,
notifyScreenreader: this.props.notifyScreenreader
})), /*#__PURE__*/React.createElement(FocusGroup, {
ref: this.handleFocusDistractorRef
}, /*#__PURE__*/React.createElement(DistractorList, {
disabled: this.props.overrideEditableForRegrading,
createNewDistractor: this.handleCreateDistractor,
distractors: distractors,
editDistractor: this.editDistractor,
blurDistractor: this.blurDistractor
// Passed in as new function to force re-render
,
distractorErrors: function distractorErrors(index) {
return _this4.distractorErrors(index);
},
notifyScreenreader: this.props.notifyScreenreader,
removeDistractor: this.handleRemoveDistractor
})))), /*#__PURE__*/React.createElement(QuestionSettingsContainer, {
additionalOptions: this.props.additionalOptions
}, /*#__PURE__*/React.createElement(QuestionSettingsPanel, {
label: t('Options'),
defaultExpanded: true
}, /*#__PURE__*/React.createElement(FormFieldGroup, {
rowSpacing: "small",
description: this.renderOptionsDescription()
}, this.props.showCalculatorOption && /*#__PURE__*/React.createElement(CalculatorOptionWithOqaatAlert, {
disabled: this.props.overrideEditableForRegrading,
calculatorValue: this.props.calculatorType,
onCalculatorTypeChange: this.handleCalculatorTypeChange,
oqaatChecked: this.props.oneQuestionAtATime,
onOqaatChange: this.props.setOneQuestionAtATime
}), /*#__PURE__*/React.createElement(Checkbox, {
label: t('Shuffle questions'),
onChange: this.handleShuffleChange,
checked: this.props.properties.shuffleRules.questions.shuffled,
disabled: this.props.overrideEditableForRegrading,
"data-automation": "sdk-shuffle-questions-checkbox"
}), this.props.partialDeepScoringEnabled && this.renderGradingOptions()))), this.renderGradingOptionsModal());
}
}]);
}(Component), _defineProperty(_MatchingEdit, "interactionType", MatchingInteractionType), _defineProperty(_MatchingEdit, "propTypes", _objectSpread(_objectSpread({
additionalOptions: QuestionSettingsContainer.propTypes.additionalOptions,
calculatorType: PropTypes.string,
changeItemState: PropTypes.func,
enableRichContentEditor: PropTypes.bool,
interactionData: PropTypes.shape({
questions: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
itemBody: PropTypes.string
})),
answers: PropTypes.arrayOf(PropTypes.string)
}).isRequired,
itemBody: PropTypes.string,
newId: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
oneQuestionAtATime: PropTypes.bool,
openImportModal: PropTypes.func,
overrideEditableForItem: PropTypes.bool,
overrideEditableForRegrading: PropTypes.bool,
partialDeepScoringEnabled: PropTypes.bool,
properties: PropTypes.object,
scoringAlgorithm: PropTypes.string,
scoringData: PropTypes.shape({
value: PropTypes.objectOf(PropTypes.string),
editData: PropTypes.shape({
matches: MatchListEdit.propTypes.matches,
distractors: DistractorList.propTypes.distractors
})
}),
setOneQuestionAtATime: PropTypes.func,
notifyScreenreader: PropTypes.func
}, withEditTools.injectedProps), {}, {
showCalculatorOption: PropTypes.bool
})), _defineProperty(_MatchingEdit, "defaultProps", {
calculatorType: 'none',
enableRichContentEditor: true,
oneQuestionAtATime: false,
overrideEditableForItem: false,
overrideEditableForRegrading: false,
newId: uuid,
setOneQuestionAtATime: Function.prototype,
notifyScreenreader: Function.prototype,
additionalOptions: void 0,
changeItemState: void 0,
interactionData: void 0,
itemBody: void 0,
onModalClose: void 0,
onModalOpen: void 0,
openImportModal: void 0,
partialDeepScoringEnabled: false,
properties: void 0,
scoringAlgorithm: null,
scoringData: void 0,
showCalculatorOption: true
}), _MatchingEdit)) || _class;
export { MatchingEdit as default };