refactor: extract PipImage component from inline PiP markup
Deduplicates mobile PiP image code shared between the rosary page and StickyImage. Adds fullscreen support to StickyImage and fixes hidden PiP elements capturing pointer events via pointer-events: none default.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<script>
|
||||
/**
|
||||
* @param {ReturnType<import('$lib/js/pip.svelte').createPip>} pip - a createPip() instance
|
||||
* @param {string} src - image source
|
||||
* @param {string} [alt] - image alt text
|
||||
* @param {boolean} [visible] - whether the PiP should be shown
|
||||
* @param {(e: Event) => void} [onload] - callback when image loads
|
||||
*/
|
||||
let { pip, src, alt = '', visible = false, onload, el = $bindable(null) } = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="pip-container"
|
||||
class:visible
|
||||
class:enlarged={pip.enlarged}
|
||||
class:fullscreen={pip.fullscreen}
|
||||
bind:this={el}
|
||||
onpointerdown={pip.onpointerdown}
|
||||
onpointermove={pip.onpointermove}
|
||||
onpointerup={pip.onpointerup}
|
||||
>
|
||||
{#if src}
|
||||
<img {src} {alt} {onload}>
|
||||
{/if}
|
||||
{#if pip.showControls}
|
||||
<button
|
||||
class="pip-fullscreen-btn"
|
||||
aria-label="Fullscreen"
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onclick={(e) => { e.stopPropagation(); pip.toggleFullscreen(); }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="8 3 3 3 3 8"/>
|
||||
<polyline points="16 3 21 3 21 8"/>
|
||||
<polyline points="8 21 3 21 3 16"/>
|
||||
<polyline points="16 21 21 21 21 16"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pip-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: opacity 0.25s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pip-container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.pip-container img {
|
||||
height: 25vh;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
transition: height 0.25s ease;
|
||||
}
|
||||
.pip-container.enlarged img {
|
||||
height: 37.5vh;
|
||||
}
|
||||
.pip-container.fullscreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
}
|
||||
.pip-container.fullscreen img {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.pip-fullscreen-btn {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: transparent;
|
||||
filter: drop-shadow(0 0 1px black);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
outline: none;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.pip-fullscreen-btn:hover,
|
||||
.pip-fullscreen-btn:active {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
.pip-container.fullscreen .pip-fullscreen-btn {
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: 10vw;
|
||||
right: 10vw;
|
||||
transform: none;
|
||||
}
|
||||
.pip-container.fullscreen .pip-fullscreen-btn:hover,
|
||||
.pip-container.fullscreen .pip-fullscreen-btn:active {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { createPip } from '$lib/js/pip.svelte';
|
||||
import PipImage from '$lib/components/PipImage.svelte';
|
||||
|
||||
/**
|
||||
* @param {'layout' | 'overlay'} mode
|
||||
@@ -13,7 +14,7 @@
|
||||
let contentEl = $state(null);
|
||||
let inView = $state(false);
|
||||
|
||||
const pip = createPip();
|
||||
const pip = createPip({ fullscreenEnabled: true });
|
||||
|
||||
function isMobile() {
|
||||
return !window.matchMedia('(min-width: 1024px)').matches;
|
||||
@@ -80,17 +81,10 @@
|
||||
</script>
|
||||
|
||||
<div class="sticky-image-layout" class:overlay={mode === 'overlay'}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="image-wrap"
|
||||
class:enlarged={pip.enlarged}
|
||||
bind:this={pipEl}
|
||||
onpointerdown={pip.onpointerdown}
|
||||
onpointermove={pip.onpointermove}
|
||||
onpointerup={pip.onpointerup}
|
||||
>
|
||||
<div class="image-wrap-desktop">
|
||||
<img {src} {alt}>
|
||||
</div>
|
||||
<PipImage {pip} {src} {alt} visible={inView} bind:el={pipEl} />
|
||||
<div class="content-scroll" bind:this={contentEl}>
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -107,32 +101,8 @@
|
||||
.sticky-image-layout.overlay {
|
||||
display: contents;
|
||||
}
|
||||
.image-wrap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.image-wrap:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.image-wrap img {
|
||||
height: 25vh;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
transition: height 0.25s ease;
|
||||
}
|
||||
.image-wrap.enlarged img {
|
||||
height: 37.5vh;
|
||||
.image-wrap-desktop {
|
||||
display: none;
|
||||
}
|
||||
.content-scroll {
|
||||
width: 100%;
|
||||
@@ -148,29 +118,18 @@
|
||||
gap: 2rem;
|
||||
width: calc(100% + 25vw + 2rem);
|
||||
}
|
||||
.overlay .image-wrap {
|
||||
.image-wrap-desktop {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 4rem;
|
||||
left: auto;
|
||||
transform: none !important;
|
||||
width: auto;
|
||||
align-self: start;
|
||||
order: 1;
|
||||
opacity: 1;
|
||||
z-index: auto;
|
||||
cursor: default;
|
||||
touch-action: auto;
|
||||
user-select: auto;
|
||||
transition: none;
|
||||
}
|
||||
.overlay .image-wrap img {
|
||||
.overlay .image-wrap-desktop img {
|
||||
height: auto;
|
||||
max-height: calc(100vh - 5rem);
|
||||
width: auto;
|
||||
max-width: 25vw;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.sticky-image-layout:not(.overlay) {
|
||||
flex-direction: row;
|
||||
@@ -180,37 +139,27 @@
|
||||
.sticky-image-layout:not(.overlay) .content-scroll {
|
||||
flex: 0 1 700px;
|
||||
}
|
||||
.sticky-image-layout:not(.overlay) .image-wrap {
|
||||
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 4rem;
|
||||
left: auto;
|
||||
transform: none !important;
|
||||
opacity: 1;
|
||||
flex: 1;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
order: 1;
|
||||
cursor: default;
|
||||
touch-action: auto;
|
||||
user-select: auto;
|
||||
transition: none;
|
||||
}
|
||||
.sticky-image-layout:not(.overlay) .image-wrap img {
|
||||
.sticky-image-layout:not(.overlay) .image-wrap-desktop img {
|
||||
max-height: calc(100vh - 4rem);
|
||||
height: auto;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.sticky-image-layout:not(.overlay) .image-wrap {
|
||||
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
|
||||
background-color: var(--nord5);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) and (min-width: 1024px) {
|
||||
.sticky-image-layout:not(.overlay) .image-wrap {
|
||||
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,12 +155,14 @@ export function createPip(opts: PipOptions = {}) {
|
||||
showControls = false;
|
||||
if (fullscreen) {
|
||||
target.style.opacity = '1';
|
||||
target.style.pointerEvents = 'auto';
|
||||
return;
|
||||
}
|
||||
const pos = getCornerPos(corner, target);
|
||||
dragPos = pos;
|
||||
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||
target.style.opacity = '1';
|
||||
target.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -178,7 +180,10 @@ export function createPip(opts: PipOptions = {}) {
|
||||
el.style.transition = '';
|
||||
}
|
||||
}
|
||||
if (el) el.style.opacity = '0';
|
||||
if (el) {
|
||||
el.style.opacity = '0';
|
||||
el.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function reposition() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, tick } from "svelte";
|
||||
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
||||
import { createPip } from "$lib/js/pip.svelte";
|
||||
import PipImage from "$lib/components/PipImage.svelte";
|
||||
import "$lib/css/christ.css";
|
||||
import "$lib/css/action_button.css";
|
||||
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
|
||||
@@ -1462,86 +1463,6 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile PiP for mystery images */
|
||||
.mystery-pip {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.mystery-pip:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.mystery-pip img {
|
||||
height: 25vh;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
transition: height 0.25s ease;
|
||||
}
|
||||
.mystery-pip.enlarged img {
|
||||
height: 37.5vh;
|
||||
}
|
||||
.mystery-pip.fullscreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
}
|
||||
.mystery-pip.fullscreen img {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.pip-fullscreen-btn {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: transparent;
|
||||
filter: drop-shadow(0 0 1px black);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
outline: none;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.pip-fullscreen-btn:hover,
|
||||
.pip-fullscreen-btn:active {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
.mystery-pip.fullscreen .pip-fullscreen-btn {
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: 10vw;
|
||||
right: 10vw;
|
||||
transform: none;
|
||||
}
|
||||
.mystery-pip.fullscreen .pip-fullscreen-btn:hover,
|
||||
.mystery-pip.fullscreen .pip-fullscreen-btn:active {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.mystery-pip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<svelte:head>
|
||||
<title>{labels.pageTitle}</title>
|
||||
@@ -1957,35 +1878,7 @@ h1 {
|
||||
|
||||
<!-- Mobile PiP for mystery images -->
|
||||
{#if hasMysteryImages}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="mystery-pip"
|
||||
class:visible={!!mysteryPipSrc}
|
||||
class:enlarged={pip.enlarged}
|
||||
class:fullscreen={pip.fullscreen}
|
||||
bind:this={rosaryPipEl}
|
||||
onpointerdown={pip.onpointerdown}
|
||||
onpointermove={pip.onpointermove}
|
||||
onpointerup={pip.onpointerup}
|
||||
>
|
||||
{#if lastPipSrc}
|
||||
<img src={lastPipSrc} alt="" onload={() => pip.reposition()}>
|
||||
{/if}
|
||||
{#if pip.showControls}
|
||||
<button
|
||||
class="pip-fullscreen-btn"
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onclick={(e) => { e.stopPropagation(); pip.toggleFullscreen(); }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="8 3 3 3 3 8"/>
|
||||
<polyline points="16 3 21 3 21 8"/>
|
||||
<polyline points="8 21 3 21 3 16"/>
|
||||
<polyline points="16 21 21 21 21 16"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<PipImage {pip} src={lastPipSrc} visible={!!mysteryPipSrc} onload={() => pip.reposition()} bind:el={rosaryPipEl} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user