Created
November 13, 2018 19:37
-
-
Save Fallenstedt/3b57dacaa814c7f083874c085e2f1d53 to your computer and use it in GitHub Desktop.
LAZY LOAD YO
This file contains hidden or 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
import 'intersection-observer' | |
/** | |
* Creates a new LazyLoader. | |
* | |
* Will search for images and video elements that have a class of 'lazy' | |
* It will then Target the element's `data-src` / data-srcset property and then | |
* Lazily load the content with an IntersectionObserver or | |
* a fallback method for ancient browswers. | |
* | |
* | |
* How `<img>` tags should be made | |
* `<img | |
class="lazy" | |
data-src="/path/to/img/for/IE11.jpg" | |
data-srcset="/path/to/large-1280px.jpg 3x, | |
/path/to/medium-640px.jpg 2x, | |
/path/to/small-320px.jpg 1x" | |
src="/path/to/loading-placeholder.jpg" | |
alt="some pic">` | |
* | |
* How `<video>` elements should be made | |
* `<video id="video" | |
class="lazy" | |
poster="path/to/poster.jpg"> | |
<source data-src="path/to/video.webm" type="video/webm"> | |
<source data-src="path/to/video.mp4" type="video/mp4"> | |
</video> ` | |
* @class LazyLoader | |
* @constructor | |
* @param {string} targetClass the class attached to elements that need to be lazyily loaded. Defaults to '.lazy' | |
*/ | |
class LazyLoader { | |
constructor(targetClass) { | |
this.active = 0 // Used for throttling. Only relevant if IntersectionObserver is unsupported. | |
this.elements = [] | |
this.maxWidth = '640px' | |
this.targetClass = targetClass ? targetClass : '.lazy' | |
this.loadedClassName = 'loaded' | |
this.documentEvents = ['scroll', 'touchmove'] | |
this.windowEvents = ['orientationchange', 'resize'] | |
this.init() | |
} | |
/** | |
* Adds event listeners on DOMContentLoader to begin lazy loader | |
*/ | |
init() { | |
this.elements = this.queryDOMForElementsWithClass() | |
if (this.isMobileDevice()) { | |
// Let video elements use their placeholder images instead. Don't load the video on mobile | |
this.elements = this.elements.filter(this.removeVideoElements) | |
} | |
// If we have elements for to lazy laod | |
if (this.elements.length) { | |
const mediaObserver = new IntersectionObserver(entries => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
this.loadMedia(entry.target) | |
mediaObserver.unobserve(entry.target) | |
} | |
}) | |
}) | |
this.elements.forEach(element => mediaObserver.observe(element)) | |
} | |
} | |
replaceAttr(node, sourceAttr, targetAttr) { | |
const src = node.getAttribute(sourceAttr) | |
if (src) { | |
node[targetAttr] = src | |
node.removeAttribute(sourceAttr) | |
} | |
} | |
/** | |
* Loads the media when called. | |
* @param {DOMElement} media A dom element which contains a data attribute for the content tha tneeds to be lazy loaded | |
*/ | |
loadMedia(media) { | |
// load <video> | |
if (media.tagName == 'VIDEO') { | |
;[].slice.call(media.querySelectorAll('source')).forEach(source => { | |
this.replaceAttr(source, 'data-src', 'src') | |
}) | |
media.load() | |
} | |
// load <picture> | |
if (media.parentNode.tagName == 'PICTURE') { | |
;[].slice | |
.call(media.parentNode.querySelectorAll('source')) | |
.forEach(source => { | |
this.replaceAttr(source, 'data-src', 'src') | |
this.replaceAttr(source, 'data-srcset', 'srcset') | |
}) | |
} | |
// load <img> | |
if (media.tagName == 'IMG') { | |
this.replaceAttr(media, 'data-src', 'src') | |
this.replaceAttr(media, 'data-srcset', 'srcset') | |
} | |
// load <div> background image and apply it via css. | |
if (media.tagName == 'DIV' || media.tagName == 'ARTICLE') { | |
const node = new BackgroundNode({ | |
node: media, | |
loadedClassName: this.loadedClassName | |
}) | |
node.load() | |
} | |
media.classList.add(this.loadedClassName) | |
} | |
/** | |
* | |
* @param {DOMElement} media The media that has been loaded and needs to removed from `this.elements` array | |
* @return A function that returns a boolean if a media element is in `this.elements` | |
*/ | |
filterLazyLoadedElement(media) { | |
return this.elements.filter(elementToLoad => { | |
return elementToLoad !== media | |
}) | |
} | |
/** | |
* Checks if the window is under a certain width | |
* @return boolean | |
*/ | |
isMobileDevice() { | |
return window.matchMedia(`(max-width: ${this.maxWidth}`).matches | |
} | |
/** | |
* Returns an array of DOM elements that have the target class | |
* @return Array<DOMElements> | |
*/ | |
queryDOMForElementsWithClass() { | |
return [].slice.call(document.querySelectorAll(this.targetClass)) | |
} | |
/** | |
* Checks if a DOM object's tagName is 'VIDEO' | |
* @param {object} e an DOM object | |
*/ | |
removeVideoElements(e) { | |
return e.tagName !== 'VIDEO' | |
} | |
} | |
function BackgroundNode({ node, loadedClassName }) { | |
let src = node.getAttribute('data-background-image-url') | |
let show = () => { | |
requestAnimationFrame(() => { | |
node.style.backgroundImage = `url(${src})` | |
node.classList.add(loadedClassName) | |
}) | |
} | |
return { | |
load: () => { | |
let img = new Image() | |
img.src = src | |
img.onload = show | |
} | |
} | |
} | |
export default LazyLoader |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment