| 1 |
/** |
| 2 |
* Optimole Image Detection Module |
| 3 |
* Handles above-the-fold image detection and dimension analysis |
| 4 |
*/ |
| 5 |
|
| 6 |
import { optmlLogger } from './logger.js'; |
| 7 |
|
| 8 |
/** |
| 9 |
* Image detection utilities |
| 10 |
*/ |
| 11 |
export const optmlImageDetector = { |
| 12 |
/** |
| 13 |
* Detect images with missing width/height attributes and calculate their dimensions |
| 14 |
* @param {NodeList} images - Collection of image elements |
| 15 |
* @returns {Object} Object mapping image IDs to their dimensions |
| 16 |
*/ |
| 17 |
detectImageDimensions: function(images) { |
| 18 |
const imageDimensions = {}; |
| 19 |
|
| 20 |
images.forEach(img => { |
| 21 |
try { |
| 22 |
const id = parseInt(img.getAttribute('data-opt-id'), 10); |
| 23 |
if (isNaN(id)) return; |
| 24 |
|
| 25 |
const hasOptSrc = img.hasAttribute('data-opt-src'); |
| 26 |
const hasOptLazyLoaded = img.hasAttribute('data-opt-lazy-loaded'); |
| 27 |
|
| 28 |
// If image has data-opt-src and data-opt-lazy-loaded, use optimized dimensions to fix https://github.com/Codeinwp/optimole-service/issues/1588#issuecomment-3373656185 |
| 29 |
if (hasOptSrc && hasOptLazyLoaded) { |
| 30 |
const optimizedWidth = parseInt(img.getAttribute('data-opt-optimized-width'), 10); |
| 31 |
const optimizedHeight = parseInt(img.getAttribute('data-opt-optimized-height'), 10); |
| 32 |
|
| 33 |
if (!isNaN(optimizedWidth) && !isNaN(optimizedHeight) && optimizedWidth > 0 && optimizedHeight > 0) { |
| 34 |
imageDimensions[id] = { |
| 35 |
w: optimizedWidth, |
| 36 |
h: optimizedHeight |
| 37 |
}; |
| 38 |
|
| 39 |
optmlLogger.info(`Image ${id} using optimized dimensions:`, { |
| 40 |
optimizedDimensions: `${optimizedWidth}x${optimizedHeight}` |
| 41 |
}); |
| 42 |
return; // Skip further processing for this image |
| 43 |
} |
| 44 |
} |
| 45 |
|
| 46 |
const hasWidth = img.hasAttribute('width') && img.getAttribute('width') !== ''; |
| 47 |
const hasHeight = img.hasAttribute('height') && img.getAttribute('height') !== ''; |
| 48 |
|
| 49 |
// Only process images that are missing width or height attributes |
| 50 |
if (!hasWidth || !hasHeight) { |
| 51 |
const naturalWidth = img.naturalWidth || 0; |
| 52 |
const naturalHeight = img.naturalHeight || 0; |
| 53 |
|
| 54 |
// Only add if we have valid natural dimensions |
| 55 |
if (naturalWidth > 0 && naturalHeight > 0) { |
| 56 |
imageDimensions[id] = { |
| 57 |
w: naturalWidth, |
| 58 |
h: naturalHeight |
| 59 |
}; |
| 60 |
|
| 61 |
optmlLogger.info(`Image ${id} missing dimensions:`, { |
| 62 |
missingWidth: !hasWidth, |
| 63 |
missingHeight: !hasHeight, |
| 64 |
naturalDimensions: `${naturalWidth}x${naturalHeight}` |
| 65 |
}); |
| 66 |
} |
| 67 |
} |
| 68 |
} catch (error) { |
| 69 |
optmlLogger.error('Error detecting dimensions for image:', img, error); |
| 70 |
} |
| 71 |
}); |
| 72 |
|
| 73 |
return imageDimensions; |
| 74 |
}, |
| 75 |
|
| 76 |
/** |
| 77 |
* Setup intersection observer for image detection |
| 78 |
* @param {Array} aboveTheFoldImages - Array to store above-fold image IDs |
| 79 |
* @param {Map} selectorMap - Map for background selectors |
| 80 |
* @returns {IntersectionObserver} Configured intersection observer |
| 81 |
*/ |
| 82 |
createIntersectionObserver: function(aboveTheFoldImages, selectorMap) { |
| 83 |
return new IntersectionObserver(entries => { |
| 84 |
entries.forEach(entry => { |
| 85 |
if (entry.isIntersecting) { |
| 86 |
const element = entry.target; |
| 87 |
|
| 88 |
// Handle img elements with data-opt-id |
| 89 |
if (element.tagName === 'IMG') { |
| 90 |
const id = parseInt(element.getAttribute('data-opt-id'), 10); |
| 91 |
if (!isNaN(id) && !aboveTheFoldImages.includes(id)) { |
| 92 |
aboveTheFoldImages.push(id); |
| 93 |
} |
| 94 |
} |
| 95 |
// Handle background image elements |
| 96 |
else if (element.hasAttribute('data-optml-bg-selector')) { |
| 97 |
const baseSelector = element.getAttribute('data-optml-bg-selector'); |
| 98 |
const specificSelector = element.getAttribute('data-optml-specific-selector'); |
| 99 |
|
| 100 |
if (baseSelector && specificSelector && selectorMap.has(baseSelector)) { |
| 101 |
// Add this specific selector to the above-fold list for this base selector |
| 102 |
const aboveTheFoldSelectors = selectorMap.get(baseSelector); |
| 103 |
if (!aboveTheFoldSelectors.includes(specificSelector)) { |
| 104 |
aboveTheFoldSelectors.push(specificSelector); |
| 105 |
optmlLogger.info(`Element with selector "${specificSelector}" is above the fold`); |
| 106 |
} |
| 107 |
} |
| 108 |
} |
| 109 |
} |
| 110 |
}); |
| 111 |
}, { |
| 112 |
threshold: 0.1 // Consider element visible when 10% is in viewport |
| 113 |
}); |
| 114 |
}, |
| 115 |
|
| 116 |
/** |
| 117 |
* Observe all images with data-opt-id attributes |
| 118 |
* @param {IntersectionObserver} observer - Intersection observer instance |
| 119 |
* @returns {Object} Object containing images and observedElements map |
| 120 |
*/ |
| 121 |
observeOptimoleImages: function(observer) { |
| 122 |
// Get all images with data-opt-id for processing |
| 123 |
const allOptimoleImages = document.querySelectorAll('img[data-opt-id]'); |
| 124 |
const observedElements = new Map(); |
| 125 |
|
| 126 |
// Observe all images with data-opt-id |
| 127 |
allOptimoleImages.forEach(img => { |
| 128 |
const id = parseInt(img.getAttribute('data-opt-id'), 10); |
| 129 |
if (isNaN(id)) { |
| 130 |
optmlLogger.warn('Invalid data-opt-id:', img.getAttribute('data-opt-id')); |
| 131 |
return; |
| 132 |
} |
| 133 |
observedElements.set(img, id); |
| 134 |
observer.observe(img); |
| 135 |
}); |
| 136 |
|
| 137 |
return { allOptimoleImages, observedElements }; |
| 138 |
}, |
| 139 |
|
| 140 |
/** |
| 141 |
* Clean up temporary data attributes from background elements |
| 142 |
*/ |
| 143 |
cleanupBackgroundElements: function() { |
| 144 |
// After observation is complete, process below-the-fold elements |
| 145 |
document.querySelectorAll('[data-optml-bg-selector]').forEach(element => { |
| 146 |
// Remove temporary data attributes |
| 147 |
element.removeAttribute('data-optml-bg-selector'); |
| 148 |
element.removeAttribute('data-optml-specific-selector'); |
| 149 |
}); |
| 150 |
} |
| 151 |
}; |
| 152 |
|