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>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { createPip } from '$lib/js/pip.svelte';
|
import { createPip } from '$lib/js/pip.svelte';
|
||||||
|
import PipImage from '$lib/components/PipImage.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {'layout' | 'overlay'} mode
|
* @param {'layout' | 'overlay'} mode
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
let contentEl = $state(null);
|
let contentEl = $state(null);
|
||||||
let inView = $state(false);
|
let inView = $state(false);
|
||||||
|
|
||||||
const pip = createPip();
|
const pip = createPip({ fullscreenEnabled: true });
|
||||||
|
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
return !window.matchMedia('(min-width: 1024px)').matches;
|
return !window.matchMedia('(min-width: 1024px)').matches;
|
||||||
@@ -80,17 +81,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sticky-image-layout" class:overlay={mode === 'overlay'}>
|
<div class="sticky-image-layout" class:overlay={mode === 'overlay'}>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<div class="image-wrap-desktop">
|
||||||
<div
|
|
||||||
class="image-wrap"
|
|
||||||
class:enlarged={pip.enlarged}
|
|
||||||
bind:this={pipEl}
|
|
||||||
onpointerdown={pip.onpointerdown}
|
|
||||||
onpointermove={pip.onpointermove}
|
|
||||||
onpointerup={pip.onpointerup}
|
|
||||||
>
|
|
||||||
<img {src} {alt}>
|
<img {src} {alt}>
|
||||||
</div>
|
</div>
|
||||||
|
<PipImage {pip} {src} {alt} visible={inView} bind:el={pipEl} />
|
||||||
<div class="content-scroll" bind:this={contentEl}>
|
<div class="content-scroll" bind:this={contentEl}>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
@@ -107,32 +101,8 @@
|
|||||||
.sticky-image-layout.overlay {
|
.sticky-image-layout.overlay {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
.image-wrap {
|
.image-wrap-desktop {
|
||||||
position: fixed;
|
display: none;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
.content-scroll {
|
.content-scroll {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -148,29 +118,18 @@
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
width: calc(100% + 25vw + 2rem);
|
width: calc(100% + 25vw + 2rem);
|
||||||
}
|
}
|
||||||
.overlay .image-wrap {
|
.image-wrap-desktop {
|
||||||
|
display: block;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 4rem;
|
top: 4rem;
|
||||||
left: auto;
|
|
||||||
transform: none !important;
|
|
||||||
width: auto;
|
|
||||||
align-self: start;
|
align-self: start;
|
||||||
order: 1;
|
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;
|
height: auto;
|
||||||
max-height: calc(100vh - 5rem);
|
max-height: calc(100vh - 5rem);
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 25vw;
|
max-width: 25vw;
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
}
|
||||||
.sticky-image-layout:not(.overlay) {
|
.sticky-image-layout:not(.overlay) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -180,37 +139,27 @@
|
|||||||
.sticky-image-layout:not(.overlay) .content-scroll {
|
.sticky-image-layout:not(.overlay) .content-scroll {
|
||||||
flex: 0 1 700px;
|
flex: 0 1 700px;
|
||||||
}
|
}
|
||||||
.sticky-image-layout:not(.overlay) .image-wrap {
|
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
|
||||||
|
display: block;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 4rem;
|
top: 4rem;
|
||||||
left: auto;
|
|
||||||
transform: none !important;
|
|
||||||
opacity: 1;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
order: 1;
|
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);
|
max-height: calc(100vh - 4rem);
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.sticky-image-layout:not(.overlay) .image-wrap {
|
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
|
||||||
background-color: var(--nord5);
|
background-color: var(--nord5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: light) and (min-width: 1024px) {
|
@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;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,12 +155,14 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
showControls = false;
|
showControls = false;
|
||||||
if (fullscreen) {
|
if (fullscreen) {
|
||||||
target.style.opacity = '1';
|
target.style.opacity = '1';
|
||||||
|
target.style.pointerEvents = 'auto';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pos = getCornerPos(corner, target);
|
const pos = getCornerPos(corner, target);
|
||||||
dragPos = pos;
|
dragPos = pos;
|
||||||
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
target.style.opacity = '1';
|
target.style.opacity = '1';
|
||||||
|
target.style.pointerEvents = 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -178,7 +180,10 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
el.style.transition = '';
|
el.style.transition = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (el) el.style.opacity = '0';
|
if (el) {
|
||||||
|
el.style.opacity = '0';
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reposition() {
|
function reposition() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
||||||
import { createPip } from "$lib/js/pip.svelte";
|
import { createPip } from "$lib/js/pip.svelte";
|
||||||
|
import PipImage from "$lib/components/PipImage.svelte";
|
||||||
import "$lib/css/christ.css";
|
import "$lib/css/christ.css";
|
||||||
import "$lib/css/action_button.css";
|
import "$lib/css/action_button.css";
|
||||||
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
|
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>
|
</style>
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{labels.pageTitle}</title>
|
<title>{labels.pageTitle}</title>
|
||||||
@@ -1957,35 +1878,7 @@ h1 {
|
|||||||
|
|
||||||
<!-- Mobile PiP for mystery images -->
|
<!-- Mobile PiP for mystery images -->
|
||||||
{#if hasMysteryImages}
|
{#if hasMysteryImages}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<PipImage {pip} src={lastPipSrc} visible={!!mysteryPipSrc} onload={() => pip.reposition()} bind:el={rosaryPipEl} />
|
||||||
<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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user