Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/view/frontend/web/css/toolbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,23 @@
.mageforge-audit-low-contrast {
outline: 3px solid var(--mageforge-color-red) !important;
outline-offset: 2px;
box-shadow: inset 0 0 0 3px var(--mageforge-color-red) !important;
}
Comment thread
dermatz marked this conversation as resolved.

/* Fixed-position overlay injected over highlighted <img> elements */
.mageforge-audit-img-overlay {
position: fixed;
pointer-events: none;
background-color: rgba(239, 68, 68, 0.5);
outline: 3px solid var(--mageforge-color-red);
outline-offset: 0;
z-index: 9999997;
}

/* Non-image elements (inputs, text) get a colour filter for visibility */
.mageforge-audit-inputs-without-label,
.mageforge-audit-low-contrast {
filter: brightness(0.5) sepia(1) saturate(12) hue-rotate(330deg) !important;
}

/* ============================================================================
Expand Down
125 changes: 125 additions & 0 deletions src/view/frontend/web/js/toolbar/audits/highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* MageForge Toolbar – Shared highlight helpers
*
* Audits that mark elements by adding a CSS class use these two helpers
* instead of duplicating the same logic. The CSS class is derived from the
* audit key: `mageforge-audit-<key>`.
*/

const OVERLAY_CLASS = 'mageforge-audit-img-overlay';

/**
* Module-level registry: tracks one overlay cleanup function per <img>
* element and the set of audit keys currently relying on that overlay.
* Using a WeakMap means entries are automatically eligible for GC once
* the image node itself is collected.
*
* @type {WeakMap<HTMLImageElement, { cleanup: function, keys: Set<string> }>}
*/
const imgOverlayRegistry = new WeakMap();

/**
* Creates a fixed-position overlay <span> that tracks an <img> element's
* position in the viewport. Updates on scroll (all containers) and resize.
* Returns a cleanup function that removes the overlay and all listeners.
*
* @param {HTMLImageElement} img
* @returns {function} cleanup
*/
function createImgOverlay(img) {
const overlay = document.createElement('span');
overlay.className = OVERLAY_CLASS;
document.body.appendChild(overlay);

function update() {
if (!img.isConnected) {
cleanup();
imgOverlayRegistry.delete(img);
return;
}
const rect = img.getBoundingClientRect();
overlay.style.top = `${rect.top}px`;
overlay.style.left = `${rect.left}px`;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;
}
Comment thread
dermatz marked this conversation as resolved.

update();

// ResizeObserver on <html> catches both window resize and layout shifts
const ro = new ResizeObserver(update);
ro.observe(document.documentElement);

// capture: true catches scroll events from any scrollable container
window.addEventListener('scroll', update, { passive: true, capture: true });

Comment thread
dermatz marked this conversation as resolved.
Outdated
Comment thread
dermatz marked this conversation as resolved.
Outdated
// Named function so update() can reference it via hoisting.
// ro is always assigned before cleanup() is ever invoked (the initial
// update() call only triggers cleanup() when img.isConnected is false,
// which cannot happen at construction time).
function cleanup() {
ro.disconnect();
window.removeEventListener('scroll', update, { capture: true });
overlay.remove();
}

return cleanup;
}

/**
* Removes the highlight class from all previously marked elements and
* destroys any associated image overlays.
*
* @param {string} key - Audit key (e.g. 'images-without-alt')
*/
export function clearHighlight(key) {
const cls = `mageforge-audit-${key}`;
document.querySelectorAll(`.${cls}`).forEach(el => {
el.classList.remove(cls);
if (el.tagName === 'IMG') {
const entry = imgOverlayRegistry.get(el);
if (entry) {
entry.keys.delete(key);
if (entry.keys.size === 0) {
entry.cleanup();
imgOverlayRegistry.delete(el);
}
}
}
});
}

/**
* Highlights a set of elements by adding the audit CSS class, scrolls to the
* first result, and updates the counter badge on the toolbar menu item.
*
* For <img> elements a fixed-position overlay is injected so the red
* background is visible regardless of parent overflow or border-radius.
*
* @param {Element[]} elements - Elements to mark
* @param {string} key - Audit key (e.g. 'images-without-alt')
* @param {object} context - Alpine toolbar component instance
*/
export function applyHighlight(elements, key, context) {
if (elements.length === 0) {
context.setAuditCounterBadge(key, '0', 'success');
return;
}
const cls = `mageforge-audit-${key}`;
elements.forEach(el => {
el.classList.add(cls);
if (el.tagName === 'IMG') {
const existing = imgOverlayRegistry.get(el);
if (existing) {
existing.keys.add(key);
} else {
imgOverlayRegistry.set(el, {
cleanup: createImgOverlay(el),
keys: new Set([key]),
});
}
}
});
Comment thread
dermatz marked this conversation as resolved.
elements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
context.setAuditCounterBadge(key, `${elements.length}`, 'error');
}
13 changes: 3 additions & 10 deletions src/view/frontend/web/js/toolbar/audits/images-without-alt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* MageForge Toolbar Audit – Images without ALT
*/

