UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

577 lines (573 loc) • 24.6 kB
/** @jsx jsx */ 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_with_holes(arr) { if (Array.isArray(arr)) return arr; } 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 _iterable_to_array_limit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){ _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally{ try { if (!_n && _i["return"] != null) _i["return"](); } finally{ if (_d) throw _e; } } return _arr; } function _non_iterable_rest() { throw new TypeError("Invalid attempt to destructure 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 _sliced_to_array(arr, i) { return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest(); } 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); } import React, { useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import find from 'lodash/fp/find'; import flatMap from 'lodash/fp/flatMap'; 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 { RadioInputGroup, RadioInput } from '@instructure/ui-radio-input'; import { IconButton } from '@instructure/ui-buttons'; import { IconQuestionLine } from '@instructure/ui-icons'; import { View } from '@instructure/ui-view'; import { SimpleModal, FormFieldGroup, Flex } from '@instructure/quiz-common'; 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 { CategorizationPresenterProvider, useCategorizationPresenter } from './CategorizationPresenter'; import CategoryForm from './CategoryForm'; import { getSortedCategories } from '../common/utils'; import withEditTools from '../../../util/withEditTools'; import t from '@instructure/quiz-i18n/format-message'; import QuestionSettingsPanel from '../../common/edit/components/QuestionSettingsPanel'; import CalculatorOptionWithOqaatAlert from '../../common/edit/components/CalculatorOptionWithOqaatAlert'; import { ScreenReaderContent } from '@instructure/ui-a11y-content'; /** --- 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 BaseCategorizationEdit = /*#__PURE__*/ React.forwardRef(function(props, ref) { var additionalOptions = props.additionalOptions, calculatorType = props.calculatorType, changeItemState = props.changeItemState, enableRichContentEditor = props.enableRichContentEditor, getErrors = props.getErrors, interactionData = props.interactionData, itemBody = props.itemBody, newId = props.newId, notifyScreenreader = props.notifyScreenreader, onModalClose = props.onModalClose, onModalOpen = props.onModalOpen, oneQuestionAtATime = props.oneQuestionAtATime, openImportModal = props.openImportModal, overrideEditableForRegrading = props.overrideEditableForRegrading, scoringData = props.scoringData, setOneQuestionAtATime = props.setOneQuestionAtATime, showCalculatorOption = props.showCalculatorOption; var categories = interactionData.categories, categoryOrder = interactionData.categoryOrder; var _useState = _sliced_to_array(useState(false), 2), isModalOpen = _useState[0], setModalOpen = _useState[1]; var sortedCategories = useMemo(function() { return getSortedCategories(categories, categoryOrder); }, [ categories, categoryOrder ]); var _useState1 = _sliced_to_array(useState(function() { return sortBy('itemBody', interactionData.distractors).map(function(param) { var id = param.id; return id; }); }), 2), distractorOrder = _useState1[0], setDistractorOrder = _useState1[1]; var _useCategorizationPresenter = useCategorizationPresenter(), onCategoryInputChange = _useCategorizationPresenter.onCategoryInputChange, onCreateAnswer = _useCategorizationPresenter.onCreateAnswer, onCreateCategory = _useCategorizationPresenter.onCreateCategory, onDistractorInputChange = _useCategorizationPresenter.onDistractorInputChange, onRemoveAnswer = _useCategorizationPresenter.onRemoveAnswer, onRemoveCategory = _useCategorizationPresenter.onRemoveCategory, onRemoveDistractor = _useCategorizationPresenter.onRemoveDistractor; var distractorRefsRef = useRef({}); var categoryRefRef = useRef(null); var stemElementRef = useRef(null); // =========== // HELPERS // =========== var getViewDistractors = function() { var answerIds = flatMap(function(param) { var value = param.scoringData.value; return value; }, scoringData.value); return flow(Object.values, filter(function(param) { var id = param.id; return !answerIds.includes(id); }), sortBy(function(param) { var id = param.id; return distractorOrder.indexOf(id); }))(interactionData.distractors); }; var getCategoryErrors = function(categoryId) { return getErrors("interactionData.categories.".concat(categoryId, ".itemBody")); }; var getDistractorErrors = function(distractorId) { return getErrors("interactionData.distractors.".concat(distractorId, ".itemBody")); }; var getAnswerIds = function(categoryId) { var _categoryScoringData_scoringData; var categoryScoringData = find({ id: categoryId }, scoringData.value); // since kinesis doesnt save empty array return (categoryScoringData === null || categoryScoringData === void 0 ? void 0 : (_categoryScoringData_scoringData = categoryScoringData.scoringData) === null || _categoryScoringData_scoringData === void 0 ? void 0 : _categoryScoringData_scoringData.value) || []; }; // =========== // HANDLERS // =========== var handleScoringAlgoChange = function(e, value) { // Update scoreMethod in scoringData, not scoringAlgorithm at item level var newScoringData = _object_spread_props(_object_spread({}, props.scoringData), { scoreMethod: value }); props.changeItemState({ scoringData: newScoringData }); }; var handleOpenModal = function() { setModalOpen(true); }; var handleCloseModal = function() { setModalOpen(false); }; var handleCalculatorTypeChange = function(e, value) { changeItemState({ calculatorType: value }); }; var handleRemoveDistractor = function(distractorId) { var nextDistractorId = getNextItemIdFromArray(distractorId, getViewDistractors()); var nextDistractorRef = distractorRefsRef.current[nextDistractorId]; if (nextDistractorRef) { nextDistractorRef.removeChoicebutton.focus(); } else { // focus on add category button categoryRefRef.current.footerRef.focus(); } onRemoveDistractor(distractorId); }; var handleRemoveCategory = function(categoryId) { var removingFirstCategory = categoryOrder && categoryOrder.indexOf(categoryId) === 0; if (removingFirstCategory) { stemElementRef.current.focus(); } else { var nextCategoryId = getNextItemIdFromArray(categoryId, sortedCategories); var nextAddAnswerRef = categoryRefRef.current.categoryRefs[nextCategoryId]; if (nextAddAnswerRef) { // will focus the previous category's "+Answer" button nextAddAnswerRef.focus(); } } onRemoveCategory(categoryId); }; var handleRemoveAnswer = function(answerId, categoryId) { var answerIds = getAnswerIds(categoryId); var removingFirstAnswer = answerIds.indexOf(answerId) === 0; if (removingFirstAnswer) { categoryRefRef.current.categoryAnswerInputRefs[categoryId].answerInput.focus(); } else { var nextAnswerId = getNextItemIdFromArray(answerId, answerIds); var nextAnswerRef = categoryRefRef.current.answerRefs[nextAnswerId]; if (nextAnswerRef) { nextAnswerRef.removeChoicebutton.focus(); } } onRemoveAnswer(answerId, categoryId); }; var handleCreateDistractor = function() { var id = newId(); var newDistractor = { id: id, itemBody: '' }; setDistractorOrder(function(prevDistractorOrder) { return prevDistractorOrder.concat([ id ]); }); changeItemState({ interactionData: set("distractors[".concat(id, "]"), newDistractor, interactionData) }); }; var handleStemRef = function(node) { stemElementRef.current = node; }; var handleCategoryRef = function(node) { categoryRefRef.current = node; }; var handleDescriptionChange = function(itemBody) { changeItemState({ itemBody: itemBody }); }; // =========== // RENDER // =========== function renderGradingOptions() { var ALL_OR_NOTHING = 'all_or_nothing'; var PARTIAL_CREDIT = 'partial_credit'; // Read scoreMethod from scoringData var currentScoreMethod = props.scoringData.scoreMethod || ALL_OR_NOTHING; return /*#__PURE__*/ jsx(View, { as: "div", margin: "medium 0", position: "relative" }, /*#__PURE__*/ jsx("div", { style: { marginBottom: '0.5rem' } }, /*#__PURE__*/ jsx(Text, null, t('Grading')), /*#__PURE__*/ jsx(IconButton, { size: "small", withBackground: false, withBorder: false, renderIcon: IconQuestionLine, onClick: handleOpenModal, screenReaderLabel: t('Open grading option information') })), /*#__PURE__*/ jsx(RadioInputGroup, { onChange: handleScoringAlgoChange, value: currentScoreMethod, name: t('Grading'), description: /*#__PURE__*/ jsx(ScreenReaderContent, null, t('Grading')) }, /*#__PURE__*/ jsx(RadioInput, { value: ALL_OR_NOTHING, label: t('Exact match'), "data-automation": "sdk-grading-exact-match-radio-input" }), /*#__PURE__*/ jsx(RadioInput, { value: PARTIAL_CREDIT, label: t('Partial credit'), "data-automation": "sdk-grading-partial-credit-radio-input" }))); } function renderGradingOptionsModal() { return /*#__PURE__*/ jsx(SimpleModal, { size: "small", title: t('Grading'), label: t('Grading'), isModalOpen: isModalOpen, onModalDismiss: handleCloseModal }, /*#__PURE__*/ jsx(Text, { weight: "bold", lineHeight: "double", as: "div" }, t('Exact match')), /*#__PURE__*/ jsx(Text, { as: "div" }, t('Students are awarded full credit if all correct items are categorized correctly and all distractors are left uncategorized.')), /*#__PURE__*/ jsx("br", null), /*#__PURE__*/ jsx(Text, { weight: "bold", lineHeight: "double", as: "div" }, t('Partial credit')), /*#__PURE__*/ jsx(Text, null, t('Students earn points for each correct decision: placing correct items in the right category AND leaving distractors uncategorized. Each decision is worth an equal portion of the total points.'))); } var renderDistractor = function(distractor, focusOnMount) { return /*#__PURE__*/ jsx(ChoiceInput, { key: distractor.id, id: distractor.id, disabledFields: overrideEditableForRegrading ? [ 'answerInput' ] : [], itemBody: distractor.itemBody, errors: getDistractorErrors(distractor.id), onInputChange: onDistractorInputChange, onModalClose: onModalClose, onModalOpen: onModalOpen, onRemoveChoice: function() { return handleRemoveDistractor(distractor.id); }, ref: function(node) { distractorRefsRef.current[distractor.id] = node; }, renderLabel: t('Distractor'), isRequired: true, focusOnMount: focusOnMount, shouldRenderRemoveChoice: !overrideEditableForRegrading, noRCE: true, notifyScreenreader: notifyScreenreader, screenReaderText: t('Additional Distractor {itemBody}', { itemBody: striptags(distractor.itemBody) }) }); }; var renderDistractorSection = function() { var distractors = getViewDistractors(); return /*#__PURE__*/ jsx(Flex, { direction: "column", gap: "small", className: "distractorsContainer", padding: "x-small 0 0 0" }, /*#__PURE__*/ jsx(Text, { color: "primary" }, t('Additional Distractors')), /*#__PURE__*/ jsx(Flex, { direction: "column", gap: "small" }, distractors.map(function(distractor, i, source) { var focusOnMount = i + 1 === source.length && !distractor.itemBody; return renderDistractor(distractor, focusOnMount); })), !overrideEditableForRegrading && /*#__PURE__*/ jsx(Flex.Item, { overflowY: "visible" }, /*#__PURE__*/ jsx(Footer, { onCreateChoice: handleCreateDistractor, buttonText: t('Distractor'), screenReaderText: t('Add Distractor'), notifyScreenreader: notifyScreenreader, automationData: "add-categorization-distractor-button" }))); }; var renderOptionsDescription = function() { return /*#__PURE__*/ jsx(ScreenReaderContent, null, t('Categorization options')); }; distractorRefsRef.current = {}; return /*#__PURE__*/ jsx("div", null, /*#__PURE__*/ jsx(QuestionContainer, { disabled: overrideEditableForRegrading, enableRichContentEditor: enableRichContentEditor, itemBody: itemBody, onDescriptionChange: handleDescriptionChange, onModalClose: onModalClose, onModalOpen: onModalOpen, openImportModal: openImportModal, stemErrors: getErrors('itemBody'), textareaRef: handleStemRef }, /*#__PURE__*/ jsx(CategoryForm, { categoriesErrors: 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(categoryId) { return getCategoryErrors(categoryId); }, distractorErrors: function(distractorId) { return getDistractorErrors(distractorId); }, categoryScoringData: getAnswerIds, distractors: interactionData.distractors, disabled: overrideEditableForRegrading, sortedCategories: sortedCategories, notifyScreenreader: notifyScreenreader, onCategoryInputChange: onCategoryInputChange, onCreateAnswer: onCreateAnswer, onCreateCategory: onCreateCategory, onDistractorInputChange: onDistractorInputChange, onModalClose: onModalClose, onModalOpen: onModalOpen, onRemoveAnswer: handleRemoveAnswer, onRemoveCategory: handleRemoveCategory, ref: handleCategoryRef }), renderDistractorSection()), /*#__PURE__*/ jsx(QuestionSettingsContainer, { additionalOptions: additionalOptions }, showCalculatorOption && /*#__PURE__*/ jsx(QuestionSettingsPanel, { label: t('Options'), defaultExpanded: true }, /*#__PURE__*/ jsx(FormFieldGroup, { rowSpacing: "small", description: renderOptionsDescription() }, /*#__PURE__*/ jsx(CalculatorOptionWithOqaatAlert, { disabled: overrideEditableForRegrading, calculatorValue: calculatorType, onCalculatorTypeChange: handleCalculatorTypeChange, oqaatChecked: oneQuestionAtATime, onOqaatChange: setOneQuestionAtATime }), props.partialDeepScoringEnabled && renderGradingOptions()))), renderGradingOptionsModal()); }); BaseCategorizationEdit.displayName = 'CategorizationEdit'; BaseCategorizationEdit.componentId = 'QuizzesCategorizationEdit'; BaseCategorizationEdit.interactionType = CategorizationInteractionType; BaseCategorizationEdit.propTypes = { additionalOptions: QuestionSettingsContainer.propTypes.additionalOptions, calculatorType: PropTypes.string, changeItemState: PropTypes.func, enableRichContentEditor: PropTypes.bool, partialDeepScoringEnabled: PropTypes.bool, scoringAlgorithm: PropTypes.string, 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, getErrors: PropTypes.func, styles: PropTypes.object, showCalculatorOption: PropTypes.bool }; BaseCategorizationEdit.defaultProps = { calculatorType: 'none', partialDeepScoringEnabled: false, scoringAlgorithm: void 0, 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 }; var CategorizationEditWithEditTools = withEditTools(BaseCategorizationEdit); var CategorizationEdit = /*#__PURE__*/ React.forwardRef(function(props, ref) { var interactionData = props.interactionData, scoringData = props.scoringData, changeItemState = props.changeItemState, _props_newId = props.newId, newId = _props_newId === void 0 ? uuid : _props_newId; return /*#__PURE__*/ jsx(CategorizationPresenterProvider, { interactionData: interactionData, scoringData: scoringData, changeItemState: changeItemState, newId: newId }, /*#__PURE__*/ jsx(CategorizationEditWithEditTools, _object_spread_props(_object_spread({}, props), { ref: ref }))); }); CategorizationEdit.displayName = 'CategorizationEdit'; CategorizationEdit.componentId = 'QuizzesCategorizationEdit'; CategorizationEdit.interactionType = CategorizationInteractionType; export default CategorizationEdit;