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