const HIGHLIGHT_CLASS = 'mageforge-audit-images-without-alt';
import { applyHighlight, clearHighlight } from './highlight.js';

/** @type {import('./index.js').AuditDefinition} */
export default {
Expand All @@ -17,7 +17,7 @@ export default {
*/
run(context, active) {
if (!active) {
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach(el => el.classList.remove(HIGHLIGHT_CLASS));
clearHighlight(this.key);
return;
}

Expand All @@ -28,13 +28,6 @@ export default {
return !img.hasAttribute('alt') || img.getAttribute('alt').trim() === '';
});

if (images.length === 0) {
context.setAuditCounterBadge('images-without-alt', '0', 'success');
return;
}

images.forEach(img => img.classList.add(HIGHLIGHT_CLASS));
images[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
context.setAuditCounterBadge('images-without-alt', `${images.length}`, 'error');
applyHighlight(images, this.key, context);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Images missing explicit width and height attributes cause Cumulative Layout Shift (CLS).
*/

const HIGHLIGHT_CLASS = 'mageforge-audit-images-without-dimensions';
import { applyHighlight, clearHighlight } from './highlight.js';

/** @type {import('./index.js').AuditDefinition} */
export default {
Expand All @@ -19,7 +19,7 @@ export default {
*/
run(context, active) {
if (!active) {
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach(el => el.classList.remove(HIGHLIGHT_CLASS));
clearHighlight(this.key);
return;
}

Expand All @@ -30,13 +30,6 @@ export default {
return !img.hasAttribute('width') || !img.hasAttribute('height');
});

if (images.length === 0) {
context.setAuditCounterBadge('images-without-dimensions', '0', 'success');
return;
}

images.forEach(img => img.classList.add(HIGHLIGHT_CLASS));
images[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
context.setAuditCounterBadge('images-without-dimensions', `${images.length}`, 'error');
applyHighlight(images, this.key, context);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* wasting bandwidth and slowing initial page load.
*/

const HIGHLIGHT_CLASS = 'mageforge-audit-images-without-lazy-load';
import { applyHighlight, clearHighlight } from './highlight.js';

/** @type {import('./index.js').AuditDefinition} */
export default {
Expand All @@ -20,7 +20,7 @@ export default {
*/
run(context, active) {
if (!active) {
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach(el => el.classList.remove(HIGHLIGHT_CLASS));
clearHighlight(this.key);
return;
}

Expand All @@ -35,13 +35,6 @@ export default {
return rect.top > viewportBottom;
});

if (images.length === 0) {
context.setAuditCounterBadge('images-without-lazy-load', '0', 'success');
return;
}

images.forEach(img => img.classList.add(HIGHLIGHT_CLASS));
images[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
context.setAuditCounterBadge('images-without-lazy-load', `${images.length}`, 'error');
applyHighlight(images, this.key, context);
},
};
13 changes: 3 additions & 10 deletions src/view/frontend/web/js/toolbar/audits/inputs-without-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* to screen reader users.
*/

const HIGHLIGHT_CLASS = 'mageforge-audit-inputs-without-label';
import { applyHighlight, clearHighlight } from './highlight.js';

/** @type {import('./index.js').AuditDefinition} */
export default {
Expand All @@ -20,7 +20,7 @@ export default {
*/
run(context, active) {
if (!active) {
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach(el => el.classList.remove(HIGHLIGHT_CLASS));
clearHighlight(this.key);
return;
}

Expand All @@ -39,13 +39,6 @@ export default {
return true;
});

if (inputs.length === 0) {
context.setAuditCounterBadge('inputs-without-label', '0', 'success');
return;
}

inputs.forEach(el => el.classList.add(HIGHLIGHT_CLASS));
inputs[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
context.setAuditCounterBadge('inputs-without-label', `${inputs.length}`, 'error');
applyHighlight(inputs, this.key, context);
},
};
13 changes: 3 additions & 10 deletions src/view/frontend/web/js/toolbar/audits/low-contrast-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* - 3:1 for large text (>=18pt or >=14pt bold)
*/

const HIGHLIGHT_CLASS = 'mageforge-audit-low-contrast';
import { applyHighlight, clearHighlight } from './highlight.js';

const _colorCanvas = document.createElement('canvas');
_colorCanvas.width = _colorCanvas.height = 1;
Expand Down Expand Up @@ -164,7 +164,7 @@ export default {
*/
run(context, active) {
if (!active) {
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach(el => el.classList.remove(HIGHLIGHT_CLASS));
clearHighlight(this.key);
return;
}

Expand All @@ -189,13 +189,6 @@ export default {
return ratio < threshold;
});

if (failing.length === 0) {
context.setAuditCounterBadge('low-contrast-text', '0', 'success');
return;
}

failing.forEach(el => el.classList.add(HIGHLIGHT_CLASS));
failing[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
context.setAuditCounterBadge('low-contrast-text', `${failing.length}`, 'error');
applyHighlight(failing, this.key, context);
},
};
Loading