@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
200 lines (167 loc) • 4.61 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import pickBy from 'lodash/pickBy'
import propTypes, {defaultProps} from './propTypes'
import {jsx} from '@instructure/emotion'
import generateComponentTheme from './theme'
import generateStyle from './styles'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
export default class Canvas extends Component {
static displayName = 'Canvas'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
...propTypes,
realRef: PropTypes.func,
drawShape: PropTypes.func.isRequired,
onMouseOut: PropTypes.func,
onMouseMove: PropTypes.func,
onDoubleClick: PropTypes.func,
onClick: PropTypes.func,
onKeyPress: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseUp: PropTypes.func,
onTouchStart: PropTypes.func,
onTouchMove: PropTypes.func,
onTouchEnd: PropTypes.func,
styles: PropTypes.object,
}
static defaultProps = {
realRef: null,
onMouseOut: null,
onMouseMove: null,
onDoubleClick: null,
onClick: null,
onKeyPress: null,
onMouseDown: null,
onMouseUp: null,
onTouchStart: null,
onTouchMove: null,
onTouchEnd: null,
...defaultProps,
}
state = {
isDrawing: false,
}
constructor(props) {
super(props)
// these handlers shouldn't change, so set them once per initialization
this.eventHandlers = {
onMouseOut: this.onMouseOut,
...pickBy(props, (prop, propName) => prop !== null && propName.startsWith('on')),
}
}
componentDidMount() {
if (this.props.realRef) {
this.props.realRef(this)
}
this.drawShape(this.props.coordinates)
}
componentDidUpdate() {
this.drawShape(this.props.coordinates)
if (this.isDrawing()) {
this.canvasElement.focus()
}
}
// ===========
// HELPERS
// ===========
isDrawing() {
return this.state.isDrawing
}
getInitialCoordinate() {
return this.props.coordinates[0]
}
getLastCoordinate() {
return this.props.coordinates[this.props.coordinates.length - 1]
}
getCurrentCoordinates(e) {
const x = e.nativeEvent.offsetX
const y = e.nativeEvent.offsetY
return {x, y}
}
convertTouchCoordinates(touchEvent) {
const rect = this.canvasElement.getBoundingClientRect()
const x = touchEvent.changedTouches[0].pageX - rect.left
const y = touchEvent.changedTouches[0].pageY - rect.top
const evt = {
nativeEvent: {
offsetX: x,
offsetY: y,
},
}
return evt
}
focus() {
this.canvasElement.focus()
}
refCanvas = node => {
this.canvasElement = node
}
// ===========
// ACTIONS
// ===========
drawShape = coordinates => {
this.clearCanvas()
this.props.drawShape(coordinates)
}
clearCanvas() {
const ctx = this.canvasElement.getContext('2d')
ctx.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height)
}
setStrokeStyle(color = this.props.styles.shapeColor) {
const ctx = this.canvasElement.getContext('2d')
ctx.lineWidth = 2
ctx.strokeStyle = color
}
setFillStyle(color = this.props.styles.shapeColor) {
const ctx = this.canvasElement.getContext('2d')
ctx.globalAlpha = 0.5
ctx.fillStyle = color
}
startDrawing(e) {
const initialCoordinate = this.getCurrentCoordinates(e)
this.props.handleSetCoordinates([initialCoordinate], true)
this.setState({
isDrawing: true,
})
}
stopDrawing(coordinates) {
this.props.handleSetCoordinates(coordinates, false)
this.setState({
isDrawing: false,
})
}
// ===========
// HANDLERS
// ===========
onMouseOut = e => {
// the mouse out event is handled by default as a click/release on the exiting canvas border
this.props.onMouseOut // eslint-disable-line no-unused-expressions
? this.props.onMouseOut(e)
: e => {}
}
// ===========
// RENDER
// ===========
renderCanvas() {
if (this.props.readOnly) {
return <canvas height={this.props.height} ref={this.refCanvas} width={this.props.width} />
}
return (
<canvas
css={this.props.styles.canvas}
height={this.props.height}
ref={this.refCanvas}
tabIndex="0"
width={this.props.width}
onBlur={this.onMouseOut}
{...this.eventHandlers}
/>
)
}
render() {
return <div css={this.props.styles.canvasWrapper}>{this.renderCanvas()}</div>
}
}