@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
550 lines (543 loc) • 21.3 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
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 _class, _NumericEdit;
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 { v4 as uuid } from 'uuid';
import { ScreenReaderContent } from '@instructure/ui-a11y-content';
import { View } from '@instructure/ui-view';
import { Grid } from '@instructure/ui-grid';
import withEditTools from '../../../util/withEditTools';
import Errors from '../../common/edit/components/Errors';
import FocusGroup from '../../common/components/FocusGroup';
import Footer from '../../common/edit/components/Footer';
import NumericInteractionType from '../../../records/interactions/numeric';
import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer';
import QuestionContainer from '../../common/edit/components/QuestionContainer';
import RemoveChoiceButton from '../../common/edit/components/RemoveChoiceButton';
import t from '@instructure/quiz-i18n/es/format-message';
import { Decimal } from '@instructure/quiz-i18n';
import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel';
import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert';
import ExactResponse from './ExactResponse';
import MarginOfError from './MarginOfError';
import PreciseResponse from './PreciseResponse';
import RequirementHelpModal from './RequirementHelpModal';
import WithinARange from './WithinARange';
import { EXACT_RESPONSE, MARGIN_OF_ERROR, PERCENT, PRECISE_RESPONSE, SIGNIFICANT_DIGITS, WITHIN_A_RANGE } from './constants';
import NumericTypeSelect from './NumericTypeSelect';
import { FormFieldGroup, parseSeparators } from '@instructure/quiz-common';
import { ApplyLocaleContext } from '@instructure/ui-i18n';
export function getNewValue(answer, type) {
var newValue = {
id: answer.id || '',
type: type
};
switch (type) {
case EXACT_RESPONSE:
Object.assign(newValue, {
value: answer.value || ''
});
break;
case MARGIN_OF_ERROR:
Object.assign(newValue, {
value: answer.value || '',
margin: answer.margin || '',
marginType: answer.marginType || PERCENT
});
break;
case WITHIN_A_RANGE:
Object.assign(newValue, {
start: answer.start || '',
end: answer.end || ''
});
break;
case PRECISE_RESPONSE:
Object.assign(newValue, {
value: answer.value || '',
precision: answer.precision || '',
precisionType: answer.precisionType || 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 = withEditTools(_class = (_NumericEdit = /*#__PURE__*/function (_Component) {
function NumericEdit() {
var _this2;
_classCallCheck(this, NumericEdit);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this2 = _callSuper(this, NumericEdit, [].concat(args));
_defineProperty(_this2, "typeSelectInputRefs", {});
_defineProperty(_this2, "choicesFocusGroup", null);
_defineProperty(_this2, "stemElement", null);
_defineProperty(_this2, "_timeouts", []);
_defineProperty(_this2, "state", {
helpModalOpen: false
});
// ===========
// HANDLERS
// ===========
_defineProperty(_this2, "handleTypeChange", function (event, _ref) {
var id = _ref.id,
type = _ref.type;
var answers = _toConsumableArray(_this2.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);
_this2.props.changeItemState({
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: answers
})
});
setTimeout(function () {
_this2.typeSelectInputRefs[id].focus();
}, 0);
});
_defineProperty(_this2, "handleChange", function (event, answer) {
var answers = _toConsumableArray(_this2.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(_objectSpread({}, answers[index]), answer);
answers[index] = updatedAnswer;
_this2.props.changeItemState({
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: answers
})
});
});
_defineProperty(_this2, "handleCalculatorTypeChange", function (e, value) {
_this2.props.changeItemState({
calculatorType: value
});
});
_defineProperty(_this2, "handleCreateAnswer", function () {
_this2._timeouts = [].concat(_toConsumableArray(_this2._timeouts), [setTimeout(function () {
return _this2.choicesFocusGroup.focusLast('select');
}, 100)]);
var id = _this2.props.newId();
var newValue = getNewValue({
id: id
}, EXACT_RESPONSE);
_this2.props.changeItemState({
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: [].concat(_toConsumableArray(_this2.answers), [newValue])
})
});
});
_defineProperty(_this2, "handleRemoveChoice", function (id) {
// In FF clicking on a button doesn't focus it
if (document.activeElement !== document.body) {
_this2.updateFocusOnRemove();
}
var answers = _this2.answers.filter(function (answer) {
return answer.id !== id;
});
_this2.props.changeItemState({
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: answers
})
});
});
_defineProperty(_this2, "openHelpModal", function () {
return _this2.setState({
helpModalOpen: true
});
});
_defineProperty(_this2, "closeHelpModal", function () {
return _this2.setState({
helpModalOpen: false
});
});
_defineProperty(_this2, "handleChoicesFocusGroupRef", function (node) {
_this2.choicesFocusGroup = node;
});
_defineProperty(_this2, "handleStemRef", function (node) {
_this2.stemElement = node;
});
_defineProperty(_this2, "setTypeSelectInputRef", function (id, node) {
_this2.typeSelectInputRefs[id] = node;
});
_defineProperty(_this2, "renderAnswerWrapper", function (answer) {
var id = answer.id,
type = answer.type;
return /*#__PURE__*/React.createElement(Grid, {
startAt: "medium",
key: id,
colSpacing: "small"
}, /*#__PURE__*/React.createElement(Grid.Row, null, /*#__PURE__*/React.createElement(Grid.Col, {
rowSpacing: "none"
}, /*#__PURE__*/React.createElement(FormFieldGroup, {
vAlign: "top",
rowSpacing: "none",
name: id,
layout: "columns",
description: /*#__PURE__*/React.createElement(ScreenReaderContent, null, t('Possible answer'))
}, _this2.renderAnswerContents(answer))), _this2.answers.length > 1 && /*#__PURE__*/React.createElement(Grid.Col, {
width: "auto"
}, /*#__PURE__*/React.createElement(View, {
as: "div",
margin: "large 0 0",
themeOverride: {
marginLarge: '1.875rem'
}
}, /*#__PURE__*/React.createElement(RemoveChoiceButton, {
onClick: function onClick() {
return _this2.handleRemoveChoice(answer.id);
},
choiceId: type,
screenReaderText: t('Remove Answer: {answer}', {
answer: _this2.stringifyAnswer(answer)
})
})))));
});
return _this2;
}
_inherits(NumericEdit, _Component);
return _createClass(NumericEdit, [{
key: "componentWillUnmount",
value: function componentWillUnmount() {
this._timeouts.forEach(clearTimeout);
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
Decimal.accountSettingDelimiters = this.props.separatorConfig ? parseSeparators(this.props.separatorConfig) : null;
}
}, {
key: "locale",
get: function get() {
return this.props.locale || this.context.locale || 'en-US';
}
}, {
key: "answers",
get: function get() {
return this.props.scoringData.value;
}
}, {
key: "stringifyAnswer",
value: function stringifyAnswer(_ref3) {
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 _defineProperty(_defineProperty(_defineProperty(_defineProperty({}, EXACT_RESPONSE, t('value of {value}', {
value: orBlank(value)
})), MARGIN_OF_ERROR, t('value of {value} with a margin of error of {margin} {marginType}', {
value: orBlank(value),
margin: orBlank(margin),
marginType: marginType
})), WITHIN_A_RANGE, t('range from {start} to {end}', {
start: orBlank(start),
end: orBlank(end)
})), PRECISE_RESPONSE, t('value of {value} with a precision of {precision} {precisionType}', {
value: orBlank(value),
precision: orBlank(precision),
precisionType: precisionType
}))[type];
}
}, {
key: "updateFocusOnRemove",
value: function updateFocusOnRemove() {
var _this3 = 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(_toConsumableArray(this._timeouts), [setTimeout(function () {
return _this3.stemElement.focus();
}, 100)]);
} else if (this.answers.length === 2) {
// if removing the second choice out of two choices
this._timeouts = [].concat(_toConsumableArray(this._timeouts), [setTimeout(function () {
return _this3.choicesFocusGroup.focusLast();
}, 100)]);
} else {
// all the other cases
this.choicesFocusGroup.focusPrevious(selector, textContent);
}
}
}, {
key: "renderAnswerField",
value:
// ===========
// RENDERS
// ===========
function renderAnswerField(_ref4, index) {
var id = _ref4.id,
value = _ref4.value;
return /*#__PURE__*/React.createElement(ExactResponse, {
id: id,
locale: this.locale,
messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}),
numericTypeSelect: this.renderNumericTypeSelect(id, EXACT_RESPONSE),
onChange: this.handleChange,
value: value
});
}
}, {
key: "renderPrecisionAnswer",
value: function renderPrecisionAnswer(_ref5, index) {
var id = _ref5.id,
precision = _ref5.precision,
precisionType = _ref5.precisionType,
value = _ref5.value;
return /*#__PURE__*/React.createElement(PreciseResponse, {
id: id,
locale: this.locale,
messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}),
numericTypeSelect: this.renderNumericTypeSelect(id, PRECISE_RESPONSE),
onChange: this.handleChange,
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 /*#__PURE__*/React.createElement(MarginOfError, {
id: id,
locale: this.locale,
margin: margin,
marginType: marginType,
messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}),
numericTypeSelect: this.renderNumericTypeSelect(id, MARGIN_OF_ERROR),
onChange: this.handleChange,
value: value
});
}
}, {
key: "renderRangeAnswer",
value: function renderRangeAnswer(_ref7, index) {
var id = _ref7.id,
start = _ref7.start,
end = _ref7.end;
return /*#__PURE__*/React.createElement(WithinARange, {
end: end,
id: id,
locale: this.locale,
messages: this.props.getErrors("scoringData.value[".concat(index, "]"), {}),
numericTypeSelect: this.renderNumericTypeSelect(id, WITHIN_A_RANGE),
onChange: this.handleChange,
start: start
});
}
}, {
key: "renderNumericTypeSelect",
value: function renderNumericTypeSelect(id, value) {
return /*#__PURE__*/React.createElement(NumericTypeSelect, {
id: id,
inputRef: this.setTypeSelectInputRef,
onChange: this.handleTypeChange,
onClickHelp: this.openHelpModal,
value: value
});
}
}, {
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 WITHIN_A_RANGE:
return this.renderRangeAnswer(answer, index);
case MARGIN_OF_ERROR:
return this.renderMarginAnswer(answer, index);
case PRECISE_RESPONSE:
return this.renderPrecisionAnswer(answer, index);
default:
return this.renderAnswerField(answer, index);
}
}
}, {
key: "renderOptionsDescription",
value: function renderOptionsDescription() {
return /*#__PURE__*/React.createElement(ScreenReaderContent, null, t('Numeric options'));
}
}, {
key: "render",
value: function render() {
var name = "edit_interaction_".concat(this.props.itemId);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(QuestionContainer, {
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,
stemErrors: this.props.getErrors('itemBody'),
textareaRef: this.handleStemRef
}, /*#__PURE__*/React.createElement(FocusGroup, {
ref: this.handleChoicesFocusGroupRef,
asComponent: Errors,
asProps: {
errorList: this.props.getErrors('scoringData.errors')
}
}, /*#__PURE__*/React.createElement(FormFieldGroup, {
vAlign: "bottom",
rowSpacing: "medium",
name: name,
description: /*#__PURE__*/React.createElement(ScreenReaderContent, null, t('A list of possible answers'))
}, this.answers.map(this.renderAnswerWrapper))), /*#__PURE__*/React.createElement(Footer, {
buttonText: t('Possible Answer'),
onCreateChoice: this.handleCreateAnswer,
screenReaderText: t('Add Possible Answer'),
notifyScreenreader: this.props.notifyScreenreader,
automationData: "sdk-add-possible-answer-button"
})), /*#__PURE__*/React.createElement(QuestionSettingsContainer, {
additionalOptions: this.props.additionalOptions
}, this.props.showCalculatorOption && /*#__PURE__*/React.createElement(QuestionSettingsPanel, {
label: t('Options'),
defaultExpanded: true
}, /*#__PURE__*/React.createElement(FormFieldGroup, {
rowSpacing: "small",
description: this.renderOptionsDescription()
}, /*#__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(RequirementHelpModal, {
onDismiss: this.closeHelpModal,
open: this.state.helpModalOpen
}));
}
}]);
}(Component), _defineProperty(_NumericEdit, "interactionType", NumericInteractionType), _defineProperty(_NumericEdit, "propTypes", _objectSpread(_objectSpread({
additionalOptions: QuestionSettingsContainer.propTypes.additionalOptions,
calculatorType: PropTypes.string,
changeItemState: PropTypes.func,
enableRichContentEditor: PropTypes.bool,
itemBody: PropTypes.string.isRequired,
itemId: PropTypes.string,
locale: PropTypes.string,
newId: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
oneQuestionAtATime: PropTypes.bool,
openImportModal: PropTypes.func,
overrideEditableForRegrading: PropTypes.bool,
scoringData: PropTypes.shape({
value: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired
})).isRequired
}).isRequired,
setOneQuestionAtATime: PropTypes.func
}, withEditTools.injectedProps), {}, {
showCalculatorOption: PropTypes.bool,
separatorConfig: PropTypes.shape({
decimalSeparator: PropTypes.string,
thousandSeparator: PropTypes.string
})
})), _defineProperty(_NumericEdit, "contextType", ApplyLocaleContext), _defineProperty(_NumericEdit, "defaultProps", {
calculatorType: 'none',
enableRichContentEditor: true,
oneQuestionAtATime: false,
overrideEditableForRegrading: false,
newId: uuid,
setOneQuestionAtATime: Function.prototype,
additionalOptions: void 0,
changeItemState: void 0,
itemId: void 0,
locale: void 0,
onModalClose: void 0,
onModalOpen: void 0,
openImportModal: void 0,
showCalculatorOption: true
}), _NumericEdit)) || _class;
export { NumericEdit as default };