1/** 2 * @file 3 * Attach the JavaScript to responsive images to load the best image style. 4 */ 5(function (Drupal, drupalSettings) { 6 'use strict'; 7 8 Drupal.behaviors.easyResponsiveImages = { 9 attach: function (context) { 10 // Fetch all images containing a "data-srcset" attribute. 11 const images = context.querySelectorAll('img[data-srcset]'); 12 13 // Return early if no images were found. 14 if (!images || images.length === 0) { 15 return; 16 } 17 18 // Find the best suitable image to display. 19 const updateImage = function (image) { 20 // When an image is already loading, we need to wait until loading has 21 // finished. Different observers might try to update the image 22 // simultaneously. 23 if (image.hasAttribute('data-loading')) { 24 setTimeout(function () { 25 updateImage(image); 26 }, 100); 27 return; 28 } 29 30 // Check if we need to apply a multiplier to the width check and load 31 // bigger images. 32 let multiplier = 1; 33 const dataMultiplier = image.getAttribute('data-multiplier'); 34 if (dataMultiplier !== null) { 35 multiplier = parseFloat(dataMultiplier.replace('x', '')); 36 } 37 const multiplierMax = multiplier >= 1 ? multiplier : 1; 38 const multiplierFactor = window.devicePixelRatio < multiplierMax ? window.devicePixelRatio : multiplierMax; 39 40 // Check if we need to use the container height to select the best 41 // image. 42 const cover = image.hasAttribute('data-cover'); 43 44 // Check the available width for the image based on the multiplier. 45 const imgWidth = Math.floor(image.clientWidth); 46 const imgHeight = Math.floor(image.clientHeight); 47 const parentWidth = Math.floor(image.parentNode.clientWidth); 48 const parentHeight = Math.floor(image.parentNode.clientHeight); 49 const availableWidth = parentWidth > imgWidth ? parentWidth : imgWidth; 50 const availableHeight = parentHeight > imgHeight ? parentHeight : imgHeight; 51 const targetWidth = multiplierFactor * availableWidth; 52 const targetHeight = multiplierFactor * availableHeight; 53 54 // Get the current width and height of the image. 55 const attrWidth = image.getAttribute('width'); 56 const attrHeight = image.getAttribute('height'); 57 58 // Get all the source values and the current image source. 59 const sources = image.getAttribute('data-srcset').split(','); 60 const currentSrc = image.getAttribute('src'); 61 62 // Fetch the image ratio if it is available. 63 let ratioWidth, ratioHeight; 64 if (image.hasAttribute('data-ratio')) { 65 [ratioWidth, ratioHeight] = image.getAttribute('data-ratio').split(':'); 66 } 67 68 // If the selected image has a src attribute, and is already bigger than 69 // the available width, we do not update the image. 70 if (image.src && attrWidth && attrWidth > availableWidth && attrHeight && attrHeight > availableHeight) { 71 image.setAttribute('data-loaded', 1); 72 return; 73 } 74 75 // Find the best matching source based on actual image space. 76 let source, responsiveImgPath, responsiveImgWidth, responsiveImgHeight; 77 for (source of sources) { 78 let array = source.split(' '); 79 responsiveImgPath = array[0]; 80 responsiveImgWidth = array[1].slice(0, -1); 81 if (cover && parentWidth && parentHeight) { 82 responsiveImgHeight = (responsiveImgWidth * parentWidth) / parentHeight; 83 } 84 else if (ratioWidth && ratioHeight) { 85 responsiveImgHeight = (responsiveImgWidth * ratioHeight) / ratioWidth; 86 } 87 else if (attrWidth && attrHeight) { 88 responsiveImgHeight = (responsiveImgWidth * attrHeight) / attrWidth; 89 } 90 // Stop looking if the image is bigger than the target width and the 91 // image height is bigger than the target height. 92 if (targetWidth < responsiveImgWidth && (!cover || !responsiveImgHeight || targetHeight < responsiveImgHeight)) { 93 break; 94 } 95 } 96 97 // If the image is already loaded, we do not update the image. 98 if (responsiveImgPath === currentSrc) { 99 image.setAttribute('data-loaded', 1); 100 return; 101 } 102 103 // Set a temporary attribute to prevent multiple observers from updating 104 // the image simultaneously. 105 image.setAttribute('data-loading', 1); 106 107 // Update the "src" with the new image path. 108 image.src = responsiveImgPath; 109 110 // Calculate the height from the dimensions of the initial width and 111 // height since this can also cause layout shifts. 112 image.setAttribute('width', availableWidth); 113 if (cover && parentWidth && parentHeight) { 114 image.setAttribute('height', Math.round((availableWidth * parentHeight) / parentWidth)); 115 } 116 else if (ratioWidth && ratioHeight) { 117 image.setAttribute('height', Math.round((availableWidth * ratioHeight) / ratioWidth)); 118 } 119 else if (attrWidth && attrHeight) { 120 image.setAttribute('height', Math.round((availableWidth * attrHeight) / attrWidth)); 121 } 122 123 // Set the "data-loaded" attribute when the image is loaded. 124 image.onload = function () { 125 this.setAttribute('data-loaded', 1); 126 127 // Remove the temporary attribute when the image finishes loading. 128 image.removeAttribute('data-loading'); 129 130 // Update the image width/height onload to the actual image 131 // dimensions. 132 if (cover || !ratioWidth || !ratioHeight) { 133 this.setAttribute('width', Math.floor(this.width)); 134 this.setAttribute('height', Math.floor(this.height)); 135 } 136 }; 137 }; 138 139 // Create a ResizeObserver to update the image "src" attribute when its 140 // parent container resizes. 141 const resizeObserver = new ResizeObserver((entries) => {
142 for (let entry of entries) { 143 const images = entry.target.querySelectorAll('img[data-srcset]'); 144 images.forEach((image) => { 145 updateImage(image); 146 }); 147 } 148 }); 149 150 // Define an offset to preload lazyloaded images before they are visible 151 // in the viewport. Default offsets are based on connection as documented 152 // on https://web.dev/articles/browser-level-image-lazy-loading#distance-from-viewport 153 let lazyLoadingThreshold = 1250; 154 155 if(drupalSettings?.easyResponsiveImages?.lazyLoadingThreshold) { 156 lazyLoadingThreshold = parseInt(drupalSettings.easyResponsiveImages.lazyLoadingThreshold); 157 } 158 159 const connection = navigator.connection; 160 const connectionType = connection?.effectiveType; 161 const connectionSaveData = connection?.saveData; 162 163 // Double the preloadOffset for 3G connections or when saveData is enabled. 164 if(connectionType === "3G" || connectionSaveData) { 165 lazyLoadingThreshold = lazyLoadingThreshold * 2; 166 } 167 168 // Create an IntersectionObserver to update the image "src" attribute when 169 // it is visible in the viewport. 170 const intersectionObserver = new IntersectionObserver((entries) => { 171 for (let entry of entries) { 172 if (entry.isIntersecting) { 173 // Remove the IntersectionObserver when the image is visible. 174 intersectionObserver.unobserve(entry.target); 175 176 // Load the correct image. 177 updateImage(entry.target); 178 179 // Attach the ResizeObserver to the image container. 180 resizeObserver.observe(entry.target.parentNode); 181 } 182 } 183 }, {rootMargin: lazyLoadingThreshold + 'px'}); 184 185 // Attach the ResizeObserver and IntersectionObserver to the images. 186 images.forEach((image) => { 187 // Make sure we only attach the observers once. 188 if (image.hasAttribute('data-observed')) { 189 return; 190 } 191 192 // Mark that the image the observers are attached. 193 image.setAttribute('data-observed', '1'); 194 195 // Attach the IntersectionObserver to the image if it is using lazy 196 // loading, otherwise attach the ResizeObserver. 197 if(image.hasAttribute('loading') && image.getAttribute('loading') === 'lazy') { 198 // When an image is using lazy loading, we unset the src to prevent it 199 // from being loaded directly. This prevents the browser from lazy 200 // loading the default image before we get the chance to set the 201 // correct src. 202 image.removeAttribute('src'); 203 204 // Attach the IntersectionObserver to the image. 205 intersectionObserver.observe(image); 206 } else { 207 // Load the correct image directly. 208 updateImage(image); 209 210 // Attach the ResizeObserver to the image container. 211 resizeObserver.observe(image.parentNode); 212 } 213 }); 214 }, 215 }; 216 217})(Drupal, drupalSettings);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.