Skip to content

Instantly share code, notes, and snippets.

@buckett
Created October 4, 2020 09:36
Show Gist options
  • Save buckett/ec3360004bde846ff21992b901acf1ba to your computer and use it in GitHub Desktop.
Save buckett/ec3360004bde846ff21992b901acf1ba to your computer and use it in GitHub Desktop.
Allow Modals to be displayed in the centre of the window when in an iframe
/*
* Copyright (c) 2020, University of Oxford
*/
import React from 'react'
import PropTypes from 'prop-types'
/**
* This is just an div that covers the whole of the iframe. When the surrounding
* page is scrolled the div is moved to the new location. You should just have
* a single one of these components in an application and all components should
* use it through the id.
*/
class FixedIFrame extends React.Component {
static propTypes = {
id: PropTypes.string
}
static defaultProps = {
id: 'lti-fixed-content'
}
state = {
height: '0px',
top: '0px'
}
/**
* Registers to get scroll events from Canvas. There's no way to unregister
* from scroll events and we don't want multiple registrations so we ensure
* that we only register once.
*/
ltiScrollEvents = () => {
// We only want to do this once.
if (!this.registeredScroll) {
window.parent.postMessage(JSON.stringify({
subject: 'lti.enableScrollEvents'
}), '*')
this.registeredScroll = true
}
}
componentDidMount() {
window.addEventListener('resize', this.resizeListener)
window.addEventListener('message', this.messageListener)
this.fetchWindowSize()
this.ltiScrollEvents()
}
componentWillUnmount() {
window.removeEventListener('message', this.resizeListener)
window.removeEventListener('resize', this.fetchWindowSize)
}
resizeListener = () => this.fetchWindowSize()
fetchWindowSize = () => {
// When running outside of the iframe don't send the message as it goes to ourselves
if (window.parent !== window) {
window.parent.postMessage(JSON.stringify({
subject: 'lti.fetchWindowSize'
}), '*')
}
}
messageListener = e => e.data && this.messageHandler(e)
messageHandler = (event) => {
let message
try {
message = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
} catch (err) {
// unparseable message may not be meant for our handlers
return
}
if (message.subject === 'lti.fetchWindowSize') {
this.message = message
this.scrollHandler()
} else if (message.subject === 'lti.scroll') {
this.message = { ...this.message, ...message }
this.scrollHandler()
}
}
scrollHandler = () => {
const message = this.message
// We might get a scroll event before we know the size of the window
if (message.offset) {
// Math.max to stop it overflowing over the beginning of the document.
const top = Math.max(message.scrollY - message.offset.top, 0)
// Math.min to stop it overflowing the bottom and generating a second scroll bar.
const bottom = Math.min(message.height + message.scrollY - message.offset.top, document.documentElement.scrollHeight)
this.setState({
top: top + 'px',
height: (bottom - top) + 'px'
})
}
}
render() {
return <div id={this.props.id} style={{
position: 'absolute',
backgroundColor: 'transparent',
width: '100%',
// This isn't working quite right, a Modal needs the element to receive events otherwise it doesn't prevent
// clicks on the mask from propagating through to the masked elements.
// We can't then move it forward in the z-index stack as it's capturing events then and so this means modal
// transitions aren't done quite right.
// pointerEvents: 'none',
// zIndex: 50,
top: this.state.top,
height: this.state.height
}}>
</div>
}
}
export default FixedIFrame
/*
* Copyright (c) 2020, University of Oxford
*/
import React from 'react'
import { Modal } from '@instructure/ui-modal'
import { passthroughProps } from '@instructure/ui-react-utils'
/**
* This correctly places Modals when displayed in an iframe.
*/
class FixedModal extends React.Component {
static defaultProps = {
fixedId: 'lti-fixed-content'
}
render() {
// This doesn't work quite right as the zIndex on the transition doesn't seem to be ontop of select elements.
const props = {
...passthroughProps(this.props),
mountNode: () => document.getElementById(this.props.fixedId),
// This is so that things work when we're not in an LTI iframe getting events
constrain: window.parent !== window ? 'parent' : 'window'
}
return <Modal {...props}>
{this.props.children}
</Modal>
}
}
export default FixedModal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment