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:
119
src/lib/components/PipImage.svelte
Normal file
119
src/lib/components/PipImage.svelte
Normal file
@@ -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