@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
218 lines (193 loc) • 5.99 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import minBy from 'lodash/fp/minBy'
import {jsx} from '@instructure/emotion'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import t from '@instructure/quiz-i18n/format-message'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
export default class TargetContainer extends Component {
static displayName = 'TargetContainer'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
hotspots: PropTypes.arrayOf(
PropTypes.shape({
coordinates: PropTypes.array,
drawingType: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
renderScoringDataFeedback: PropTypes.func,
}),
),
readOnly: PropTypes.bool,
handleSetCoordinates: PropTypes.func,
justifyContent: PropTypes.oneOf(['left', 'center']),
renderUserResponseFeedback: PropTypes.func,
results: PropTypes.bool,
url: PropTypes.string.isRequired,
userResponseCoordinate: PropTypes.arrayOf(
PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
correct: PropTypes.string,
}),
),
makeStyles: PropTypes.func,
styles: PropTypes.object,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
}
static defaultProps = {
justifyContent: 'center',
readOnly: false,
results: false,
handleSetCoordinates: void 0,
renderUserResponseFeedback: void 0,
userResponseCoordinate: [],
}
state = {
imageWidth: 0,
imageHeight: 0,
}
canvas = null
image = null
componentDidMount() {
window.addEventListener('resize', this.updateImageDimensions)
this.props.makeStyles()
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateImageDimensions)
this.props.makeStyles()
}
// ===========
// HELPERS
// ===========
convertCoordinate = item => ({
x: item.x * this.state.imageWidth,
y: item.y * this.state.imageHeight,
})
currentCoordinates = coordinates => {
if (!coordinates || !coordinates.length) return null
return coordinates.map(this.convertCoordinate)
}
validCoordinates(coordinates) {
if (!coordinates) return false
const {x, y, correct} = coordinates
return x != null && y != null && correct != null
}
getResponseDetails = (index, feedbackCoordinates) => {
let flipResponse = false
let responseCoordinates = null
let isCorrect = null
const userResponse = this.props.userResponseCoordinate?.[index]
if (this.validCoordinates(userResponse)) {
responseCoordinates = this.convertCoordinate(userResponse)
isCorrect = userResponse.correct
flipResponse = this.shouldFlipResponse(responseCoordinates, feedbackCoordinates)
}
return {responseCoordinates, isCorrect, flipResponse}
}
shouldFlipResponse = (responseCoordinates, feedbackCoordinates) => {
if (feedbackCoordinates) {
const min = minBy('y', feedbackCoordinates)
return min && responseCoordinates.y > min.y
}
return true
}
// ===========
// HANDLERS
// ===========
handleCanvasRef = node => {
this.canvas = node
}
handleImageRef = node => {
this.image = node
}
updateImageDimensions = () => {
const rectObject = ReactDOM.findDOMNode(this.image).getBoundingClientRect()
this.setState({
imageWidth: rectObject.width,
imageHeight: rectObject.height,
})
}
handleImageLoad = event => {
this.setState({
imageWidth: event.target.offsetWidth,
imageHeight: event.target.offsetHeight,
})
}
// ===========
// RENDER
// ===========
renderCanvas = () => {
const {hotspots} = this.props
if (!hotspots?.length) return null
if (hotspots?.length > 0) {
return hotspots.map(hotspot => {
const DrawingType = hotspot.drawingType
const convertedCoordinates = this.currentCoordinates(hotspot.coordinates)
return (
<DrawingType
key={JSON.stringify(hotspot.coordinates)}
coordinates={convertedCoordinates || []}
readOnly={this.props.readOnly}
height={this.state.imageHeight}
handleSetCoordinates={this.props.handleSetCoordinates}
width={this.state.imageWidth}
/>
)
})
}
}
renderImage = () => {
return (
<img
alt={t('Target Image')}
css={this.props.styles.image}
onLoad={this.handleImageLoad}
ref={this.handleImageRef}
src={this.props.url}
/>
)
}
renderFeedbackComponents = () => {
if (!this.props.results) {
return null
}
return this.props.hotspots.map((hotspot, index) => {
const feedbackCoordinates = this.currentCoordinates(hotspot.coordinates)
const renderFeedback = hotspot.renderScoringDataFeedback
const {responseCoordinates, isCorrect, flipResponse} = this.getResponseDetails(
index,
feedbackCoordinates,
)
return (
<div key={hotspot.id || index}>
{this.props.renderUserResponseFeedback(responseCoordinates, flipResponse, isCorrect)}
{renderFeedback(feedbackCoordinates, !flipResponse)}
</div>
)
})
}
render() {
return (
<div
className="fs-mask"
css={this.props.styles.targetContainerWrapper}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
>
<div css={this.props.styles.targetContainerMain}>
<div css={this.props.styles.mainContainerContentWrapper}>
<div css={this.props.styles.mainContainerContent}>
{this.renderImage()}
{this.renderCanvas()}
</div>
</div>
{this.renderFeedbackComponents()}
</div>
</div>
)
}
}