UNPKG

plastic-image

Version:

iron-image extension supporting srcset and lazy loading

980 lines (861 loc) 39.8 kB
import '../@polymer/polymer/polymer-element.js'; import '../@polymer/iron-image/iron-image.js'; import { timeOut, idlePeriod } from '../@polymer/polymer/lib/utils/async.js'; import { beforeNextRender } from '../@polymer/polymer/lib/utils/render-status.js'; /** * `plastic-image` * iron-image wrapper supporting srcset * * @customElement * @polymer * @demo demo/index.html */ /** @license MIT */ /** `plastic-image` is a Polymer 2.0 element which adds extra plasticity to \<iron-image\> with support for srcset and lazy loading. `plastic-image` extends `iron-image` by adding a `srcset` attribute for client side image size selection and `lazy-load` for loading the image only when it has scrolled into the viewport. Please review the documentation for `iron-image` as it applies to this element also. plastic-image srcset The `srcset` attribute is a string consisting of one or more image selection strings separated by commas. Each image selection string is composed of: 1. A url to an image 2. One or _more_ descriptors - width descriptor: a positive integer directly followed by 'w'. e.g. `700w` - height descriptor: a positive integer directly followed by 'h'. e.g. `345h` - pixel density descriptor: a positive floating point number directly followed by 'x'. e.g. `2.0x` `plastic-image` extends the `<img srcset="...">` feature by allowing multiple descriptors for an image and mixed descriptors in a single srcset. `plastic-image` also extends srcset use by optionally allowing image selection to be based on the render size of the `plastic-image` control instead of the viewport, which is the standard. To use this optional function include the `use-element-dim` attribute. Example srcset srcset="foo-s.jpg 150w, foo-sh.jpg 150w 2.0x, foo-m.jpg 405w, foo-mh 2.0x 405w, foo-l 1024w, foo-t 500w 750h" Automatic Density If _none_ of the image selection strings includes a pixel density descriptor ('x' e.g. `4.0x`), then the image selection process will automatically compensate for the viewport's pixel density. Examples: Use the control as you would an `iron-image` but with the srcset. <plastic-image preload fade sizing="contain" srcset="images/foo-s.jpg 150w, images/foo-sh.jpg 150w 2.0x, images/foo-m.jpg 405w, images/foo-mh 2.0x 405w, images/foo-l 1024w, images/foo-t 500w 750h" placeholder="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAmElEQVQImWNmYGBgSExMzBATE7dSVFT8eO/evTcMDAwMjIFe5iYSIjybL136cunNW56FulIaEoJcfBdY5GWjvJ4/+SJhIcUhwavI5SbIxR+YvzRqH8unx7/Osf8VYpAVEWLgZuO8ljrfbwMDAwMD07u/j/ZYun5f9JfjSfGnHx9dGaCAJcBimwXjZ4Z+HllGn0XbXr+ASQAAi5UxQq88/fsAAAAASUVORK5CYII="></plastic-image> To base image selection on the rendered size of the control add the `use-element-dim` attribute. <plastic-image preload fade sizing="contain" use-element-dim srcset="images/foo-s.jpg 150w, images/foo-sh.jpg 150w 2.0x, images/foo-m.jpg 405w, images/foo-mh 2.0x 405w, images/foo-l 1024w, images/foo-t 500w 750h" placeholder="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAmElEQVQImWNmYGBgSExMzBATE7dSVFT8eO/evTcMDAwMjIFe5iYSIjybL136cunNW56FulIaEoJcfBdY5GWjvJ4/+SJhIcUhwavI5SbIxR+YvzRqH8unx7/Osf8VYpAVEWLgZuO8ljrfbwMDAwMD07u/j/ZYun5f9JfjSfGnHx9dGaCAJcBimwXjZ4Z+HllGn0XbXr+ASQAAi5UxQq88/fsAAAAASUVORK5CYII="></plastic-image> To automatically compensate for pixel density do not supply _any_ pixel density descriptors in the srcset. <plastic-image preload fade sizing="contain" use-element-dim srcset="images/foo-s.jpg 150w, images/foo-sh.jpg 300w, images/foo-m.jpg 405w, images/foo-mh 810w, images/foo-l 1024w, images/foo-t 500w 750h" placeholder="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAmElEQVQImWNmYGBgSExMzBATE7dSVFT8eO/evTcMDAwMjIFe5iYSIjybL136cunNW56FulIaEoJcfBdY5GWjvJ4/+SJhIcUhwavI5SbIxR+YvzRqH8unx7/Osf8VYpAVEWLgZuO8ljrfbwMDAwMD07u/j/ZYun5f9JfjSfGnHx9dGaCAJcBimwXjZ4Z+HllGn0XbXr+ASQAAi5UxQq88/fsAAAAASUVORK5CYII="></plastic-image> Lazy Loading Add the attribute `lazy-load` to the element and image loading will be delayed until the element is in the viewport. <plastic-image lazy-load preload fade sizing="contain" srcset="images/foo-s.jpg 150w, images/foo-sh.jpg 150w 2.0x, images/foo-m.jpg 405w, images/foo-mh 2.0x 405w, images/foo-l 1024w, images/foo-t 500w 750h" placeholder="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAmElEQVQImWNmYGBgSExMzBATE7dSVFT8eO/evTcMDAwMjIFe5iYSIjybL136cunNW56FulIaEoJcfBdY5GWjvJ4/+SJhIcUhwavI5SbIxR+YvzRqH8unx7/Osf8VYpAVEWLgZuO8ljrfbwMDAwMD07u/j/ZYun5f9JfjSfGnHx9dGaCAJcBimwXjZ4Z+HllGn0XbXr+ASQAAi5UxQq88/fsAAAAASUVORK5CYII="></plastic-image> Standard iron-image properties that should not be used Do not use `preventLoad` / `prevent-load`. This is used internally by `plastic-image` to allow the srcset processing step. To achieve that function use `delayLoad` / `delay-load` instead. Do not use `src`. That will be overwritten by the srcset evaluation. To specify a fallback image use the `fallbackSrc` / `fallback-src` attribute instead. @element plastic-image @demo demo/index.html */ class PlasticImage extends customElements.get('iron-image') { static get is() { return 'plastic-image'; } // clone the iron-image template for issue 14 - no idea why this works static get template() { if (!window.PlasticImageTemplate) { window.PlasticImageTemplate = customElements.get('iron-image').template.cloneNode(true); } return window.PlasticImageTemplate; } static get importMeta() { return import.meta; } static get properties() { return { /** * a srcset string providing rules for * which image to select */ srcset: { type: String }, /** * The image url selected from srcset rules */ _selectedImgUrl: { type: String, notify: true }, /** * While true, the control refrains from * evaluating the srcset. Set in the markup * as delay-load and when you are ready set * the property value to false. * * For example you may be retrieving the srcset * from an ajax call. You would set this to true * in the markup and set it to false once the data * is available. */ delayLoad: { type: Boolean, value: false, notify: true, reflectToAttribute: true }, /** * When true, the dimensions of this control * are used instead of the viewport size to * make the image selection from the srcset expression. */ useElementDim: { type: Boolean, reflectToAttribute: true, value: false }, /** * This is not really needed as long as there * is at least 1 image selection spec in the * srcset. However, specifying this value * makes explicit an alternative that has no * dimensional or density spec. */ fallbackSrc: { type: String, reflectToAttribute: true }, /** * Indicates if the image should only be loaded * when it is scrolled into view */ lazyLoad: { type: Boolean, value: false }, _lazyLoadPending: { type: Boolean, value: true }, /** * indicates that the browser supports webp images * @private */ _browserSupportsWebp: { type: Boolean, value: false }, /** * regex test for .webp extension * @protected */ _webpRegex: { type: RegExp, value: /.+\.webp$/i }, /** * override default webp detection regex */ webpRegex: { type: String, observer: "_webpRegexObserver" }, /** * Indicates that the srcset string has at least one webp image * @private */ _hasWebp: { type: Boolean, value: false } }; } static get observers() { return [ '_doImageSelectionObserver(fallbackSrc, srcset, delayLoad, _lazyLoadPending, _webpRegex)', '_cleanupWillChange(fade, loaded, src)' ] } /** * At connection, prevent iron-image from loading the image. * When attached, parse the srcset string and pick * the best image * @private */ connectedCallback() { super.connectedCallback(); this.preventLoad = true; //Because we're fading, hint the browser that this property //will change and thus placing it on a separate GPU layer. if (this.fade) { this.$.placeholder.style.willChange = 'opacity' } // if lazy loading, set up the intersection // observer. Otherwise, go ahead and process // the image selection if (this.lazyLoad) { this._initLazyLoad(); } else { this._lazyLoadPending = false; if (!this.delayLoad) { this.assignImgSrc(); } } } /** * When disconnected, remove the intersectionObserver * if lazy loading is in effect. * @private */ disconnectedCallback() { super.disconnectedCallback(); if (this.lazyLoad && this._lazyLoadPending && window.plasticImageIntersectionObserver) { window.plasticImageIntersectionObserver.observer.unobserve(this); // Firefox is frequently calling disconnectedCallback twice // per element. So basically this counter is useless, // but on the bright side, it's no longer needed. if (window.plasticImageIntersectionObserver.counter > 0) { window.plasticImageIntersectionObserver.counter--; } } } /** * Observer for determining if it is time for * the control to set the src property in the * underlying iron-image. * @private */ _doImageSelectionObserver(fallbackSrc, srcset, delayLoad, lazyLoadPending) { if (!delayLoad && !lazyLoadPending) { this.assignImgSrc(); } } /** * By removing the will-change property the image can go off the separate layer (freeing up memory). */ _cleanupWillChange(fade, loaded, src) { if (!fade || !loaded || !src) { //Nothing to clean up (yet) return; } //Execute no sooner than the animation has finished //and let the browser its self determine the right moment. timeOut.run(_ => idlePeriod.run(_ => { this.$.placeholder.style.willChange = '' }), 500); } /** * Assign the best image to the iron-image src property * and allow it to be loaded * @private */ assignImgSrc() { let srcArray = this.srcset && this.srcset.length > 1 ? this.srcsetParse(this.srcset) : []; this._hasWebp = srcArray.filter((srcitem) => { return this._webpRegex.test(srcitem.url); }).length > 0; // webp filtering promise this._checkBrowserWebpSupport(srcArray) .then((arr) => { // Since we might need dimensions, schedule this // right before the render of the frame so that // clientWidth and clientHeight are available. beforeNextRender(this, () => { this._selectedImgUrl = srcArray.length > 0 ? this.bestMatchingImage(arr) : this.fallbackSrc; if (!this.src || this.src !== this._selectedImgUrl) { this.src = this._selectedImgUrl; } this.preventLoad = false; }); }); } /** * Returns a sorted array of only the unique elements of an array * @param arr {array} object array * @private */ deepUnique(arr) { return arr.sort().filter((el, i) => JSON.stringify(el) !== JSON.stringify(arr[i - 1])); } /** * Srcset Parser * * By Alex Bell | MIT License | https://github.com/albell/parse-srcset * * JS Parser for the string value that appears in markup <img srcset="here"> * * @returns Array [{url: _, density: _, width: _, height:_}, ...] * * Based super duper closely on the reference algorithm at: * https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-srcset-attribute * * Most comments are copied in directly from the spec * (except for comments in parens). */ srcsetParse(input) { // UTILITY FUNCTIONS // Manual is faster than RegEx // http://bjorn.tipling.com/state-and-regular-expressions-in-javascript // http://jsperf.com/whitespace-character/5 function isSpace(c) { return (c === "\u0020" || // space c === "\u0009" || // horizontal tab c === "\u000A" || // new line c === "\u000C" || // form feed c === "\u000D"); // carriage return } function collectCharacters(regEx) { var chars, match = regEx.exec(input.substring(pos)); if (match) { chars = match[0]; pos += chars.length; return chars; } } var inputLength = input.length, // (Don't use \s, to avoid matching non-breaking space) regexLeadingSpaces = /^[ \t\n\r\u000c]+/, regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/, regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/, regexTrailingCommas = /[,]+$/, regexNonNegativeInteger = /^\d+$/, // ( Positive or negative or unsigned integers or decimals, without or without exponents. // Must include at least one digit. // According to spec tests any decimal point must be followed by a digit. // No leading plus sign is allowed.) // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/, url, descriptors, currentDescriptor, state, c, // 2. Let position be a pointer into input, initially pointing at the start // of the string. pos = 0, // 3. Let candidates be an initially empty source set. candidates = []; // 4. Splitting loop: Collect a sequence of characters that are space // characters or U+002C COMMA characters. If any U+002C COMMA characters // were collected, that is a parse error. while (true) { collectCharacters(regexLeadingCommasOrSpaces); // 5. If position is past the end of input, return candidates and abort these steps. if (pos >= inputLength) { return candidates; // (we're done, this is the sole return path) } // 6. Collect a sequence of characters that are not space characters, // and let that be url. url = collectCharacters(regexLeadingNotSpaces); // 7. Let descriptors be a new empty list. descriptors = []; // 8. If url ends with a U+002C COMMA character (,), follow these substeps: // (1). Remove all trailing U+002C COMMA characters from url. If this removed // more than one character, that is a parse error. if (url.slice(-1) === ",") { url = url.replace(regexTrailingCommas, ""); // (Jump ahead to step 9 to skip tokenization and just push the candidate). parseDescriptors(); // Otherwise, follow these substeps: } else { tokenize(); } // (close else of step 8) // 16. Return to the step labeled splitting loop. } // (Close of big while loop.) /** * Tokenizes descriptor properties prior to parsing * Returns undefined. */ function tokenize() { // 8.1. Descriptor tokeniser: Skip whitespace collectCharacters(regexLeadingSpaces); // 8.2. Let current descriptor be the empty string. currentDescriptor = ""; // 8.3. Let state be in descriptor. state = "in descriptor"; while (true) { // 8.4. Let c be the character at position. c = input.charAt(pos); // Do the following depending on the value of state. // For the purpose of this step, "EOF" is a special character representing // that position is past the end of input. // In descriptor if (state === "in descriptor") { // Do the following, depending on the value of c: // Space character // If current descriptor is not empty, append current descriptor to // descriptors and let current descriptor be the empty string. // Set state to after descriptor. if (isSpace(c)) { if (currentDescriptor) { descriptors.push(currentDescriptor); currentDescriptor = ""; state = "after descriptor"; } // U+002C COMMA (,) // Advance position to the next character in input. If current descriptor // is not empty, append current descriptor to descriptors. Jump to the step // labeled descriptor parser. } else if (c === ",") { pos += 1; if (currentDescriptor) { descriptors.push(currentDescriptor); } parseDescriptors(); return; // U+0028 LEFT PARENTHESIS (() // Append c to current descriptor. Set state to in parens. } else if (c === "\u0028") { currentDescriptor = currentDescriptor + c; state = "in parens"; // EOF // If current descriptor is not empty, append current descriptor to // descriptors. Jump to the step labeled descriptor parser. } else if (c === "") { if (currentDescriptor) { descriptors.push(currentDescriptor); } parseDescriptors(); return; // Anything else // Append c to current descriptor. } else { currentDescriptor = currentDescriptor + c; } // (end "in descriptor" // In parens } else if (state === "in parens") { // U+0029 RIGHT PARENTHESIS ()) // Append c to current descriptor. Set state to in descriptor. if (c === ")") { currentDescriptor = currentDescriptor + c; state = "in descriptor"; // EOF // Append current descriptor to descriptors. Jump to the step labeled // descriptor parser. } else if (c === "") { descriptors.push(currentDescriptor); parseDescriptors(); return; // Anything else // Append c to current descriptor. } else { currentDescriptor = currentDescriptor + c; } // After descriptor } else if (state === "after descriptor") { // Do the following, depending on the value of c: // Space character: Stay in this state. if (isSpace(c)) { // EOF: Jump to the step labeled descriptor parser. } else if (c === "") { parseDescriptors(); return; // Anything else // Set state to in descriptor. Set position to the previous character in input. } else { state = "in descriptor"; pos -= 1; } } // Advance position to the next character in input. pos += 1; // Repeat this step. } // (close while true loop) } /** * Adds descriptor properties to a candidate, pushes to the candidates array * @return undefined */ // Declared outside of the while loop so that it's only created once. function parseDescriptors() { // 9. Descriptor parser: Let error be no. var pError = false, // 10. Let width be absent. // 11. Let density be absent. // 12. Let future-compat-h be absent. (We're implementing it now as h) w, d, h, i, candidate = {}, desc, lastChar, value, intVal, floatVal; // 13. For each descriptor in descriptors, run the appropriate set of steps // from the following list: for (i = 0; i < descriptors.length; i++) { desc = descriptors[i]; lastChar = desc[desc.length - 1]; value = desc.substring(0, desc.length - 1); intVal = parseInt(value, 10); floatVal = parseFloat(value); // If the descriptor consists of a valid non-negative integer followed by // a U+0077 LATIN SMALL LETTER W character if (regexNonNegativeInteger.test(value) && (lastChar === "w")) { // plastic-map-info issue 34 - density is allowed with width // If width and density are not both absent, then let error be yes. // if (w || d) { if (w) { pError = true; } // Apply the rules for parsing non-negative integers to the descriptor. // If the result is zero, let error be yes. // Otherwise, let width be the result. if (intVal === 0) { pError = true; } else { w = intVal; } // If the descriptor consists of a valid floating-point number followed by // a U+0078 LATIN SMALL LETTER X character } else if (regexFloatingPoint.test(value) && (lastChar === "x")) { // plastic-map-info issue 34 - density is allowed with width and or height // If width, density and future-compat-h are not all absent, then let error // be yes. // if (w || d || h) { if (d) { pError = true; } // Apply the rules for parsing floating-point number values to the descriptor. // If the result is less than zero, let error be yes. Otherwise, let density // be the result. if (floatVal < 0) { pError = true; } else { d = floatVal; } // If the descriptor consists of a valid non-negative integer followed by // a U+0068 LATIN SMALL LETTER H character } else if (regexNonNegativeInteger.test(value) && (lastChar === "h")) { // plastic-map-info issue 34 - density is allowed with height // If height and density are not both absent, then let error be yes. // if (h || d) { if (h) { pError = true; } // Apply the rules for parsing non-negative integers to the descriptor. // If the result is zero, let error be yes. Otherwise, let future-compat-h // be the result. if (intVal === 0) { pError = true; } else { h = intVal; } // Anything else, Let error be yes. } else { pError = true; } } // (close step 13 for loop) // 15. If error is still no, then append a new image source to candidates whose // URL is url, associated with a width width if not absent and a pixel // density density if not absent. Otherwise, there is a parse error. if (!pError) { candidate.url = url; if (w) { candidate.width = w; } if (d) { candidate.density = d; } if (h) { candidate.height = h; } candidates.push(candidate); } else if (console && console.log) { console.log("Invalid srcset descriptor found in '" + input + "' at '" + desc + "'."); } } // (close parseDescriptors fn) } /** * Converts a srcset array to a srcset string * @param arr {Array<Isrcset>} srcset array */ srcsetStringify(arr) { return arr.map((el) => { if (!el.url) { throw new Error('URL is required.'); } let ret = [el.url]; if (el.width) { ret.push(el.width + 'w'); } if (el.height) { ret.push(el.height + 'h'); } if (el.density) { ret.push(el.density + 'x'); } return ret.join(' '); }).join(', '); } /** * Get best image from srcset * @param arr {Array<Isrcset>} srcset array * @private */ bestMatchingImage(arr) { let densityMultiplier = 1; // Check if denisty is provided in srcset let hasDensity = false; arr.forEach((src) => { if ("density" in src) { hasDensity = true; } }); if (!hasDensity) { // When no density is set use devicePixelRatio as a multiplier for reference dimensions densityMultiplier = (window.devicePixelRatio || 1.0); } // reference dimensions let refDim = this.useElementDim ? { width: (this.clientWidth || window.innerWidth || document.documentElement.clientWidth) * densityMultiplier, height: (this.clientHeight || window.innerHeight || document.documentElement.clientHeight) * densityMultiplier, // When no density is provided densityMultiplier is used and density filter can be set to 1, otherwise use devicePixelRatio density: !hasDensity ? 1 : (window.devicePixelRatio || 1.0) } : { width: (window.innerWidth || document.documentElement.clientWidth) * densityMultiplier, height: (window.innerHeight || document.documentElement.clientHeight) * densityMultiplier, // When no density is provided densityMultiplier is used and density filter can be set to 1, otherwise use devicePixelRatio density: !hasDensity ? 1 : (window.devicePixelRatio || 1.0) }; let filtered = this.fallbackSrc ? arr.concat([{ url: this.fallbackSrc }]) : arr; filtered = this.filterLarge( this.filterLarge( this.filterLarge(filtered, 'width', refDim.width), 'height', refDim.height), 'density', refDim.density); filtered = this.filterSmall(this.filterSmall(this.filterSmall(filtered, 'width'), 'height'), 'density'); return filtered[0].url; } /** * filter out disqualified items less than the refVal * @private */ filterLarge(arr, attr, refVal) { if (arr.length < 2) return arr; let largest = false; for (let i = 0; i < arr.length; i++) { if (arr[i][attr]) { if (!largest || largest[attr] < arr[i][attr]) { largest = arr[i]; } } } if (!largest) return arr; let filtered = []; for (let i = 0; i < arr.length; i++) { if (!arr[i][attr] || arr[i][attr] >= refVal) filtered.push(arr[i]); } if (filtered.length == 0) filtered.push(largest); return filtered; } /** * filter to the smallest items of a dimension * @private */ filterSmall(arr, attr) { if (arr.length < 2) return arr; let smallest = false; for (let i = 0; i < arr.length; i++) { if (arr[i][attr]) { if (!smallest || smallest[attr] > arr[i][attr]) { smallest = arr[i]; } } } if (!smallest) return arr; let filtered = []; for (let i = 0; i < arr.length; i++) { if (!arr[i][attr] || arr[i][attr] <= smallest[attr]) filtered.push(arr[i]); } return filtered; } /** * initialize lazy loading process by * creating an intersection observer and * adding this element to the observation * list. * * Waits for the polyfill to load, if necessary * * All instances of plastic-image share the same * IntersectionObserver. * @param {Boolean} polyfilled - is this being called after polyfill loaded * @private */ _initLazyLoad(polyfilled = false) { // if the polyfill is needed and not yet loaded, // wait for it to load first. if (!('IntersectionObserver' in window)) { // There could be multiple plastic-image elements in the document // but we only need to load the polyfill for IntersectionObserver // one time. let polyfillScript = document.getElementById('polyfill-IntersectionObserver'); if (!polyfillScript) { // load the intersection-observer polyfill script polyfillScript = document.createElement("script"); polyfillScript.id = 'polyfill-IntersectionObserver'; // The current version, 0.3.0, supports Safari which now has // native shadow DOM. The version currently served by polyfill.io // does not support native shadow dom. // // Until the polyfill is updated to 0.3.0 or greater on polyfill.io // we will use version 0.3.0 and include it with the element. // // polyfillScript.src = // "https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"; // polyfillScript.src = this.importPath + "intersection-observer.js"; polyfillScript.async = true; document.head.appendChild(polyfillScript); } // listen for the polyfill to finish loading // then retry the initLazyLoad process polyfillScript.addEventListener("load", _ => this._initLazyLoad(true)); } else { // IntersectionObserver is available, initialize observation // Create the observer for this page if it doesn't exist if (!window.plasticImageIntersectionObserver) { window.plasticImageIntersectionObserver = { /* the number of elements sharing this observer */ counter: 0, /* an IntersectionObserver with only default arguments */ observer: new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { entry.target._lazyLoadCallback(entry); }); }, {}) }; if (polyfilled) { // issue 23 - Edge does not reliably dispatch scroll events // issue 36 - Safari iOS does not reliably get scroll events with iron-scroll-target // At this point all pollyfilled browsers need polling :( window.plasticImageIntersectionObserver.observer.POLL_INTERVAL = 120; } } // observe this element window.plasticImageIntersectionObserver.observer.observe(this); window.plasticImageIntersectionObserver.counter++; } } /** * called by the intersection observer * to indicate a change in intersection * @private */ _lazyLoadCallback(e) { // if we are intersecting, set _lazyLoadPending false to allow the image to load // and stop observing intersections for this element if (this._lazyLoadPending && (e.isIntersecting || e.intersectionRatio >= 0.001)) { this._lazyLoadPending = false; if (window.plasticImageIntersectionObserver && window.plasticImageIntersectionObserver.observer) { window.plasticImageIntersectionObserver.observer.unobserve(this); if (window.plasticImageIntersectionObserver.counter > 0) { window.plasticImageIntersectionObserver.counter--; } } } } /** * Checks if the srcset has a webp image, and if so if the * browser supports webp. Returns a filtered copy of the srcset * array depending on support. Saves the detection so that other * instances of plastic image do not have to perform the feature * test. * * @param srcArray {source set array} * @return promise of filtered source set array * @private */ _checkBrowserWebpSupport(srcArray) { return new Promise((resolve, reject) => { // if there are no webp images in the srcArray, we do not need to detect support if (!this._hasWebp) { resolve(srcArray); } else { // if webp has previously been detected, use the prior detection if (window.plasticImageWebp) { this._supportsWebp = window.plasticImageWebp.supports; resolve(this._filterWebp(srcArray, window.plasticImageWebp.supports)); } else { // support has not been previously detected so // do a new test for webp support let webpImg = new Image(); webpImg.onload = () => { let result = (webpImg.width > 0) && (webpImg.height > 0); window.plasticImageWebp = { supports: result }; // not used by element, but used by WCT tests this._supportsWebp = result; resolve(this._filterWebp(srcArray, result)); }; webpImg.onerror = () => { window.plasticImageWebp = { supports: false }; // not used by element, but used by WCT tests this._supportsWebp = false; resolve(this._filterWebp(srcArray, false)); } webpImg.src = "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=="; } } }); } /** * Returns a filtered copy of the source set array * including only webp images if supports is true, * or only non webp images if supports is false. * * @param srcArray {source set array} * @param supports {boolean} indicates if the browser supports webp * @returns filtered source set array * @private */ _filterWebp(srcArray, supports) { return srcArray.filter((srcitem) => { return this._webpRegex.test(srcitem.url) === supports; }) } /** * Observer for this.webpRegex which provides the element consumer * api access to modify the webp detection regex. This function * handle a change to webpRegex * by applying the change to _webpRegex */ _webpRegexObserver(n, o) { if (n) { // webpRegex can be either a matching string, e.g. // "\.webp\?foo=bar" or it can be a full regex expression, e.g. // "/\.webp\?foo=bar/i" i.e. with the enclosing "/" and modifiers, if any. let regParts = n.match(/^\/(.*?)\/([gim]*)$/); if (regParts) { this._webpRegex = new RegExp(regParts[1], regParts[2]); } else { // if the string did not have delimiters // create a regex with just the pattern and ignore case this._webpRegex = new RegExp(n, "i"); } } else { // reset to default webp detection this._webpRegex = /.+\.webp$/i; } } } window.customElements.define(PlasticImage.is, PlasticImage);