UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

451 lines (445 loc) • 19 kB
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, _CategorizationEdit; 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 find from 'lodash/fp/find'; import flatMap from 'lodash/fp/flatMap'; import isEqual from 'lodash/fp/isEqual'; import flow from 'lodash/fp/flow'; import filter from 'lodash/fp/filter'; import sortBy from 'lodash/fp/sortBy'; import set from 'lodash/fp/set'; import { v4 as uuid } from 'uuid'; import striptags from 'striptags'; import { Text } from '@instructure/ui-text'; import { jsx } from '@instructure/emotion'; import CategorizationInteractionType from '../../../records/interactions/categorization'; import ChoiceInput from '../../common/edit/components/ChoiceInput'; import Footer from '../../common/edit/components/Footer'; import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer'; import QuestionContainer from '../../common/edit/components/QuestionContainer'; import { getNextItemIdFromArray } from '../../../util/focusHelpers'; import CategorizationPresenter from './CategorizationPresenter'; import CategoryForm from './CategoryForm'; import { getSortedCategories } from '../common/utils'; import withEditTools from '../../../util/withEditTools'; import t from '@instructure/quiz-i18n/es/format-message'; import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel'; import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert'; import { ScreenReaderContent } from '@instructure/ui-a11y-content'; import { FormFieldGroup, Flex } from '@instructure/quiz-common'; /** --- category: Categorization --- Categorization Edit component ```jsx_example function Example (props) { const exampleProps = { itemBody: 'Match the name of the celestial body on the left with its correct classification:', interactionData: { categoryOrder: ['uuid1', 'uuid2'], categories: { uuid1: { id: 'uuid1', itemBody: 'Planet' }, uuid2: { id: 'uuid2', itemBody: 'Moon' } }, distractors: { uuid3: { id: 'uuid3', itemBody: 'Mars' }, uuid4: { id: 'uuid4', itemBody: 'Europa' }, uuid5: { id: 'uuid5', itemBody: 'Venus' }, uuid6: { id: 'uuid6', itemBody: 'Phobos' }, uuid7: { id: 'uuid7', itemBody: 'Deimos' }, uuid8: { id: 'uuid8', itemBody: 'Jupiter' }, uuid9: { id: 'uuid9', itemBody: 'America' }, uuid10: { id: 'uuid10', itemBody: 'Asia' } } }, scoringData: { value: [{ id: 'uuid1', scoringAlgorithm: 'AllOrNothing', scoringData: { value: ['uuid3', 'uuid5', 'uuid8'] } }, { id: 'uuid2', scoringAlgorithm: 'AllOrNothing', scoringData: { value: ['uuid4', 'uuid6', 'uuid7'] } }] } } return ( <CategorizationEdit {...exampleProps} {...props} /> ) } <SettingsSwitcher locales={LOCALES}> <EditStateProvider> <Example /> </EditStateProvider> </SettingsSwitcher> ``` **/ var CategorizationEdit = withEditTools(_class = (_CategorizationEdit = /*#__PURE__*/function (_Component) { function CategorizationEdit(props) { var _this2; _classCallCheck(this, CategorizationEdit); _this2 = _callSuper(this, CategorizationEdit, [props]); _defineProperty(_this2, "distractorRefs", {}); _defineProperty(_this2, "categoryRef", null); _defineProperty(_this2, "stemElement", null); // =========== // HELPERS // =========== _defineProperty(_this2, "getViewDistractors", function () { var answerIds = flatMap(function (_ref) { var value = _ref.scoringData.value; return value; }, _this2.props.scoringData.value); return flow(Object.values, filter(function (_ref2) { var id = _ref2.id; return !answerIds.includes(id); }), sortBy(function (_ref3) { var id = _ref3.id; return _this2.state.distractorOrder.indexOf(id); }))(_this2.props.interactionData.distractors); }); _defineProperty(_this2, "getCategoryErrors", function (categoryId) { return _this2.props.getErrors("interactionData.categories.".concat(categoryId, ".itemBody")); }); _defineProperty(_this2, "getDistractorErrors", function (distractorId) { return _this2.props.getErrors("interactionData.distractors.".concat(distractorId, ".itemBody")); }); _defineProperty(_this2, "getAnswerIds", function (categoryId) { var categoryScoringData = find({ id: categoryId }, _this2.props.scoringData.value); // since kinesis doesnt save empty array return categoryScoringData.scoringData.value || []; }); // =========== // HANDLERS // =========== _defineProperty(_this2, "handleCalculatorTypeChange", function (e, value) { _this2.props.changeItemState({ calculatorType: value }); }); _defineProperty(_this2, "handleRemoveDistractor", function (distractorId) { var nextDistractorId = getNextItemIdFromArray(distractorId, _this2.getViewDistractors()); var nextDistractorRef = _this2.distractorRefs[nextDistractorId]; if (nextDistractorRef) { nextDistractorRef.removeChoicebutton.focus(); } else { // focus on add category button _this2.categoryRef.footerRef.focus(); } _this2.presenter.onRemoveDistractor(distractorId); }); _defineProperty(_this2, "handleRemoveCategory", function (categoryId) { var categoryOrder = _this2.props.interactionData.categoryOrder; var removingFirstCategory = categoryOrder && categoryOrder.indexOf(categoryId) === 0; if (removingFirstCategory) { _this2.stemElement.focus(); } else { var nextCategoryId = getNextItemIdFromArray(categoryId, _this2.state.sortedCategories); var nextAddAnswerRef = _this2.categoryRef.categoryRefs[nextCategoryId]; if (nextAddAnswerRef) { // will focus the previous category's "+Answer" button nextAddAnswerRef.focus(); } } _this2.presenter.onRemoveCategory(categoryId); }); _defineProperty(_this2, "handleRemoveAnswer", function (answerId, categoryId) { var answerIds = _this2.getAnswerIds(categoryId); var removingFirstAnswer = answerIds.indexOf(answerId) === 0; if (removingFirstAnswer) { _this2.categoryRef.categoryAnswerInputRefs[categoryId].answerInput.focus(); } else { var nextAnswerId = getNextItemIdFromArray(answerId, answerIds); var nextAnswerRef = _this2.categoryRef.answerRefs[nextAnswerId]; if (nextAnswerRef) { nextAnswerRef.removeChoicebutton.focus(); } } _this2.presenter.onRemoveAnswer(answerId, categoryId); }); _defineProperty(_this2, "handleCreateDistractor", function () { var id = _this2.props.newId(); var newDistractor = { id: id, itemBody: '' }; _this2.setState(function (prevState) { return { distractorOrder: prevState.distractorOrder.concat([id]) }; }); _this2.props.changeItemState({ interactionData: set("distractors[".concat(id, "]"), newDistractor, _this2.props.interactionData) }); }); _defineProperty(_this2, "handleStemRef", function (node) { _this2.stemElement = node; }); _defineProperty(_this2, "handleCategoryRef", function (node) { _this2.categoryRef = node; }); _defineProperty(_this2, "handleDescriptionChange", function (itemBody) { _this2.props.changeItemState({ itemBody: itemBody }); }); _this2.presenter = new CategorizationPresenter(props); var _this2$props$interact = _this2.props.interactionData, categories = _this2$props$interact.categories, _categoryOrder = _this2$props$interact.categoryOrder; _this2.state = { sortedCategories: getSortedCategories(categories, _categoryOrder), distractorOrder: sortBy('itemBody', props.interactionData.distractors).map(function (_ref4) { var id = _ref4.id; return id; }) }; return _this2; } _inherits(CategorizationEdit, _Component); return _createClass(CategorizationEdit, [{ key: "UNSAFE_componentWillReceiveProps", value: function UNSAFE_componentWillReceiveProps(nextProps) { this.presenter.UNSAFE_componentWillReceiveProps(nextProps); var _nextProps$interactio = nextProps.interactionData, categories = _nextProps$interactio.categories, categoryOrder = _nextProps$interactio.categoryOrder; if (!isEqual(this.props.interactionData.categories, categories) || !isEqual(this.props.interactionData.categoryOrder, categoryOrder)) { this.setState({ sortedCategories: getSortedCategories(categories, categoryOrder) }); } } }, { key: "renderDistractor", value: // =========== // RENDER // =========== function renderDistractor(distractor, focusOnMount) { var _this3 = this; return jsx(ChoiceInput, { key: distractor.id, id: distractor.id, disabledFields: this.props.overrideEditableForRegrading ? ['answerInput'] : [], itemBody: distractor.itemBody, errors: this.getDistractorErrors(distractor.id), onInputChange: this.presenter.onDistractorInputChange, onModalClose: this.props.onModalClose, onModalOpen: this.props.onModalOpen, onRemoveChoice: function onRemoveChoice() { return _this3.handleRemoveDistractor(distractor.id); }, ref: function ref(node) { _this3.distractorRefs[distractor.id] = node; }, renderLabel: t('Distractor'), isRequired: true, focusOnMount: focusOnMount, shouldRenderRemoveChoice: !this.props.overrideEditableForRegrading, noRCE: true, notifyScreenreader: this.props.notifyScreenreader, screenReaderText: t('Additional Distractor {itemBody}', { itemBody: striptags(distractor.itemBody) }) }); } }, { key: "renderDistractors", value: function renderDistractors(distractors) { var _this4 = this; return distractors.map(function (distractor, i, source) { var focusOnMount = i + 1 === source.length && !distractor.itemBody; return jsx(Flex.Item, { key: "".concat(distractor.id, "-i"), overflowY: "visible" }, _this4.renderDistractor(distractor, focusOnMount)); }); } }, { key: "renderDistractorSection", value: function renderDistractorSection() { var _this5 = this; var distractors = this.getViewDistractors(); return jsx(Flex, { direction: "column", gap: "small", className: "distractorsContainer", padding: "x-small 0 0 0" }, jsx(Text, { color: "primary" }, t('Additional Distractors')), jsx(Flex, { direction: "column", gap: "small" }, distractors.map(function (distractor, i, source) { var focusOnMount = i + 1 === source.length && !distractor.itemBody; return _this5.renderDistractor(distractor, focusOnMount); })), !this.props.overrideEditableForRegrading && jsx(Flex.Item, { overflowY: "visible" }, jsx(Footer, { onCreateChoice: this.handleCreateDistractor, buttonText: t('Distractor'), screenReaderText: t('Add Distractor'), notifyScreenreader: this.props.notifyScreenreader, automationData: "add-categorization-distractor-button" }))); } }, { key: "renderOptionsDescription", value: function renderOptionsDescription() { return jsx(ScreenReaderContent, null, t('Categorization options')); } }, { key: "render", value: function render() { var _this6 = this; this.distractorRefs = {}; return jsx("div", null, jsx(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 }, jsx(CategoryForm, { categoriesErrors: this.props.getErrors('scoringData.value.$errors') // These error callbacks are passed in as new functions, to force // the component to re-render when errors change. Not ideal - we // should be passing the errors in directly , getCategoryErrors: function getCategoryErrors(categoryId) { return _this6.getCategoryErrors(categoryId); }, distractorErrors: function distractorErrors(distractorId) { return _this6.getDistractorErrors(distractorId); }, categoryScoringData: this.getAnswerIds, distractors: this.props.interactionData.distractors, disabled: this.props.overrideEditableForRegrading, sortedCategories: this.state.sortedCategories, notifyScreenreader: this.props.notifyScreenreader, onCategoryInputChange: this.presenter.onCategoryInputChange, onCreateAnswer: this.presenter.onCreateAnswer, onCreateCategory: this.presenter.onCreateCategory, onDistractorInputChange: this.presenter.onDistractorInputChange, onModalClose: this.props.onModalClose, onModalOpen: this.props.onModalOpen, onRemoveAnswer: this.handleRemoveAnswer, onRemoveCategory: this.handleRemoveCategory, ref: this.handleCategoryRef }), this.renderDistractorSection()), jsx(QuestionSettingsContainer, { additionalOptions: this.props.additionalOptions }, this.props.showCalculatorOption && jsx(QuestionSettingsPanel, { label: t('Options'), defaultExpanded: true }, jsx(FormFieldGroup, { rowSpacing: "small", description: this.renderOptionsDescription() }, jsx(CalculatorOptionWithOqaatAlert, { disabled: this.props.overrideEditableForRegrading, calculatorValue: this.props.calculatorType, onCalculatorTypeChange: this.handleCalculatorTypeChange, oqaatChecked: this.props.oneQuestionAtATime, onOqaatChange: this.props.setOneQuestionAtATime }))))); } }]); }(Component), _defineProperty(_CategorizationEdit, "displayName", 'CategorizationEdit'), _defineProperty(_CategorizationEdit, "componentId", "Quizzes".concat(_CategorizationEdit.displayName)), _defineProperty(_CategorizationEdit, "interactionType", CategorizationInteractionType), _defineProperty(_CategorizationEdit, "propTypes", _objectSpread(_objectSpread({ additionalOptions: QuestionSettingsContainer.propTypes.additionalOptions, calculatorType: PropTypes.string, changeItemState: PropTypes.func, enableRichContentEditor: PropTypes.bool, errors: PropTypes.shape({ itemBody: PropTypes.arrayOf(PropTypes.string), interactionData: PropTypes.shape({ distractors: PropTypes.objectOf(PropTypes.shape({ itemBody: PropTypes.arrayOf(PropTypes.string) })), categories: PropTypes.objectOf(PropTypes.shape({ itemBody: PropTypes.arrayOf(PropTypes.string) })) }), scoringData: PropTypes.shape({ value: PropTypes.shape({ $errors: PropTypes.arrayOf(PropTypes.string) }) }) }), interactionData: PropTypes.shape({ categoryOrder: PropTypes.arrayOf(PropTypes.string), categories: PropTypes.objectOf(PropTypes.shape({ id: PropTypes.string, itemBody: PropTypes.string })), distractors: PropTypes.objectOf(PropTypes.shape({ id: PropTypes.string, itemBody: PropTypes.string })) }).isRequired, itemBody: PropTypes.string, notifyScreenreader: PropTypes.func.isRequired, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, oneQuestionAtATime: PropTypes.bool, openImportModal: PropTypes.func.isRequired, overrideEditableForRegrading: PropTypes.bool, scoringData: PropTypes.shape({ value: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string })) }).isRequired, setOneQuestionAtATime: PropTypes.func, newId: PropTypes.func }, withEditTools.injectedProps), {}, { styles: PropTypes.object, showCalculatorOption: PropTypes.bool })), _defineProperty(_CategorizationEdit, "defaultProps", { calculatorType: 'none', enableRichContentEditor: true, oneQuestionAtATime: false, overrideEditableForRegrading: false, newId: uuid, setOneQuestionAtATime: Function.prototype, additionalOptions: void 0, changeItemState: void 0, errors: void 0, itemBody: void 0, onModalClose: void 0, onModalOpen: void 0, showCalculatorOption: true }), _CategorizationEdit)) || _class; export { CategorizationEdit as default };