UNPKG

quiz-interactions

Version:

A React UI component Library for quiz interaction types.

612 lines (524 loc) 21.5 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); Object.defineProperty(exports, "__esModule", { value: true }); exports.getNewValue = getNewValue; exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); 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 _getPrototypeOf3 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); var _react = _interopRequireWildcard(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _uuid = require("uuid"); var _uiFormField = require("@instructure/ui-form-field"); var _uiA11y = require("@instructure/ui-a11y"); var _uiLayout = require("@instructure/ui-layout"); var _withEditTools = _interopRequireDefault(require("../../../util/withEditTools")); var _Errors = _interopRequireDefault(require("../../common/edit/components/Errors")); var _FocusGroup = _interopRequireDefault(require("../../common/components/FocusGroup")); var _Footer = _interopRequireDefault(require("../../common/edit/components/Footer")); var _numeric = _interopRequireDefault(require("../../../records/interactions/numeric")); var _QuestionSettingsContainer = _interopRequireDefault(require("../../common/edit/components/QuestionSettingsContainer")); var _QuestionContainer = _interopRequireDefault(require("../../common/edit/components/QuestionContainer")); var _RemoveChoiceButton = _interopRequireDefault(require("../../common/edit/components/RemoveChoiceButton")); 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 _ExactResponse = _interopRequireDefault(require("./ExactResponse")); var _MarginOfError = _interopRequireDefault(require("./MarginOfError")); var _PreciseResponse = _interopRequireDefault(require("./PreciseResponse")); var _RequirementHelpModal = _interopRequireDefault(require("./RequirementHelpModal")); var _WithinARange = _interopRequireDefault(require("./WithinARange")); var _constants = require("./constants"); var _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; } function getNewValue(answer, type) { var newValue = { id: answer.id || '', type: type }; switch (type) { case _constants.EXACT_RESPONSE: Object.assign(newValue, { value: answer.value || '' }); break; case _constants.MARGIN_OF_ERROR: Object.assign(newValue, { value: answer.value || '', margin: answer.margin || '', marginType: answer.marginType || _constants.PERCENT }); break; case _constants.WITHIN_A_RANGE: Object.assign(newValue, { start: answer.start || '', end: answer.end || '' }); break; case _constants.PRECISE_RESPONSE: Object.assign(newValue, { value: answer.value || '', precision: answer.precision || '', precisionType: answer.precisionType || _constants.SIGNIFICANT_DIGITS }); break; } return newValue; } /** --- category: Numeric --- Numeric Edit component ```jsx_example class Example extends React.Component { render () { const exampleProps = { itemId: '1', itemBody: 'x is an integer and 9 < x^2 < 99. What\'s the max value of x, minus the minimum value of x', scoringData: { value: [{ type: 'exactResponse', id: '1', value: '1800.5' }, { type: 'withinARange', id: '2', start: '1800.5', end: '1801.5' }, { type: 'preciseResponse', id: '3', value: '1800.5', precision: '1', precisionType: 'decimals' }, { type: 'marginOfError', id: '4', value: '1800.5', margin: '0.4', marginType: 'absolute' }] } } return ( <NumericEdit {...exampleProps} {...this.props} /> ) } } <SettingsSwitcher locales={LOCALES}> <EditStateProvider> <Example /> </EditStateProvider> </SettingsSwitcher> ``` **/ var NumericEdit = (0, _withEditTools.default)(_class = (_temp = _class2 = /*#__PURE__*/ function (_Component) { (0, _inherits2.default)(NumericEdit, _Component); function NumericEdit() { var _getPrototypeOf2; var _this; (0, _classCallCheck2.default)(this, NumericEdit); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(NumericEdit)).call.apply(_getPrototypeOf2, [this].concat(args))); _this.choicesFocusGroup = null; _this.stemElement = null; _this._timeouts = []; _this.state = { helpModalOpen: false }; _this.handleTypeChange = function (event, _ref) { var id = _ref.id, type = _ref.type; var answers = (0, _toConsumableArray2.default)(_this.answers); var index = answers.findIndex(function (answer) { return answer.id === id; }); console.assert(index !== -1); // eslint-disable-line no-console answers[index] = getNewValue(answers[index], type); _this.props.changeItemState({ scoringData: _objectSpread({}, _this.props.scoringData, { value: answers }) }); }; _this.handleChange = function (event, answer) { var answers = (0, _toConsumableArray2.default)(_this.answers); var index = answers.findIndex(function (_ref2) { var id = _ref2.id; return id === answer.id; }); console.assert(index !== -1); // eslint-disable-line no-console var updatedAnswer = _objectSpread({}, answers[index], {}, answer); answers[index] = updatedAnswer; _this.props.changeItemState({ scoringData: _objectSpread({}, _this.props.scoringData, { value: answers }) }); }; _this.handleCalculatorTypeChange = function (e, value) { _this.props.changeItemState({ calculatorType: value }); }; _this.handleCreateAnswer = function () { _this._timeouts = [].concat((0, _toConsumableArray2.default)(_this._timeouts), [setTimeout(function () { return _this.choicesFocusGroup.focusLast('select'); }, 100)]); var id = _this.props.newId(); var newValue = getNewValue({ id: id }, _constants.EXACT_RESPONSE); _this.props.changeItemState({ scoringData: _objectSpread({}, _this.props.scoringData, { value: [].concat((0, _toConsumableArray2.default)(_this.answers), [newValue]) }) }); }; _this.handleRemoveChoice = function (id) { // In FF clicking on a button doesn't focus it if (document.activeElement !== document.body) { _this.updateFocusOnRemove(); } var answers = _this.answers.filter(function (answer) { return answer.id !== id; }); _this.props.changeItemState({ scoringData: _objectSpread({}, _this.props.scoringData, { value: answers }) }); }; _this.openHelpModal = function () { return _this.setState({ helpModalOpen: true }); }; _this.closeHelpModal = function () { return _this.setState({ helpModalOpen: false }); }; _this.handleChoicesFocusGroupRef = function (node) { _this.choicesFocusGroup = node; }; _this.handleStemRef = function (node) { _this.stemElement = node; }; _this.renderAnswerWrapper = function (answer) { var id = answer.id, type = answer.type; return _react.default.createElement(_uiLayout.Grid, { startAt: "medium", key: id, colSpacing: "none" }, _react.default.createElement(_uiLayout.Grid.Row, null, _react.default.createElement(_uiLayout.Grid.Col, { rowSpacing: "none" }, _react.default.createElement(_uiFormField.FormFieldGroup, { vAlign: "top", rowSpacing: "none", name: id, layout: "columns", description: _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('Possible answer')) }, _this.renderAnswerContents(answer))), _this.renderRemoveChoiceButton(answer, type))); }; return _this; } (0, _createClass2.default)(NumericEdit, [{ key: "componentWillUnmount", value: function componentWillUnmount() { this._timeouts.forEach(clearTimeout); } }, { key: "stringifyAnswer", value: function stringifyAnswer(_ref3) { var _EXACT_RESPONSE$MARGI; var type = _ref3.type, value = _ref3.value, start = _ref3.start, end = _ref3.end, margin = _ref3.margin, marginType = _ref3.marginType, precision = _ref3.precision, precisionType = _ref3.precisionType; var orBlank = function orBlank(val) { return val || 'blank'; }; return (_EXACT_RESPONSE$MARGI = {}, (0, _defineProperty2.default)(_EXACT_RESPONSE$MARGI, _constants.EXACT_RESPONSE, (0, _formatMessage.default)('value of {value}', { value: orBlank(value) })), (0, _defineProperty2.default)(_EXACT_RESPONSE$MARGI, _constants.MARGIN_OF_ERROR, (0, _formatMessage.default)('value of {value} with a margin of error of {margin} {marginType}', { value: orBlank(value), margin: orBlank(margin), marginType: marginType })), (0, _defineProperty2.default)(_EXACT_RESPONSE$MARGI, _constants.WITHIN_A_RANGE, (0, _formatMessage.default)('range from {start} to {end}', { start: orBlank(start), end: orBlank(end) })), (0, _defineProperty2.default)(_EXACT_RESPONSE$MARGI, _constants.PRECISE_RESPONSE, (0, _formatMessage.default)('value of {value} with a precision of {precision} {precisionType}', { value: orBlank(value), precision: orBlank(precision), precisionType: precisionType })), _EXACT_RESPONSE$MARGI)[type]; } }, { key: "updateFocusOnRemove", value: function updateFocusOnRemove() { var _this2 = this; var selector = 'button'; var textContent = 'Remove Answer'; if (!this.choicesFocusGroup.previousExists(selector, textContent)) { // if removing the first choice, focus on stem // added timeout to compensate for RCE sluggishness this._timeouts = [].concat((0, _toConsumableArray2.default)(this._timeouts), [setTimeout(function () { return _this2.stemElement.focus(); }, 100)]); } else if (this.answers.length === 2) { // if removing the second choice out of two choices this._timeouts = [].concat((0, _toConsumableArray2.default)(this._timeouts), [setTimeout(function () { return _this2.choicesFocusGroup.focusLast(); }, 100)]); } else { // all the other cases this.choicesFocusGroup.focusPrevious(selector, textContent); } } // =========== // HANDLERS // =========== }, { key: "renderAnswerField", // =========== // RENDERS // =========== value: function renderAnswerField(_ref4, index) { var id = _ref4.id, value = _ref4.value; return _react.default.createElement(_ExactResponse.default, { id: id, locale: this.locale, messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}), onChange: this.handleChange, onChangeType: this.handleTypeChange, onClickHelp: this.openHelpModal, value: value }); } }, { key: "renderPrecisionAnswer", value: function renderPrecisionAnswer(_ref5, index) { var id = _ref5.id, precision = _ref5.precision, precisionType = _ref5.precisionType, value = _ref5.value; return _react.default.createElement(_PreciseResponse.default, { id: id, locale: this.locale, messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}), onChange: this.handleChange, onChangeType: this.handleTypeChange, onClickHelp: this.openHelpModal, precision: precision, precisionType: precisionType, value: value }); } }, { key: "renderMarginAnswer", value: function renderMarginAnswer(_ref6, index) { var id = _ref6.id, margin = _ref6.margin, marginType = _ref6.marginType, value = _ref6.value; return _react.default.createElement(_MarginOfError.default, { id: id, locale: this.locale, margin: margin, marginType: marginType, messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}), onChange: this.handleChange, onChangeType: this.handleTypeChange, onClickHelp: this.openHelpModal, value: value }); } }, { key: "renderRangeAnswer", value: function renderRangeAnswer(_ref7, index) { var id = _ref7.id, start = _ref7.start, end = _ref7.end; return _react.default.createElement(_WithinARange.default, { end: end, id: id, locale: this.locale, messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}), onChange: this.handleChange, onChangeType: this.handleTypeChange, onClickHelp: this.openHelpModal, start: start }); } }, { key: "renderAnswerContents", value: function renderAnswerContents(answer) { var index = this.answers.findIndex(function (_ref8) { var id = _ref8.id; return id === answer.id; }); console.assert(index !== -1); // eslint-disable-line no-console switch (answer.type) { case _constants.WITHIN_A_RANGE: return this.renderRangeAnswer(answer, index); case _constants.MARGIN_OF_ERROR: return this.renderMarginAnswer(answer, index); case _constants.PRECISE_RESPONSE: return this.renderPrecisionAnswer(answer, index); default: return this.renderAnswerField(answer, index); } } }, { key: "renderRemoveChoiceButton", value: function renderRemoveChoiceButton(answer, name) { var _this3 = this; if (this.answers.length <= 1) { return null; } return _react.default.createElement(_uiLayout.Grid.Col, { width: 1 }, _react.default.createElement(_RemoveChoiceButton.default, { onClick: function onClick() { return _this3.handleRemoveChoice(answer.id); }, choiceId: name, screenReaderText: (0, _formatMessage.default)('Remove Answer: {answer}', { answer: this.stringifyAnswer(answer) }), padding: "2rem 0 0 0" })); } }, { key: "renderOptionsDescription", value: function renderOptionsDescription() { return _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('Numeric options')); } }, { key: "render", value: function render() { var name = "edit_interaction_".concat(this.props.itemId); return _react.default.createElement("div", null, _react.default.createElement(_QuestionContainer.default, { disabled: this.props.overrideEditableForRegrading, enableRichContentEditor: this.props.enableRichContentEditor, itemBody: this.props.itemBody, onDescriptionChange: this.props.onDescriptionChange, onModalClose: this.props.onModalClose, onModalOpen: this.props.onModalOpen, openImportModal: this.props.openImportModal, renderImportModal: this.props.renderImportModal, stemErrors: this.props.getErrors('itemBody'), textareaRef: this.handleStemRef }, _react.default.createElement(_FocusGroup.default, { ref: this.handleChoicesFocusGroupRef, asComponent: _Errors.default, asProps: { errorList: this.props.getErrors('scoringData.errors') } }, _react.default.createElement(_uiFormField.FormFieldGroup, { vAlign: "bottom", rowSpacing: "medium", name: name, description: _react.default.createElement(_uiA11y.ScreenReaderContent, null, (0, _formatMessage.default)('A list of possible answers')) }, this.answers.map(this.renderAnswerWrapper))), _react.default.createElement(_Footer.default, { buttonText: (0, _formatMessage.default)('Possible Answer'), onCreateChoice: this.handleCreateAnswer, screenReaderText: (0, _formatMessage.default)('Add Possible Answer') })), _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 })))), _react.default.createElement(_RequirementHelpModal.default, { onDismiss: this.closeHelpModal, open: this.state.helpModalOpen })); } }, { key: "locale", get: function get() { return this.props.locale || this.context.locale || 'en-US'; } }, { key: "answers", get: function get() { return this.props.scoringData.value; } }]); NumericEdit.displayName = "NumericEdit"; return NumericEdit; }(_react.Component), _class2.interactionType = _numeric.default, _class2.propTypes = _objectSpread({ additionalOptions: _QuestionSettingsContainer.default.propTypes.additionalOptions, calculatorType: _propTypes.default.string, changeItemState: _propTypes.default.func, enableRichContentEditor: _propTypes.default.bool, hiddenOptions: _propTypes.default.shape({ calculator: _propTypes.default.bool }), itemBody: _propTypes.default.string.isRequired, itemId: _propTypes.default.string, locale: _propTypes.default.string, newId: _propTypes.default.func, onModalClose: _propTypes.default.func, onModalOpen: _propTypes.default.func, oneQuestionAtATime: _propTypes.default.bool, openImportModal: _propTypes.default.func, overrideEditableForRegrading: _propTypes.default.bool, renderImportModal: _propTypes.default.func, scoringData: _propTypes.default.shape({ value: _propTypes.default.arrayOf(_propTypes.default.shape({ id: _propTypes.default.string.isRequired, type: _propTypes.default.string.isRequired })).isRequired }).isRequired, setOneQuestionAtATime: _propTypes.default.func }, _withEditTools.default.injectedProps), _class2.contextTypes = { locale: _propTypes.default.string }, _class2.defaultProps = { calculatorType: 'none', enableRichContentEditor: true, hiddenOptions: { calculator: true }, oneQuestionAtATime: false, overrideEditableForRegrading: false, newId: _uuid.v4, setOneQuestionAtATime: Function.prototype, additionalOptions: void 0, changeItemState: void 0, itemId: void 0, locale: void 0, onModalClose: void 0, onModalOpen: void 0, openImportModal: void 0, renderImportModal: void 0 }, _temp)) || _class; exports.default = NumericEdit;