diff options
author | Matthew Somerville <matthew@mysociety.org> | 2020-05-23 11:38:25 +0100 |
---|---|---|
committer | Matthew Somerville <matthew@mysociety.org> | 2020-06-15 11:53:11 +0100 |
commit | 9553c8daac73f06af704a87ace7294f751a77b8f (patch) | |
tree | e75d32ef7a1f20230339f5a756f6bacb399d0d87 | |
parent | 56d6d1524b4559ca407bcaf33f47a12960e83576 (diff) |
Add lazy image loading on list items.
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | templates/web/base/common_scripts.html | 2 | ||||
-rw-r--r-- | templates/web/base/report/_item.html | 4 | ||||
-rw-r--r-- | templates/web/base/report/_item_expandable.html | 4 | ||||
-rw-r--r-- | web/js/loading-attribute-polyfill.js | 213 | ||||
-rw-r--r-- | web/js/map-OpenLayers.js | 3 |
6 files changed, 226 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2413892c1..0ae55f306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * Unreleased - New features: - Add Open Location Codes support to search box. #3047 + - Front end improvements: + - Add lazy image loading on list items. - Changes: - Mark user as active when sent an email alert. - Bugfixes: diff --git a/templates/web/base/common_scripts.html b/templates/web/base/common_scripts.html index b8c4db343..32ec9b00f 100644 --- a/templates/web/base/common_scripts.html +++ b/templates/web/base/common_scripts.html @@ -13,6 +13,7 @@ IF bodyclass.match('frontpage'); scripts.push( version('/js/front.js'), version('/js/geolocation.js'), + version('/js/loading-attribute-polyfill.js'), ); ELSIF bodyclass.match('alertpage'); scripts.push( @@ -64,6 +65,7 @@ IF bodyclass.match('mappage'); version('/cobrands/fixmystreet/map.js'), version('/vendor/dropzone.min.js'), version('/vendor/fancybox/jquery.fancybox-1.3.4.pack.js'), + version('/js/loading-attribute-polyfill.js'), ); ELSE; scripts.push( diff --git a/templates/web/base/report/_item.html b/templates/web/base/report/_item.html index baba04d3e..6f567de01 100644 --- a/templates/web/base/report/_item.html +++ b/templates/web/base/report/_item.html @@ -50,7 +50,9 @@ END; [% PROCESS 'report/_item_photo_title.html' ~%] [% CATCH file ~%] [% IF problem.photo %] - <img class="img" height="60" width="90" src="[% problem.photos.first.url_fp %]" alt=""> + <noscript class="loading-lazy"> + <img loading="lazy" class="img" height="60" width="90" src="[% problem.photos.first.url_fp %]" alt=""> + </noscript> [% END %] [% TRY %] [% PROCESS 'report/_item_heading.html' %] diff --git a/templates/web/base/report/_item_expandable.html b/templates/web/base/report/_item_expandable.html index fad935407..ab363482a 100644 --- a/templates/web/base/report/_item_expandable.html +++ b/templates/web/base/report/_item_expandable.html @@ -18,7 +18,9 @@ [% IF problem.photo %] <a href="[% c.cobrand.relative_url_for_report( problem ) %][% problem.url %]" class="item-list__item--expandable__hide-when-expanded"> - <img class="img" height="60" width="90" src="[% problem.photos.first.url_fp %]" alt=""> + <noscript class="loading-lazy"> + <img loading="lazy" class="img" height="60" width="90" src="[% problem.photos.first.url_fp %]" alt=""> + </noscript> </a> [% END %] diff --git a/web/js/loading-attribute-polyfill.js b/web/js/loading-attribute-polyfill.js new file mode 100644 index 000000000..f11397985 --- /dev/null +++ b/web/js/loading-attribute-polyfill.js @@ -0,0 +1,213 @@ +/* + * Loading attribute polyfill - https://github.com/mfranzke/loading-attribute-polyfill + * @license Copyright(c) 2019 by Maximilian Franzke + * Credits for the initial kickstarter / script to @Sora2455, and supported by @cbirdsong, @eklingen, @DaPo, @nextgenthemes, @diogoterremoto, @dracos, @Flimm, @TomS- and @vinyfc93 - many thanks for that ! + */ +/* + * A minimal and dependency-free vanilla JavaScript loading attribute polyfill. + * Supports standard's functionality and tests for native support upfront. + * Elsewhere the functionality gets emulated with the support of noscript wrapper tags. + * Use an IntersectionObserver polyfill in case of IE11 support necessary. + * + * MS - Removed iframe/picture/srcset parts, unneeded at present, and added external API + */ + +(function () { + 'use strict'; + + var config = { + // Start download if the item gets within 256px in the Y axis + rootMargin: '256px 0px', + threshold: 0.01 + }; + + // Device/browser capabilities object + var capabilities = { + loading: 'loading' in HTMLImageElement.prototype, + scrolling: 'onscroll' in window + }; + + // Nodelist foreach polyfill / source: https://stackoverflow.com/a/46929259 + if ( + typeof NodeList !== 'undefined' && + NodeList.prototype && + !NodeList.prototype.forEach + ) { + // Yes, there's really no need for `Object.defineProperty` here + NodeList.prototype.forEach = Array.prototype.forEach; + } + + // Define according to browsers support of the IntersectionObserver feature (missing e.g. on IE11 or Safari 11) + var intersectionObserver; + + if ('IntersectionObserver' in window) { + intersectionObserver = new IntersectionObserver(onIntersection, config); + } + + // On using a browser w/o requestAnimationFrame support (IE9, Opera Mini), just run the passed function + var rAFWrapper; + + if ('requestAnimationFrame' in window) { + rAFWrapper = window.requestAnimationFrame; + } else { + rAFWrapper = function (func) { + func(); + }; + } + + /** + * Put the source back where it belongs - now that the elements content is attached to the document, it will load now + * @param {Object} lazyItem Current item to be restored after lazy loading. + */ + function restoreSource(lazyItem) { + lazyItem.setAttribute('src', lazyItem.getAttribute('data-lazy-src')); + lazyItem.removeAttribute('data-lazy-src'); // Not using delete .dataset here for compatibility down to IE9 + } + + /** + * Handle IntersectionObservers callback + * @param {Object} entries Target elements Intersection observed changes + * @param {Object} observer IntersectionObserver instance reference + */ + function onIntersection(entries, observer) { + entries.forEach(function (entry) { + // Mitigation for EDGE lacking support of .isIntersecting until v15, compare to e.g. https://github.com/w3c/IntersectionObserver/issues/211#issuecomment-309144669 + if (entry.intersectionRatio === 0) { + return; + } + + // If the item is visible now, load it and stop watching it + var lazyItem = entry.target; + + observer.unobserve(lazyItem); + + restoreSource(lazyItem); + }); + } + + /** + * Handle printing the page + */ + function onPrinting() { + if (typeof window.matchMedia === 'undefined') { + return; + } + + var mediaQueryList = window.matchMedia('print'); + + mediaQueryList.addListener(function (mql) { + if (mql.matches) { + document + .querySelectorAll('img[loading="lazy"][data-lazy-src]') + .forEach(function (lazyItem) { + restoreSource(lazyItem); + }); + } + }); + } + + /** + * Get and prepare the HTML code depending on feature detection, + * and if not scrolling supported, because it's a Google or Bing Bot + * @param {String} lazyAreaHtml Noscript inner HTML code that src-urls need to get rewritten + */ + function getAndPrepareHTMLCode(noScriptTag) { + // The contents of a <noscript> tag are treated as text to JavaScript + var lazyAreaHtml = noScriptTag.textContent || noScriptTag.innerHTML; + + var getImageWidth = lazyAreaHtml.match(/width=['"](\d+)['"]/) || false; + var temporaryImageWidth = getImageWidth[1] || 1; + var getImageHeight = lazyAreaHtml.match(/height=['"](\d+)['"]/) || false; + var temporaryImageHeight = getImageHeight[1] || 1; + + var temporaryImage = + 'data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 ' + + temporaryImageWidth + + ' ' + + temporaryImageHeight + + '%27%3E%3C/svg%3E'; + + if (!capabilities.loading && capabilities.scrolling) { + // Check for IntersectionObserver support + if (typeof intersectionObserver === 'undefined') { + // Attach abandonned attribute 'lazyload' to the HTML tags on browsers w/o IntersectionObserver being available + lazyAreaHtml = lazyAreaHtml.replace( + /(?:\r\n|\r|\n|\t| )src=/g, + ' lazyload="1" src=' + ); + } else { + // Temporarily replace a expensive resource load with a simple one by storing the actual source for later and point src to a temporary replacement (data URI) + lazyAreaHtml = lazyAreaHtml + .replace( + /(?:\r\n|\r|\n|\t| )src=/g, + ' src="' + temporaryImage + '" data-lazy-src=' + ); + } + } + + return lazyAreaHtml; + } + + /** + * Retrieve the elements from the 'lazy load' <noscript> tag and prepare them for display + * @param {Object} noScriptTag noscript HTML tag that should get initially transformed + */ + function prepareElement(noScriptTag) { + // Sticking the noscript HTML code in the innerHTML of a new <div> tag to 'load' it after creating that <div> + var lazyArea = document.createElement('div'); + + lazyArea.innerHTML = getAndPrepareHTMLCode(noScriptTag); + + // Move all children out of the element + while (lazyArea.firstChild) { + if ( + !capabilities.loading && + capabilities.scrolling && + typeof intersectionObserver !== 'undefined' && + lazyArea.firstChild.tagName && + lazyArea.firstChild.tagName.toLowerCase() === 'img' + ) { + // Observe the item so that loading could start when it gets close to the viewport + intersectionObserver.observe(lazyArea.firstChild); + } + + noScriptTag.parentNode.insertBefore(lazyArea.firstChild, noScriptTag); + } + + // Remove the empty element - not using .remove() here for IE11 compatibility + noScriptTag.parentNode.removeChild(noScriptTag); // Preferred .removeChild over .remove here for IE + } + + /* Add a function we can call externally */ + fixmystreet.loading_recheck = function() { + var lazyLoadAreas = document.querySelectorAll('noscript.loading-lazy'); + lazyLoadAreas.forEach(prepareElement); + }; + + /** + * Get all the <noscript> tags on the page and setup the printing + */ + function prepareElements() { + fixmystreet.loading_recheck(); + + // Bind for someone printing the page + onPrinting(); + } + + // If the page has loaded already, run setup - if it hasn't, run as soon as it has. + // Use requestAnimationFrame as this will propably cause repaints + // document.readyState values: https://www.w3schools.com/jsref/prop_doc_readystate.asp + if (/comp|inter/.test(document.readyState)) { + rAFWrapper(prepareElements); + } else if ('addEventListener' in document) { + document.addEventListener('DOMContentLoaded', function () { + rAFWrapper(prepareElements); + }); + } else { + document.attachEvent('onreadystatechange', function () { + if (document.readyState === 'complete') { + prepareElements(); + } + }); + } +})(); diff --git a/web/js/map-OpenLayers.js b/web/js/map-OpenLayers.js index 182cd79a1..d5fd36e97 100644 --- a/web/js/map-OpenLayers.js +++ b/web/js/map-OpenLayers.js @@ -1278,6 +1278,9 @@ OpenLayers.Format.FixMyStreet = OpenLayers.Class(OpenLayers.Format.JSON, { var reports_list; if (typeof(obj.reports_list) != 'undefined' && (reports_list = document.getElementById('js-reports-list'))) { reports_list.innerHTML = obj.reports_list; + if (fixmystreet.loading_recheck) { + fixmystreet.loading_recheck(); + } if ( $('.item-list--reports').data('show-old-reports') ) { $('#show_old_reports_wrapper').removeClass('hidden'); } else { |