UNPKG

ab-interchange

Version:

AB-interchange: While responsive image loading is not really an easy task even today, here is a solution to manage conditional (based on breakpoints) loading of img, background-image or even HTML content.

266 lines (214 loc) 7.3 kB
'use strict'; window.AB = require('ab-mediaquery'); var pluginName = 'abInterchange'; var attr = 'data-ab-interchange'; var attrSrc = attr +'-src'; var defaultSettings = { mode: 'lazy-img', lazySettings: { offscreen: 1.25, layout: 'fluid', // or 'fixed' for fixed dimensions }, }; // Run right methods depending on 'mode' function _replace() { if (this._replaced) return; switch(this.mode) { case 'lazy-img': _replaceImg.call(this, true); break; case 'background': _replaceBackground.call(this); break; case 'lazy-background': _replaceBackground.call(this, true); break; } } // Replace source: lazy-img function _replaceImg(lazy) { if (this._imgNode.src === this.currentPath || (lazy && !this.inView())) return; this._imgNode.src = this.currentPath; this._imgNode.addEventListener('load', _isReplaced.bind(this)); // we are done this._replaced = true; } // Replace source: background or lazy-background function _replaceBackground(lazy) { if (this.el.style.backgroundImage === 'url("'+ this.currentPath +'")' || (lazy && !this.inView())) return; this.el.style.backgroundImage = this.currentPath ? 'url('+ this.currentPath +')' : 'none'; this.el.addEventListener('load', _isReplaced.bind(this)); } // build the DOM for lazy-img mode function _setPlaceholder() { var placeholderNode = document.createElement('div'); var imgNode = document.createElement('img'); var alt = this.el.getAttribute('alt'); var widthHeight = _getWidthHeight.call(this); var width = widthHeight.width; var height = widthHeight.height; var isNotReady = !width || !height; var fragment = document.createDocumentFragment(); if (this.mode !== 'lazy-img' || isNotReady) return; this.el.innerHTML = ''; this.el.style.overflow = 'hidden'; this.el.style.position = 'relative'; this.el.classList.add('ab-interchange-loading'); if (this.settings.lazySettings.layout === 'fixed') { this.el.style.height = height +'px'; this.el.style.width = width +'px'; } placeholderNode.classList.add('ab-interchange-placeholder'); placeholderNode.style.paddingTop = (height / width * 100).toFixed(2) + "%"; imgNode.style.position = 'absolute'; imgNode.style.top = imgNode.style.right = imgNode.style.bottom = imgNode.style.left = 0; imgNode.style.maxHeight = imgNode.style.minHeight = imgNode.style.maxWidth = imgNode.style.minWidth = '100%'; imgNode.style.height = 0; imgNode.alt = (alt === null) ? '' : alt; // always put an 'alt' fragment.appendChild(placeholderNode); fragment.appendChild(imgNode); this.el.appendChild(fragment); this._imgNode = this.el.querySelector('img'); } // run after source is replaced function _isReplaced() { this.el.classList.remove('ab-interchange-loading'); var event = new CustomEvent(pluginName +'.replaced', { detail: { element: this.el } }); window.dispatchEvent(event); } function _events() { var observer; var rootMargin = ""; // update path, then replace window.addEventListener('changed.ab-mediaquery', this.resetDisplay.bind(this)); if (this.mode === 'lazy-img' || this.mode === 'lazy-background') { if ('IntersectionObserver' in window) { rootMargin = parseInt((this.settings.lazySettings.offscreen - 1) * window.innerHeight) +'px'; observer = new IntersectionObserver(_onScroll.bind(this), { root: null, rootMargin: '0px 0px '+ rootMargin +' 0px', threshold: 0, }); observer.observe(this.el); } else { // 'ab-scroll' is a custom event from AB dependency window.addEventListener('ab-scroll', _onScroll.bind(this)); } } } // build rules from attribute function _generateRules() { // retro compatibility: sources inside 'attr' var getAttrSrc = this.el.getAttribute(attrSrc) || this.el.getAttribute(attr); var rulesList = []; var rules = getAttrSrc.match(/\[[^\]]+\]/g); var rule, path, query; for (var i = 0, len = rules.length; i < len; i++) { rule = rules[i].slice(1, -1).split(', '); path = rule.slice(0, -1).join(''); query = rule[rule.length - 1]; rulesList.push({ path: path, query: query, }); } this.rules = rulesList; } // Change source path depending on rules function _updatePath() { var path = ''; var rules = this.rules; // Iterate through each rule for (var i = 0, len = rules.length; i < len; i++) { if (window.AB.mediaQuery.is(rules[i].query)) { path = rules[i].path; } } // if path hasn't changed, return if (this.currentPath === path) return; this.currentPath = path; _replace.call(this); } function _onScroll() { // when inView, no need to use 'delayed' if (this.inView() && !this._replaced) { _replace.call(this); } } // get width and height from attributes and manage multiple dimensions function _getWidthHeight() { var width = this.el.getAttribute('width'); var height = this.el.getAttribute('height'); var widthObj = {}; var heightObj = {}; if (window.AB.isJson(width) && window.AB.isJson(height)) { widthObj = JSON.parse(width); heightObj = JSON.parse(height); for (var key in widthObj) { if (widthObj.hasOwnProperty(key) && window.AB.mediaQuery.is(key)) { width = widthObj[key]; height = heightObj[key]; } } } return { width: width, height: height, }; } // init instance function _init() { _setPlaceholder.call(this); _events.call(this); _generateRules.call(this); _updatePath.call(this); } var Plugin = function (el, options) { this.el = el; var dataOptions = window.AB.isJson(this.el.getAttribute(attr)) ? JSON.parse(this.el.getAttribute(attr)) : {}; this.settings = window.AB.extend(true, defaultSettings, options, dataOptions); this.rules = []; this.currentPath = ''; this.mode = this.settings.mode; this.replaced = false; this._imgNode = this.el; // where the source will be updated // no need when using 'img' on browsers supporting that, except when using lazy loading if ((this.el.tagName === 'IMG' || this.el.parentNode.tagName === 'PICTURE' || this.el.getAttribute('srcset')) && window.HTMLPictureElement) return; _init.call(this); }; Plugin.prototype = { // Force element refresh resetDisplay: function() { this._replaced = false; _setPlaceholder.call(this); _updatePath.call(this); }, inView: function() { var windowHeight = window.innerHeight; var rect = this.el.getBoundingClientRect(); var elHeight = this.el.offsetHeight; var checkTop = - (elHeight) - windowHeight * (this.settings.lazySettings.offscreen - 1); var checkBottom = windowHeight + windowHeight * (this.settings.lazySettings.offscreen - 1); return (rect.top >= checkTop && rect.top <= checkBottom); }, }; function interchange(options) { var elements = document.querySelectorAll('['+ attr +']'); for (var i = 0, len = elements.length; i < len; i++) { if (elements[i][pluginName]) continue; elements[i][pluginName] = new Plugin(elements[i], options); } // register plugin and options if (!window.AB.options[pluginName]) { window.AB.options[pluginName] = options; } } window.AB.plugins.interchange = interchange; module.exports = window.AB;