MediaWiki:Gadget-FreezeGifs.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* Gadget-FreezeGifs.js
*
* This gadget finds animated GIFs and autoplaying videos on a page,
* freezes them, and overlays a play icon. The animation or video plays
* only when the user hovers their mouse over the element.
*
* - Freezes GIFs by drawing their first frame to a <canvas> element.
* - Correctly handles lazy-loaded images and gallery thumbnails.
* - Excludes specific areas (like .charbox) from being processed.
*/
(function (mw, $) {
// A flag to ensure the script doesn't run multiple times on the same page load.
if (window.PAUSE_ANIMATIONS_GADGET_LOADED) {
return;
}
window.PAUSE_ANIMATIONS_GADGET_LOADED = true;
var config = {
containerClass: 'pa-container',
overlayClass: 'pa-overlay',
playIconClass: 'pa-play-icon',
processedClass: 'pa-processed',
pendingClass: 'pa-pending', // Class to prevent race conditions on lazy-loaded images
staticCanvasClass: 'pa-static-canvas',
animatedGifClass: 'pa-animated-gif',
// CSS styles are injected dynamically.
css:
// 1. The container class is added to the IMG's existing parent, setting up the positioning context.
'.pa-container { position: relative !important; }' +
// 2. The original animated GIF remains in the normal layout flow but is made invisible.
'.pa-container > img.' + 'pa-animated-gif' + ' { visibility: hidden !important; }' +
// 3. The canvas and overlay are layered on top using absolute positioning. They will fill the container.
'.pa-container > .pa-static-canvas, .pa-container > .pa-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }' +
// 4. The canvas is visible by default, covering the space of the hidden GIF.
'.pa-container > .pa-static-canvas { visibility: visible; z-index: 1; }' +
// 5. The overlay sits on the highest layer.
'.pa-overlay { z-index: 2; background-color: rgba(0, 0, 0, 0.3); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: opacity 0.2s ease-in-out; opacity: 1; pointer-events: none; }' +
'.pa-play-icon { box-sizing: border-box; width: 0; height: 30px; border-color: transparent transparent transparent white; border-style: solid; border-width: 15px 0 15px 25px; }' +
// 6. On hover, fade out the overlay.
'.pa-container:hover .pa-overlay { opacity: 0; }' +
// 7. On hover, make the original animated GIF visible again...
'.pa-container:hover > img.' + 'pa-animated-gif' + ' { visibility: visible !important; }' +
// 8. ...and hide the static canvas.
'.pa-container:hover > .' + 'pa-static-canvas' + ' { visibility: hidden !important; }'
};
/**
* Finds and freezes all autoplaying videos.
*/
function handleVideos() {
$('video[autoplay]').not('.' + config.processedClass).each(function () {
var $video = $(this);
this.pause();
this.currentTime = 0;
$video.addClass(config.processedClass).removeAttr('autoplay').attr('preload', 'metadata').prop('loop', true);
$video.wrap($('<div>').addClass(config.containerClass));
var $container = $video.parent();
$container.append($('<div>').addClass(config.overlayClass).append($('<div>').addClass(config.playIconClass)));
if ($video.css('float') !== 'none') {
$container.css('float', $video.css('float'));
$video.css('float', 'none');
}
$container.on('mouseenter', function () {
this.querySelector('video').play().catch(function() {
this.querySelector('video').muted = true;
this.querySelector('video').play();
});
}).on('mouseleave', function () {
var videoEl = this.querySelector('video');
videoEl.pause();
videoEl.currentTime = 0;
});
});
}
/**
* Creates a static canvas and layers it correctly without breaking layouts.
*/
function createStaticCanvas(imgElement) {
var $img = $(imgElement);
var $parent = $img.parent();
// For galleries, the image may be wrapped in an extra span or anchor
if ($parent.hasClass('galleryfilename') || $parent.hasClass('thumb') || $parent.hasClass('gallerybox')) {
$parent = $img.closest('.thumb, .gallerybox, .galleryfilename');
}
$img.removeClass(config.pendingClass).addClass(config.processedClass);
// Create canvas
var canvas = document.createElement('canvas');
canvas.width = imgElement.naturalWidth;
canvas.height = imgElement.naturalHeight;
canvas.getContext('2d').drawImage(imgElement, 0, 0);
$img.addClass(config.animatedGifClass);
$(canvas).addClass(config.staticCanvasClass);
// Ensure container is positionable
$parent.css({ position: 'relative', display: 'inline-block' }).addClass(config.containerClass);
// Insert canvas and overlay on top
$parent.append(canvas);
$parent.append($('<div>').addClass(config.overlayClass).append($('<div>').addClass(config.playIconClass)));
}
/**
* Finds and freezes all animated GIFs.
*/
function handleGifs() {
// Select GIFs that aren't in a charbox and haven't been marked as pending or processed.
$('img[src$=".gif"]')
.not('.charbox img, .' + config.processedClass + ', .' + config.pendingClass)
.each(function () {
var imageElement = this;
if (imageElement.complete && imageElement.naturalWidth > 0) {
// If image is already loaded, process it immediately.
createStaticCanvas(imageElement);
} else {
// If image is not loaded, mark it as 'pending' immediately.
var $img = $(imageElement);
$img.addClass(config.pendingClass);
// Then, attach a ONE-TIME listener to process it when it's ready.
$img.one('load', function () {
createStaticCanvas(this);
}).one('error', function() {
// If the image fails, just mark it as processed so we don't try again.
$(this).removeClass(config.pendingClass).addClass(config.processedClass);
});
}
});
// Also check gallery images specifically
$('.gallerybox img[src$=".gif"]')
.not('.' + config.processedClass + ', .' + config.pendingClass)
.each(function () {
var imageElement = this;
if (imageElement.complete && imageElement.naturalWidth > 0) {
createStaticCanvas(imageElement);
} else {
var $img = $(imageElement);
$img.addClass(config.pendingClass);
$img.one('load', function () {
createStaticCanvas(this);
}).one('error', function () {
$(this).removeClass(config.pendingClass).addClass(config.processedClass);
});
}
});
}
// Main execution block.
mw.loader.using(['mediawiki.util'], function () {
$(function () {
mw.util.addCSS(config.css);
var runPause = function() {
handleVideos();
handleGifs();
};
runPause();
var targetNode = document.getElementById('mw-content-text');
if (targetNode) {
new MutationObserver(runPause).observe(targetNode, {
childList: true,
subtree: true
});
}
});
});
}(mediaWiki, jQuery));