@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
617 lines (607 loc) • 24.6 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";
var _dec, _dec2, _class, _FormulaEdit;
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));
}
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; }
/** @jsx jsx */
import { Component } from 'react';
import PropTypes from 'prop-types';
import update from 'immutability-helper';
import sortBy from 'lodash/sortBy';
import get from 'lodash/get';
import omit from 'lodash/omit';
import NumberInput from '@instructure/quiz-number-input';
import { PresentationContent, ScreenReaderContent } from '@instructure/ui-a11y-content';
import { Text } from '@instructure/ui-text';
import { Table } from '@instructure/ui-table';
import { jsx } from '@instructure/emotion';
import { isScientificNotation } from '@instructure/quiz-scientific-notation';
import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer';
import QuestionContainer from '../../common/edit/components/QuestionContainer';
import * as util from './util';
import FormulaSection from './FormulaSection';
import GenerateSolutionsService from './GenerateSolutionsService';
import VariableInput from './VariableInput';
import { mathjsIsLoaded, loadMathjs } from '../common/util';
import { toErrors } from '../../../util/instUIMessages';
import { variablesFromItemBody, parseFormulaDecimalSeparator } from '../../../util/formula';
import FormulaInteractionType from '../../../records/interactions/formula';
import withEditTools from '../../../util/withEditTools';
import withAsyncDeps from '../../../util/withAsyncDeps';
import generateStyle from './styles';
import generateComponentTheme from './theme';
import t from '@instructure/quiz-i18n/es/format-message';
import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel';
import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert';
import { withStyleOverrides, FormFieldGroup } from '@instructure/quiz-common';
/**
---
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 isScientificNotation(n) ? n : Number(n);
};
var normalizeVariables = function normalizeVariables(variables) {
return variables.map(function (group) {
return _objectSpread(_objectSpread({}, group), {}, {
min: formatNumber(group.min),
max: formatNumber(group.max),
precision: Number(group.precision)
});
});
};
var FormulaEdit = (_dec = withAsyncDeps(mathjsIsLoaded, loadMathjs), _dec2 = withStyleOverrides(generateStyle, generateComponentTheme), _dec(_class = withEditTools(_class = _dec2(_class = (_FormulaEdit = /*#__PURE__*/function (_Component) {
function FormulaEdit(props) {
var _this2;
_classCallCheck(this, FormulaEdit);
_this2 = _callSuper(this, FormulaEdit, [props]);
// ==============================
// HOOKS FOR GENERATING SOLUTIONS
// ==============================
_defineProperty(_this2, "serviceOnStart", function () {
_this2.setState({
status: util.STATUS_RUNNING
});
});
_defineProperty(_this2, "serviceOnComplete", function (status) {
return function (solutions) {
_this2.setState({
status: status
});
var scoringData = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: solutions
}
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
var message = util.buildSolutionsGeneratedMessage(status, solutions.length);
_this2.props.notifyScreenreader("".concat(t('Solutions updated.'), " ").concat(message));
};
});
_defineProperty(_this2, "serviceOnCancel", function () {
_this2.setState({
status: util.STATUS_CANCELED
});
});
// ====================
// INPUT EVENT HANDLERS
// ====================
_defineProperty(_this2, "handleCalculatorTypeChange", function (e, value) {
_this2.props.changeItemState({
calculatorType: value
});
});
_defineProperty(_this2, "handleItemBodyChange", function (itemBody) {
var newVariableNames = variablesFromItemBody(itemBody);
var oldVariables = _this2.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 = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
variables: {
$set: sortBy(newVariables, function (v) {
return v.name;
})
}
}
});
_this2.props.changeItemState({
itemBody: itemBody,
scoringData: scoringData
});
_this2.generateSolutionsService.cancel();
});
_defineProperty(_this2, "handleVariableChange", function (variableIdx, field) {
return function (value) {
var scoringData = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
variables: _defineProperty({}, variableIdx, _defineProperty({}, field, {
$set: value
}))
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
_this2.generateSolutionsService.cancel();
};
});
_defineProperty(_this2, "handlePrecisionChange", function (variableIdx) {
return function (e, value, normalized) {
if (normalized === null) return;
var variable = _this2.props.scoringData.value.variables[variableIdx];
if (normalized == variable.precision) return; // intentional double-equals
var scoringData = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
variables: _defineProperty({}, variableIdx, {
$set: {
name: variable.name,
precision: normalized,
min: util.toPrecision(variable.min, normalized),
max: util.toPrecision(variable.max, normalized)
}
})
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
_this2.generateSolutionsService.cancel();
};
});
_defineProperty(_this2, "handleFormulaChange", function (e) {
var scoringData = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
formula: {
$set: e.target.value
}
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
_this2.generateSolutionsService.cancel();
});
_defineProperty(_this2, "handleMarginOfErrorTypeChange", function (e, _ref) {
var value = _ref.value;
var scoringData = update(_this2.props.scoringData, {
value: {
numeric: {
marginType: {
$set: value
}
}
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
});
_defineProperty(_this2, "handleMarginOfErrorChange", function (e, value, normalizedValue) {
if (normalizedValue == _this2.props.scoringData.value.numeric.margin) return; // intentional double-equals
var scoringData = update(_this2.props.scoringData, {
value: {
numeric: {
margin: {
$set: Number(normalizedValue).toString()
}
}
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
});
_defineProperty(_this2, "handleScientificNotationChange", function (e) {
var scientificNotation = !_this2.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(_objectSpread({}, _this2.props.scoringData), {}, {
value: _objectSpread(_objectSpread({}, _this2.props.scoringData.value), {}, {
generatedSolutions: [],
numeric: numeric,
scientificNotation: scientificNotation
})
});
_this2.props.changeItemState({
scoringData: scoringData
});
});
_defineProperty(_this2, "handleAnswerCountChange", function (e, answerCount) {
var scoringData = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
answerCount: {
$set: answerCount
}
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
_this2.generateSolutionsService.cancel();
});
_defineProperty(_this2, "handleAnswerPrecisionChange", function (_e, _answerPrecision, answerPrecisionNormalized) {
var answerPrecision = Number(answerPrecisionNormalized);
if (answerPrecision === Number(_this2.props.scoringData.value.answerPrecision || 0)) return;
var scoringData = update(_this2.props.scoringData, {
value: {
generatedSolutions: {
$set: []
},
answerPrecision: {
$set: answerPrecision
}
}
});
_this2.props.changeItemState({
scoringData: scoringData
});
_this2.generateSolutionsService.cancel();
});
_defineProperty(_this2, "handleGenerateSolutions", function () {
var _this2$props$scoringD = _this2.props.scoringData.value,
answerCount = _this2$props$scoringD.answerCount,
answerPrecision = _this2$props$scoringD.answerPrecision,
variables = _this2$props$scoringD.variables,
formula = _this2$props$scoringD.formula,
scientificNotation = _this2$props$scoringD.scientificNotation;
var parsedAnswerCount = parseInt(answerCount, 10) || 0;
var scoringDataSetupErrors = _this2.scoringDataSetupErrors();
if (Object.keys(scoringDataSetupErrors).length > 0) {
_this2.notifyScreenreaderOfSetupErrors(scoringDataSetupErrors);
_this2.setState({
status: util.STATUS_FORMULA_SETUP_INVALID
});
return;
} else if (_this2.state.status === util.STATUS_FORMULA_SETUP_INVALID) {
_this2.setState({
status: util.STATUS_STOPPED
});
}
_this2.generateSolutionsService.start(parsedAnswerCount, normalizeVariables(variables), parseFormulaDecimalSeparator(_this2.getLocale(), formula), answerPrecision, scientificNotation);
});
_defineProperty(_this2, "getLocale", function () {
return _this2.context.locale || 'en-US';
});
// ===================
// RENDERING FUNCTIONS
// ===================
_defineProperty(_this2, "renderVariable", function (variableRecord, idx) {
var variableName = variableRecord.name;
var precision = Number(variableRecord.precision);
var errorPath = ['scoringData', 'value', 'variables', idx];
return jsx(Table.Row, {
key: variableName
}, jsx(Table.RowHeader, null, jsx(PresentationContent, null, variableName), jsx(ScreenReaderContent, {
tabIndex: 0
}, t('Variable {variable}', {
variable: variableName
}))), jsx(Table.Cell, null, jsx(VariableInput, {
disabled: _this2.props.overrideEditableForRegrading,
decimalPrecision: precision,
messages: toErrors(_this2.errorsFor([].concat(errorPath, ['min']))),
onUpdate: _this2.handleVariableChange(idx, 'min'),
value: variableRecord.min,
width: "6rem",
renderLabel: jsx(ScreenReaderContent, null, t('Minimum value for variable {variable}', {
variable: variableName
}))
})), jsx(Table.Cell, null, jsx(VariableInput, {
disabled: _this2.props.overrideEditableForRegrading,
decimalPrecision: precision,
messages: toErrors(_this2.errorsFor([].concat(errorPath, ['max']))),
onUpdate: _this2.handleVariableChange(idx, 'max'),
value: variableRecord.max,
width: "6rem",
renderLabel: jsx(ScreenReaderContent, null, t('Maximum value for variable {variable}', {
variable: variableName
}))
})), jsx(Table.Cell, null, jsx(NumberInput, {
disabled: _this2.props.overrideEditableForRegrading,
max: 10,
messages: toErrors(_this2.errorsFor([].concat(errorPath, ['precision']))),
min: 0,
onChange: _this2.handlePrecisionChange(idx),
showArrows: true,
step: 1,
value: variableRecord.precision,
width: "6rem",
renderLabel: jsx(ScreenReaderContent, null, t('decimals of precision for variable {variable}', {
variable: variableName
}))
})));
});
_this2.generateSolutionsService = new GenerateSolutionsService({
onStart: _this2.serviceOnStart,
onSuccess: _this2.serviceOnComplete(util.STATUS_STOPPED),
onFailure: _this2.serviceOnComplete(util.STATUS_FAILED),
onCancel: _this2.serviceOnCancel
});
_this2.state = {
status: util.STATUS_STOPPED
};
return _this2;
}
_inherits(FormulaEdit, _Component);
return _createClass(FormulaEdit, [{
key: "scoringDataSetupErrors",
value:
// =================
// UTILITY FUNCTIONS
// =================
function scoringDataSetupErrors() {
var scoringDataErrors = get(this.errors(), ['scoringData', 'value'], {});
return omit(scoringDataErrors, ['generatedSolutions']);
}
}, {
key: "notifyScreenreaderOfSetupErrors",
value: function notifyScreenreaderOfSetupErrors(sdSetupErrors) {
var _this3 = this;
var vars = Object.keys(sdSetupErrors.variables || {}).map(function (v) {
return _this3.props.scoringData.value.variables[v].name.replace(/`/g, '');
});
var errorMsg;
if (vars.length > 0) {
errorMsg = t('variables containing errors: {vars}', {
vars: vars.join(', ')
});
}
if (this.props.scoringData.value.variables.length === 0) {
errorMsg = t('must define at least one variable');
}
if (errorMsg) {
this.props.notifyScreenreader(t('The following error prevented generating solutions: {errorMsg}', {
errorMsg: errorMsg
}));
}
}
}, {
key: "renderVariablesTable",
value: function renderVariablesTable() {
var variables = normalizeVariables(this.props.scoringData.value.variables);
return jsx(Table, {
caption: ""
}, jsx(Table.Body, null, jsx(Table.Row, null, jsx(Table.ColHeader, {
id: "formula-edit-variable"
}, t('Variable')), jsx(Table.ColHeader, {
id: "formula-edit-min"
}, jsx(ScreenReaderContent, null, t('Minimum Value')), jsx(PresentationContent, null, jsx("div", {
title: t('Minimum Value')
}, t('Min')))), jsx(Table.ColHeader, {
id: "formula-edit-max"
}, jsx(ScreenReaderContent, null, t('Maximum Value')), jsx(PresentationContent, null, jsx("div", {
title: t('Maximum Value')
}, t('Max')))), jsx(Table.ColHeader, {
id: "formula-edit-decimals"
}, t('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 get(this.errors(), path, []);
}
}, {
key: "errors",
value: function errors() {
return new FormulaInteractionType(_objectSpread(_objectSpread({}, omit(this.props, ['getErrors', 'scoringData'])), {}, {
scoringData: this.props.scoringData
})).getErrors();
}
}, {
key: "renderOptionsDescription",
value: function renderOptionsDescription() {
return jsx(ScreenReaderContent, null, t('Formula options'));
}
}, {
key: "render",
value: function render() {
return jsx("div", null, jsx("div", null, jsx(Text, {
color: "primary"
}, t('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.'))), jsx("div", {
css: this.props.styles.sectionHeading
}, jsx(Text, {
size: "large"
}, t('Question'))), jsx("div", {
css: this.props.styles.instructions
}, jsx(Text, {
color: "primary"
}, t('You can define variables by typing variable names surrounded by backticks (e.g., "what is 5 plus `x`?")'))), jsx(QuestionContainer, {
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,
stemErrors: this.errorsFor(['itemBody']),
textareaRef: this.handleStemRef
}, jsx("div", {
css: this.props.styles.sectionHeading
}, jsx(Text, {
size: "large"
}, t('Answers'))), jsx("div", {
css: this.props.styles.instructions
}, jsx(Text, {
color: "primary"
}, t('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.'))), jsx("div", {
"data-section": "variable_definitions"
}, this.renderVariablesTable()), jsx("div", {
"data-section": "formula"
}, jsx(FormulaSection, {
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
}))), jsx(QuestionSettingsContainer, {
additionalOptions: this.props.additionalOptions
}, this.props.showCalculatorOption && jsx(QuestionSettingsPanel, {
label: t('Options'),
defaultExpanded: true
}, jsx(FormFieldGroup, {
rowSpacing: "small",
description: this.renderOptionsDescription()
}, jsx(CalculatorOptionWithOqaatAlert, {
disabled: this.props.overrideEditableForRegrading,
calculatorValue: this.props.calculatorType,
onCalculatorTypeChange: this.handleCalculatorTypeChange,
oqaatChecked: this.props.oneQuestionAtATime,
onOqaatChange: this.props.setOneQuestionAtATime
})))));
}
}]);
}(Component), _defineProperty(_FormulaEdit, "displayName", 'FormulaEdit'), _defineProperty(_FormulaEdit, "componentId", "Quizzes".concat(_FormulaEdit.displayName)), _defineProperty(_FormulaEdit, "interactionType", FormulaInteractionType), _defineProperty(_FormulaEdit, "propTypes", _objectSpread(_objectSpread({
additionalOptions: PropTypes.array,
calculatorType: PropTypes.string,
changeItemState: PropTypes.func,
enableRichContentEditor: PropTypes.bool,
errorsAreShowing: PropTypes.bool,
interactionData: PropTypes.object,
itemBody: PropTypes.string,
notifyScreenreader: PropTypes.func.isRequired,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
oneQuestionAtATime: PropTypes.bool,
openImportModal: PropTypes.func,
overrideEditableForRegrading: PropTypes.bool,
properties: PropTypes.object,
scoringData: PropTypes.object,
setOneQuestionAtATime: PropTypes.func
}, withEditTools.injectedProps), {}, {
styles: PropTypes.object,
showCalculatorOption: PropTypes.bool,
separatorConfig: PropTypes.shape({
decimalSeparator: PropTypes.string,
thousandSeparator: PropTypes.string
})
})), _defineProperty(_FormulaEdit, "defaultProps", {
additionalOptions: [],
calculatorType: 'none',
enableRichContentEditor: 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,
scoringData: void 0,
showCalculatorOption: true
}), _defineProperty(_FormulaEdit, "contextTypes", {
locale: PropTypes.string
}), _FormulaEdit)) || _class) || _class) || _class);
export { FormulaEdit as default };