Created
August 19, 2019 01:41
-
-
Save janicduplessis/172cf7a8b36426627e99b2243a08ee08 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global IntersectionObserver */ | |
import * as React from 'react'; | |
// @ts-ignore | |
import { findNodeHandle } from 'react-native-web'; | |
import { BaseImage, BaseImageProps } from './BaseImage'; | |
import { Box } from '../Box'; | |
// Cache if we've seen an image before so we don't both with | |
// lazy-loading & fading in on subsequent mounts. | |
const imageCache = new Set(); | |
type IOListener = [HTMLElement, () => void]; | |
let io: IntersectionObserver | null = null; | |
const listeners: IOListener[] = []; | |
function getIO() { | |
if ( | |
io === null && | |
typeof window !== `undefined` && | |
typeof IntersectionObserver !== 'undefined' | |
) { | |
io = new IntersectionObserver( | |
entries => { | |
entries.forEach(entry => { | |
listeners.forEach(l => { | |
if (io !== null && l[0] === entry.target) { | |
// Edge doesn't currently support isIntersecting, so also test for an intersectionRatio > 0 | |
if (entry.isIntersecting || entry.intersectionRatio > 0) { | |
io.unobserve(l[0]); | |
l[1](); | |
} | |
} | |
}); | |
}); | |
}, | |
{ rootMargin: `200px` }, | |
); | |
} | |
return io; | |
} | |
const listenToIntersections = (el: HTMLElement, cb: () => void) => { | |
const observer = getIO(); | |
if (observer !== null) { | |
observer.observe(el); | |
listeners.push([el, cb]); | |
} | |
}; | |
type State = { | |
isVisible: boolean; | |
imgLoaded: boolean; | |
IOSupported: boolean; | |
fadeIn?: boolean; | |
seenBefore: boolean; | |
}; | |
const resolveSource = (source: BaseImageProps['source']) => { | |
if (source == null) { | |
return null; | |
} | |
if (typeof source === 'object' && source.uri != null) { | |
return source.uri; | |
} else if (typeof source === 'string') { | |
return source; | |
} else { | |
return null; | |
} | |
}; | |
export class Image extends React.Component<BaseImageProps, State> { | |
static defaultProps = { | |
critical: false, | |
fadeIn: true, | |
objectPosition: 'center', | |
}; | |
state: State; | |
constructor(props: BaseImageProps) { | |
super(props); | |
// If this browser doesn't support the IntersectionObserver API | |
// we default to start downloading the image right away. | |
let isVisible = true; | |
let imgLoaded = true; | |
let IOSupported = false; | |
const fadeIn = props.fadeIn; | |
// If this image has already been loaded before then we can assume it's | |
// already in the browser cache so it's cheap to just show directly. | |
const imageUrl = resolveSource(props.source); | |
const seenBefore = imageUrl != null && imageCache.has(imageUrl); | |
if ( | |
!seenBefore && | |
typeof window !== 'undefined' && | |
typeof IntersectionObserver !== 'undefined' | |
) { | |
isVisible = false; | |
imgLoaded = false; | |
IOSupported = true; | |
} | |
// Always don't render image while server rendering | |
if (typeof window === 'undefined') { | |
isVisible = false; | |
imgLoaded = false; | |
} | |
this.state = { | |
isVisible, | |
imgLoaded, | |
IOSupported, | |
fadeIn, | |
seenBefore, | |
}; | |
} | |
_imageRef = React.createRef<HTMLImageElement>(); | |
componentDidMount() { | |
if (this.props.critical) { | |
const img = this._imageRef.current; | |
if (img && img.complete) { | |
this._handleImageLoaded(); | |
} | |
} | |
} | |
_handleRef = (ref: any) => { | |
if (this.state.IOSupported && ref != null) { | |
listenToIntersections(findNodeHandle(ref), () => { | |
this.setState({ isVisible: true }); | |
}); | |
} | |
}; | |
_handleImageLoaded = () => { | |
this.setState({ imgLoaded: true }); | |
if (this.state.seenBefore) { | |
this.setState({ fadeIn: false }); | |
} else { | |
const imageUrl = resolveSource(this.props.source); | |
if (imageUrl != null) { | |
imageCache.add(imageUrl); | |
} | |
} | |
this.props.onLoad && this.props.onLoad(); | |
}; | |
render() { | |
const { | |
source, | |
onLoad, | |
resizeMode, | |
tintColor, | |
resizeMethod, | |
...others | |
} = this.props; | |
return ( | |
<Box | |
ref={this._handleRef} | |
opacity={ | |
// This avoid a weird bug with server rendering in WKWebview were | |
// opacity stays set a 0. | |
typeof window === 'undefined' | |
? undefined | |
: this.state.imgLoaded || this.state.fadeIn === false | |
? 1 | |
: 0 | |
} | |
css={{ | |
// @ts-ignore | |
transition: this.state.fadeIn === true ? 'opacity 0.5s' : 'none', | |
}} | |
overflow="hidden" | |
width={ | |
source != null && typeof source === 'object' | |
? source.width | |
: undefined | |
} | |
height={ | |
source != null && typeof source === 'object' | |
? source.height | |
: undefined | |
} | |
{...others} | |
> | |
{this.state.isVisible && ( | |
<BaseImage | |
source={source} | |
onLoad={this._handleImageLoaded} | |
resizeMode={resizeMode} | |
resizeMethod={resizeMethod} | |
tintColor={tintColor} | |
flexGrow={1} | |
/> | |
)} | |
</Box> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment