@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
391 lines (386 loc) • 16.4 kB
JavaScript
/** @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;