FIX: Improvements to animated image pausing (#12839)
This commit is contained in:
parent
aa9a8d1041
commit
548c044809
|
@ -1,35 +1,53 @@
|
||||||
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
|
import { prefersReducedMotion } from "discourse/lib/utilities";
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
|
||||||
let _gifClickHandlers = {};
|
let _gifClickHandlers = {};
|
||||||
|
|
||||||
|
function _pauseAnimation(img, opts = {}) {
|
||||||
|
let canvas = document.createElement("canvas");
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
|
||||||
|
canvas.setAttribute("aria-hidden", "true");
|
||||||
|
canvas.setAttribute("role", "presentation");
|
||||||
|
|
||||||
|
if (opts.manualPause) {
|
||||||
|
img.classList.add("manually-paused");
|
||||||
|
}
|
||||||
|
img.parentNode.classList.add("paused-animated-image");
|
||||||
|
img.parentNode.insertBefore(canvas, img);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _resumeAnimation(img) {
|
||||||
|
img.previousSibling.remove();
|
||||||
|
img.parentNode.classList.remove("paused-animated-image", "manually-paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
function animatedImgs() {
|
||||||
|
return document.querySelectorAll("img.animated:not(.manually-paused)");
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "animated-images-pause-on-click",
|
name: "animated-images-pause-on-click",
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
withPluginApi("0.8.7", (api) => {
|
withPluginApi("0.8.7", (api) => {
|
||||||
function _cleanUp() {
|
function _cleanUp() {
|
||||||
Object.values(_gifClickHandlers || {}).forEach((handler) =>
|
Object.values(_gifClickHandlers || {}).forEach((handler) => {
|
||||||
handler.removeEventListener("click", _handleClick)
|
handler.removeEventListener("click", _handleEvent);
|
||||||
);
|
handler.removeEventListener("load", _handleEvent);
|
||||||
|
});
|
||||||
|
|
||||||
_gifClickHandlers = {};
|
_gifClickHandlers = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleClick(event) {
|
function _handleEvent(event) {
|
||||||
const img = event.target;
|
const img = event.target;
|
||||||
if (img && !img.previousSibling) {
|
if (img && !img.previousSibling) {
|
||||||
let canvas = document.createElement("canvas");
|
_pauseAnimation(img, { manualPause: true });
|
||||||
canvas.width = img.width;
|
|
||||||
canvas.height = img.height;
|
|
||||||
canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
|
|
||||||
canvas.setAttribute("aria-hidden", "true");
|
|
||||||
canvas.setAttribute("role", "presentation");
|
|
||||||
|
|
||||||
img.parentNode.classList.add("paused-animated-image");
|
|
||||||
img.parentNode.insertBefore(canvas, img);
|
|
||||||
} else {
|
} else {
|
||||||
img.previousSibling.remove();
|
_resumeAnimation(img);
|
||||||
img.parentNode.classList.remove("paused-animated-image");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,17 +59,37 @@ export default {
|
||||||
let images = post.querySelectorAll("img.animated");
|
let images = post.querySelectorAll("img.animated");
|
||||||
|
|
||||||
images.forEach((img) => {
|
images.forEach((img) => {
|
||||||
|
// skip for edge case of multiple animated images in same block
|
||||||
|
if (img.parentNode.querySelectorAll("img").length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_gifClickHandlers[img.src]) {
|
if (_gifClickHandlers[img.src]) {
|
||||||
_gifClickHandlers[img.src].removeEventListener(
|
_gifClickHandlers[img.src].removeEventListener(
|
||||||
"click",
|
"click",
|
||||||
_handleClick
|
_handleEvent
|
||||||
|
);
|
||||||
|
_gifClickHandlers[img.src].removeEventListener(
|
||||||
|
"load",
|
||||||
|
_handleEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
delete _gifClickHandlers[img.src];
|
delete _gifClickHandlers[img.src];
|
||||||
}
|
}
|
||||||
|
|
||||||
_gifClickHandlers[img.src] = img;
|
_gifClickHandlers[img.src] = img;
|
||||||
img.addEventListener("click", _handleClick, false);
|
img.addEventListener("click", _handleEvent, false);
|
||||||
|
|
||||||
|
if (prefersReducedMotion()) {
|
||||||
|
img.addEventListener("load", _handleEvent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.parentNode.classList.add("pausable-animated-image");
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.classList.add("animated-image-overlay");
|
||||||
|
overlay.setAttribute("aria-hidden", "true");
|
||||||
|
overlay.setAttribute("role", "presentation");
|
||||||
|
overlay.innerHTML = `${iconHTML("pause")}${iconHTML("play")}`;
|
||||||
|
img.parentNode.appendChild(overlay);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +99,39 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
api.cleanupStream(_cleanUp);
|
api.cleanupStream(_cleanUp);
|
||||||
|
|
||||||
|
// paused on load when prefers-reduced-motion is active, no need for blur/focus events
|
||||||
|
if (!prefersReducedMotion()) {
|
||||||
|
window.addEventListener("blur", this.blurEvent);
|
||||||
|
window.addEventListener("focus", this.focusEvent);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
blurEvent() {
|
||||||
|
animatedImgs().forEach((img) => {
|
||||||
|
if (
|
||||||
|
img.parentNode.querySelectorAll("img").length === 1 &&
|
||||||
|
!img.previousSibling
|
||||||
|
) {
|
||||||
|
_pauseAnimation(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
focusEvent() {
|
||||||
|
animatedImgs().forEach((img) => {
|
||||||
|
if (
|
||||||
|
img.parentNode.querySelectorAll("img").length === 1 &&
|
||||||
|
img.previousSibling
|
||||||
|
) {
|
||||||
|
_resumeAnimation(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
window.removeEventListener("blur", this.blurEvent);
|
||||||
|
window.removeEventListener("focus", this.focusEvent);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -433,6 +433,10 @@ export function isiOSPWA() {
|
||||||
return window.matchMedia("(display-mode: standalone)").matches && caps.isIOS;
|
return window.matchMedia("(display-mode: standalone)").matches && caps.isIOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prefersReducedMotion() {
|
||||||
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
}
|
||||||
|
|
||||||
export function isAppWebview() {
|
export function isAppWebview() {
|
||||||
return window.ReactNativeWebView !== undefined;
|
return window.ReactNativeWebView !== undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1240,15 +1240,59 @@ a.mention-group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.paused-animated-image {
|
.pausable-animated-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: inline-block;
|
||||||
> canvas {
|
|
||||||
|
> canvas,
|
||||||
|
> .animated-image-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
bottom: 0;
|
||||||
left: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
background-color: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .animated-image-overlay {
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-end;
|
||||||
|
> .d-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.5em;
|
||||||
|
background-color: rgba(var(--always-black-rgb), 0.25);
|
||||||
|
color: var(--secondary-or-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.animated {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.no-touch
|
||||||
|
&:not(.paused-animated-image)
|
||||||
|
.animated:hover
|
||||||
|
+ .animated-image-overlay
|
||||||
|
.d-icon-pause {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.paused-animated-image
|
||||||
|
.animated.manually-paused
|
||||||
|
+ .animated-image-overlay
|
||||||
|
.d-icon-play {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paused-animated-image {
|
||||||
img.animated {
|
img.animated {
|
||||||
// need to keep the image hidden but clickable
|
// need to keep the image hidden but clickable
|
||||||
// so the user can resume animation
|
// so the user can resume animation
|
||||||
|
|
|
@ -154,6 +154,7 @@ module SvgSprite
|
||||||
"moon",
|
"moon",
|
||||||
"paint-brush",
|
"paint-brush",
|
||||||
"paper-plane",
|
"paper-plane",
|
||||||
|
"pause",
|
||||||
"pencil-alt",
|
"pencil-alt",
|
||||||
"play",
|
"play",
|
||||||
"plug",
|
"plug",
|
||||||
|
|
Loading…
Reference in New Issue