UNPKG

quiz-interactions

Version:

A React UI component Library for quiz interaction types.

694 lines (592 loc) 26.9 kB
"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;