aboutsummaryrefslogtreecommitdiffstats
path: root/web/js/loading-attribute-polyfill.js
blob: f1139798528abcfa6ac39a666b60ae72c55851ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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();
			}
		});
	}
})();