Skip to content

Commit

Permalink
Add support for running an srcDoc style iFrame (#410)
Browse files Browse the repository at this point in the history
* Add additional props to allow passing in of parent doc and window refs

* Add temp log so we can ensure fork running in tests

* Revert back to setting parent doc and window internally

* Remove unnecessary reference check

* An an iframed example to allow testing

* Fix typing of iframe example component

* Add a test to check that styles are being applied to iframe document
  • Loading branch information
glendaviesnz authored Sep 23, 2022
1 parent a46a274 commit 6cc485a
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 18 deletions.
22 changes: 22 additions & 0 deletions cypress/integration/iframe_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
describe('Iframed assertions', function () {
beforeEach(function () {
cy.viewport(1000, 600)
cy.visit('/?iframed=true')
})

const getIframeBody = () => {
return cy
.get('iframe[data-cy="iframe"]')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
}

it('Display the crop area with correct styles applied to the iframe', () => {
getIframeBody()
.find('.reactEasyCrop_CropArea')
.should('exist')
.should('have.css', 'color')
.and('eq', 'rgba(0, 0, 0, 0.5)')
})
})
48 changes: 48 additions & 0 deletions examples/src/iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react'

import './styles.css'
import { createPortal } from 'react-dom'

interface Props {
children: React.ReactNode
}

export default function Iframe({ children }: Props) {
const [iframeBody, setIframeBody] = React.useState<HTMLElement>()

const iFrameRef = React.useRef<HTMLIFrameElement>(null)

React.useEffect(() => {
function setDocumentIfReady() {
const { contentDocument } = iFrameRef.current as HTMLIFrameElement
const { readyState, documentElement } = contentDocument as Document

if (readyState !== 'interactive' && readyState !== 'complete') {
return false
}

setIframeBody(documentElement.getElementsByTagName('body')[0])

return true
}

// Document set with srcDoc is not immediately ready.
if (iFrameRef.current) {
iFrameRef.current.addEventListener('load', setDocumentIfReady)
}
}, [iFrameRef])

return (
<>
<iframe
style={{ height: '100vh', width: '100vw' }}
ref={iFrameRef}
srcDoc="<!doctype html>"
title="test iframed"
data-cy="iframe"
>
<>{iframeBody && createPortal(children, iframeBody)}</>
</iframe>
</>
)
}
31 changes: 31 additions & 0 deletions examples/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client'
import debounce from 'lodash/debounce'
import Cropper, { Area, Point } from '../../src/index'
import './styles.css'
import Iframe from './iframe'

const TEST_IMAGES = {
'/images/dog.jpeg': 'Landscape',
Expand Down Expand Up @@ -38,6 +39,7 @@ type State = {
initialCroppedAreaPixels: Area | undefined
requireCtrlKey: boolean
requireMultiTouch: boolean
iframed: boolean
}

const hashNames = ['imageSrc', 'hashType', 'x', 'y', 'width', 'height', 'rotation'] as const
Expand Down Expand Up @@ -66,6 +68,7 @@ class App extends React.Component<{}, State> {
let initialCroppedAreaPixels: Area | undefined = undefined
let hashType: HashType = 'percent'
let imageSrc = imageSrcFromQuery
const query = new URLSearchParams(window.location.search)

if (window && !urlArgs.setInitialCrop) {
const hashArray = window.location.hash.slice(1).split(',')
Expand Down Expand Up @@ -119,6 +122,7 @@ class App extends React.Component<{}, State> {
initialCroppedAreaPixels,
requireCtrlKey: false,
requireMultiTouch: false,
iframed: !!query.get('iframed'),
}
}

Expand Down Expand Up @@ -175,6 +179,33 @@ class App extends React.Component<{}, State> {
}

render() {
if (this.state.iframed) {
return (
<Iframe>
<div className="crop-container">
<Cropper
image={this.state.imageSrc}
crop={this.state.crop}
rotation={this.state.rotation}
zoom={this.state.zoom}
aspect={this.state.aspect}
cropShape={this.state.cropShape}
showGrid={this.state.showGrid}
zoomSpeed={this.state.zoomSpeed}
restrictPosition={this.state.restrictPosition}
onCropChange={this.onCropChange}
onRotationChange={this.onRotationChange}
onCropComplete={this.onCropComplete}
onCropAreaChange={this.onCropAreaChange}
onZoomChange={this.onZoomChange}
onInteractionStart={this.onInteractionStart}
onInteractionEnd={this.onInteractionEnd}
/>
</div>
</Iframe>
)
}

return (
<div className="App">
<div className="controls">
Expand Down
44 changes: 26 additions & 18 deletions src/Cropper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,37 @@ class Cropper extends React.Component<CropperProps, State> {
rafDragTimeout: number | null = null
rafPinchTimeout: number | null = null
wheelTimer: number | null = null
currentDoc: Document = document
currentWindow: Window = window

state: State = {
cropSize: null,
hasWheelJustStarted: false,
}

componentDidMount() {
window.addEventListener('resize', this.computeSizes)
if (this.containerRef) {
if (this.containerRef.ownerDocument) {
this.currentDoc = this.containerRef.ownerDocument
}
if (this.currentDoc.defaultView) {
this.currentWindow = this.currentDoc.defaultView
}
this.currentWindow.addEventListener('resize', this.computeSizes)
this.props.zoomWithScroll &&
this.containerRef.addEventListener('wheel', this.onWheel, { passive: false })
this.containerRef.addEventListener('gesturestart', this.preventZoomSafari)
this.containerRef.addEventListener('gesturechange', this.preventZoomSafari)
}

if (!this.props.disableAutomaticStylesInjection) {
this.styleRef = document.createElement('style')
this.styleRef = this.currentDoc.createElement('style')
this.styleRef.setAttribute('type', 'text/css')
if (this.props.nonce) {
this.styleRef.setAttribute('nonce', this.props.nonce)
}
this.styleRef.innerHTML = cssStyles
document.head.appendChild(this.styleRef)
this.currentDoc.head.appendChild(this.styleRef)
}

// when rendered via SSR, the image can already be loaded and its onLoad callback will never be called
Expand All @@ -144,7 +152,7 @@ class Cropper extends React.Component<CropperProps, State> {
}

componentWillUnmount() {
window.removeEventListener('resize', this.computeSizes)
this.currentWindow.removeEventListener('resize', this.computeSizes)
if (this.containerRef) {
this.containerRef.removeEventListener('gesturestart', this.preventZoomSafari)
this.containerRef.removeEventListener('gesturechange', this.preventZoomSafari)
Expand Down Expand Up @@ -191,10 +199,10 @@ class Cropper extends React.Component<CropperProps, State> {
preventZoomSafari = (e: Event) => e.preventDefault()

cleanEvents = () => {
document.removeEventListener('mousemove', this.onMouseMove)
document.removeEventListener('mouseup', this.onDragStopped)
document.removeEventListener('touchmove', this.onTouchMove)
document.removeEventListener('touchend', this.onDragStopped)
this.currentDoc.removeEventListener('mousemove', this.onMouseMove)
this.currentDoc.removeEventListener('mouseup', this.onDragStopped)
this.currentDoc.removeEventListener('touchmove', this.onTouchMove)
this.currentDoc.removeEventListener('touchend', this.onDragStopped)
}

clearScrollEvent = () => {
Expand Down Expand Up @@ -327,7 +335,7 @@ class Cropper extends React.Component<CropperProps, State> {
naturalWidth,
naturalHeight,
}

// set media size in the parent
if (this.props.setMediaSize) {
this.props.setMediaSize(this.mediaSize)
Expand Down Expand Up @@ -372,8 +380,8 @@ class Cropper extends React.Component<CropperProps, State> {

onMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.preventDefault()
document.addEventListener('mousemove', this.onMouseMove)
document.addEventListener('mouseup', this.onDragStopped)
this.currentDoc.addEventListener('mousemove', this.onMouseMove)
this.currentDoc.addEventListener('mouseup', this.onDragStopped)
this.onDragStart(Cropper.getMousePoint(e))
}

Expand All @@ -384,8 +392,8 @@ class Cropper extends React.Component<CropperProps, State> {
return
}

document.addEventListener('touchmove', this.onTouchMove, { passive: false }) // iOS 11 now defaults to passive: true
document.addEventListener('touchend', this.onDragStopped)
this.currentDoc.addEventListener('touchmove', this.onTouchMove, { passive: false }) // iOS 11 now defaults to passive: true
this.currentDoc.addEventListener('touchend', this.onDragStopped)

if (e.touches.length === 2) {
this.onPinchStart(e)
Expand All @@ -411,9 +419,9 @@ class Cropper extends React.Component<CropperProps, State> {
}

onDrag = ({ x, y }: Point) => {
if (this.rafDragTimeout) window.cancelAnimationFrame(this.rafDragTimeout)
if (this.rafDragTimeout) this.currentWindow.cancelAnimationFrame(this.rafDragTimeout)

this.rafDragTimeout = window.requestAnimationFrame(() => {
this.rafDragTimeout = this.currentWindow.requestAnimationFrame(() => {
if (!this.state.cropSize) return
if (x === undefined || y === undefined) return
const offsetX = x - this.dragStartPosition.x
Expand Down Expand Up @@ -456,8 +464,8 @@ class Cropper extends React.Component<CropperProps, State> {
const center = getCenter(pointA, pointB)
this.onDrag(center)

if (this.rafPinchTimeout) window.cancelAnimationFrame(this.rafPinchTimeout)
this.rafPinchTimeout = window.requestAnimationFrame(() => {
if (this.rafPinchTimeout) this.currentWindow.cancelAnimationFrame(this.rafPinchTimeout)
this.rafPinchTimeout = this.currentWindow.requestAnimationFrame(() => {
const distance = getDistanceBetweenPoints(pointA, pointB)
const newZoom = this.props.zoom * (distance / this.lastPinchDistance)
this.setNewZoom(newZoom, center, { shouldUpdatePosition: false })
Expand Down Expand Up @@ -488,7 +496,7 @@ class Cropper extends React.Component<CropperProps, State> {
if (this.wheelTimer) {
clearTimeout(this.wheelTimer)
}
this.wheelTimer = window.setTimeout(
this.wheelTimer = this.currentWindow.setTimeout(
() => this.setState({ hasWheelJustStarted: false }, () => this.props.onInteractionEnd?.()),
250
)
Expand Down

0 comments on commit 6cc485a

Please sign in to comment.