feat: image full screen overlay for popoups

This commit is contained in:
2026-04-12 20:06:03 +09:30
parent 1d9b8f3747
commit 89f50f774f
5 changed files with 253 additions and 0 deletions
+87
View File
@@ -3692,6 +3692,93 @@ div.day-empty {
object-position: center; object-position: center;
} }
.popup-media-fullscreenable {
cursor: pointer;
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.popup-media-fullscreenable:hover {
opacity: 0.95;
}
.popup-media-fullscreenable:focus {
outline: none;
}
.popup-media-fullscreenable:focus-visible {
outline: 2px solid color-mix(in srgb, var(--text-primary) 70%, transparent);
outline-offset: 4px;
}
.bsplus-popup-media-overlay-backdrop {
position: fixed;
inset: 0;
z-index: 2147483646;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(20px, 4vw, 48px);
box-sizing: border-box;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
opacity: 0;
transition: opacity 0.28s cubic-bezier(0.22, 0.03, 0.26, 1);
}
.bsplus-popup-media-overlay-backdrop.bsplus-popup-media-overlay-backdrop--visible {
opacity: 1;
}
.bsplus-popup-media-overlay-backdrop.bsplus-popup-media-overlay--instant {
transition: none;
}
.bsplus-popup-media-overlay-inner {
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: min(96vw, 1320px);
max-height: calc(100vh - clamp(40px, 10vw, 96px));
border-radius: 20px;
overflow: hidden;
background: var(--background-primary);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35);
opacity: 0;
transform: scale(0.94) translateY(12px);
transition:
opacity 0.28s cubic-bezier(0.22, 0.03, 0.26, 1),
transform 0.28s cubic-bezier(0.22, 0.03, 0.26, 1);
}
.bsplus-popup-media-overlay-backdrop.bsplus-popup-media-overlay-backdrop--visible
.bsplus-popup-media-overlay-inner {
opacity: 1;
transform: scale(1) translateY(0);
}
.bsplus-popup-media-overlay-backdrop.bsplus-popup-media-overlay--instant
.bsplus-popup-media-overlay-inner {
transition: none;
}
.bsplus-popup-media-overlay-slot {
width: 100%;
max-height: inherit;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(16px, 3vw, 28px);
box-sizing: border-box;
}
.bsplus-popup-media-overlay-media {
max-width: 100%;
max-height: calc(100vh - clamp(120px, 22vh, 200px));
width: auto;
height: auto;
object-fit: contain;
border-radius: 12px;
}
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
background-position: -1000px 0; background-position: -1000px 0;
@@ -1,6 +1,7 @@
import stringToHTML from "../stringToHTML"; import stringToHTML from "../stringToHTML";
import { settingsState } from "../listeners/SettingsState"; import { settingsState } from "../listeners/SettingsState";
import { openPopup } from "./PopupManager"; import { openPopup } from "./PopupManager";
import { attachPopupMediaFullscreenIfPresent } from "./attachPopupMediaFullscreen";
/** Same hosting pattern as the privacy statement branding images (avoids page-relative extension URLs on Engage). */ /** Same hosting pattern as the privacy statement branding images (avoids page-relative extension URLs on Engage). */
const ENGAGE_PROMO_IMG_URL = const ENGAGE_PROMO_IMG_URL =
@@ -49,6 +50,8 @@ export function showEngageParentsAnnouncement(onDismissed?: () => void) {
</div> </div>
`).firstChild as HTMLElement; `).firstChild as HTMLElement;
attachPopupMediaFullscreenIfPresent(text, ".engageParentsPromoImg");
settingsState.engageParentsAnnouncementShown = true; settingsState.engageParentsAnnouncementShown = true;
openPopup({ openPopup({
@@ -1,6 +1,7 @@
import stringToHTML from "../stringToHTML"; import stringToHTML from "../stringToHTML";
import { settingsState } from "../listeners/SettingsState"; import { settingsState } from "../listeners/SettingsState";
import { openPopup } from "./PopupManager"; import { openPopup } from "./PopupManager";
import { attachPopupMediaFullscreenIfPresent } from "./attachPopupMediaFullscreen";
const PRIVACY_STATEMENT_VERSION = "2025-12-19"; const PRIVACY_STATEMENT_VERSION = "2025-12-19";
@@ -59,6 +60,8 @@ export function showPrivacyNotification(onDismissed?: () => void) {
</div> </div>
`).firstChild as HTMLElement; `).firstChild as HTMLElement;
attachPopupMediaFullscreenIfPresent(text, "img.aboutImg");
settingsState.privacyStatementLastUpdated = "2025-12-20"; settingsState.privacyStatementLastUpdated = "2025-12-20";
settingsState.privacyStatementShown = true; settingsState.privacyStatementShown = true;
@@ -2,6 +2,7 @@ import stringToHTML from "../stringToHTML";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import kofi from "@/resources/kofi.png?base64"; import kofi from "@/resources/kofi.png?base64";
import { openPopup } from "./PopupManager"; import { openPopup } from "./PopupManager";
import { attachPopupMediaFullscreen } from "./attachPopupMediaFullscreen";
export function OpenWhatsNewPopup(onDismissed?: () => void) { export function OpenWhatsNewPopup(onDismissed?: () => void) {
const header = stringToHTML( const header = stringToHTML(
@@ -28,6 +29,7 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
video.appendChild(source); video.appendChild(source);
video.classList.add("whatsnewImg"); video.classList.add("whatsnewImg");
imageContainer.appendChild(video); imageContainer.appendChild(video);
attachPopupMediaFullscreen(video);
const text = stringToHTML(/* html */ ` const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;"> <div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
@@ -0,0 +1,158 @@
/**
* 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";
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;
for (const s of v.querySelectorAll("source")) {
const ns = document.createElement("source");
ns.src = (s as HTMLSourceElement).src;
const t = (s as HTMLSourceElement).type;
if (t) ns.type = t;
nv.appendChild(ns);
}
nv.addEventListener(
"loadeddata",
() => {
try {
nv.currentTime = v.currentTime;
} catch {
/* ignore */
}
void nv.play().catch(() => {});
},
{ once: true },
);
v.pause();
nv.load();
media = nv;
} else {
const img = document.createElement("img");
img.classList.add("bsplus-popup-media-overlay-media");
img.src = source.currentSrc || source.src;
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);
}
}