UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

338 lines (309 loc) • 12.4 kB
/** @jsx jsx */ import React, {useMemo, 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 update from 'immutability-helper' import {Checkbox} from '@instructure/ui-checkbox' import {jsx} from '@instructure/emotion' import QuestionSettingsContainer from '../../common/edit/components/QuestionSettingsContainer' import QuestionContainer from '../../common/edit/components/QuestionContainer' import RichFillBlankInteractionType from '../../../records/interactions/rich_fill_blank' import BlankTypeSelect from '../../fill_blank/Edit/answer_field/BlankTypeSelect' import WordBankDistractors from './WordBankDistractors' 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 {Text} from '@instructure/ui-text' import {Flex} from '@instructure/quiz-common/components/Flex/index' import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides' import {FormFieldGroup} from '@instructure/quiz-common/components/FormFieldGroup/index' import useBlankManagement from './useBlankManagement' function BlanksOptions({ blanks, scoringDataValue, fitbCommonWordbankEnabled, overrideEditableForRegrading, wordBankChoices, notifyScreenreader, onModalClose, onModalOpen, validationErrorsFromApi, getErrors, updateBlank, updateScoringDataForBlank, isSurvey, }) { const hasIndividualWordBanks = blanks.some(blank => { return blank.answerType === 'wordbank' && blank.choices && blank.choices.length }) return blanks.map((blank, blankIndex) => { const blankScoringData = find({id: blank.id}, scoringDataValue) || {} const choicesErrors = concat( getErrors(`scoringData.value[${blankIndex}].scoringData.value.$errors`) || [], getErrors(`interactionData.blanks[${blankIndex}].choices.$errors`) || [], ) return ( <BlankTypeSelect key={blank.id} answerType={blank.answerType} blankPosition={blankIndex + 1} blankScoringAlgorithm={blankScoringData.scoringAlgorithm} blankScoringData={blankScoringData.scoringData} choices={blank.choices} disabled={overrideEditableForRegrading} interactionDataErrors={getErrors(`interactionData.blanks[${blankIndex}]`) || {}} name={blank.id} notifyScreenreader={notifyScreenreader} onModalClose={onModalClose} onModalOpen={onModalOpen} scoringDataErrors={getErrors(`scoringData.value[${blankIndex}].scoringData`) || {}} choicesErrors={choicesErrors} validationErrorsFromApi={validationErrorsFromApi[blank.id] || []} wordBankChoices={wordBankChoices} updateBlank={updateBlank(blank.id)} updateBlankScoringData={updateScoringDataForBlank(blank.id)} useCommonWordbank={fitbCommonWordbankEnabled && !hasIndividualWordBanks} isSurvey={isSurvey} /> ) }) } BlanksOptions.propTypes = { blanks: PropTypes.array.isRequired, scoringDataValue: PropTypes.array, fitbCommonWordbankEnabled: PropTypes.bool, overrideEditableForRegrading: PropTypes.bool, wordBankChoices: PropTypes.array, notifyScreenreader: PropTypes.func.isRequired, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, validationErrorsFromApi: PropTypes.object, getErrors: PropTypes.func.isRequired, updateBlank: PropTypes.func.isRequired, updateScoringDataForBlank: PropTypes.func.isRequired, isSurvey: PropTypes.bool, } const RichFillBlankEdit = React.forwardRef((props, ref) => { const [deletedBlanksPool, setDeletedBlanksPool] = useState([]) const [deletedDistractors, setDeletedDistractors] = useState([]) const validBlankRegex = useMemo(() => { return new RegExp(props.interactionType.validBlankRegex, 'g') }, [props.interactionType.validBlankRegex]) // Other edit components use the `getErrors` helper passed in from the // withEditTools higher-order component, but fill blank has its own error // handling logic const getErrors = path => { if (props.errorsAreShowing) { return get(props.errors, path, null) } return null } const {handleDescriptionChange, updateBlank, updateScoringDataForBlank} = useBlankManagement({ interactionData: props.interactionData, scoringData: props.scoringData, changeItemState: props.changeItemState, properties: props.properties, validBlankRegex, deletedBlanksPool, setDeletedBlanksPool, deletedDistractors, setDeletedDistractors, fitbCommonWordbankEnabled: props.fitbCommonWordbankEnabled, }) const indexForWordBankChoice = choiceId => { return props.interactionData.wordBankChoices.findIndex( wordBankChoice => wordBankChoice.id === choiceId, ) } const makeNewProperties = interactionData => { // syncs shuffling properties data with new interaction data. // this runs each time we changeItemState so that we dont have // to manage syncing properties and interactionData on each individual // change to blank type or blank creation/deletion const blankRules = interactionData.blanks.reduce((memo, blank, blankIndex) => { // eslint-disable-next-line no-param-reassign memo[blankIndex] = blank.answerType === 'openEntry' ? {children: null} : {children: {choices: {shuffled: true}}} return memo }, {}) const newShuffleRules = { shuffleRules: { blanks: { children: blankRules, }, }, } return Object.assign({}, props.properties, newShuffleRules) } const changeItemState = (modifications, blankId) => { const newestIntData = Object.assign({}, props.interactionData, modifications.interactionData) const newProperties = makeNewProperties(newestIntData) const newMods = {properties: newProperties} if (modifications.scoringData) { newMods.scoringData = modifications.scoringData } const newModifications = Object.assign({}, modifications, newMods) props.changeItemState(newModifications, blankId) } // ================ // HANDLERS // ================ const handleCheckboxChange = event => { const interactionData = update(props.interactionData, { [event.target.value]: {$set: !props.interactionData[event.target.value]}, }) changeItemState({interactionData}) } const handleCalculatorTypeChange = (e, value) => { changeItemState({ calculatorType: value, }) } const stemErrors = getErrors('scoringData.workingItemBody') || [] const blankErrors = getErrors(['interactionData', 'blanks', '$errors']) || [] return ( <div ref={ref}> <Flex direction="column" padding="0 0 small 0"> <Text> {t( 'Type a statement and surround a word with backticks to indicate where a student will fill in the answer. (e.g., "Roses are `red`, violets are `blue`")', )} </Text> </Flex> <QuestionContainer disabled={props.overrideEditableForRegrading} enableRichContentEditor={props.enableRichContentEditor} itemBody={props.scoringData.workingItemBody} onDescriptionChange={handleDescriptionChange} onModalClose={props.onModalClose} onModalOpen={props.onModalOpen} openImportModal={props.openImportModal} stemErrors={stemErrors.concat(blankErrors) || []} > <BlanksOptions blanks={props.interactionData.blanks} scoringDataValue={props.scoringData.value} fitbCommonWordbankEnabled={props.fitbCommonWordbankEnabled} overrideEditableForRegrading={props.overrideEditableForRegrading} wordBankChoices={props.interactionData.wordBankChoices} notifyScreenreader={props.notifyScreenreader} onModalClose={props.onModalClose} onModalOpen={props.onModalOpen} validationErrorsFromApi={props.validationErrorsFromApi} getErrors={getErrors} updateBlank={updateBlank} updateScoringDataForBlank={updateScoringDataForBlank} isSurvey={props.isSurvey} /> <WordBankDistractors changeItemState={props.changeItemState} indexForWordBankChoice={indexForWordBankChoice} interactionData={props.interactionData} interactionDataErrors={getErrors('interactionData.wordBankChoices') || {}} notifyScreenreader={props.notifyScreenreader} onModalClose={props.onModalClose} onModalOpen={props.onModalOpen} overrideEditableForRegrading={props.overrideEditableForRegrading} scoringData={props.scoringData} isSurvey={props.isSurvey} /> </QuestionContainer> <QuestionSettingsContainer additionalOptions={props.additionalOptions}> <QuestionSettingsPanel label={t('Options')} defaultExpanded> <FormFieldGroup rowSpacing="small" description={ <ScreenReaderContent>{t('Fill in the blank options')}</ScreenReaderContent> } > {props.interactionData.wordBankChoices && ( <Checkbox label={t('Allow word bank choices to be reused')} value="reuseWordBankChoices" onChange={handleCheckboxChange} checked={props.interactionData.reuseWordBankChoices} disabled={props.overrideEditableForRegrading} /> )} {props.showCalculatorOption && ( <CalculatorOptionWithOqaatAlert disabled={props.overrideEditableForRegrading} calculatorValue={props.calculatorType} onCalculatorTypeChange={handleCalculatorTypeChange} oqaatChecked={props.oneQuestionAtATime} onOqaatChange={props.setOneQuestionAtATime} /> )} </FormFieldGroup> </QuestionSettingsPanel> </QuestionSettingsContainer> </div> ) }) RichFillBlankEdit.displayName = 'RichFillBlankEdit' RichFillBlankEdit.interactionType = RichFillBlankInteractionType RichFillBlankEdit.propTypes = { additionalOptions: PropTypes.array, calculatorType: PropTypes.string, changeItemState: PropTypes.func.isRequired, enableRichContentEditor: PropTypes.bool, errors: PropTypes.object, errorsAreShowing: PropTypes.bool, fitbCommonWordbankEnabled: PropTypes.bool, interactionData: PropTypes.object.isRequired, interactionType: PropTypes.shape({ validBlankRegex: PropTypes.string, }).isRequired, notifyScreenreader: PropTypes.func.isRequired, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, oneQuestionAtATime: PropTypes.bool, openImportModal: PropTypes.func, overrideEditableForRegrading: PropTypes.bool, properties: PropTypes.shape({ shuffleRules: PropTypes.shape({ blanks: PropTypes.shape({ children: PropTypes.object, }), }), }), scoringData: PropTypes.object.isRequired, setOneQuestionAtATime: PropTypes.func, validationErrorsFromApi: PropTypes.object, styles: PropTypes.object, showCalculatorOption: PropTypes.bool, isSurvey: PropTypes.bool, } RichFillBlankEdit.defaultProps = { calculatorType: 'none', enableRichContentEditor: true, fitbCommonWordbankEnabled: 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, properties: {}, scoringData: void 0, validationErrorsFromApi: {}, showCalculatorOption: true, } const RichFillBlankEditWithTools = withEditTools(RichFillBlankEdit) const RichFillBlankEditWithStyles = withStyleOverrides( generateStyle, generateComponentTheme, )(RichFillBlankEditWithTools) RichFillBlankEditWithStyles.componentId = `Quizzes${RichFillBlankEdit.displayName}` export default RichFillBlankEditWithStyles