@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
476 lines (468 loc) • 19.5 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, _OrderingEdit;
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));
}
/** @jsx jsx */
import { Component } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuid } from 'uuid';
import striptags from 'striptags';
import assignIn from 'lodash/fp/assignIn';
import set from 'lodash/fp/set';
import last from 'lodash/fp/last';
import omit from 'lodash/omit';
import without from 'lodash/without';
import { jsx } from '@instructure/emotion';
import { ScreenReaderContent, PresentationContent } from '@instructure/ui-a11y-content';
import { Grid } from '@instructure/ui-grid';
import { Checkbox } from '@instructure/ui-checkbox';
import { Text } from '@instructure/ui-text';
import { View } from '@instructure/ui-view';
import AnswerInput from '../../common/edit/components/AnswerInput';
import Footer from '../../common/edit/components/Footer';
import OrderingInteractionType from '../../../records/interactions/ordering';
import QuestionContainer from '../../common/edit/components/QuestionContainer';
import RemoveChoiceButton from '../../common/edit/components/RemoveChoiceButton';
import withEditTools from '../../../util/withEditTools';
import Card from '../../common/components/Card';
import ReorderChoiceButton from '../common/ReorderChoiceButton';
import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer';
import t from '@instructure/quiz-i18n/es/format-message';
import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel';
import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert';
import { TextInput, Flex, FormFieldGroup } from '@instructure/quiz-common';
/**
---
category: Ordering
---
Ordering Edit component
```jsx_example
const WrappedExample = DragDropContext(HTML5Backend)(OrderingEdit)
function Example (props) {
const exampleProps = {
itemBody: 'Order these characters from tallest to shortest:',
interactionData: {
choices: {
uuid6: { id: 'uuid6', itemBody: 'Gandalf' },
uuid5: { id: 'uuid5', itemBody: 'Legolas' },
uuid4: { id: 'uuid4', itemBody: 'Aragorn' },
uuid3: { id: 'uuid3', itemBody: 'Gimli' },
uuid2: { id: 'uuid2', itemBody: 'Frodo' },
uuid1: { id: 'uuid1', itemBody: 'Gollum' },
}
},
properties: {
displayAnswersParagraph: true,
includeLabels: true,
topLabel: 'Taller',
bottomLabel: 'Shorter'
},
scoringData: {
value: ['uuid6','uuid5','uuid4','uuid3','uuid2','uuid1']
}
}
return (
<WrappedExample {...exampleProps} {...props} />
)
}
<SettingsSwitcher locales={LOCALES}>
<EditStateProvider>
<Example />
</EditStateProvider>
</SettingsSwitcher>
```
**/
var OrderingEdit = withEditTools(_class = (_OrderingEdit = /*#__PURE__*/function (_Component) {
function OrderingEdit() {
var _this2;
_classCallCheck(this, OrderingEdit);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this2 = _callSuper(this, OrderingEdit, [].concat(args));
_defineProperty(_this2, "stemElement", null);
_defineProperty(_this2, "topLabelRef", null);
_defineProperty(_this2, "bottomLabelRef", null);
_defineProperty(_this2, "inputRefs", []);
_defineProperty(_this2, "reorderRefs", []);
_defineProperty(_this2, "removeChoiceRefs", []);
_defineProperty(_this2, "_timeouts", []);
_defineProperty(_this2, "_choiceWasCreated", false);
// ===========
// HANDLERS
// ===========
_defineProperty(_this2, "handleStemRef", function (node) {
_this2.stemElement = node;
});
_defineProperty(_this2, "handleTopLabelRef", function (node) {
_this2.topLabelRef = node;
});
_defineProperty(_this2, "handleBottomLabelRef", function (node) {
_this2.bottomLabelRef = node;
});
_defineProperty(_this2, "handleCalculatorTypeChange", function (e, value) {
_this2.props.changeItemState({
calculatorType: value
});
});
_defineProperty(_this2, "handleMoveChoice", function (currI, targetI) {
// Create a new array to preserve immutability
var choices = _this2.props.scoringData.value.slice(0)
// Use destructuring to swap the two values
;
var _ref = [choices[currI], choices[targetI]];
choices[targetI] = _ref[0];
choices[currI] = _ref[1];
_this2.props.changeItemState({
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: choices
})
});
_this2._timeouts = [].concat(_toConsumableArray(_this2._timeouts), [setTimeout(function () {
return _this2.reorderRefs[targetI].focus();
})]);
});
_defineProperty(_this2, "handleRemoveChoice", function (choiceId, index) {
var choiceOrder = _this2.props.scoringData.value;
if (index === 0) {
if (_this2.props.properties.includeLabels) {
_this2.topLabelRef.focus();
} else {
// added timeout to compensate for RCE sluggishness
_this2._timeouts = [].concat(_toConsumableArray(_this2._timeouts), [setTimeout(function () {
return _this2.stemElement.focus();
})]);
}
} else {
if (choiceOrder.length > 3) {
_this2.removeChoiceRefs[index - 1].focus();
} else {
_this2.reorderRefs[index - 1].focus();
}
}
_this2.props.changeItemState({
interactionData: _objectSpread(_objectSpread({}, _this2.props.interactionData), {}, {
choices: omit(_this2.props.interactionData.choices, choiceId)
}),
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: without(choiceOrder, choiceId)
})
});
});
_defineProperty(_this2, "handleCreateChoice", function () {
var id = uuid();
var newChoice = {
id: id,
itemBody: ''
};
_this2._choiceWasCreated = true;
_this2.props.changeItemState({
interactionData: _objectSpread(_objectSpread({}, _this2.props.interactionData), {}, {
choices: assignIn(_this2.props.interactionData.choices, _defineProperty({}, id, newChoice))
}),
scoringData: _objectSpread(_objectSpread({}, _this2.props.scoringData), {}, {
value: [].concat(_toConsumableArray(_this2.props.scoringData.value), [id])
})
});
_this2.props.notifyScreenreader(t('Navigate up to find new choice input'));
});
_defineProperty(_this2, "handleIncludeLabelsChange", function (e) {
var properties = _this2.props.properties;
if (!properties.includeLabels) {
_this2.props.notifyScreenreader(t('Navigate up to find label fields'));
}
_this2.props.changeItemState({
properties: _objectSpread(_objectSpread({}, _this2.props.properties), {}, {
includeLabels: !properties['includeLabels']
})
});
});
_defineProperty(_this2, "handleDisplayAnswersParagraphChange", function (e) {
var properties = _this2.props.properties;
_this2.props.changeItemState({
properties: _objectSpread(_objectSpread({}, _this2.props.properties), {}, {
displayAnswersParagraph: !properties['displayAnswersParagraph']
})
});
});
_defineProperty(_this2, "handleInputChange", function (choiceId, event, _ref2) {
var editorContent = _ref2.editorContent;
_this2.props.changeItemState({
interactionData: set("choices[".concat(choiceId, "].itemBody"), editorContent, _this2.props.interactionData)
});
});
return _this2;
}
_inherits(OrderingEdit, _Component);
return _createClass(OrderingEdit, [{
key: "componentWillUnmount",
value: function componentWillUnmount() {
this._timeouts.forEach(clearTimeout);
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate() {
if (this._choiceWasCreated) {
this._choiceWasCreated = false;
last(this.inputRefs).focus();
}
}
}, {
key: "renderChoice",
value:
// ===========
// RENDER
// ===========
function renderChoice(choice, isFinalChoice, index) {
var _this3 = this;
var choicesLength = this.props.scoringData.value.length;
var nonEditable = this.props.overrideEditableForRegrading;
return jsx(Card, {
id: choice.id,
index: index,
key: "".concat(choice.id, "_").concat(index),
moveCard: this.handleMoveChoice
}, jsx(Grid, {
colSpacing: "small"
}, jsx(Grid.Row, null, jsx(Grid.Col, {
width: "auto"
}, jsx(View, {
as: "div",
margin: "xx-large 0 0 0",
themeOverride: {
marginXxLarge: '39px'
}
}, jsx(Flex, {
direction: "row",
alignItems: "start"
}, jsx(Flex.Item, null, jsx(ScreenReaderContent, null, t('position {position, number}', {
position: index + 1
})), jsx(PresentationContent, null, jsx(Text, {
color: "primary"
}, t.number(index + 1))))))), jsx(Grid.Col, {
vAlign: "middle"
}, jsx(View, {
as: "div",
background: "primary"
}, jsx(AnswerInput, {
disabled: nonEditable,
errors: this.props.getErrors("interactionData.choices[".concat(choice.id, "].itemBody")),
id: choice.id,
itemBody: choice.itemBody,
noRCE: !this.props.enableRichContentEditor,
onChangeHandler: this.handleInputChange,
onModalClose: this.props.onModalClose,
onModalOpen: this.props.onModalOpen,
openImportModal: this.props.openImportModal,
ref: function ref(node) {
_this3.inputRefs[index] = node;
},
automationData: "sdk-ordering-answer-".concat(choice.id),
isRequired: true
}))), jsx(Grid.Col, {
width: "auto"
}, jsx(View, {
as: "div",
margin: "xx-large 0 0 0",
themeOverride: {
marginXxLarge: '30px'
}
}, jsx(Flex, {
direction: "row",
justifyItems: "start"
}, jsx(Flex.Item, null, jsx(ReorderChoiceButton, {
ref: function ref(node) {
_this3.reorderRefs[index] = node;
},
id: choice.id,
isFinalChoice: isFinalChoice,
isFirstChoice: index === 0,
onMoveChoiceUp: function onMoveChoiceUp() {
return _this3.handleMoveChoice(index, index - 1);
},
onMoveChoiceDown: function onMoveChoiceDown() {
return _this3.handleMoveChoice(index, index + 1);
},
screenReaderText: t('Reorder Choice: {choice}', {
choice: striptags(choice.itemBody)
})
})), choicesLength > 2 && !nonEditable && jsx(Flex.Item, null, jsx(RemoveChoiceButton, {
choiceId: choice.id,
ref: function ref(node) {
_this3.removeChoiceRefs[index] = node;
},
onRemoveChoice: function onRemoveChoice() {
return _this3.handleRemoveChoice(choice.id, index);
},
screenReaderText: t('Remove Choice: {choice}', {
choice: striptags(choice.itemBody)
})
}))))))));
}
}, {
key: "renderChoices",
value: function renderChoices() {
var _this4 = this;
var choiceOrder = this.props.scoringData.value;
var choicesLength = choiceOrder.length;
return choiceOrder.map(function (choiceId, i) {
var isFinalChoice = i + 1 === choicesLength;
var choice = _this4.props.interactionData.choices[choiceId];
return jsx(View, {
key: "orderchoice-".concat(choiceId),
as: "div",
margin: "x-small 0"
}, _this4.renderChoice(choice, isFinalChoice, i));
});
}
}, {
key: "renderLabel",
value: function renderLabel(labelName, description) {
var _this5 = this;
var automation = 'sdk-ordering-edit-' + labelName;
return jsx(View, {
as: "div",
padding: "x-small 0"
}, jsx(TextInput, {
ref: labelName === 'topLabel' ? this.handleTopLabelRef : this.handleBottomLabelRef,
renderLabel: description,
isRequired: false,
interaction: this.props.overrideEditableForRegrading ? 'disabled' : 'enabled',
name: labelName,
defaultValue: this.props.properties[labelName] || '',
onChange: function onChange(event) {
_this5.props.changeItemState({
properties: _objectSpread(_objectSpread({}, _this5.props.properties), {}, _defineProperty({}, labelName, event.target.value))
});
},
width: "20rem",
messages: this.props.getErrors("properties.".concat(labelName)),
"data-automation": automation
}));
}
}, {
key: "renderOptionsDescription",
value: function renderOptionsDescription() {
return jsx(ScreenReaderContent, null, t('Ordering options'));
}
}, {
key: "render",
value: function render() {
var _this$props$propertie = this.props.properties,
includeLabels = _this$props$propertie.includeLabels,
displayAnswersParagraph = _this$props$propertie.displayAnswersParagraph;
var nonEditable = this.props.overrideEditableForRegrading;
// clean up the references
this.inputRefs = [];
this.removeChoiceRefs = [];
this.reorderRefs = [];
return jsx("div", null, jsx(QuestionContainer, {
disabled: nonEditable,
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
}, includeLabels && this.renderLabel('topLabel', t('Top Label')), jsx(View, {
as: "div"
}, this.renderChoices()), !nonEditable && jsx(Footer, {
onCreateChoice: this.handleCreateChoice,
notifyScreenreader: this.props.notifyScreenreader,
automationData: "sdk-ordering-add-answer"
}), includeLabels && this.renderLabel('bottomLabel', t('Bottom Label'))), jsx(QuestionSettingsContainer, {
additionalOptions: this.props.additionalOptions
}, jsx(QuestionSettingsPanel, {
label: t('Options'),
defaultExpanded: true
}, jsx(FormFieldGroup, {
rowSpacing: "small",
description: this.renderOptionsDescription()
}, this.props.showCalculatorOption && jsx(CalculatorOptionWithOqaatAlert, {
disabled: nonEditable,
calculatorValue: this.props.calculatorType,
onCalculatorTypeChange: this.handleCalculatorTypeChange,
oqaatChecked: this.props.oneQuestionAtATime,
onOqaatChange: this.props.setOneQuestionAtATime
}), jsx(Checkbox, {
label: t('Display Answers in a Paragraph'),
onChange: this.handleDisplayAnswersParagraphChange,
checked: displayAnswersParagraph || false,
disabled: nonEditable,
"data-automation": "sdk-display-answer-in-paragraph-checkbox"
}), jsx(Checkbox, {
label: t('Include Labels'),
onChange: this.handleIncludeLabelsChange,
checked: includeLabels || false,
disabled: nonEditable,
"data-automation": "sdk-include-labels-checkbox"
})))));
}
}]);
}(Component), _defineProperty(_OrderingEdit, "displayName", 'OrderingEdit'), _defineProperty(_OrderingEdit, "componentId", "Quizzes".concat(_OrderingEdit.displayName)), _defineProperty(_OrderingEdit, "interactionType", OrderingInteractionType), _defineProperty(_OrderingEdit, "propTypes", _objectSpread(_objectSpread({
additionalOptions: QuestionSettingsContainer.propTypes.additionalOptions,
calculatorType: PropTypes.string,
enableRichContentEditor: PropTypes.bool,
interactionData: PropTypes.shape({
choices: PropTypes.objectOf(PropTypes.shape({
id: PropTypes.string.isRequired,
itemBody: PropTypes.string.isRequired
})).isRequired
}).isRequired,
itemBody: PropTypes.string,
notifyScreenreader: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
oneQuestionAtATime: PropTypes.bool,
openImportModal: PropTypes.func,
overrideEditableForRegrading: PropTypes.bool,
properties: PropTypes.shape({
displayAnswersParagraph: PropTypes.bool,
includeLabels: PropTypes.bool,
topLabel: PropTypes.string,
bottomLabel: PropTypes.string
}).isRequired,
scoringData: PropTypes.shape({
value: PropTypes.arrayOf(PropTypes.string).isRequired
}).isRequired,
setOneQuestionAtATime: PropTypes.func
}, withEditTools.injectedProps), {}, {
styles: PropTypes.object,
showCalculatorOption: PropTypes.bool
})), _defineProperty(_OrderingEdit, "defaultProps", {
additionalOptions: void 0,
calculatorType: 'none',
enableRichContentEditor: true,
itemBody: void 0,
onModalClose: void 0,
onModalOpen: void 0,
oneQuestionAtATime: false,
openImportModal: void 0,
overrideEditableForRegrading: false,
notifyScreenreader: Function.prototype,
setOneQuestionAtATime: Function.prototype,
showCalculatorOption: true
}), _OrderingEdit)) || _class;
export { OrderingEdit as default };