mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-17 17:07:07 +00:00
8a5424c5a4
Address audit findings across background handlers, openers, plugins, and UI: URL allowlists, XSS reductions, popup lifecycle fixes, plugin dispose/cleanup, cloud sync hardening, global search mathjs sandbox, and settings storage fixes.
172 lines
4.7 KiB
TypeScript
172 lines
4.7 KiB
TypeScript
/**
|
|
* Makes popup hero images/videos open a padded overlay (not browser fullscreen) on click.
|
|
* Escape or backdrop click dismisses it. Clicks use stopPropagation so the
|
|
* parent SEQTA popup does not close.
|
|
*/
|
|
|
|
import { settingsState } from "../listeners/SettingsState";
|
|
import { allowedPopupImageUrl } from "./allowedPopupImageUrl";
|
|
|
|
const FULLSCREENABLE_CLASS = "popup-media-fullscreenable";
|
|
const OVERLAY_VISIBLE_CLASS = "bsplus-popup-media-overlay-backdrop--visible";
|
|
const OVERLAY_ANIM_MS = 280;
|
|
|
|
function isImageOrVideo(el: Element): el is HTMLImageElement | HTMLVideoElement {
|
|
return el instanceof HTMLImageElement || el instanceof HTMLVideoElement;
|
|
}
|
|
|
|
export function attachPopupMediaFullscreen(el: HTMLImageElement | HTMLVideoElement) {
|
|
el.classList.add(FULLSCREENABLE_CLASS);
|
|
el.setAttribute("tabindex", "0");
|
|
el.setAttribute("role", "button");
|
|
el.setAttribute("aria-label", "View larger");
|
|
el.title = "Click to view larger";
|
|
|
|
const open = (e: Event) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openMediaOverlayViewer(el);
|
|
};
|
|
|
|
el.addEventListener("click", open);
|
|
el.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
open(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
function openMediaOverlayViewer(source: HTMLImageElement | HTMLVideoElement) {
|
|
const backdrop = document.createElement("div");
|
|
backdrop.id = "bsplus-popup-media-overlay";
|
|
backdrop.className = "bsplus-popup-media-overlay-backdrop";
|
|
|
|
const inner = document.createElement("div");
|
|
inner.className = "bsplus-popup-media-overlay-inner";
|
|
|
|
const slot = document.createElement("div");
|
|
slot.className = "bsplus-popup-media-overlay-slot";
|
|
|
|
let media: HTMLImageElement | HTMLVideoElement;
|
|
if (source instanceof HTMLVideoElement) {
|
|
const v = source;
|
|
const nv = document.createElement("video");
|
|
nv.classList.add("bsplus-popup-media-overlay-media");
|
|
nv.controls = true;
|
|
nv.playsInline = true;
|
|
nv.loop = v.loop;
|
|
nv.muted = v.muted;
|
|
nv.volume = v.volume;
|
|
let hasValidSource = false;
|
|
for (const s of v.querySelectorAll("source")) {
|
|
const src = allowedPopupImageUrl((s as HTMLSourceElement).src);
|
|
if (!src) continue;
|
|
hasValidSource = true;
|
|
const ns = document.createElement("source");
|
|
ns.src = src;
|
|
const t = (s as HTMLSourceElement).type;
|
|
if (t) ns.type = t;
|
|
nv.appendChild(ns);
|
|
}
|
|
if (!hasValidSource) {
|
|
const directSrc = allowedPopupImageUrl(v.currentSrc || v.src);
|
|
if (!directSrc) return;
|
|
nv.src = directSrc;
|
|
}
|
|
nv.addEventListener(
|
|
"loadeddata",
|
|
() => {
|
|
try {
|
|
nv.currentTime = v.currentTime;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
void nv.play().catch(() => {});
|
|
},
|
|
{ once: true },
|
|
);
|
|
v.pause();
|
|
nv.load();
|
|
media = nv;
|
|
} else {
|
|
const rawSrc = source.currentSrc || source.src;
|
|
const safeSrc = allowedPopupImageUrl(rawSrc);
|
|
if (!safeSrc) return;
|
|
const img = document.createElement("img");
|
|
img.classList.add("bsplus-popup-media-overlay-media");
|
|
img.src = safeSrc;
|
|
img.alt = source.alt || "";
|
|
media = img;
|
|
}
|
|
|
|
media.addEventListener("click", (e) => e.stopPropagation());
|
|
|
|
slot.appendChild(media);
|
|
inner.append(slot);
|
|
backdrop.appendChild(inner);
|
|
document.body.append(backdrop);
|
|
|
|
if (!settingsState.animations) {
|
|
backdrop.classList.add("bsplus-popup-media-overlay--instant");
|
|
backdrop.classList.add(OVERLAY_VISIBLE_CLASS);
|
|
} else {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
backdrop.classList.add(OVERLAY_VISIBLE_CLASS);
|
|
});
|
|
});
|
|
}
|
|
|
|
inner.addEventListener("click", (e) => e.stopPropagation());
|
|
|
|
let done = false;
|
|
const removeOverlay = () => {
|
|
if (source instanceof HTMLVideoElement && media instanceof HTMLVideoElement) {
|
|
try {
|
|
source.currentTime = media.currentTime;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
void source.play().catch(() => {});
|
|
}
|
|
backdrop.remove();
|
|
};
|
|
|
|
const close = () => {
|
|
if (done) return;
|
|
done = true;
|
|
document.removeEventListener("keydown", onDocKey, true);
|
|
|
|
if (!settingsState.animations) {
|
|
removeOverlay();
|
|
return;
|
|
}
|
|
|
|
backdrop.classList.remove(OVERLAY_VISIBLE_CLASS);
|
|
window.setTimeout(removeOverlay, OVERLAY_ANIM_MS);
|
|
};
|
|
|
|
const onDocKey = (ev: KeyboardEvent) => {
|
|
if (ev.key === "Escape") {
|
|
ev.stopPropagation();
|
|
close();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("keydown", onDocKey, true);
|
|
|
|
backdrop.addEventListener("click", () => {
|
|
close();
|
|
});
|
|
}
|
|
|
|
export function attachPopupMediaFullscreenIfPresent(
|
|
root: ParentNode,
|
|
selector: string,
|
|
) {
|
|
const el = root.querySelector(selector);
|
|
if (el && isImageOrVideo(el)) {
|
|
attachPopupMediaFullscreen(el);
|
|
}
|
|
}
|