JezK
Edit File: resize-detection.js
(function(window, document) { let started; const scalingError = document.createElement('div'); scalingError.id = 'ewww-scaling-error'; window.onload = function() { started = Date.now(); checkImageSizes(); document.body.append(scalingError); const adminBarCheckButton = document.getElementById('wp-admin-bar-image-detective-check'); if (adminBarCheckButton) { adminBarCheckButton.onclick = function() { adminBarCheckButton.classList.toggle('ewww-fade'); clearScaledImages(); setTimeout(function() { checkImageSizes(); adminBarCheckButton.classList.toggle('ewww-fade'); }, 500); return false; }; } const adminBarClearButton = document.getElementById('wp-admin-bar-image-detective-clear'); if (adminBarClearButton) { adminBarClearButton.onclick = function() { adminBarClearButton.classList.toggle('ewww-fade'); shouldTagLCP = false; clearLCPMarker(); clearScaledImages(); setTimeout(function() { adminBarClearButton.classList.toggle('ewww-fade'); }, 500); return false; }; } initCriticalImages(); }; const lcpElements = []; const lcpTag = document.createElement('div'); const excludeMenuTag = document.createElement('div'); excludeMenuTag.id = 'ewww-lazy-exclude-menu'; let excludeButtonVisible = false; let excludeMenuVisible = false; let lastLCP = false; let shouldTagLCP = true; const initCriticalImages = function() { if (! atfImagesLoaded() && Date.now() - started < 5000) { console.log('hold up, need to wait a bit...'); setTimeout(initCriticalImages, 100); return; } console.log('lets get the LCP image!'); setupLCPObserver(); }; const setupLCPObserver = function() { lcpTag.id = 'ewww-lcp-tag'; lcpTag.innerText = 'LCP'; const lcpObserver = new PerformanceObserver((list) => { const lcpEntries = list.getEntries(); if (lcpElements.length > 0) { clearLCPMarker(); } else { document.body.append(lcpTag); document.body.append(excludeMenuTag); } lastLCP = lcpEntries[lcpEntries.length - 1]; tagLCP(lastLCP.element); lcpElements.push(lastLCP); }); lcpObserver.observe({type: 'largest-contentful-paint', buffered: true}); setTimeout( function() { lcpObserver.disconnect(); }, 60000 ); for (const type of ['keydown', 'click']) { addEventListener(type, function() { shouldTagLCP = false; }); } }; const tagLCP = function(lcpElement) { if (! shouldTagLCP) { console.log('nope, do not tag LCP anymore'); return; } matchPosition(lcpElement, lcpTag); console.log('LCP Element:', lcpElement); lcpElement.classList.add('ewww-lcp-element'); if (imageDetectiveVars.llActive && lcpElement.tagName.toLowerCase() === 'img' && (hasClass(lcpElement,'lazyload') || hasClass(lcpElement,'lazyloaded'))) { lcpElement.addEventListener('mouseover', showExcludeMenu); } }; const matchPosition = function(sourceElem, destElem) { const rect = sourceElem.getBoundingClientRect(); destElem.style.top = (rect.top + window.scrollY) + 'px'; destElem.style.left = (rect.left + window.scrollX) + 'px'; destElem.style.zIndex = sourceElem.style.zIndex + 999; destElem.style.maxWidth = (rect.width - 60) + 'px'; }; const hideExcludeMenu = function(e) { if (hasClass(excludeMenuTag, 'ewww-error')) { document.removeEventListener('mouseup', hideExcludeMenu); return; } const currentLCP = document.querySelector('.ewww-lcp-element'); if (!currentLCP.contains(e.target) && ! excludeMenuTag.contains(e.target) && 'ewww-lcp-tag' !== e.target.id) { excludeMenuTag.style.display = 'none'; excludeButtonVisible = false; currentLCP.addEventListener('mouseover', showExcludeMenu); document.removeEventListener('mouseup', hideExcludeMenu); } } const sendExclusion = async function() { this.removeEventListener('click', sendExclusion); const exclusion = this.dataset.ewwwLazyExcludeValue; console.log('sending exclusion:', exclusion, this.dataset.globalExclusion); const exclusionData = new FormData(); exclusionData.append('action', 'ewww_add_lazy_exclusion'); exclusionData.append('ewww_wpnonce', imageDetectiveVars.nonce); exclusionData.append('global', this.dataset.globalExclusion); exclusionData.append('exclusion', exclusion); exclusionData.append('post_id', imageDetectiveVars.postId); exclusionData.append('request_uri', imageDetectiveVars.requestUri); try { const exclusionResponse = await fetch( imageDetectiveVars.ajaxUrl, { method: 'POST', body: exclusionData, } ); if (!exclusionResponse.ok) { excludeMenuTag.innerHTML = imageDetectiveVars.invalid_response; excludeMenuTag.classList.add('ewww-error'); window.console.log(`Attempt to add Lazy Load Exclusion resulted in an HTTP error with code ${exclusionResponse.status}`); return; } const exclusionResult = await exclusionResponse.json(); if (exclusionResult.success && exclusionResult.message) { excludeMenuTag.innerHTML = exclusionResult.message; excludeMenuTag.classList.add('ewww-success'); } else if (exclusionResult.data) { excludeMenuTag.innerHTML = exclusionResult.data; excludeMenuTag.classList.add('ewww-error'); } else { excludeMenuTag.innerHTML = imageDetectiveVars.invalid_response; excludeMenuTag.classList.add('ewww-error'); window.console.log('Attempt to add Lazy Load Exclusion encountered an invalid response:', exclusionResult); } } catch (error) { excludeMenuTag.innerHTML = imageDetectiveVars.invalid_response; excludeMenuTag.classList.add('ewww-error'); window.console.log('Attempt to add Lazy Load Exclusion resulted in an error:', error); } excludeMenuVisible = false; } const sendIgnoreImage = async function() { event.preventDefault(); this.removeEventListener('click', sendIgnoreImage); currentScalingErrorElem.removeEventListener('mouseover', showScalingError); // How do we display success? Just nuke the error message? // But likely need to make sure we remove the ignoreTag const ignorePath = this.dataset.imagePath; console.log('sending ignore request:', ignorePath ); const ignoreData = new FormData(); ignoreData.append('action', 'ewww_add_ignore_scaling_rule'); ignoreData.append('ewww_wpnonce', imageDetectiveVars.nonce); ignoreData.append('ignore', ignorePath); ignoreData.append('post_id', imageDetectiveVars.postId); ignoreData.append('request_uri', imageDetectiveVars.requestUri); try { const ignoreResponse = await fetch( imageDetectiveVars.ajaxUrl, { method: 'POST', body: ignoreData, } ); if (!ignoreResponse.ok) { scalingError.innerHTML = imageDetectiveVars.invalid_response; scalingError.classList.add('ewww-error'); window.console.log(`Attempt to add Scaling Ignore Rule resulted in an HTTP error with code ${ignoreResponse.status}`); return false; } const ignoreResult = await ignoreResponse.json(); if (ignoreResult.success && ignoreResult.message) { currentScalingErrorElem.classList.remove('ewww-improperly-scaled'); scalingError.innerHTML = ignoreResult.message; scalingError.classList.add('ewww-success'); imageDetectiveVars.scalingExclusions.push(ignorePath); } else if (ignoreResult.data) { scalingError.innerHTML = ignoreResult.data; scalingError.classList.add('ewww-error'); } else { scalingError.innerHTML = imageDetectiveVars.invalid_response; scalingError.classList.add('ewww-error'); window.console.log('Attempt to add Scaling Ignore Rule encountered an invalid response:', ignoreResult); } } catch (error) { scalingError.innerHTML = imageDetectiveVars.invalid_response; scalingError.classList.add('ewww-error'); window.console.log('Attempt to add Scaling Ignore Rule resulted in an error:', error); } return false; } const addExcludeMenuItem = function(exclusion,global) { const exclusionMenuItem = document.createElement('li'); if (global) { exclusionMenuItem.innerHTML = imageDetectiveVars.menuItemGlobalText; exclusionMenuItem.dataset.globalExclusion = 1; } else { exclusionMenuItem.innerHTML = imageDetectiveVars.menuItemPageText; exclusionMenuItem.dataset.globalExclusion = 0; } exclusionMenuItem.dataset.ewwwLazyExcludeValue = exclusion; exclusionMenuItem.querySelector('span.ewww-lazy-exclude-item-name').innerText = exclusion; exclusionMenuItem.addEventListener('click', sendExclusion); return exclusionMenuItem; } const getImagePath = function(img) { if ('string' == typeof img.src && img.src.search(/data:image/) === -1) { console.log('attempting to get image path:', img.src); try { const imageURL = new URL(img.src); if (imageURL.pathname.length > 0) { return imageURL.pathname; } } catch (error) { console.log('could not parse', error); } } return ''; } const showExcludeMenu = function() { this.removeEventListener('mouseover', showExcludeMenu); if (hasClass(excludeMenuTag, 'ewww-error')) { return; } if (hasClass(excludeMenuTag, 'ewww-success')) { excludeMenuTag.classList.remove('ewww-success'); excludeMenuTag.innerHTML = ''; } excludeButtonVisible = true; const excludeMenuItems = []; let excludeMenuList = excludeMenuTag.querySelector('ul'); if (null === excludeMenuList) { const imagePath = getImagePath(this); if (imagePath) { console.log('adding menu items with ' + imagePath); excludeMenuItems.push(addExcludeMenuItem(imagePath, true)); excludeMenuItems.push(addExcludeMenuItem(imagePath, false)); } if (this.classList.length > 0) { const ignoredClasses = ['ewww-lcp-element','lazyload','lazyloaded','lazyloading','lazyautosizes','ls-is-cached','ewww-improperly-scaled']; this.classList.forEach(function(classValue){ if (ignoredClasses.includes(classValue)) { return; } excludeMenuItems.push(addExcludeMenuItem(classValue, true)); }); } if (this.id.length > 0) { excludeMenuItems.push(addExcludeMenuItem(this.id, true)); } } if (excludeMenuItems.length > 0) { console.log('creating menu list'); const excludeMenuLabel = document.createElement('div'); excludeMenuLabel.id = 'ewww-lazy-exclude-menu-label'; excludeMenuLabel.innerText = imageDetectiveVars.excludeMenuText; excludeMenuLabel.onclick = function() { if (excludeMenuVisible) { excludeMenuVisible = false; excludeMenuTag.querySelector('ul').style.display = 'none'; } else { excludeMenuVisible = true; excludeMenuTag.querySelector('ul').style.display = 'block'; } }; excludeMenuTag.append(excludeMenuLabel); excludeMenuList = document.createElement('ul'); for (let i = 0, len = excludeMenuItems.length; i < len; i++) { console.log('adding menu list item'); excludeMenuList.append(excludeMenuItems[i]); } excludeMenuTag.append(excludeMenuList); } if (hasClass(this, 'ewww-improperly-scaled')) { excludeMenuTag.style.marginTop = '110px'; excludeMenuTag.dataset.badScale = 1; } else { excludeMenuTag.dataset.badScale = 0; } matchPosition(this, excludeMenuTag); if ('none' === excludeMenuTag.style.display) { excludeMenuTag.style.display = 'flex'; } document.addEventListener('mouseup', hideExcludeMenu); } let currentScalingErrorElem = {}; const showScalingError = function() { currentScalingErrorElem = this; scalingError.classList.remove('ewww-error'); scalingError.classList.remove('ewww-success'); scalingError.innerHTML = imageDetectiveVars.scalingErrorText; scalingError.querySelector('span.ewww-scaling-error-natural-width').innerText = this.srcsetImg && this.srcsetImg.naturalWidth > 0 ? this.srcsetImg.naturalWidth : this.naturalWidth; scalingError.querySelector('span.ewww-scaling-error-natural-height').innerText = this.srcsetImg && this.srcsetImg.naturalHeight > 0 ? this.srcsetImg.naturalHeight : this.naturalHeight; scalingError.querySelector('span.ewww-scaling-error-container-width').innerText = this.clientWidth; scalingError.querySelector('span.ewww-scaling-error-container-height').innerText = this.clientHeight; if (hasClass(this, 'ewww-lcp-element')) { scalingError.style.marginTop = '60px'; } else { scalingError.style.removeProperty('margin-top'); const imagePath = getImagePath(this); if (imagePath) { let ignoreTag = document.createElement('a'); ignoreTag.classList.add('ewww-ignore-scaling-error'); ignoreTag.href = '#'; ignoreTag.innerText = imageDetectiveVars.ignoreErrorText; ignoreTag.addEventListener('click', sendIgnoreImage); ignoreTag.dataset.imagePath = imagePath; scalingError.append(' ', ignoreTag); } } matchPosition(this, scalingError); } const hideScalingError = function() { scalingError.innerText = ''; scalingError.style.top = '-1000px'; scalingError.style.left = '-1000px'; } const clearScalingError = function(elem) { elem.classList.remove('ewww-improperly-scaled'); delete elem.dataset.ewwwScalingError; elem.removeEventListener('mouseover', showScalingError); } const clearScaledImages = function() { const scaledImages = document.querySelectorAll('img.ewww-improperly-scaled'); for (let i = 0, len = scaledImages.length; i < len; i++) { clearScalingError(scaledImages[i]); } hideScalingError(); } const clearLCPMarker = function() { for (let i = 0, len = lcpElements.length; i < len; i++){ lcpElements[i].element.classList.remove('ewww-lcp-element'); lcpElements[i].element.removeEventListener('mouseover', showExcludeMenu); } lcpTag.style.top = '-1000px'; lcpTag.style.left = '-1000px'; } const checkImageSizes = function() { // Find images which have width or height greater than their natural // width or height and highlight them. const imgs = document.getElementsByTagName('img'); for (i = 0; i < imgs.length; i++) { clearScalingError(imgs[i]); checkImageScale(imgs[i]); } if (lastLCP) { shouldTagLCP = true; tagLCP(lastLCP.element); } return false; }; const atfImages = []; let isBodyHidden; const getATFImages = function() { if (atfImages.length > 0) { return; } const imgs = document.getElementsByTagName('img'); for (i = 0; i < imgs.length; i++) { if (imageInView(imgs[i])) { atfImages.push(imgs[i]); } } }; const atfImagesLoaded = function() { getATFImages(); for (i = 0; i < atfImages.length; i++) { if ( ! imageLoaded(atfImages[i])) { console.log(atfImages[i].src + ' not loaded yet:'); console.log(atfImages[i]); return false; } } return true; }; const imageLoaded = function(img) { if (! img.complete) { return false; } if (0 === img.naturalWidth) { return false; } if (hasClass(img, 'lazyload')) { return false; } return true; }; const imageInView = function(img) { const rect = img.getBoundingClientRect(); const screenWidth = window.innerWidth || document.documentElement.clientWidth; const screenHeight = window.innerHeight || document.documentElement.clientHeight; if ( (rect.bottom || rect.right || rect.left || rect.top) && ( (rect.top >= 0 && rect.top <= screenHeight) || (rect.bottom >= 0 && rect.bottom <= screenHeight) ) && ( (rect.left >= 0 && rect.left <= screenWidth) || (rect.right >= 0 && rect.right <= screenWidth) ) && isVisible(img) ) { return true; } /*console.log(img.dataset.src); console.log(rect.top + ' >= 0'); console.log(rect.left + ' >= 0'); console.log(rect.right + ' <= ' + (window.innerWidth || document.documentElement.clientWidth)); console.log(rect.bottom + ' <= ' + (window.innerHeight || document.documentElement.clientHeight));*/ return false; }; const isVisible = function (elem) { if (isBodyHidden == null) { isBodyHidden = getCSS(document.body, 'visibility') == 'hidden'; } return isBodyHidden || !(getCSS(elem.parentNode, 'visibility') == 'hidden' && getCSS(elem, 'visibility') == 'hidden'); }; const getCSS = function (elem, style) { return (getComputedStyle(elem, null) || {})[style]; }; const regClassCache = {}; const hasClass = function(ele, cls) { if(!regClassCache[cls]){ regClassCache[cls] = new RegExp('(\\s|^)'+cls+'(\\s|$)'); } return regClassCache[cls].test(ele.getAttribute('class') || '') && regClassCache[cls]; }; const checkImageScale = function(img) { if (!img.src) { return; } if ('string' == typeof img.src && img.src.search(/\.svg/) > -1) { return; } if ('string' == typeof img.src && img.src.search(/data:image/) > -1) { return; } const imagePath = getImagePath(img); if (imageDetectiveVars.scalingExclusions.includes(imagePath)) { return; } if (img.complete && img.naturalWidth > 0) { console.log('checking size of: ' + img.src); if (img.naturalWidth > 25 && img.naturalHeight > 25 && img.clientWidth > 25 && img.clientHeight > 25) { // For each image with a natural width greater than 25x25... if (img.srcset && img.currentSrc && img.srcsetImg && img.srcsetImg.src == img.currentSrc) { compareNaturalToExpected(img, img.srcsetImg.naturalWidth, img.srcsetImg.naturalHeight); } else if (img.srcset && img.currentSrc && 'string' == typeof img.currentSrc) { img.srcsetImg = new Image(); img.srcsetImg.onload = function() { console.log(`natural dimensions from srcset source ${img.srcsetImg.src} are ${img.srcsetImg.naturalWidth}x${img.srcsetImg.naturalHeight}`); compareNaturalToExpected(img, img.srcsetImg.naturalWidth, img.srcsetImg.naturalHeight); } img.srcsetImg.src = img.currentSrc; } else { compareNaturalToExpected(img, img.naturalWidth, img.naturalHeight); } } } else { console.log('defering check until onload: ' + img.src); img.onload = function() { checkImageScale(this); } } }; const compareNaturalToExpected = function(img, physicalWidth, physicalHeight) { const dPR = (window.devicePixelRatio || 1); console.log(`comparing natural ${physicalWidth}x${physicalHeight} vs. expected ${img.clientWidth}x${img.clientHeight} using dpr ${dPR}`); const wrongWidth = (img.clientWidth * 1.25 * dPR < physicalWidth) || (img.clientWidth > 768 && img.clientWidth * dPR + 100 < physicalWidth); const wrongHeight = (img.clientHeight * 1.25 * dPR < physicalHeight) || (img.clientHeight > 768 && img.clientHeight * dPR + 100 < physicalHeight); const widthDiff = physicalWidth - img.clientWidth * dPR; const heightDiff = physicalHeight - img.clientHeight * dPR; console.log(`width difference is ${widthDiff} and height difference is ${heightDiff}`); if ((wrongWidth && heightDiff > 25) || (wrongHeight && widthDiff > 25)) { img.classList.add('ewww-improperly-scaled'); img.addEventListener('mouseover', showScalingError); } } document.addEventListener('lazyloaded', function(e) { clearScalingError(e.target); checkImageScale(e.target); }); })(window, document);