UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

198 lines (163 loc) • 5.78 kB
/** @jsx jsx */ import React, {useEffect, useLayoutEffect, useRef, useState} from 'react' import PropTypes from 'prop-types' import debounce from 'lodash/debounce' import partial from 'lodash/partial' import {jsx} from '@instructure/emotion' import {AccessibleContent, ScreenReaderContent} from '@instructure/ui-a11y-content' import {Tag} from '@instructure/ui-tag' import {View} from '@instructure/ui-view' import Errors from '../../../common/edit/components/Errors' import ContentEditable from '../../../common/edit/components/ContentEditable' import EditStemController from './editStemController' import generateStyle from './styles' import generateComponentTheme from './theme' import t from '@instructure/quiz-i18n/format-message' import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides' const NBSP_CHAR_CODE = 160 function BaseEditStem(props) { const { disabled, errors, destroyBlank, onCreateBlankHotkey, onStemChange, setSelectedText, stemItems, styles, makeStyles, } = props const [isFocused, setIsFocused] = useState(false) const controllerRef = useRef(new EditStemController(props)) const stemItemRefsRef = useRef({}) const domRef = useRef(null) const oldOnSelectionChangeRef = useRef(null) const prevStemItemsRef = useRef(stemItems) controllerRef.current.updateProps(props) useEffect(() => { const oldOnSelectionChange = document.onselectionchange const newOnSelectionChange = debounce(() => { const sel = window.getSelection() const anchor = sel.anchorNode if (!anchor || !domRef.current) return if ( domRef.current.contains(anchor) || // next line needed because of weird IE11 behavior domRef.current.contains(anchor.parentNode) ) { setSelectedText(sel.toString()) } }, 100) document.onselectionchange = e => { oldOnSelectionChange && oldOnSelectionChange(e) newOnSelectionChange() } oldOnSelectionChangeRef.current = oldOnSelectionChange return () => { document.onselectionchange = oldOnSelectionChangeRef.current } }, [setSelectedText]) useLayoutEffect(() => { makeStyles() if (!isFocused) { prevStemItemsRef.current = stemItems return } // Check if stemItems structurally changed (blank added/removed) const prevProps = {stemItems: prevStemItemsRef.current} const focusData = controllerRef.current.getFocusData(prevProps) // Update ref after checking for changes prevStemItemsRef.current = stemItems if (!focusData) { return } const focusRef = stemItemRefsRef.current[focusData.stemItem.id] if (focusRef) { focusData.stemItem.type === 'blank' ? focusRef.focus() : focusRef.focusAndSetCursorOnPosition(focusData.cursorPosition) } }) const handlePaste = (stemItem, e) => { controllerRef.current.handlePaste(e) onStemChange(stemItem, e) } const setFocus = focusState => { setIsFocused(focusState) } const renderContentEditable = stemItem => { const usePlaceholder = stemItems.length <= 1 const placeholder = usePlaceholder ? t('Type a statement...') : '' let content = controllerRef.current.textForStemItem(stemItem) if (!content || content === String.fromCharCode(NBSP_CHAR_CODE)) { // nbsp is used to give a width to a final stem item if blank // if we only have one stem, just use the placeholder to do this content = usePlaceholder ? '' : String.fromCharCode(NBSP_CHAR_CODE) } return ( <ContentEditable label={<ScreenReaderContent>{t('Statement')}</ScreenReaderContent>} key={stemItem.id} id={stemItem.id} ref={span => { stemItemRefsRef.current[stemItem.id] = span }} placeholder={placeholder} enableFormatting={false} disabled={disabled} content={content} onChange={partial(onStemChange, stemItem)} onPaste={partial(handlePaste, stemItem)} onKeyDown={onCreateBlankHotkey} customStyles={styles.editableText} /> ) } const renderBlank = stemItem => { const dismissible = !disabled const text = controllerRef.current.textForStemItem(stemItem) return ( <Tag key={stemItem.id} dismissible={dismissible} elementRef={button => { stemItemRefsRef.current[stemItem.id] = button }} onClick={dismissible ? partial(destroyBlank, stemItem) : null} size="large" text={ <AccessibleContent alt={t('Delete "{ text }" blank', {text})}>{text}</AccessibleContent> } variant="inline" /> ) } const renderStemItem = stemItem => { return stemItem.type === 'text' ? renderContentEditable(stemItem) : renderBlank(stemItem) } stemItemRefsRef.current = {} return ( <div ref={domRef} onFocus={partial(setFocus, true)} onBlur={partial(setFocus, false)}> <Errors errorList={errors}>{stemItems.map(renderStemItem)}</Errors> <View as="div" borderWidth="small none none none" margin="small none" /> </div> ) } BaseEditStem.displayName = 'EditStem' BaseEditStem.componentId = 'QuizzesEditStem' BaseEditStem.propTypes = { disabled: PropTypes.bool, errors: PropTypes.array, destroyBlank: PropTypes.func.isRequired, onCreateBlankHotkey: PropTypes.func.isRequired, onStemChange: PropTypes.func.isRequired, setSelectedText: PropTypes.func.isRequired, stemItems: PropTypes.array.isRequired, styles: PropTypes.object, makeStyles: PropTypes.func, } BaseEditStem.defaultProps = { disabled: false, errors: void 0, } export default withStyleOverrides(generateStyle, generateComponentTheme)(BaseEditStem)