From 146b6ba4114278477b623a37998e6c711ddbf76e Mon Sep 17 00:00:00 2001
From: FrankQiu <iam.frankqiu@gmail.com>
Date: Thu, 10 Sep 2020 15:53:13 +0800
Subject: [PATCH] feat: add a mutationObserver to reload when some valid
 attributes changes (#192)

* feat: add a mutationObserver to reload when some valid attributes changes
---
 .gitignore        |  1 +
 README.md         |  4 +++-
 demo/index.html   | 12 ++++++++++--
 dist/lozad.es.js  | 37 ++++++++++++++++++++++++++++++++-----
 dist/lozad.js     | 40 ++++++++++++++++++++++++++++++++++++----
 dist/lozad.min.js | 11 ++++++++---
 src/lozad.js      | 35 +++++++++++++++++++++++++++++++----
 test/index.js     | 30 ++++++++++++++++++++++++++++--
 8 files changed, 149 insertions(+), 21 deletions(-)

diff --git a/.gitignore b/.gitignore
index bb08723..0714061 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ node_modules
 .DS_Store
 thumbs.db
 *.log*
+coverage
diff --git a/README.md b/README.md
index fd65563..035516e 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,9 @@
 - supports &lt;img&gt;, &lt;picture&gt;, iframes, videos, audios, responsive images, background images and multiple background images etc.
 - even supports LQIP (Low Quality Image Placeholder)
 - is completely free and open source.
+- it will reload when the valid attributes change.
 
-It is written with an aim to lazy load images, iframes, ads, videos or any other element using the recently added [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) with tremendous performance benefits.
+It is written with an aim to lazy load images, iframes, ads, videos or any other element using the recently added [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) with tremendous performance benefits.
 
 ## Featured in:
 - [Web | Google Developers](https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video/)
@@ -112,6 +113,7 @@ or with custom options:
 const observer = lozad('.lozad', {
     rootMargin: '10px 0px', // syntax similar to that of CSS Margin
     threshold: 0.1 // ratio of element convergence
+    enableAutoReload: true // it will reload the new image when validating attributes changes
 });
 observer.observe();
 ```
diff --git a/demo/index.html b/demo/index.html
index c544b4e..45aedb9 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -58,7 +58,7 @@ <h1><a href="https://github.com/ApoorvSaxena/lozad.js"><strong>Lozad.js</strong>
                 <h1>Lozad.js - Performant Lazy Loading Library</h1>
             </article>
             <article class="thumb">
-                <img class="lozad" data-placeholder-background="#583624" data-src="images/thumbs/01.jpg" data-index="1" />
+                <img class="lozad" id="mutativeImg1" data-placeholder-background="#583624" data-src="images/thumbs/01.jpg" data-index="1" />
                 <noscript><img src="images/thumbs/01.jpg" data-index="1" /></noscript>
                     <h2>Image 1</h2>
             </article>
@@ -68,7 +68,7 @@ <h2>Image 1</h2>
                     <h2>Image 2</h2>
             </article>
             <article class="thumb">
-                <img class="lozad" data-placeholder-background="#352629" data-src="images/thumbs/03.jpg" data-index="3" />
+                <img class="lozad" id="mutativeImg2" data-placeholder-background="#352629" data-src="images/thumbs/03.jpg" data-index="3" />
                 <noscript><img src="images/thumbs/03.jpg" data-index="3" /></noscript>
                     <h2>Image 3</h2>
             </article>
@@ -284,6 +284,7 @@ <h1>&lt;video> example</h1>
     // Initialize library to lazy load images
     var observer = lozad('.lozad', {
         threshold: 0.1,
+        enableAutoReload: true,
         load: function(el) {
             el.src = el.getAttribute("data-src");
             el.onload = function() {
@@ -298,6 +299,13 @@ <h1>&lt;video> example</h1>
         threshold: 0.1
     })
 
+    window.onload = function () {
+        setTimeout(function () {
+            document.querySelector('#mutativeImg1').dataset.src = 'images/thumbs/02.jpg'
+            document.querySelector('#mutativeImg2').dataset.src = 'images/thumbs/02.jpg'
+            toastr["success"]("Once data-src change, the element render again.")
+        }, 3000)
+    }
     // Background observer
     // with default `load` method
     var backgroundObserver = lozad('.lozad-background', {
diff --git a/dist/lozad.es.js b/dist/lozad.es.js
index af7c14c..71bd7b0 100644
--- a/dist/lozad.es.js
+++ b/dist/lozad.es.js
@@ -1,4 +1,4 @@
-/*! lozad.js - v1.16.0 - 2020-09-06
+/*! lozad.js - v1.16.0 - 2020-09-10
 * https://github.com/ApoorvSaxena/lozad.js
 * Copyright (c) 2020 Apoorv Saxena; Licensed MIT */
 
@@ -10,9 +10,19 @@
  */
 const isIE = typeof document !== 'undefined' && document.documentMode;
 
+/**
+ *
+ * @param {string} type
+ *
+ */
+const support = type => window && window[type];
+
+const validAttribute = ['data-iesrc', 'data-alt', 'data-src', 'data-srcset', 'data-background-image', 'data-toggle-class'];
+
 const defaultConfig = {
   rootMargin: '0px',
   threshold: 0,
+  enableAutoReload: false,
   load(element) {
     if (element.nodeName.toLowerCase() === 'picture') {
       let img = element.querySelector('img');
@@ -114,6 +124,14 @@ const onIntersection = (load, loaded) => (entries, observer) => {
   });
 };
 
+const onMutation = load => entries => {
+  entries.forEach(entry => {
+    if (isLoaded(entry.target) && entry.type === 'attributes' && validAttribute.indexOf(entry.attributeName) > -1) {
+      load(entry.target);
+    }
+  });
+};
+
 const getElements = (selector, root = document) => {
   if (selector instanceof Element) {
     return [selector]
@@ -127,10 +145,10 @@ const getElements = (selector, root = document) => {
 };
 
 function lozad (selector = '.lozad', options = {}) {
-  const {root, rootMargin, threshold, load, loaded} = Object.assign({}, defaultConfig, options);
+  const {root, rootMargin, threshold, enableAutoReload, load, loaded} = Object.assign({}, defaultConfig, options);
   let observer;
-
-  if (typeof window !== 'undefined' && window.IntersectionObserver) {
+  let mutationObserver;
+  if (support('IntersectionObserver')) {
     observer = new IntersectionObserver(onIntersection(load, loaded), {
       root,
       rootMargin,
@@ -138,6 +156,10 @@ function lozad (selector = '.lozad', options = {}) {
     });
   }
 
+  if (support('MutationObserver') && enableAutoReload) {
+    mutationObserver = new MutationObserver(onMutation(load, loaded));
+  }
+
   const elements = getElements(selector, root);
   for (let i = 0; i < elements.length; i++) {
     preLoad(elements[i]);
@@ -153,6 +175,10 @@ function lozad (selector = '.lozad', options = {}) {
         }
 
         if (observer) {
+          if (mutationObserver && enableAutoReload) {
+            mutationObserver.observe(elements[i], {subtree: true, attributes: true, attributeFilter: validAttribute});
+          }
+
           observer.observe(elements[i]);
           continue
         }
@@ -171,7 +197,8 @@ function lozad (selector = '.lozad', options = {}) {
       markAsLoaded(element);
       loaded(element);
     },
-    observer
+    observer,
+    mutationObserver
   }
 }
 
diff --git a/dist/lozad.js b/dist/lozad.js
index 0354bbc..f5fe826 100644
--- a/dist/lozad.js
+++ b/dist/lozad.js
@@ -1,4 +1,4 @@
-/*! lozad.js - v1.16.0 - 2020-09-06
+/*! lozad.js - v1.16.0 - 2020-09-10
 * https://github.com/ApoorvSaxena/lozad.js
 * Copyright (c) 2020 Apoorv Saxena; Licensed MIT */
 
@@ -16,9 +16,21 @@
    */
   var isIE = typeof document !== 'undefined' && document.documentMode;
 
+  /**
+   *
+   * @param {string} type
+   *
+   */
+  var support = function support(type) {
+    return window && window[type];
+  };
+
+  var validAttribute = ['data-iesrc', 'data-alt', 'data-src', 'data-srcset', 'data-background-image', 'data-toggle-class'];
+
   var defaultConfig = {
     rootMargin: '0px',
     threshold: 0,
+    enableAutoReload: false,
     load: function load(element) {
       if (element.nodeName.toLowerCase() === 'picture') {
         var img = element.querySelector('img');
@@ -124,6 +136,16 @@
     };
   };
 
+  var onMutation = function onMutation(load) {
+    return function (entries) {
+      entries.forEach(function (entry) {
+        if (isLoaded(entry.target) && entry.type === 'attributes' && validAttribute.indexOf(entry.attributeName) > -1) {
+          load(entry.target);
+        }
+      });
+    };
+  };
+
   var getElements = function getElements(selector) {
     var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
 
@@ -146,12 +168,13 @@
         root = _Object$assign.root,
         rootMargin = _Object$assign.rootMargin,
         threshold = _Object$assign.threshold,
+        enableAutoReload = _Object$assign.enableAutoReload,
         load = _Object$assign.load,
         loaded = _Object$assign.loaded;
 
     var observer = void 0;
-
-    if (typeof window !== 'undefined' && window.IntersectionObserver) {
+    var mutationObserver = void 0;
+    if (support('IntersectionObserver')) {
       observer = new IntersectionObserver(onIntersection(load, loaded), {
         root: root,
         rootMargin: rootMargin,
@@ -159,6 +182,10 @@
       });
     }
 
+    if (support('MutationObserver') && enableAutoReload) {
+      mutationObserver = new MutationObserver(onMutation(load, loaded));
+    }
+
     var elements = getElements(selector, root);
     for (var i = 0; i < elements.length; i++) {
       preLoad(elements[i]);
@@ -174,6 +201,10 @@
           }
 
           if (observer) {
+            if (mutationObserver && enableAutoReload) {
+              mutationObserver.observe(elements[_i], { subtree: true, attributes: true, attributeFilter: validAttribute });
+            }
+
             observer.observe(elements[_i]);
             continue;
           }
@@ -193,7 +224,8 @@
         loaded(element);
       },
 
-      observer: observer
+      observer: observer,
+      mutationObserver: mutationObserver
     };
   }
 
diff --git a/dist/lozad.min.js b/dist/lozad.min.js
index 7effbca..1ef50d2 100644
--- a/dist/lozad.min.js
+++ b/dist/lozad.min.js
@@ -1,4 +1,4 @@
-/*! lozad.js - v1.16.0 - 2020-09-06
+/*! lozad.js - v1.16.0 - 2020-09-10
 * https://github.com/ApoorvSaxena/lozad.js
 * Copyright (c) 2020 Apoorv Saxena; Licensed MIT */
 !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.lozad=e()}(this,function(){"use strict";
@@ -6,5 +6,10 @@
    * Detect IE browser
    * @const {boolean}
    * @private
-   */var g="undefined"!=typeof document&&document.documentMode,f={rootMargin:"0px",threshold:0,load:function(t){if("picture"===t.nodeName.toLowerCase()){var e=t.querySelector("img"),r=!1;null===e&&(e=document.createElement("img"),r=!0),g&&t.getAttribute("data-iesrc")&&(e.src=t.getAttribute("data-iesrc")),t.getAttribute("data-alt")&&(e.alt=t.getAttribute("data-alt")),r&&t.append(e)}if("video"===t.nodeName.toLowerCase()&&!t.getAttribute("data-src")&&t.children){for(var a=t.children,o=void 0,i=0;i<=a.length-1;i++)(o=a[i].getAttribute("data-src"))&&(a[i].src=o);t.load()}t.getAttribute("data-poster")&&(t.poster=t.getAttribute("data-poster")),t.getAttribute("data-src")&&(t.src=t.getAttribute("data-src")),t.getAttribute("data-srcset")&&t.setAttribute("srcset",t.getAttribute("data-srcset"));var n=",";if(t.getAttribute("data-background-delimiter")&&(n=t.getAttribute("data-background-delimiter")),t.getAttribute("data-background-image"))t.style.backgroundImage="url('"+t.getAttribute("data-background-image").split(n).join("'),url('")+"')";else if(t.getAttribute("data-background-image-set")){var d=t.getAttribute("data-background-image-set").split(n),u=d[0].substr(0,d[0].indexOf(" "))||d[0];// Substring before ... 1x
-u=-1===u.indexOf("url(")?"url("+u+")":u,1===d.length?t.style.backgroundImage=u:t.setAttribute("style",(t.getAttribute("style")||"")+"background-image: "+u+"; background-image: -webkit-image-set("+d+"); background-image: image-set("+d+")")}t.getAttribute("data-toggle-class")&&t.classList.toggle(t.getAttribute("data-toggle-class"))},loaded:function(){}};function A(t){t.setAttribute("data-loaded",!0)}var m=function(t){return"true"===t.getAttribute("data-loaded")},v=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:document;return t instanceof Element?[t]:t instanceof NodeList?t:e.querySelectorAll(t)};return function(){var r,a,o=0<arguments.length&&void 0!==arguments[0]?arguments[0]:".lozad",t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},e=Object.assign({},f,t),i=e.root,n=e.rootMargin,d=e.threshold,u=e.load,g=e.loaded,s=void 0;"undefined"!=typeof window&&window.IntersectionObserver&&(s=new IntersectionObserver((r=u,a=g,function(t,e){t.forEach(function(t){(0<t.intersectionRatio||t.isIntersecting)&&(e.unobserve(t.target),m(t.target)||(r(t.target),A(t.target),a(t.target)))})}),{root:i,rootMargin:n,threshold:d}));for(var c,l=v(o,i),b=0;b<l.length;b++)(c=l[b]).getAttribute("data-placeholder-background")&&(c.style.background=c.getAttribute("data-placeholder-background"));return{observe:function(){for(var t=v(o,i),e=0;e<t.length;e++)m(t[e])||(s?s.observe(t[e]):(u(t[e]),A(t[e]),g(t[e])))},triggerLoad:function(t){m(t)||(u(t),A(t),g(t))},observer:s}}});
+   */var g="undefined"!=typeof document&&document.documentMode,v=function(t){return window&&window[t]},h=["data-iesrc","data-alt","data-src","data-srcset","data-background-image","data-toggle-class"],p={rootMargin:"0px",threshold:0,enableAutoReload:!1,load:function(t){if("picture"===t.nodeName.toLowerCase()){var e=t.querySelector("img"),r=!1;null===e&&(e=document.createElement("img"),r=!0),g&&t.getAttribute("data-iesrc")&&(e.src=t.getAttribute("data-iesrc")),t.getAttribute("data-alt")&&(e.alt=t.getAttribute("data-alt")),r&&t.append(e)}if("video"===t.nodeName.toLowerCase()&&!t.getAttribute("data-src")&&t.children){for(var a=t.children,o=void 0,i=0;i<=a.length-1;i++)(o=a[i].getAttribute("data-src"))&&(a[i].src=o);t.load()}t.getAttribute("data-poster")&&(t.poster=t.getAttribute("data-poster")),t.getAttribute("data-src")&&(t.src=t.getAttribute("data-src")),t.getAttribute("data-srcset")&&t.setAttribute("srcset",t.getAttribute("data-srcset"));var n=",";if(t.getAttribute("data-background-delimiter")&&(n=t.getAttribute("data-background-delimiter")),t.getAttribute("data-background-image"))t.style.backgroundImage="url('"+t.getAttribute("data-background-image").split(n).join("'),url('")+"')";else if(t.getAttribute("data-background-image-set")){var d=t.getAttribute("data-background-image-set").split(n),u=d[0].substr(0,d[0].indexOf(" "))||d[0];// Substring before ... 1x
+u=-1===u.indexOf("url(")?"url("+u+")":u,1===d.length?t.style.backgroundImage=u:t.setAttribute("style",(t.getAttribute("style")||"")+"background-image: "+u+"; background-image: -webkit-image-set("+d+"); background-image: image-set("+d+")")}t.getAttribute("data-toggle-class")&&t.classList.toggle(t.getAttribute("data-toggle-class"))},loaded:function(){}};
+/**
+   *
+   * @param {string} type
+   *
+   */function k(t){t.setAttribute("data-loaded",!0)}var y=function(t){return"true"===t.getAttribute("data-loaded")},w=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:document;return t instanceof Element?[t]:t instanceof NodeList?t:e.querySelectorAll(t)};return function(){var r,a,e,o=0<arguments.length&&void 0!==arguments[0]?arguments[0]:".lozad",t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},i=Object.assign({},p,t),n=i.root,d=i.rootMargin,u=i.threshold,g=i.enableAutoReload,s=i.load,c=i.loaded,l=void 0,b=void 0;v("IntersectionObserver")&&(l=new IntersectionObserver((r=s,a=c,function(t,e){t.forEach(function(t){(0<t.intersectionRatio||t.isIntersecting)&&(e.unobserve(t.target),y(t.target)||(r(t.target),k(t.target),a(t.target)))})}),{root:n,rootMargin:d,threshold:u})),v("MutationObserver")&&g&&(b=new MutationObserver((e=s,function(t){t.forEach(function(t){y(t.target)&&"attributes"===t.type&&-1<h.indexOf(t.attributeName)&&e(t.target)})})));for(var f,m=w(o,n),A=0;A<m.length;A++)(f=m[A]).getAttribute("data-placeholder-background")&&(f.style.background=f.getAttribute("data-placeholder-background"));return{observe:function(){for(var t=w(o,n),e=0;e<t.length;e++)y(t[e])||(l?(b&&g&&b.observe(t[e],{subtree:!0,attributes:!0,attributeFilter:h}),l.observe(t[e])):(s(t[e]),k(t[e]),c(t[e])))},triggerLoad:function(t){y(t)||(s(t),k(t),c(t))},observer:l,mutationObserver:b}}});
diff --git a/src/lozad.js b/src/lozad.js
index 5c54e62..9a318e3 100644
--- a/src/lozad.js
+++ b/src/lozad.js
@@ -5,9 +5,19 @@
  */
 const isIE = typeof document !== 'undefined' && document.documentMode
 
+/**
+ *
+ * @param {string} type
+ *
+ */
+const support = type => window && window[type]
+
+const validAttribute = ['data-iesrc', 'data-alt', 'data-src', 'data-srcset', 'data-background-image', 'data-toggle-class']
+
 const defaultConfig = {
   rootMargin: '0px',
   threshold: 0,
+  enableAutoReload: false,
   load(element) {
     if (element.nodeName.toLowerCase() === 'picture') {
       let img = element.querySelector('img')
@@ -109,6 +119,14 @@ const onIntersection = (load, loaded) => (entries, observer) => {
   })
 }
 
+const onMutation = load => entries => {
+  entries.forEach(entry => {
+    if (isLoaded(entry.target) && entry.type === 'attributes' && validAttribute.indexOf(entry.attributeName) > -1) {
+      load(entry.target)
+    }
+  })
+}
+
 const getElements = (selector, root = document) => {
   if (selector instanceof Element) {
     return [selector]
@@ -122,10 +140,10 @@ const getElements = (selector, root = document) => {
 }
 
 export default function (selector = '.lozad', options = {}) {
-  const {root, rootMargin, threshold, load, loaded} = Object.assign({}, defaultConfig, options)
+  const {root, rootMargin, threshold, enableAutoReload, load, loaded} = Object.assign({}, defaultConfig, options)
   let observer
-
-  if (typeof window !== 'undefined' && window.IntersectionObserver) {
+  let mutationObserver
+  if (support('IntersectionObserver')) {
     observer = new IntersectionObserver(onIntersection(load, loaded), {
       root,
       rootMargin,
@@ -133,6 +151,10 @@ export default function (selector = '.lozad', options = {}) {
     })
   }
 
+  if (support('MutationObserver') && enableAutoReload) {
+    mutationObserver = new MutationObserver(onMutation(load, loaded))
+  }
+
   const elements = getElements(selector, root)
   for (let i = 0; i < elements.length; i++) {
     preLoad(elements[i])
@@ -148,6 +170,10 @@ export default function (selector = '.lozad', options = {}) {
         }
 
         if (observer) {
+          if (mutationObserver && enableAutoReload) {
+            mutationObserver.observe(elements[i], {subtree: true, attributes: true, attributeFilter: validAttribute})
+          }
+
           observer.observe(elements[i])
           continue
         }
@@ -166,6 +192,7 @@ export default function (selector = '.lozad', options = {}) {
       markAsLoaded(element)
       loaded(element)
     },
-    observer
+    observer,
+    mutationObserver
   }
 }
diff --git a/test/index.js b/test/index.js
index 04cda3d..02f3891 100644
--- a/test/index.js
+++ b/test/index.js
@@ -98,10 +98,32 @@ describe('lozad', () => {
 
     it('should load the image', () => {
       const node = document.querySelector('img')
+      const newSrc = Math.random()
+        .toString(36)
+        .substring(7)
       const observer = lozad(node)
       observer.observe()
       assert.strictEqual('true', node.dataset.loaded)
       assert.strictEqual(node.getAttribute('src'), node.dataset.src)
+      node.dataset.src = newSrc
+      setTimeout(() => {
+        assert.notStrictEqual(node.getAttribute('src'), newSrc)
+      }, 2000)
+    })
+
+    it('should load the new image when sets enableAutoReload true and validating attributes changes', () => {
+      const node = document.querySelector('img')
+      const newSrc = Math.random()
+        .toString(36)
+        .substring(7)
+      const observer = lozad(node, {
+        enableAutoReload: true
+      })
+      observer.observe()
+      node.dataset.src = newSrc
+      setTimeout(() => {
+        assert.strictEqual(node.getAttribute('src'), newSrc)
+      }, 2000)
     })
   })
 
@@ -407,7 +429,7 @@ describe('lozad', () => {
     })
   })
 
-  describe('exported IntersectionObserver', () => {
+  describe('exported IntersectionObserver and MutationObserver', () => {
     beforeEach(() => {
       document.body.innerHTML = ''
       const el = document.createElement('div')
@@ -416,10 +438,14 @@ describe('lozad', () => {
       document.body.appendChild(el)
     })
 
-    it('should export the IntersectionObserver instance', () => {
+    it('should export the IntersectionObserver instance and MutationObserver instance', () => {
       const observer = lozad()
 
       assert.strictEqual(true, Object.keys(observer).includes('observer'))
+      assert.strictEqual(
+        true,
+        Object.keys(observer).includes('mutationObserver')
+      )
     })
   })
 })
