UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

227 lines (204 loc) • 8.12 kB
/** @jsx jsx */ import React, {useCallback, useMemo} from 'react' import PropTypes from 'prop-types' import {Alert} from '@instructure/ui-alerts' import {Text} from '@instructure/ui-text' import {jsx} from '@instructure/emotion' import {ItemBodyWrapper} from '@instructure/quiz-rce/components/ItemBodyWrapper/index' import t from '@instructure/quiz-i18n/format-message' import Wordbank from '../drag_components/Wordbank' import TakeStem from './take_components/TakeStem' import generateStyle from './styles' import generateComponentTheme from './theme' import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides' /** --- category: FillInTheBlank --- Fill in the Blank Take component ```jsx_example function Example (props) { const exampleProps = { interactionData: { prompt: '<p><strong>Please</strong> fill in all the blanks</p>', stemItems: [ { id: 'stem_uuid-1', position: 1, type: 'text', value: ' ... ' }, { id: 'stem_uuid0', position: 2, type: 'blank', blankId: 'fitb_uuid1' }, { id: 'stem_uuid1', position: 3, type: 'text', value: ' Columbus sailed in ' }, { id: 'stem_uuid2', position: 4, type: 'blank', blankId: 'fitb_uuid2' }, { id: 'stem_uuid3', position: 5, type: 'text', value: ' , from the country of ' }, { id: 'stem_uuid4', position: 6, type: 'blank', blankId: 'fitb_uuid3' }, { id: 'stem_uuid5', position: 7, type: 'text', value: ' , on the continent of ' }, { id: 'stem_uuid6', position: 8, type: 'blank', blankId: 'fitb_uuid4' }, { id: 'stem_uuid7', position: 9, type: 'text', value: ' , across the ' }, { id: 'stem_uuid8', position: 10, type: 'blank', blankId: 'fitb_uuid5' }, { id: 'stem_uuid9', position: 11, type: 'text', value: ' , and found... ' }, { id: 'stem_uuid10', position: 12, type: 'blank', blankId: 'fitb_uuid6' }, { id: 'stem_uuid11', position: 13, type: 'text', value: ' !!! ' }, { id: 'stem_uuid12', position: 14, type: 'text', value: ' Where the people were " ' }, { id: 'stem_uuid13', position: 15, type: 'blank', blankId: 'fitb_uuid7' }, { id: 'stem_uuid14', position: 16, type: 'text', value: ' " ' } ], blanks: [{ id: 'fitb_uuid1', answerType: 'openEntry' }, { id: 'fitb_uuid2', answerType: 'openEntry' }, { id: 'fitb_uuid3', answerType: 'openEntry' }, { id: 'fitb_uuid4', answerType: 'openEntry' }, { id: 'fitb_uuid5', answerType: 'openEntry' }, { id: 'fitb_uuid6', choices: [ { id: 'choice_uuid11_brazil', position: 1, itemBody: 'Brazil' }, { id: 'choice_uuid12_austria', position: 2, itemBody: 'Austria' }, { id: 'choice_uuid13_america', position: 3, itemBody: 'America' } ], answerType: 'wordbank' }, { id: 'fitb_uuid7', choices: [ { id: 'choice_uuid11_peaceful', position: 1, itemBody: 'peaceful' }, { id: 'choice_uuid12_war-torn', position: 2, itemBody: 'war-torn' }, { id: 'choice_uuid13_confused', position: 3, itemBody: 'confused' } ], answerType: 'dropdown' }] }, userResponse: { value: [ { id: 'fitb_uuid1', value: 'Christopher', type: 'Text'}, { id: 'fitb_uuid2', value: '1493', type: 'Text'}, { id: 'fitb_uuid3', value: 'Span', type: 'Text'}, { id: 'fitb_uuid4', value: 'Europe', type: 'Text'}, { id: 'fitb_uuid5', value: 'Atlantic', type: 'Text'}, { id: 'fitb_uuid6', value: 'choice_uuid12_austria', type: 'Text'}, { id: 'fitb_uuid7', value: 'choice_uuid13_confused', type: 'Text'} ] } } return ( <DndProvider backend={HTML5Backend}> <FillBlankTake {...exampleProps} {...props} /> </DndProvider> ) } <SettingsSwitcher locales={LOCALES}> <TakeStateProvider> <Example /> </TakeStateProvider> </SettingsSwitcher> ``` **/ // ============================================ // Sub-components // ============================================ function DeprecationWarning() { return ( <Alert variant="error"> <Text color="primary"> <b>{t('This question type is deprecated')}</b> </Text> <ul> <li>{t('Quizzes containing deprecated question types will fail to copy.')}</li> <li>{t('This question was unable to be automatically upgraded due to malformed data.')}</li> <li> {t( 'Please delete this question and recreate it using the current Fill in the Blank question type.', )} </li> </ul> </Alert> ) } function WordbankSection({blanks, response, returnChoiceToWordbank, wordBankChoices}) { if (blanks.find(blank => ['wordbank', 'dropdown'].includes(blank.answerType))) { return ( <Wordbank blanks={blanks} response={response} returnChoiceToWordbank={returnChoiceToWordbank} wordBankChoices={wordBankChoices} /> ) } return null } // ============================================ // Main Component // ============================================ const FillBlankTake = React.forwardRef((props, ref) => { const {handleResponseUpdate, interactionData, userResponse, readOnly = false, styles} = props const blanks = useMemo(() => interactionData?.blanks || [], [interactionData?.blanks]) const moveChoiceToWordbank = useCallback( (blankId, choiceId) => { const responses = userResponse.value const blank = responses.find(r => r.id === blankId) // Removes the answer for blank only if choice is selected response for blank if (blank?.value === choiceId) { const blankIndex = userResponse.value.findIndex(r => r.id === blankId) const newResponses = responses.map((r, i) => (i === blankIndex ? {...r, value: null} : r)) handleResponseUpdate(newResponses) } }, [userResponse.value, handleResponseUpdate], ) const remainingWordBankChoices = useMemo(() => { const wordbankBlanks = interactionData.blanks.filter(blank => blank.answerType === 'wordbank') const choices = [...wordbankBlanks.map(blank => blank.choices).flat()] // filter the choices which are already used userResponse?.value?.forEach(response => { const choiceIndex = choices.findIndex(choice => choice.id === response.value) if (choiceIndex > -1) { choices.splice(choiceIndex, 1) } }) return choices }, [interactionData.blanks, userResponse.value]) return ( <ItemBodyWrapper itemBody={interactionData.prompt}> {readOnly && <DeprecationWarning />} <div css={styles.question}> <div css={styles.headerSection}> <TakeStem onUpdate={handleResponseUpdate} interactionData={interactionData} returnChoiceToWordbank={moveChoiceToWordbank} userResponses={userResponse.value} readOnly={readOnly} wordBankChoices={remainingWordBankChoices} /> </div> <WordbankSection blanks={blanks} response={userResponse.value} returnChoiceToWordbank={moveChoiceToWordbank} wordBankChoices={remainingWordBankChoices} /> </div> </ItemBodyWrapper> ) }) FillBlankTake.displayName = 'FillBlankTake' FillBlankTake.propTypes = { handleResponseUpdate: PropTypes.func, interactionData: PropTypes.object.isRequired, userResponse: PropTypes.object.isRequired, readOnly: PropTypes.bool, styles: PropTypes.object, } const FillBlankTakeWithStyles = withStyleOverrides( generateStyle, generateComponentTheme, )(FillBlankTake) FillBlankTakeWithStyles.displayName = 'FillBlankTake' FillBlankTakeWithStyles.componentId = `Quizzes${FillBlankTake.displayName}` FillBlankTakeWithStyles.originalType = FillBlankTake export default FillBlankTakeWithStyles