UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

622 lines (621 loc) • 28.3 kB
function _array_like_to_array(arr, len) { if (len == null || len > arr.length) len = arr.length; for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i]; return arr2; } function _array_without_holes(arr) { if (Array.isArray(arr)) return _array_like_to_array(arr); } function _assert_this_initialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _call_super(_this, derived, args) { derived = _get_prototype_of(derived); return _possible_constructor_return(_this, _is_native_reflect_construct() ? Reflect.construct(derived, args || [], _get_prototype_of(_this).constructor) : derived.apply(_this, args)); } function _class_call_check(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for(var i = 0; i < props.length; i++){ var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _create_class(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _define_property(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _get_prototype_of(o) { _get_prototype_of = Object.setPrototypeOf ? Object.getPrototypeOf : function getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _get_prototype_of(o); } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _set_prototype_of(subClass, superClass); } function _iterable_to_array(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _non_iterable_spread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _object_spread(target) { for(var i = 1; i < arguments.length; i++){ var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === "function") { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function(key) { _define_property(target, key, source[key]); }); } return target; } 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 _object_spread_props(target, source) { source = source != null ? source : {}; 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 _possible_constructor_return(self, call) { if (call && (_type_of(call) === "object" || typeof call === "function")) { return call; } return _assert_this_initialized(self); } function _set_prototype_of(o, p) { _set_prototype_of = Object.setPrototypeOf || function setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _set_prototype_of(o, p); } function _to_consumable_array(arr) { return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_spread(); } function _type_of(obj) { "@swc/helpers - typeof"; return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; } function _unsupported_iterable_to_array(o, minLen) { if (!o) return; if (typeof o === "string") return _array_like_to_array(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(n); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen); } function _is_native_reflect_construct() { try { var result = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {})); } catch (_) {} return (_is_native_reflect_construct = function() { return !!result; })(); } function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } import React, { Component } from 'react'; import PropTypes from 'prop-types'; import map from 'lodash/fp/map'; import filter from 'lodash/fp/filter'; import set from 'lodash/fp/set'; import fromPairs from 'lodash/fp/fromPairs'; import pullAt from 'lodash/fp/pullAt'; import findIndex from 'lodash/fp/findIndex'; import isEmpty from 'lodash/fp/isEmpty'; import uniq from 'lodash/fp/uniq'; import shuffle from 'lodash/fp/shuffle'; import { v4 as uuid } from 'uuid'; import FocusGroup from '../../common/components/FocusGroup'; import MatchingInteractionType from '../../../records/interactions/matching'; import QuestionContainer from '../../common/edit/components/QuestionContainer'; import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer'; import DistractorList from './DistractorList'; import MatchListEdit from './MatchListEdit'; import withEditTools from '../../../util/withEditTools'; import t from '@instructure/quiz-i18n/format-message'; import { RadioInputGroup, RadioInput } from '@instructure/ui-radio-input'; import { Checkbox } from '@instructure/ui-checkbox'; import { View } from '@instructure/ui-view'; import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel'; import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert'; import { ScreenReaderContent } from '@instructure/ui-a11y-content'; import { IconButton } from '@instructure/ui-buttons'; import { IconQuestionLine } from '@instructure/ui-icons'; import { Text } from '@instructure/ui-text'; import { SimpleModal, FormFieldGroup } from '@instructure/quiz-common'; var QUESTION_SELECTOR = '[data-role=questionWrapper] input'; // Scoring Algorithms: var DEEP_EQUALS = 'DeepEquals'; var PARTIAL_DEEP = 'PartialDeep'; // return editData if supplied, otherwise reconstruct it from props export var getEditData = function(param) { var scoringData = param.scoringData, interactionData = param.interactionData; var editData = scoringData.editData; if (!isEmpty(editData)) { return { matches: editData.matches || [], distractors: editData.distractors || [] }; } var matches = map(function(param) { var id = param.id, itemBody = param.itemBody; return { questionId: id, questionBody: itemBody, answerBody: scoringData.value[id] }; }, interactionData.questions); var matchBodies = map('answerBody', matches); var distractors = filter(function(answerBody) { return !matchBodies.includes(answerBody); }, interactionData.answers); return { matches: matches, distractors: distractors }; }; // given editData, calculate the rest of the props export var getPropsFromEditData = function(editData) { return { interactionData: { questions: map(function(match) { return { id: match.questionId, itemBody: match.questionBody }; }, editData.matches), answers: shuffle(uniq(_to_consumable_array(map('answerBody', editData.matches)).concat(_to_consumable_array(editData.distractors)))) }, scoringData: { value: fromPairs(map(function(match) { return [ match.questionId, match.answerBody ]; }, editData.matches)), editData: editData } }; }; var MatchingEdit = /*#__PURE__*/ function(Component) { "use strict"; _inherits(MatchingEdit, Component); function MatchingEdit() { _class_call_check(this, MatchingEdit); var _this; _this = _call_super(this, MatchingEdit, arguments), _define_property(_this, "state", { isModalOpen: false }), _define_property(_this, "_timeouts", []), _define_property(_this, "focusMatch", null), _define_property(_this, "focusDistractor", null), _define_property(_this, "stemElement", null), _define_property(_this, "questionErrors", function(index) { return _this.props.getErrors("scoringData.editData.matches[".concat(index, "].questionBody")).slice(0, 1); }), _define_property(_this, "answerErrors", function(index) { return _this.props.getErrors("scoringData.editData.matches[".concat(index, "].answerBody")).slice(0, 1); }), _define_property(_this, "distractorErrors", function(index) { return _this.props.getErrors("scoringData.editData.distractors[".concat(index, "]")).slice(0, 1); }), // ============= // HANDLERS // ============= _define_property(_this, "handleScoringAlgoChange", function(e, value) { _this.props.changeItemState({ scoringAlgorithm: value }, { scoringAlgorithm: value }); }), _define_property(_this, "blurDistractor", function(index, e) { _this.editDistractor(index, { target: { value: e.target.value.trim() } }); }), _define_property(_this, "editDistractor", function(index, e) { var editData = getEditData(_this.props); if (index >= 0 && index < editData.distractors.length) { _this.updateEditData(set("distractors[".concat(index, "]"), e.target.value, editData)); } }), _define_property(_this, "editQuestion", function(questionId, text) { var editData = getEditData(_this.props); var questionIndex = findIndex({ questionId: questionId }, editData.matches); if (questionIndex !== -1) { _this.updateEditData(set("matches[".concat(questionIndex, "].questionBody"), text, editData)); } }), _define_property(_this, "editAnswer", function(questionId, text) { var editData = getEditData(_this.props); var questionIndex = findIndex({ questionId: questionId }, editData.matches); if (questionIndex !== -1) { _this.updateEditData(set("matches[".concat(questionIndex, "].answerBody"), text, editData)); } }), _define_property(_this, "handleShuffleChange", function(event) { var properties = set('shuffleRules.questions.shuffled', event.target.checked, _this.props.properties); _this.props.changeItemState({ properties: properties }, { properties: properties }); }), _define_property(_this, "handleRemoveMatch", function(questionId) { if (document.activeElement !== document.body) { // In FF clicking on a button doesn't focus it, so don't update focus in such case _this.updateFocusOnRemoveMatch(); } var editData = getEditData(_this.props); _this.updateEditData(set('matches', editData.matches.filter(function(match) { return match.questionId !== questionId; }), editData)); }), _define_property(_this, "handleCreateMatch", function() { _this._timeouts = _to_consumable_array(_this._timeouts).concat([ setTimeout(function() { return _this.focusMatch.focusLast(QUESTION_SELECTOR); }, 100) ]); var editData = getEditData(_this.props); _this.updateEditData(set('matches', _to_consumable_array(editData.matches).concat([ { questionId: _this.props.newId(), questionBody: '', answerBody: '' } ]), editData)); }), _define_property(_this, "handleRemoveDistractor", function(index) { if (_this.focusDistractor.previousExists('button')) { _this.focusDistractor.focusPrevious('button'); } else { _this.focusMatch.focusLast(); } var editData = getEditData(_this.props); _this.updateEditData(set('distractors', pullAt([ index ], editData.distractors), editData)); }), _define_property(_this, "handleCreateDistractor", function() { _this._timeouts = _to_consumable_array(_this._timeouts).concat([ setTimeout(function() { return _this.focusDistractor.focusLast('input'); }, 100) ]); var editData = getEditData(_this.props); _this.updateEditData(set('distractors', _to_consumable_array(editData.distractors).concat([ '' ]), editData)); }), _define_property(_this, "handleCalculatorTypeChange", function(e, value) { _this.props.changeItemState({ calculatorType: value }); }), _define_property(_this, "handleDescriptionChange", function(itemBody) { _this.props.changeItemState({ itemBody: itemBody }); }), _define_property(_this, "handleFocusMatchRef", function(node) { _this.focusMatch = node; }), _define_property(_this, "handleFocusDistractorRef", function(node) { _this.focusDistractor = node; }), _define_property(_this, "handleStemRef", function(node) { _this.stemElement = node; }), _define_property(_this, "handleCloseModal", function() { _this.setState({ isModalOpen: false }); }), _define_property(_this, "handleOpenModal", function() { _this.setState({ isModalOpen: true }); }); return _this; } _create_class(MatchingEdit, [ { key: "componentDidMount", value: function componentDidMount() { this.setDefaultScoringAlgorithm(); } }, { key: "componentWillUnmount", value: function componentWillUnmount() { this._timeouts.forEach(clearTimeout); } }, { key: "componentDidUpdate", value: function componentDidUpdate() { this.setDefaultScoringAlgorithm(); } }, { key: "setDefaultScoringAlgorithm", value: function setDefaultScoringAlgorithm() { if (!this.props.scoringAlgorithm && this.props.partialDeepScoringEnabled) { this.props.changeItemState({ scoringAlgorithm: PARTIAL_DEEP }); } } }, { key: "overrideEditable", value: function overrideEditable() { return this.props.overrideEditableForItem || this.props.overrideEditableForRegrading; } }, { // ============= // HELPERS // ============= key: "updateEditData", value: function updateEditData(newEditData) { this.props.changeItemState(getPropsFromEditData(newEditData)); } }, { key: "updateFocusOnRemoveMatch", value: function updateFocusOnRemoveMatch() { var _this = this; if (!this.focusMatch.previousExists('button')) { // if removing the first choice, focus on stem this.stemElement.focus(); } else if (this.props.interactionData.questions.length === 2) { // if removing the second choice out of two choices this._timeouts = _to_consumable_array(this._timeouts).concat([ setTimeout(function() { return _this.focusMatch.focusLast('input'); }, 100) ]); } else { // all the other cases this.focusMatch.focusPrevious('button'); } } }, { // ============= // RENDERING // ============= key: "renderOptionsDescription", value: function renderOptionsDescription() { return /*#__PURE__*/ React.createElement(ScreenReaderContent, null, t('Matching options')); } }, { key: "renderGradingOptions", value: function renderGradingOptions() { return /*#__PURE__*/ React.createElement(View, { as: "div", margin: "medium 0", position: "relative" }, /*#__PURE__*/ React.createElement(RadioInputGroup, { onChange: this.handleScoringAlgoChange, name: t('Grading'), value: this.props.scoringAlgorithm, description: /*#__PURE__*/ React.createElement(ScreenReaderContent, null, t('Grading')) }, /*#__PURE__*/ React.createElement("span", null, /*#__PURE__*/ React.createElement(Text, null, t('Grading')), /*#__PURE__*/ React.createElement(IconButton, { size: "small", withBackground: false, withBorder: false, renderIcon: IconQuestionLine, onClick: this.handleOpenModal, screenReaderLabel: t('Open grading option information') })), /*#__PURE__*/ React.createElement(RadioInput, { value: PARTIAL_DEEP, label: t('Partial credit'), "data-automation": "sdk-grading-partial-credit-radio-input" }), /*#__PURE__*/ React.createElement(RadioInput, { value: DEEP_EQUALS, label: t('Exact match'), "data-automation": "sdk-grading-exact-match-radio-input" }))); } }, { key: "renderGradingOptionsModal", value: function renderGradingOptionsModal() { return /*#__PURE__*/ React.createElement(SimpleModal, { size: "small", title: t('Grading'), label: t('Grading'), isModalOpen: this.state.isModalOpen, onModalDismiss: this.handleCloseModal }, /*#__PURE__*/ React.createElement(Text, { weight: "bold", lineHeight: "double" }, t('Partial credit')), /*#__PURE__*/ React.createElement("br", null), /*#__PURE__*/ React.createElement(Text, null, t('Students are awarded points for every correct answer.')), /*#__PURE__*/ React.createElement("br", null), /*#__PURE__*/ React.createElement("br", null), /*#__PURE__*/ React.createElement(Text, { weight: "bold", lineHeight: "double" }, t('Exact match')), /*#__PURE__*/ React.createElement("br", null), /*#__PURE__*/ React.createElement(Text, null, t('Students are awarded full credit if all correct answers are selected and no incorrect answers are selected.'))); } }, { key: "render", value: function render() { var _this = this; var _getEditData = getEditData(this.props), matches = _getEditData.matches, distractors = _getEditData.distractors; return /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement(QuestionContainer, { disabled: this.props.overrideEditableForRegrading, enableRichContentEditor: this.props.enableRichContentEditor, itemBody: this.props.itemBody, onDescriptionChange: this.handleDescriptionChange, onModalClose: this.props.onModalClose, onModalOpen: this.props.onModalOpen, openImportModal: this.props.openImportModal, stemErrors: this.props.getErrors('itemBody'), textareaRef: this.handleStemRef }, /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement(FocusGroup, { ref: this.handleFocusMatchRef }, /*#__PURE__*/ React.createElement(MatchListEdit, { // Passed in as new function to force re-render answerErrors: function(index) { return _this.answerErrors(index); }, createNewMatch: this.handleCreateMatch, editAnswer: this.editAnswer, editQuestion: this.editQuestion, // Passed in as new function to force re-render questionErrors: function(index) { return _this.questionErrors(index); }, removeMatch: this.handleRemoveMatch, disabled: this.props.overrideEditableForRegrading, matches: matches, notifyScreenreader: this.props.notifyScreenreader })), /*#__PURE__*/ React.createElement(FocusGroup, { ref: this.handleFocusDistractorRef }, /*#__PURE__*/ React.createElement(DistractorList, { disabled: this.props.overrideEditableForRegrading, createNewDistractor: this.handleCreateDistractor, distractors: distractors, editDistractor: this.editDistractor, blurDistractor: this.blurDistractor, // Passed in as new function to force re-render distractorErrors: function(index) { return _this.distractorErrors(index); }, notifyScreenreader: this.props.notifyScreenreader, removeDistractor: this.handleRemoveDistractor })))), /*#__PURE__*/ React.createElement(QuestionSettingsContainer, { additionalOptions: this.props.additionalOptions }, /*#__PURE__*/ React.createElement(QuestionSettingsPanel, { label: t('Options'), defaultExpanded: true }, /*#__PURE__*/ React.createElement(FormFieldGroup, { rowSpacing: "small", description: this.renderOptionsDescription() }, this.props.showCalculatorOption && /*#__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(Checkbox, { label: t('Shuffle questions'), onChange: this.handleShuffleChange, checked: this.props.properties.shuffleRules.questions.shuffled, disabled: this.props.overrideEditableForRegrading, "data-automation": "sdk-shuffle-questions-checkbox" }), this.props.partialDeepScoringEnabled && this.renderGradingOptions()))), this.renderGradingOptionsModal()); } } ]); return MatchingEdit; }(Component); _define_property(MatchingEdit, "interactionType", MatchingInteractionType); _define_property(MatchingEdit, "propTypes", _object_spread_props(_object_spread({ additionalOptions: QuestionSettingsContainer.propTypes.additionalOptions, calculatorType: PropTypes.string, changeItemState: PropTypes.func, enableRichContentEditor: PropTypes.bool, interactionData: PropTypes.shape({ questions: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, itemBody: PropTypes.string })), answers: PropTypes.arrayOf(PropTypes.string) }).isRequired, itemBody: PropTypes.string, newId: PropTypes.func, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, oneQuestionAtATime: PropTypes.bool, openImportModal: PropTypes.func, overrideEditableForItem: PropTypes.bool, overrideEditableForRegrading: PropTypes.bool, partialDeepScoringEnabled: PropTypes.bool, properties: PropTypes.object, scoringAlgorithm: PropTypes.string, scoringData: PropTypes.shape({ value: PropTypes.objectOf(PropTypes.string), editData: PropTypes.shape({ matches: MatchListEdit.propTypes.matches, distractors: DistractorList.propTypes.distractors }) }), setOneQuestionAtATime: PropTypes.func, notifyScreenreader: PropTypes.func }, withEditTools.injectedProps), { showCalculatorOption: PropTypes.bool })); _define_property(MatchingEdit, "defaultProps", { calculatorType: 'none', enableRichContentEditor: true, oneQuestionAtATime: false, overrideEditableForItem: false, overrideEditableForRegrading: false, newId: uuid, setOneQuestionAtATime: Function.prototype, notifyScreenreader: Function.prototype, additionalOptions: void 0, changeItemState: void 0, interactionData: void 0, itemBody: void 0, onModalClose: void 0, onModalOpen: void 0, openImportModal: void 0, partialDeepScoringEnabled: false, properties: void 0, scoringAlgorithm: null, scoringData: void 0, showCalculatorOption: true }); export { MatchingEdit as default }; MatchingEdit = _ts_decorate([ withEditTools ], MatchingEdit);