UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

391 lines (386 loc) • 16.4 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 _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 _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, { useRef, useState } from 'react'; import PropTypes from 'prop-types'; import concat from 'lodash/fp/concat'; import find from 'lodash/fp/find'; import get from 'lodash/get'; import { Button } from '@instructure/ui-buttons'; import { Text } from '@instructure/ui-text'; import { jsx } from '@instructure/emotion'; import Errors from '../../common/edit/components/Errors'; import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer'; import QuestionContainer from '../../common/edit/components/QuestionContainer'; import FillBlankInteractionType from '../../../records/interactions/fill_blank'; import BlankTypeSelect from './answer_field/BlankTypeSelect'; import EditStem from './EditStem'; import EditController from './editController'; import generateStyle from './styles'; import generateComponentTheme from './theme'; 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'; import { withStyleOverrides } from '@instructure/quiz-common/util/withStyleOverrides'; import { FormFieldGroup } from '@instructure/quiz-common/components/FormFieldGroup/index'; var SPACE_KEY_CODE = 32; var ENTER_KEY_CODE = 13; /** --- category: FillInTheBlank --- Fill in the Blank Edit component ```jsx_example function Example (props) { const exampleProps = { interactionData: { prompt: '<p><strong>Please</strong> fill in all the blanks</p>', blanks: [{ answerType: 'openEntry', id: 'uuid-blank1' }, { answerType: 'openEntry', id: 'uuid-blank2' }, { answerType: 'openEntry', id: 'uuid-blank3' }, { answerType: 'openEntry', id: 'uuid-blank4' }, { answerType: 'openEntry', id: 'uuid-blank5' }, { answerType: 'dropdown', id: 'uuid-blank6', choices: [ { id: 'uuid6-choice1', position: 1, itemBody: 'with' }, { id: 'uuid6-choice2', position: 2, itemBody: 'on' }, { id: 'uuid6-choice3', position: 3, itemBody: 'which' } ] }, { answerType: 'wordbank', id: 'uuid-blank7', choices: [ { id: 'uuid7-choice1', position: 1, itemBody: 'bathwater' }, { id: 'uuid7-choice2', position: 2, itemBody: 'bathtub' }, { id: 'uuid7-choice3', position: 3, itemBody: 'water' } ] }], stemItems: [ { id: 'uuid-stem0', position: 1, type: 'text', value: ' ' }, { id: 'uuid-stem1', position: 2, type: 'blank', blankId: 'uuid-blank1' }, { id: 'uuid-stem2', position: 3, type: 'text', value: ' ' }, { id: 'uuid-stem3', position: 4, type: 'blank', blankId: 'uuid-blank2' }, { id: 'uuid-stem4', position: 5, type: 'text', value: ' ' }, { id: 'uuid-stem5', position: 6, type: 'blank', blankId: 'uuid-blank3' }, { id: 'uuid-stem6', position: 7, type: 'text', value: ' ' }, { id: 'uuid-stem7', position: 8, type: 'blank', blankId: 'uuid-blank4' }, { id: 'uuid-stem8', position: 9, type: 'text', value: ' ' }, { id: 'uuid-stem9', position: 10, type: 'blank', blankId: 'uuid-blank5' }, { id: 'uuid-stem10', position: 11, type: 'text', value: ' ' }, { id: 'uuid-stem11', position: 12, type: 'blank', blankId: 'uuid-blank6' }, { id: 'uuid-stem12', position: 13, type: 'text', value: ' <the> ' }, { id: 'uuid-stem13', position: 14, type: 'blank', blankId: 'uuid-blank7' }, { id: 'uuid-stem14', position: 15, type: 'text', value: '.' } ] }, scoringData: { value: [{ id: 'uuid-blank1', scoringAlgorithm: 'TextCloseEnough', scoringData: { blankText: 'Don\'t', editDistance: '1', ignoreCase: true, value: 'Don\'t' } }, { id: 'uuid-blank2', scoringAlgorithm: 'TextEquivalence', scoringData: { blankText: 'throw', value: 'throw' } }, { id: 'uuid-blank3', scoringAlgorithm: 'TextContainsAnswer', scoringData: { blankText: 'the', value: 'the' } }, { id: 'uuid-blank4', scoringAlgorithm: 'TextInChoices', scoringData: { blankText: 'baby', value: ['baby', 'kid', 'child'] } }, { id: 'uuid-blank5', scoringAlgorithm: 'TextRegex', scoringData: { blankText: 'out', value: 'out' } }, { id: 'uuid-blank6', scoringAlgorithm: 'Equivalence', scoringData: { blankText: 'with', value: 'uuid6-choice1' } }, { id: 'uuid-blank7', scoringAlgorithm: 'Equivalence', scoringData: { blankText: 'bathwater', value: 'uuid7-choice1' } }] } } return ( <FillBlankEdit {...exampleProps} {...props} /> ) } <SettingsSwitcher locales={LOCALES}> <EditStateProvider> <Example /> </EditStateProvider> </SettingsSwitcher> ``` **/ var BaseFillBlankEdit = /*#__PURE__*/ React.forwardRef(function(props, ref) { var additionalOptions = props.additionalOptions, calculatorType = props.calculatorType, enableRichContentEditor = props.enableRichContentEditor, errors = props.errors, errorsAreShowing = props.errorsAreShowing, fitbCommonWordbankEnabled = props.fitbCommonWordbankEnabled, interactionData = props.interactionData, isSurvey = props.isSurvey, notifyScreenreader = props.notifyScreenreader, onModalClose = props.onModalClose, onModalOpen = props.onModalOpen, oneQuestionAtATime = props.oneQuestionAtATime, openImportModal = props.openImportModal, overrideEditableForRegrading = props.overrideEditableForRegrading, scoringData = props.scoringData, setOneQuestionAtATime = props.setOneQuestionAtATime, validationErrorsFromApi = props.validationErrorsFromApi, styles = props.styles, showCalculatorOption = props.showCalculatorOption; var _useState = _sliced_to_array(useState(''), 2), selectedText = _useState[0], setSelectedText = _useState[1]; var controllerRef = useRef(new EditController(props)); controllerRef.current.updateProps(props); // Other edit components use the `getErrors` helper passed in from the // withEditTools higher-order component, but fill blank has its own error // handling logic var getErrors = function(path) { if (errorsAreShowing) { return get(errors, path, null); } return null; }; // ================ // HANDLERS // ================ var handleSetSelectedText = function(text) { setSelectedText(text); }; var handleCreateBlank = function(event) { controllerRef.current.onCreateBlank(event); setSelectedText(''); }; var handleCreateBlankHotkey = function(event) { if (event.which === ENTER_KEY_CODE || event.which === SPACE_KEY_CODE && event.ctrlKey && event.altKey) { event.preventDefault(); handleCreateBlank(event); notifyScreenreader(t('Blank created. Navigate to edit blank below.')); } }; var handleDescriptionChange = function(prompt) { controllerRef.current.updateInteractionData({ prompt: prompt }); }; var updateBlank = function(blankId) { return function(blankMods) { controllerRef.current.updateBlank(blankId, blankMods); }; }; var updateBlankScoringData = function(blankId) { return function(scoringDataModifications) { controllerRef.current.updateScoringDataForBlank(blankId, scoringDataModifications); }; }; // ============= // RENDERING // ============= var renderBlanksOptions = function() { return controllerRef.current.sortedBlanks().map(function(blank, blankIndex) { var blankScoringData = find({ id: blank.id }, scoringData.value) || {}; var choicesErrors = concat(getErrors("scoringData.value[".concat(blankIndex, "].scoringData.value.$errors")) || [], getErrors("interactionData.blanks[".concat(blankIndex, "].choices.$errors")) || []); return /*#__PURE__*/ jsx(BlankTypeSelect, { key: blank.id, answerType: blank.answerType, blankPosition: blankIndex + 1, blankScoringAlgorithm: blankScoringData.scoringAlgorithm, blankScoringData: blankScoringData.scoringData, disabled: overrideEditableForRegrading, choices: blank.choices, fitbCommonWordbankEnabled: fitbCommonWordbankEnabled, interactionDataErrors: getErrors("interactionData.blanks[".concat(blankIndex, "]")) || {}, isSurvey: isSurvey, name: blank.id, notifyScreenreader: notifyScreenreader, onModalClose: onModalClose, onModalOpen: onModalOpen, scoringDataErrors: getErrors("scoringData.value[".concat(blankIndex, "].scoringData")) || {}, choicesErrors: choicesErrors, validationErrorsFromApi: validationErrorsFromApi[blank.id] || [], updateBlank: updateBlank(blank.id), updateBlankScoringData: updateBlankScoringData(blank.id), useCommonWordbank: false }); }); }; var renderCreateBlankButton = function() { if (selectedText.trim() && !overrideEditableForRegrading) { return /*#__PURE__*/ jsx("div", { css: styles.createButton }, /*#__PURE__*/ jsx(Button, { onClick: handleCreateBlank, color: "primary-inverse" }, /*#__PURE__*/ jsx("span", { css: styles.createBlankText }, t('Create Blank Space')), /*#__PURE__*/ jsx("span", { css: styles.createBlankHotkeyText }, t('(Enter)')))); } }; var renderOptionsDescription = function() { return /*#__PURE__*/ jsx(ScreenReaderContent, null, t('Fill in the blank options')); }; return /*#__PURE__*/ jsx("div", null, /*#__PURE__*/ jsx(QuestionContainer, { disabled: overrideEditableForRegrading, enableRichContentEditor: enableRichContentEditor, itemBody: interactionData.prompt, onDescriptionChange: handleDescriptionChange, onModalClose: onModalClose, onModalOpen: onModalOpen, openImportModal: openImportModal, placeholder: t('Add Question Stem (optional)...'), stemErrors: [] }, /*#__PURE__*/ jsx("div", { css: styles.hint }, /*#__PURE__*/ jsx(Text, { color: "primary" }, t('Type a statement, select text, and press Enter to create a new blank.'))), /*#__PURE__*/ jsx(EditStem, { disabled: overrideEditableForRegrading, errors: getErrors('itemBody') || [], destroyBlank: controllerRef.current.onDestroyBlank, onCreateBlankHotkey: handleCreateBlankHotkey, onStemChange: controllerRef.current.onStemChange, scoringData: scoringData, setSelectedText: handleSetSelectedText, stemItems: controllerRef.current.sortedStemItems() }), /*#__PURE__*/ jsx("div", null, renderCreateBlankButton(), /*#__PURE__*/ jsx(Errors, { errorList: getErrors([ 'interactionData', 'blanks', '$errors' ]) || [] }, renderBlanksOptions()))), /*#__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: controllerRef.current.handleCalculatorTypeChange, oqaatChecked: oneQuestionAtATime, onOqaatChange: setOneQuestionAtATime }))))); }); BaseFillBlankEdit.displayName = 'FillBlankEdit'; BaseFillBlankEdit.componentId = 'QuizzesFillBlankEdit'; BaseFillBlankEdit.interactionType = FillBlankInteractionType; BaseFillBlankEdit.propTypes = { additionalOptions: PropTypes.array, calculatorType: PropTypes.string, enableRichContentEditor: PropTypes.bool, errors: PropTypes.object, errorsAreShowing: PropTypes.bool, fitbCommonWordbankEnabled: PropTypes.bool, interactionData: PropTypes.object.isRequired, isSurvey: PropTypes.bool, notifyScreenreader: PropTypes.func.isRequired, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, oneQuestionAtATime: PropTypes.bool, openImportModal: PropTypes.func, overrideEditableForRegrading: PropTypes.bool, scoringData: PropTypes.object.isRequired, setOneQuestionAtATime: PropTypes.func, validationErrorsFromApi: PropTypes.object, styles: PropTypes.object, showCalculatorOption: PropTypes.bool }; BaseFillBlankEdit.defaultProps = { calculatorType: 'none', enableRichContentEditor: true, fitbCommonWordbankEnabled: false, isSurvey: false, oneQuestionAtATime: false, overrideEditableForRegrading: false, setOneQuestionAtATime: Function.prototype, additionalOptions: void 0, errors: void 0, errorsAreShowing: void 0, onModalClose: void 0, onModalOpen: void 0, openImportModal: void 0, scoringData: void 0, validationErrorsFromApi: {}, showCalculatorOption: true }; var FillBlankEditWithStyles = withStyleOverrides(generateStyle, generateComponentTheme)(BaseFillBlankEdit); var FillBlankEdit = withEditTools(FillBlankEditWithStyles); FillBlankEdit.displayName = 'FillBlankEdit'; FillBlankEdit.componentId = 'QuizzesFillBlankEdit'; FillBlankEdit.interactionType = FillBlankInteractionType; export default FillBlankEdit;