UNPKG

uncloakjs

Version:

IntersectionObserver-based lazy loader and content revealer

633 lines (522 loc) 19.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Uncloak = factory()); }(this, (function () { 'use strict'; var UncloakItem = function UncloakItem(node, instance, options) { node.removeAttribute('data-uncloak-new'); this.callbacks = { init: [], uncloak: [] }; for (var key in this.callbacks) { if (key in options.callbacks) { this.callbacks[key] = options.callbacks[key]; } } this.cloaked = true; this.uncloakReady = false; this.delayTimer = { y0: 0, y1: null }; this.delayType = node.getAttribute('data-uncloak-delay-type') || null; this.delayTypes = options.delayTypes || {}; this.instance = instance; this.lazyContent = node.hasAttribute('data-uncloak-ignore-lazy') ? [] : Array.from(node.querySelectorAll('[data-uncloak-src], [data-uncloak-srcset], [data-uncloak-picture]')).filter(function (el) { return !el.hasAttribute('data-uncloak-ignore'); }); this.lazyContentLoadStatus = (this.lazyContent[0] ? -1 : 2); // NB: -1 => unloaded, 1 => loading, 2 => loaded this.node = node; this.threshold = parseFloat(node.getAttribute('data-uncloak-threshold')) || 0; this.lazyContentObserver = null; }; UncloakItem.prototype.init = function init () { var this$1 = this; if (this.lazyContent[0]) { this.lazyContentObserver = new IntersectionObserver(function (entries) { var entry = entries[0]; if (entry.isIntersecting) { this$1.loadLazyContent(); // images only need loading once, so stop observing once loaded this$1.lazyContentObserver.disconnect(); } }, { rootMargin: '50%' }); this.lazyContentObserver.observe(this.node); } this.runCallbacks('init'); }; UncloakItem.prototype.process = function process (base_delay) { if (this.cloaked) { // only calculate delay if item has a delay type and hasn't already been calculated if (this.delayType !== null && this.delayTimer.y1 === null) { this.delayTimer = { y0: performance.now(), y1: this.createDelayTimeout(base_delay, this.delayType) }; base_delay++; } if (this.mediaLoaded()) { this.uncloak(); } else { // set uncloak ready status for when images have finished loading this.uncloakReady = true; } } return base_delay }; UncloakItem.prototype.uncloak = function uncloak () { var this$1 = this; if (!this.cloaked) { return } this.cloaked = false; // only trigger uncloak once var dy = (performance.now() - this.delayTimer.y0); var final_delay = this.delayTimer.y1 - dy; var doUncloak = function () { this$1.node.classList.remove('uncloak--cloaked'); this$1.runCallbacks('uncloak'); }; if (final_delay <= 0) { doUncloak(); return } setTimeout(doUncloak, final_delay); }; UncloakItem.prototype.reset = function reset (reobserve) { if ( reobserve === void 0 ) reobserve = true; this.cloaked = true; this.delayTimer = { y0: 0, y1: null }; this.node.classList.add('uncloak--cloaked'); // re-observe node in Uncloak instance if (reobserve) { this.instance.nodeObserver.observe(this.node); } }; // CALLBACK helper UncloakItem.prototype.runCallbacks = function runCallbacks (type) { var cb = this.callbacks[type]; if (!cb || !cb.length) { return } for (var i = 0; i < cb.length; i++) { cb[i](this); } }; // DELAY helper UncloakItem.prototype.createDelayTimeout = function createDelayTimeout (factor, type) { // requires user to input their own delay functions if (!this.delayTypes) { return 0 } return (this.delayTypes[type](factor) || 0) }; // MEDIA helpers UncloakItem.prototype.loadLazyContent = function loadLazyContent () { var this$1 = this; if (this.mediaLoaded() || this.lazyContentLoadStatus === 1) { return } this.lazyContentLoadStatus = 1; var left_to_load = this.lazyContent.length; var loaded = function (root_element, listener_element) { return function () { listener_element.removeEventListener('load', loaded, false); root_element.removeAttribute('data-uncloak-src'); root_element.removeAttribute('data-uncloak-srcset'); root_element.removeAttribute('data-uncloak-ie-src'); root_element.removeAttribute('data-uncloak-alt'); root_element.removeAttribute('data-uncloak-class'); root_element.removeAttribute('data-uncloak-picture'); left_to_load -= 1; if (left_to_load === 0) { this$1.lazyContentLoadStatus = 2; if (this$1.uncloakReady) { this$1.uncloak(); } } } }; for (var i = 0; i < this.lazyContent.length; i++) { var el = this.lazyContent[i]; var lazy_srcset = el.getAttribute('data-uncloak-srcset') || null; var listener_el = el; if (lazy_srcset) { el.srcset = lazy_srcset; } if (el.hasAttribute('data-uncloak-src')) { el.src = el.getAttribute('data-uncloak-src'); } if (el.hasAttribute('data-uncloak-picture')) { var img = document.createElement('img'); listener_el = img; if (this.instance.isIE && el.hasAttribute('data-uncloak-ie-src')) { img.src = el.getAttribute('data-uncloak-ie-src'); } if (el.hasAttribute('data-uncloak-alt')) { img.alt = el.getAttribute('data-uncloak-alt'); } if (el.hasAttribute('data-uncloak-class')) { img.className = el.getAttribute('data-uncloak-class'); } el.appendChild(img); } listener_el.addEventListener('load', loaded(el, listener_el), false); } }; UncloakItem.prototype.mediaLoaded = function mediaLoaded () { return (this.lazyContentLoadStatus === 2) }; var VideoPlayer = function VideoPlayer(options, auto_init) { this.container = options.container; this.video = options.video || options.container.querySelector('video'); this.autoplay = options.autoplay || 0; this.error = false; this.isResponsive = false; this.loadStatus = 0; // 0 = not loaded, 1 = loading, 2 = loaded this.promise = null; this.requiresResize = this.container.hasAttribute('data-video-resize'); this.sources = []; this.callbacks = { durationchange: [], firstPlay: [], playing: [], error: [] }; if (auto_init) { this.init(); } }; VideoPlayer.prototype.init = function init () { var this$1 = this; if (this.loadStatus > 0) { return } this.loadStatus = 1; this.extractSources(); if (!this.video.src) { this.setSrc(); } this.video.addEventListener('loadedmetadata', function () { this$1.video.currentTime = 0.1; if (this$1.requiresResize) { this$1.resizeVideo(); } }, false); this.video.addEventListener('durationchange', function () { this$1.loadStatus = 2; this$1.video.setAttribute('muted', ''); this$1.runCallbacks('durationchange'); if (this$1.autoplay) { this$1.play(); } }, false); var firstPlayHook = function () { this$1.container.classList.add('video-loaded'); this$1.runCallbacks('firstPlay'); this$1.video.removeEventListener('playing', firstPlayHook, false); }; this.video.addEventListener('playing', firstPlayHook, false); this.video.addEventListener('playing', function () { this$1.runCallbacks('playing'); }, false); this.video.addEventListener('error', function () { this$1.disable(); }, false); }; VideoPlayer.prototype.disable = function disable () { this.container.classList.add('video-disabled'); this.error = true; this.runCallbacks('error'); }; VideoPlayer.prototype.extractSources = function extractSources () { var sources = this.video.getAttribute('data-video-srcset') || null; if (sources !== null) { var srcset_regex = /^\s*(.+)\s+(\d+)([wh])?\s*$/; sources = sources.split(','); for (var i = 0; i < sources.length; i++) { var source = sources[i].match(srcset_regex); if (source) { this.sources.push({ src: source[1], width: parseInt(source[2]) }); } } // sort sources by smallest -> largest widths this.sources.sort(function (a, b) { return a.width - b.width }); } else { sources = this.video.getAttribute('data-video-src'); if (sources) { this.sources.push({ src: sources }); this.video.removeAttribute('data-video-src'); } } this.isResponsive = (this.sources.length > 1); }; VideoPlayer.prototype.play = function play () { var this$1 = this; if (this.isLoading() || !this.video.paused || this.promise !== null) { return } if (this.loadStatus === 0) { this.init(); } var play_promise = this.video.play(); if (play_promise !== undefined) { this.promise = play_promise; play_promise.then(function () { this$1.promise = null; }).catch(function () { if (this$1.promise.status && this$1.promise.status === 'rejected') { this$1.disable(); } }); } // on older versions of ios, durationchange event gets hits but media doesn't play // set a timeout for these devices and slow connections setTimeout(function () { if (this$1.video.paused) { this$1.disable(); } }, 100); }; VideoPlayer.prototype.pause = function pause () { if (this.pauseReady()) { this.video.pause(); } }; VideoPlayer.prototype.pauseReady = function pauseReady () { return (!this.video.paused && this.promise === null && this.loadStatus === 2) }; VideoPlayer.prototype.reset = function reset () { if (this.pauseReady()) { this.video.pause(); this.video.currentTime = 0.1; } }; VideoPlayer.prototype.setSrc = function setSrc () { // nb safari 11 gets angry when setting source if video not muted // so muted attribute should be set in html if (this.isResponsive) { var width = this.container.clientWidth; var current_src = this.video.src.replace(/(https?:)/, ''); var is_paused = this.video.paused; // sources are ordered small to large, so we want the next // largest source after the container width for (var i = 0; i < this.sources.length; i++) { if (width <= this.sources[i].width || i === this.sources.length - 1) { if (current_src !== this.sources[i].src) { this.video.src = this.sources[i].src; if (!is_paused) { this.play(); } } return } } } this.video.src = this.sources[0].src; }; VideoPlayer.prototype.addCallback = function addCallback (type, fx) { this.callbacks[type].push(fx); }; VideoPlayer.prototype.runCallbacks = function runCallbacks (type) { var cb = this.callbacks[type]; if (!cb.length) { return } for (var i = 0; i < cb.length; i++) { cb[i](); } }; VideoPlayer.prototype.isDisabled = function isDisabled () { return this.error }; VideoPlayer.prototype.isPaused = function isPaused () { return this.video.paused }; VideoPlayer.prototype.isLoaded = function isLoaded () { return (this.loadStatus === 2) }; VideoPlayer.prototype.isLoading = function isLoading () { return (this.loadStatus === 1) }; VideoPlayer.prototype.resizeVideo = function resizeVideo () { var video_ratio = this.video.videoHeight / this.video.videoWidth; var container_ratio = this.container.offsetHeight / this.container.offsetWidth; if (video_ratio <= container_ratio) { this.video.style.height = '100%'; this.video.style.width = 'auto'; return } this.video.style.height = 'auto'; this.video.style.width = '100%'; }; var UncloakVideoItem = /*@__PURE__*/(function (UncloakItem) { function UncloakVideoItem(node, instance, options) { var this$1 = this; UncloakItem.call(this, node, instance, options); this.videoPlayer = null; this.videoAutoplay = !node.hasAttribute('data-uncloak-video-manual'); if (typeof VideoPlayer !== 'undefined') { var container = node.querySelector('.' + node.getAttribute('data-uncloak-video')); this.videoPlayer = new VideoPlayer({ container: container, autoplay: 0 }, false); this.videoPlayer.addCallback('error', function () { this$1.loadLazyContent(); this$1.uncloak(); }); if (this.videoAutoplay) { this.videoPlayer.addCallback('firstPlay', function () { this$1.uncloak(); }); var play = function () { this$1.toggleVideoPlay(true); }; this.callbacks.uncloak.push(play); } } } if ( UncloakItem ) UncloakVideoItem.__proto__ = UncloakItem; UncloakVideoItem.prototype = Object.create( UncloakItem && UncloakItem.prototype ); UncloakVideoItem.prototype.constructor = UncloakVideoItem; UncloakVideoItem.prototype.init = function init () { var this$1 = this; this.lazyContentObserver = new IntersectionObserver(function (entries) { var entry = entries[0]; if (!this$1.videoPlayer.isDisabled()) { this$1.toggleVideoPlay(entry.isIntersecting); return } // Only handle lazy content if video is not working this$1.loadLazyContent(); // can stop observing if we no longer need to play/pause video this$1.lazyContentObserver.disconnect(); }, { rootMargin: '10%' }); this.lazyContentObserver.observe(this.videoPlayer.container); this.runCallbacks('init'); }; // MEDIA helpers UncloakVideoItem.prototype.mediaLoaded = function mediaLoaded () { return (!this.videoPlayer.isDisabled() || this.lazyContentLoadStatus === 2) }; UncloakVideoItem.prototype.toggleVideoPlay = function toggleVideoPlay (should_play) { if (should_play) { this.videoPlayer.play(); return } this.videoPlayer.pause(); }; return UncloakVideoItem; }(UncloakItem)); var Uncloak = function Uncloak(options) { this.items = []; this.hasResizeListener = false; this.nodeObserver = null; this.isIE = (typeof document !== 'undefined' && document.documentMode); // Sent to UncloakItem this.itemOptions = { delayTypes: {}, callbacks: {} }; if (options && options.itemOptions) { for (var key in this.itemOptions) { if (options.itemOptions[key]) { this.itemOptions[key] = options.itemOptions[key] || {}; } } } this.init(); }; Uncloak.prototype.init = function init () { var this$1 = this; if (!('IntersectionObserver' in window)) { console.error('uncloak: IntersectionObserver not supported in this browser.'); } this.nodeObserver = new IntersectionObserver(function (entries) { entries.reduce(function (base_delay, entry) { var should_reveal_item = false; var uncloak_item = null; // Use try/catch as some browsers report entry.rootBounds as null in iframe try { var b_rect = entry.boundingClientRect; // browsers give negative result if entry covers the screen, so test to see if it covers the screen // use document.body.clientWidth instead of window.innerWidth for IE scrollbars var is_intersecting = entry.isIntersecting || entry.intersectionRatio > 0 || (b_rect.top <= window.innerHeight && b_rect.left <= 0 && b_rect.width >= document.body.clientWidth && b_rect.height >= window.innerHeight); if (!is_intersecting) { return base_delay } uncloak_item = this$1.getItemByNode(entry.target); var item_height = b_rect.height; var viewport_visibility_threshold = (uncloak_item.threshold * entry.rootBounds.height); var item_can_reach_threshold = (item_height >= viewport_visibility_threshold); should_reveal_item = item_can_reach_threshold ? entry.intersectionRect.height >= viewport_visibility_threshold : is_intersecting; } catch (e) { // Silently fail, just reveal item should_reveal_item = true; uncloak_item = this$1.getItemByNode(entry.target); } if (should_reveal_item && uncloak_item) { base_delay = uncloak_item.process(base_delay); this$1.nodeObserver.unobserve(entry.target); } return base_delay }, 0); }, { rootMargin: '10% 0%', threshold: [ 0.1, 0.2, 0.3, 0.5, 0.6, 0.7, 0.8, 0.9 ] }); // initial load this.findNewItems(); }; Uncloak.prototype.findNewItems = function findNewItems (raw_elements) { var this$1 = this; if ( raw_elements === void 0 ) raw_elements = document.querySelectorAll('[data-uncloak-new]'); if (!raw_elements[0]) { return } var offset = this.items.length; var should_add_resize_listener = false; for (var i = 0; i < raw_elements.length; i++) { var raw_el = raw_elements[i]; var true_index = offset + i; var uncloak_item = (void 0); if (raw_el.hasAttribute('data-uncloak-video')) { uncloak_item = new UncloakVideoItem(raw_el, this, this.itemOptions); should_add_resize_listener = true; } else { uncloak_item = new UncloakItem(raw_el, this, this.itemOptions); } raw_el.id = 'uncloak-' + true_index; uncloak_item.init(); this.nodeObserver.observe(raw_el); this.items.push(uncloak_item); } if (should_add_resize_listener && !this.hasResizeListener) { this.hasResizeListener = true; var update_videos = function () { for (var i = 0; i < this$1.items.length; i++) { if ('videoPlayer' in this$1.items[i]) { if (this$1.items[i].videoPlayer.isResponsive) { this$1.items[i].videoPlayer.setSrc(); } if (this$1.items[i].videoPlayer.requiresResize) { this$1.items[i].videoPlayer.resizeVideo(); } } } }; window.addEventListener('orientationchange', update_videos); window.addEventListener('resize', update_videos); } }; Uncloak.prototype.getItemByNode = function getItemByNode (node) { var index = node.id.split('-')[1]; return this.items[index] }; return Uncloak; })));