UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

281 lines (249 loc) • 8.56 kB
import React, {Component} from 'react' import PropTypes from 'prop-types' import {v4 as uuid} from 'uuid' import {Text} from '@instructure/ui-text' import {ScreenReaderContent} from '@instructure/ui-a11y-content' import {ItemBodyWrapper} from '@instructure/quiz-rce/components/ItemBodyWrapper/index' import { RichContentInput, RCE_THREE_LINES_HEIGHT, } from '@instructure/quiz-rce/components/RichContentInput/index' import {RichContentRenderer} from '@instructure/quiz-rce/components/RichContentRenderer/index' import {WordCount} from '../helpers/WordCount' import t from '@instructure/quiz-i18n/format-message' import { DOCUMENT_TREE_DEPTH_LIMIT, CONTENT_SIZE_LIMIT, CONTENT_SIZE_BY_ANSWER, } from '@instructure/quiz-common/constants' import {TextArea} from '@instructure/quiz-common/components/TextArea/index' /** --- category: Essay --- Essay Take component ```jsx_example function Example (props) { const exampleProps = { itemBody: 'Why did the Roman Empire fall?', interactionData: { rce: true, spellCheck: true, wordCount: true, wordLimitEnabled: true, wordLimitMax: 10, wordLimitMin: 1, notes: 'Teachers grading notes' }, userResponse: { value: 'So it could learn to pick itself back up again.' } } return ( <EssayTake {...exampleProps} {...props} /> ) } <SettingsSwitcher locales={LOCALES}> <TakeStateProvider> <Example /> </TakeStateProvider> </SettingsSwitcher> ``` **/ export default class EssayTake extends Component { static propTypes = { handleResponseUpdate: PropTypes.func, interactionData: PropTypes.object.isRequired, itemBody: PropTypes.string.isRequired, userResponse: PropTypes.object, notifyScreenreader: PropTypes.func, openImportModal: PropTypes.func, readOnly: PropTypes.bool, disableDocumentAccess: PropTypes.bool, } static defaultProps = { handleResponseUpdate: () => {}, openImportModal: () => {}, readOnly: false, userResponse: void 0, notifyScreenreader: void 0, disableDocumentAccess: false, } static contextTypes = { disableTextAreaAutoGrow: PropTypes.bool, } constructor(props) { super(props) this.uniqId = uuid() const workingEssayContent = (props.userResponse && props.userResponse.value) || '' const documentTreeDepthOverLimit = this.isDocumentTreeDepthOverLimit(workingEssayContent) const dynamoRecordSize = this.getContentSize(workingEssayContent) this.state = { workingEssayContent, documentTreeDepthOverLimit, dynamoRecordSize, contentId: uuid(), } } componentDidUpdate(prevProps) { const {userResponse} = this.props const prevValue = prevProps.userResponse?.value const currentValue = userResponse?.value if (currentValue && prevValue === undefined) { this.setState({ workingEssayContent: currentValue || '', contentId: uuid(), }) } } verifyErrors = text => { const documentTreeDepthOverLimit = this.isDocumentTreeDepthOverLimit(text) const dynamoRecordSize = this.getContentSize(text) this.setState({documentTreeDepthOverLimit, dynamoRecordSize}) } isDocumentTreeDepthOverLimit = text => { if (!text) return false try { let template = document.createElement('template') template.innerHTML = text return this.isLevelOfChildrenOverLimit(template.content) } catch (e) { console.error(e) return false } } isLevelOfChildrenOverLimit = (element, level = 0) => { if (level > DOCUMENT_TREE_DEPTH_LIMIT) return true if (!element) return false const children = element.childNodes if (children.length === 0) return false const childrenLevel = level + 1 return Array.from(children).some(e => this.isLevelOfChildrenOverLimit(e, childrenLevel)) } getContentSize = text => { if (!text) return 0 const match = new RegExp('[\&\<\>]', 'g') // eslint-disable-line no-useless-escape // In backend the whole response is serialized into Unicode // every answer occupies CONTENT_SIZE_BY_ANSWER // every text answer is serialized twice // and then every ampersand, greater than and less than character // when converted to Unicode occupies 5 more bytes twice // Example, an & is turned into \u0026 return CONTENT_SIZE_BY_ANSWER + text.length * 2 + (text.match(match) || []).length * 10 } handleOnBlur = (event, rceContent) => { if (!this.props.readOnly) { const newVal = this.props.interactionData.rce ? rceContent.editorContent : event.target.value if (newVal !== void 0 && newVal !== this.state.workingEssayContent) { this.props.handleResponseUpdate(newVal) this.verifyErrors(newVal) this.setState({workingEssayContent: newVal}) } } } handleOnChange = (event, rceContent) => { if (!this.props.readOnly) { const newVal = this.props.interactionData.rce ? rceContent.editorContent : event.target.value if (newVal !== void 0 && newVal !== this.state.workingEssayContent) { this.props.handleResponseUpdate(newVal) this.setState({workingEssayContent: newVal}) } } } renderEditor() { const commonProps = { label: ( <ScreenReaderContent> <RichContentRenderer content={this.props.itemBody} /> </ScreenReaderContent> ), readOnly: this.props.readOnly, onChange: this.handleOnChange, onBlur: this.handleOnBlur, } if (this.props.interactionData.rce) { const {spellCheck, wordCount} = this.props.interactionData return ( <RichContentInput {...commonProps} textareaId={`rceTextArea_${this.uniqId}`} onKeyUp={this.handleOnChange} openImportModal={this.props.openImportModal} defaultContent={this.state.workingEssayContent} stem={this.props.itemBody} height={RCE_THREE_LINES_HEIGHT} editorOptions={{ spellCheck, wordCount, }} key={this.state.contentId} disableDocumentAccess={this.props.disableDocumentAccess} /> ) } return ( <TextArea {...commonProps} value={this.state.workingEssayContent} resize="vertical" spellCheck={this.props.interactionData.spellCheck} autoGrow={this.context.disableTextAreaAutoGrow ? false : null} /> ) } renderErrors() { const {documentTreeDepthOverLimit, dynamoRecordSize} = this.state return ( <div> <Text color="danger" size="small"> {documentTreeDepthOverLimit ? t( 'Formatting error. Please ensure that the text entry has not been pasted from a different source. Type text to submit response.', ) : ''} {dynamoRecordSize > CONTENT_SIZE_LIMIT ? t('This submission is too large. Please adjust and try to submit again.') : ''} </Text> </div> ) } renderWordCountMessage() { const {wordCount, wordLimitMin, wordLimitEnabled, wordLimitMax} = this.props.interactionData if (wordCount || wordLimitEnabled) { return ( <ScreenReaderContent> {wordLimitEnabled && wordLimitMin ? t('This essay has a minimum word count of {wordLimitMin}.', {wordLimitMin}) : ''} {wordLimitEnabled && wordLimitMax ? t('This essay has a maximum word count of {wordLimitMax}.', {wordLimitMax}) : ''} {wordCount ? t('There is a word count below the text area.') : ''} </ScreenReaderContent> ) } } render() { const {interactionData, itemBody, notifyScreenreader} = this.props return ( <ItemBodyWrapper itemBody={itemBody}> {this.renderWordCountMessage()} <div className="fs-mask">{this.renderEditor()}</div> <div> <WordCount rce={interactionData.rce} isEditing={true} essay={this.state.workingEssayContent} wordCount={interactionData.wordCount} wordLimitEnabled={interactionData.wordLimitEnabled} wordLimitMin={parseInt(interactionData.wordLimitMin, 10)} wordLimitMax={parseInt(interactionData.wordLimitMax, 10)} notifyScreenreader={notifyScreenreader} /> </div> {this.renderErrors()} </ItemBodyWrapper> ) } }