quiz-interactions
Version:
A React UI component Library for quiz interaction types.
694 lines (592 loc) • 26.9 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _immutabilityHelper = _interopRequireDefault(require("immutability-helper"));
var _sortBy = _interopRequireDefault(require("lodash/sortBy"));
var _get = _interopRequireDefault(require("lodash/get"));
var _omit = _interopRequireDefault(require("lodash/omit"));
var _quizNumberInput = _interopRequireDefault(require("@instructure/quiz-number-input"));
var _uiA11y = require("@instructure/ui-a11y");
var _uiElements = require("@instructure/ui-elements");
var _uiThemeable = require("@instructure/ui-themeable");
var _quizScientificNotation = require("@instructure/quiz-scientific-notation");
var _QuestionSettingsContainer = _interopRequireDefault(require("../../common/edit/components/QuestionSettingsContainer"));
var _QuestionContainer = _interopRequireDefault(require("../../common/edit/components/QuestionContainer"));
var util = _interopRequireWildcard(require("./util"));
var _FormulaSection = _interopRequireDefault(require("./FormulaSection"));
var _GenerateSolutionsService = _interopRequireDefault(require("./GenerateSolutionsService"));
var _VariableInput = _interopRequireDefault(require("./VariableInput"));
var _util2 = require("../common/util");
var _instUIMessages = require("../../../util/instUIMessages");
var _formula = require("../../../util/formula");
var _formula2 = _interopRequireDefault(require("../../../records/interactions/formula"));
var _withEditTools = _interopRequireDefault(require("../../../util/withEditTools"));
var _withAsyncDeps = _interopRequireDefault(require("../../../util/withAsyncDeps"));
var _theme = _interopRequireDefault(require("./theme"));
var _formatMessage = _interopRequireDefault(require("@instructure/quiz-i18n/lib/format-message"));
var _QuestionSettingsPanel = _interopRequireDefault(require("../../common/edit/components/QuestionSettingsPanel"));
var _CalculatorOptionWithOqaatAlert = _interopRequireDefault(require("../../common/edit/components/CalculatorOptionWithOqaatAlert"));
var _uiFormField = require("@instructure/ui-form-field");
var _dec, _dec2, _class, _class2, _temp;
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(source, true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
var styles = {
componentId: 'fCxPy',
template: function template(theme) {
return "\n\n.fCxPy_fKmi{padding-bottom:".concat(theme.sectionHeadingPaddingBottom || 'inherit', ";padding-top:").concat(theme.sectionHeadingPaddingTop || 'inherit', "}\n\n.fCxPy_pJNU{padding-bottom:").concat(theme.instructionsPaddingBottom || 'inherit', "}\n\n.fCxPy_ftwL{padding-top:").concat(theme.generateSolutionsInputPaddingTop || 'inherit', "}\n\n.fCxPy_cWFj{padding:").concat(theme.generateButtonPaddingLeft || 'inherit', "}\n\n.fCxPy_dVha{padding-top:").concat(theme.generatedSolutionsPaddingTop || 'inherit', "}\n\n.fCxPy_fqEc{padding-left:").concat(theme.marginTextPadding || 'inherit', ";padding-right:").concat(theme.marginTextPadding || 'inherit', "}\n\n.fCxPy_cdqO{color:").concat(theme.marginPlusMinusColor || 'inherit', ";font-weight:").concat(theme.marginPlusMinusWeight || 'inherit', "}\n\n.fCxPy_rMrG{overflow:auto}\n\n.fCxPy_rMrG>table{max-width:none}\n\n.fCxPy_ekQP{position:-webkit-sticky;position:sticky;right:0}");
},
'sectionHeading': 'fCxPy_fKmi',
'instructions': 'fCxPy_pJNU',
'generateSolutionsInput': 'fCxPy_ftwL',
'generateButtonWrapper': 'fCxPy_cWFj',
'generatedSolutions': 'fCxPy_dVha',
'marginText': 'fCxPy_fqEc',
'marginPlusMinus': 'fCxPy_cdqO',
'tableWrapper': 'fCxPy_rMrG',
'outputCol': 'fCxPy_ekQP'
};
/**
---
category: Formula
---
Formula Edit component
```jsx_example
class Example extends React.Component {
render () {
const variables = 'abcdefghijklmnopqrstuvwxyz'.split('')
const exampleProps = {
itemBody: variables.map(v => `\`${v}\``).join('+'),
scoringData: {
value: {
answerCount: '10',
answerPrecision: 0,
formula: variables.join('+'),
generatedSolutions: [],
numeric: {
marginType: 'absolute',
margin: 1,
},
scientificNotation: false,
variables: variables.map(char => ({
name: char,
min: 90000,
max: 99999,
precision: 0
}))
}
},
overrideEditableForRegrading: false,
additionalOptions: [{
key: 'outcomes',
title: 'Align to Outcomes',
component: 'Placeholder'
}]
}
return (
<FormulaEdit {...exampleProps} {...this.props} />
)
}
}
<SettingsSwitcher locales={LOCALES}>
<EditStateProvider>
<Example />
</EditStateProvider>
</SettingsSwitcher>
```
**/
var formatNumber = function formatNumber(n) {
return (0, _quizScientificNotation.isScientificNotation)(n) ? n : Number(n);
};
var normalizeVariables = function normalizeVariables(variables) {
return variables.map(function (group) {
return _objectSpread({}, group, {
min: formatNumber(group.min),
max: formatNumber(group.max),
precision: Number(group.precision)
});
});
};
var FormulaEdit = (_dec = (0, _withAsyncDeps.default)(_util2.mathjsIsLoaded, _util2.loadMathjs), _dec2 = (0, _uiThemeable.themeable)(_theme.default, styles), _dec(_class = (0, _withEditTools.default)(_class = _dec2(_class = (_temp = _class2 =
/*#__PURE__*/
function (_Component) {
(0, _inherits2.default)(FormulaEdit, _Component);
function FormulaEdit(props) {
var _this;
(0, _classCallCheck2.default)(this, FormulaEdit);
_this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(FormulaEdit).call(this, props));
_this.serviceOnStart = function () {
_this.setState({
status: util.STATUS_RUNNING
});
};
_this.serviceOnComplete = function (status) {
return function (solutions) {
_this.setState({
status: status
});
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: solutions
}
}
});
_this.props.changeItemState({
scoringData: scoringData
});
var message = (0, _formatMessage.default)('Solutions updated');
_this.props.notifyScreenreader(message);
};
};
_this.serviceOnCancel = function () {
_this.setState({
status: util.STATUS_CANCELED
});
};
_this.handleCalculatorTypeChange = function (e, value) {
_this.props.changeItemState({
calculatorType: value
});
};
_this.handleItemBodyChange = function (itemBody) {
var newVariableNames = (0, _formula.variablesFromItemBody)(itemBody);
var oldVariables = _this.props.scoringData.value.variables;
var newVariables = [];
newVariableNames.forEach(function (variableName) {
var defaultVariable = {
name: variableName,
min: 0,
max: 10,
precision: 0
};
var newVariable = oldVariables.find(function (v) {
return v.name === variableName;
}) || defaultVariable;
newVariables.push(newVariable);
});
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
variables: {
$set: (0, _sortBy.default)(newVariables, function (v) {
return v.name;
})
}
}
});
_this.props.changeItemState({
itemBody: itemBody,
scoringData: scoringData
});
_this.generateSolutionsService.cancel();
};
_this.handleVariableChange = function (variableIdx, field) {
return function (value) {
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
variables: (0, _defineProperty2.default)({}, variableIdx, (0, _defineProperty2.default)({}, field, {
$set: value
}))
}
});
_this.props.changeItemState({
scoringData: scoringData
});
_this.generateSolutionsService.cancel();
};
};
_this.handlePrecisionChange = function (variableIdx) {
return function (e, value, normalized) {
if (normalized === null) return;
var variable = _this.props.scoringData.value.variables[variableIdx];
if (normalized == variable.precision) return; // intentional double-equals
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
variables: (0, _defineProperty2.default)({}, variableIdx, {
$set: {
name: variable.name,
precision: normalized,
min: util.toPrecision(variable.min, normalized),
max: util.toPrecision(variable.max, normalized)
}
})
}
});
_this.props.changeItemState({
scoringData: scoringData
});
_this.generateSolutionsService.cancel();
};
};
_this.handleFormulaChange = function (e) {
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
formula: {
$set: e.target.value
}
}
});
_this.props.changeItemState({
scoringData: scoringData
});
_this.generateSolutionsService.cancel();
};
_this.handleMarginOfErrorTypeChange = function (e, _ref) {
var value = _ref.value;
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
numeric: {
marginType: {
$set: value
}
}
}
});
_this.props.changeItemState({
scoringData: scoringData
});
};
_this.handleMarginOfErrorChange = function (e, value, normalizedValue) {
if (normalizedValue == _this.props.scoringData.value.numeric.margin) return; // intentional double-equals
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
numeric: {
margin: {
$set: Number(normalizedValue).toString()
}
}
}
});
_this.props.changeItemState({
scoringData: scoringData
});
};
_this.handleScientificNotationChange = function (e) {
var scientificNotation = !_this.props.scoringData.value.scientificNotation; // The numeric scoring algorithm doesn't support scientific notation for margin of error
var numeric = scientificNotation ? {
type: 'exactResponse'
} : {
type: 'marginOfError',
marginType: 'absolute',
margin: 0
};
var scoringData = _objectSpread({}, _this.props.scoringData, {
value: _objectSpread({}, _this.props.scoringData.value, {
generatedSolutions: [],
numeric: numeric,
scientificNotation: scientificNotation
})
});
_this.props.changeItemState({
scoringData: scoringData
});
};
_this.handleAnswerCountChange = function (e, answerCount) {
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
answerCount: {
$set: answerCount
}
}
});
_this.props.changeItemState({
scoringData: scoringData
});
_this.generateSolutionsService.cancel();
};
_this.handleAnswerPrecisionChange = function (_e, _answerPrecision, answerPrecisionNormalized) {
var answerPrecision = Number(answerPrecisionNormalized);
if (answerPrecision === Number(_this.props.scoringData.value.answerPrecision || 0)) return;
var scoringData = (0, _immutabilityHelper.default)(_this.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
answerPrecision: {
$set: answerPrecision
}
}
});
_this.props.changeItemState({
scoringData: scoringData
});
_this.generateSolutionsService.cancel();
};
_this.handleGenerateSolutions = function () {
var _this$props$scoringDa = _this.props.scoringData.value,
answerCount = _this$props$scoringDa.answerCount,
answerPrecision = _this$props$scoringDa.answerPrecision,
variables = _this$props$scoringDa.variables,
formula = _this$props$scoringDa.formula,
scientificNotation = _this$props$scoringDa.scientificNotation;
var parsedAnswerCount = parseInt(answerCount, 10) || 0;
var scoringDataSetupErrors = _this.scoringDataSetupErrors();
if (Object.keys(scoringDataSetupErrors).length > 0) {
_this.notifyScreenreaderOfSetupErrors(scoringDataSetupErrors);
_this.setState({
status: util.STATUS_FORMULA_SETUP_INVALID
});
return;
} else if (_this.state.status === util.STATUS_FORMULA_SETUP_INVALID) {
_this.setState({
status: util.STATUS_STOPPED
});
}
_this.generateSolutionsService.start(parsedAnswerCount, normalizeVariables(variables), formula, answerPrecision, scientificNotation);
};
_this.getLocale = function () {
return _this.context.locale || 'en-US';
};
_this.renderVariable = function (variableRecord, idx) {
var variableName = variableRecord.name;
var precision = Number(variableRecord.precision);
var errorPath = ['scoringData', 'value', 'variables', idx];
return _react.default.createElement("tr", {
key: variableName
}, _react.default.createElement("td", null, variableName), _react.default.createElement("td", null, _react.default.createElement(_VariableInput.default, {
disabled: _this.props.overrideEditableForRegrading,
decimalPrecision: precision,
messages: (0, _instUIMessages.toErrors)(_this.errorsFor([].concat(errorPath, ['min']))),
onUpdate: _this.handleVariableChange(idx, 'min'),
value: variableRecord.min,
width: "6rem",
label: _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('min value for variable {variable}', {
variable: variableName
}))
})), _react.default.createElement("td", null, _react.default.createElement(_VariableInput.default, {
disabled: _this.props.overrideEditableForRegrading,
decimalPrecision: precision,
messages: (0, _instUIMessages.toErrors)(_this.errorsFor([].concat(errorPath, ['max']))),
onUpdate: _this.handleVariableChange(idx, 'max'),
value: variableRecord.max,
width: "6rem",
label: _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('max value for variable {variable}', {
variable: variableName
}))
})), _react.default.createElement("td", null, _react.default.createElement(_quizNumberInput.default, {
disabled: _this.props.overrideEditableForRegrading,
max: 10,
messages: (0, _instUIMessages.toErrors)(_this.errorsFor([].concat(errorPath, ['precision']))),
min: 0,
onChange: _this.handlePrecisionChange(idx),
showArrows: true,
step: 1,
value: variableRecord.precision,
width: "6rem",
label: _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('decimals of precision for variable {variable}', {
variable: variableName
}))
})));
};
_this.generateSolutionsService = new _GenerateSolutionsService.default({
onStart: _this.serviceOnStart,
onSuccess: _this.serviceOnComplete(util.STATUS_STOPPED),
onFailure: _this.serviceOnComplete(util.STATUS_FAILED),
onCancel: _this.serviceOnCancel
});
_this.state = {
status: util.STATUS_STOPPED
};
return _this;
} // ==============================
// HOOKS FOR GENERATING SOLUTIONS
// ==============================
(0, _createClass2.default)(FormulaEdit, [{
key: "scoringDataSetupErrors",
// =================
// UTILITY FUNCTIONS
// =================
value: function scoringDataSetupErrors() {
var scoringDataErrors = (0, _get.default)(this.errors(), ['scoringData', 'value'], {});
return (0, _omit.default)(scoringDataErrors, ['generatedSolutions']);
}
}, {
key: "notifyScreenreaderOfSetupErrors",
value: function notifyScreenreaderOfSetupErrors(sdSetupErrors) {
var _this2 = this;
var vars = Object.keys(sdSetupErrors.variables || {}).map(function (v) {
return _this2.props.scoringData.value.variables[v].name.replace(/`/g, '');
});
var errorMsg;
if (vars.length > 0) {
errorMsg = (0, _formatMessage.default)('variables containing errors: {vars}', {
vars: vars.join(', ')
});
}
if (this.props.scoringData.value.variables.length === 0) {
errorMsg = (0, _formatMessage.default)('must define at least one variable');
}
if (errorMsg) {
this.props.notifyScreenreader((0, _formatMessage.default)('The following error prevented generating solutions: {errorMsg}', {
errorMsg: errorMsg
}));
}
}
}, {
key: "renderVariablesTable",
value: function renderVariablesTable() {
var variables = normalizeVariables(this.props.scoringData.value.variables);
return _react.default.createElement(_uiElements.Table, {
caption: ""
}, _react.default.createElement("tbody", null, _react.default.createElement("tr", null, _react.default.createElement("th", null, (0, _formatMessage.default)('Variable')), _react.default.createElement("th", null, (0, _formatMessage.default)('Min')), _react.default.createElement("th", null, (0, _formatMessage.default)('Max')), _react.default.createElement("th", null, (0, _formatMessage.default)('Decimals'))), variables.map(this.renderVariable)));
}
}, {
key: "errorsFor",
value: function errorsFor(path) {
if (!this.props.errorsAreShowing && this.state.status !== util.STATUS_FORMULA_SETUP_INVALID) {
return [];
}
return (0, _get.default)(this.errors(), path, []);
}
}, {
key: "errors",
value: function errors() {
return new _formula2.default(_objectSpread({}, (0, _omit.default)(this.props, ['getErrors', 'scoringData']), {
scoringData: this.props.scoringData
})).getErrors();
}
}, {
key: "renderOptionsDescription",
value: function renderOptionsDescription() {
return _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('Formula options'));
}
}, {
key: "render",
value: function render() {
return _react.default.createElement("div", null, _react.default.createElement("div", null, _react.default.createElement(_uiElements.Text, {
color: "primary"
}, (0, _formatMessage.default)('Enter your question, build a formula, and generate a set of possible answer' + ' combinations. Students will see the question with a randomly selected set' + ' of variables filled in and have to type the correct numerical answer.'))), _react.default.createElement("div", {
className: styles.sectionHeading
}, _react.default.createElement(_uiElements.Text, {
size: "large"
}, (0, _formatMessage.default)('Question'))), _react.default.createElement("div", {
className: styles.instructions
}, _react.default.createElement(_uiElements.Text, {
color: "primary"
}, (0, _formatMessage.default)('You can define variables by typing variable names surrounded by backticks (e.g., "what is 5 plus `x`?")'))), _react.default.createElement(_QuestionContainer.default, {
disabled: this.props.overrideEditableForRegrading,
enableRichContentEditor: this.props.enableRichContentEditor,
itemBody: this.props.itemBody,
onDescriptionChange: this.handleItemBodyChange,
onModalClose: this.props.onModalClose,
onModalOpen: this.props.onModalOpen,
openImportModal: this.props.openImportModal,
renderImportModal: this.props.renderImportModal,
stemErrors: this.errorsFor(['itemBody']),
textareaRef: this.handleStemRef
}, _react.default.createElement("div", {
className: styles.sectionHeading
}, _react.default.createElement(_uiElements.Text, {
size: "large"
}, (0, _formatMessage.default)('Answers'))), _react.default.createElement("div", {
className: styles.instructions
}, _react.default.createElement(_uiElements.Text, {
color: "primary"
}, (0, _formatMessage.default)('Once you have entered your variables above, you should see them' + ' listed below. You can specify the range of possible values for' + ' each variable below.'))), _react.default.createElement("div", {
"data-section": "variable_definitions"
}, this.renderVariablesTable()), _react.default.createElement("div", {
"data-section": "formula"
}, _react.default.createElement(_FormulaSection.default, {
locale: this.getLocale(),
formulaErrors: this.errorsFor(['scoringData', 'value', 'formula']),
generatedSolutionsErrors: this.errorsFor(['scoringData', 'value', 'generatedSolutions', '$errors']),
handleAnswerCountChange: this.handleAnswerCountChange,
handleAnswerPrecisionChange: this.handleAnswerPrecisionChange,
handleFormulaChange: this.handleFormulaChange,
handleGenerateSolutions: this.handleGenerateSolutions,
handleMarginOfErrorTypeChange: this.handleMarginOfErrorTypeChange,
handleMarginOfErrorChange: this.handleMarginOfErrorChange,
handleScientificNotationChange: this.handleScientificNotationChange,
overrideEditableForRegrading: this.props.overrideEditableForRegrading,
scoringData: this.props.scoringData,
status: this.state.status
}))), _react.default.createElement(_QuestionSettingsContainer.default, {
additionalOptions: this.props.additionalOptions
}, !this.props.hiddenOptions.calculator && _react.default.createElement(_QuestionSettingsPanel.default, {
label: (0, _formatMessage.default)('Options'),
defaultExpanded: true
}, _react.default.createElement(_uiFormField.FormFieldGroup, {
rowSpacing: "small",
description: this.renderOptionsDescription()
}, _react.default.createElement(_CalculatorOptionWithOqaatAlert.default, {
disabled: this.props.overrideEditableForRegrading,
calculatorValue: this.props.calculatorType,
onCalculatorTypeChange: this.handleCalculatorTypeChange,
oqaatChecked: this.props.oneQuestionAtATime,
onOqaatChange: this.props.setOneQuestionAtATime
})))));
}
}]);
FormulaEdit.displayName = "FormulaEdit";
return FormulaEdit;
}(_react.Component), _class2.interactionType = _formula2.default, _class2.propTypes = _objectSpread({
additionalOptions: _propTypes.default.array,
calculatorType: _propTypes.default.string,
changeItemState: _propTypes.default.func,
enableRichContentEditor: _propTypes.default.bool,
errorsAreShowing: _propTypes.default.bool,
hiddenOptions: _propTypes.default.shape({
calculator: _propTypes.default.bool
}),
interactionData: _propTypes.default.object,
itemBody: _propTypes.default.string,
notifyScreenreader: _propTypes.default.func.isRequired,
onModalClose: _propTypes.default.func,
onModalOpen: _propTypes.default.func,
oneQuestionAtATime: _propTypes.default.bool,
openImportModal: _propTypes.default.func,
overrideEditableForRegrading: _propTypes.default.bool,
properties: _propTypes.default.object,
renderImportModal: _propTypes.default.func,
scoringData: _propTypes.default.object,
setOneQuestionAtATime: _propTypes.default.func
}, _withEditTools.default.injectedProps), _class2.defaultProps = {
additionalOptions: [],
calculatorType: 'none',
enableRichContentEditor: true,
hiddenOptions: {
calculator: true
},
oneQuestionAtATime: false,
overrideEditableForRegrading: false,
setOneQuestionAtATime: Function.prototype,
changeItemState: void 0,
errorsAreShowing: void 0,
interactionData: void 0,
itemBody: void 0,
onModalClose: void 0,
onModalOpen: void 0,
openImportModal: void 0,
properties: void 0,
renderImportModal: void 0,
scoringData: void 0
}, _class2.contextTypes = {
locale: _propTypes.default.string
}, _temp)) || _class) || _class) || _class);
exports.default = FormulaEdit;