Pinafore-Web-Client-Frontend/src/routes/_components/dialog/components/MediaDialog.html

400 lines
12 KiB
HTML

<ModalDialog
{id}
{label}
background="var(--muted-modal-bg)"
muted="true"
clickHeaderToClose={true}
className="media-modal-dialog"
on:show="onShow()"
>
<div class="media-container">
<ul class="media-scroll" ref:scroller on:click="onImageClick(event)">
{#each mediaItems as media (media.id)}
<li class="media-scroll-item">
<div class="media-scroll-item-inner">
<div class="media-scroll-item-image-area">
{#if canPinchZoom && pinchZoomMode}
<PinchZoomable className='media-pinch-zoom' >
<MediaInDialog {media} />
</PinchZoomable>
{:else}
<MediaInDialog {media} />
{/if}
</div>
</div>
</li>
{/each}
</ul>
<div class="media-controls-outside" on:click="onMediaControlsClick(event)">
{#if canPinchZoom}
<IconButton
className="media-control-button media-control-button-dummy-spacer"
svgClassName="media-control-button-svg"
href="#fa-search"
label=""
ariaHidden={true}
/>
{/if}
{#if dots.length > 1}
<!-- Roughly based on https://www.w3.org/WAI/tutorials/carousels/functionality/
Since this toolbar contains a mix of left/right/first/second/third/fourth buttons,
just list them and explicitly label the current one as "current." -->
<ul class="media-controls" aria-label="{intl.navigateMedia}">
<li class="media-control">
<IconButton
className="media-control-button"
svgClassName="media-control-button-svg"
disabled={scrolledItem === 0}
label="{intl.showPreviousMedia}"
href="#fa-angle-left"
on:click="prev()"
/>
</li>
{#each dots as dot, i (dot.i)}
<li class="media-control">
<IconButton
className="media-control-button"
svgClassName="media-control-button-svg"
pressable={true}
label="{createLabel(i, false)}"
pressedLabel="{createLabel(i, true)}"
pressed={i === scrolledItem}
href={i === scrolledItem ? '#fa-circle' : '#fa-circle-o'}
sameColorWhenPressed={true}
on:click="onButtonClick(i)"
/>
</li>
{/each}
<li class="media-control">
<IconButton
className="media-control-button"
svgClassName="media-control-button-svg"
disabled={scrolledItem === length - 1}
label="{intl.showNextMedia}"
href="#fa-angle-right"
on:click="next()"
/>
</li>
</ul>
{/if}
{#if canPinchZoom}
<IconButton
className="media-control-button"
svgClassName="media-control-button-svg"
pressable={true}
pressed={pinchZoomMode}
label="{intl.enterPinchZoom}"
pressedLabel="{intl.exitPinchZoom}"
href="#fa-search"
on:click="togglePinchZoomMode()"
/>
{/if}
</div>
</div>
</ModalDialog>
{#if !$leftRightChangesFocus }
<Shortcut scope='modal-{id}' key="ArrowLeft" on:pressed="prev()" />
<Shortcut scope='modal-{id}' key="ArrowRight" on:pressed="next()" />
{/if}
<style>
:global(.media-modal-dialog) {
max-width: 100%;
}
.media-container {
height: calc(100% - 64px); /* 44px X button height + 20px padding */
width: 100vw;
display: flex;
flex-direction: column;
}
.media-scroll {
-webkit-overflow-scrolling: touch;
display: flex;
align-items: center;
overflow-x: auto;
width: 100%;
flex: 1;
scrollbar-width: none;
-ms-overflow-style: none;
}
ul.media-scroll {
padding: 0;
margin: 0;
list-style: none;
}
.media-scroll::-webkit-scrollbar {
display: none;
}
.media-scroll-item {
height: 100%;
}
.media-scroll-item-inner {
width: 100vw;
height: 100%;
overflow: hidden;
}
.media-scroll-item-image-area {
height: calc(100% - 20px); /* 15px padding top + 5px padding bottom */
width: calc(100% - 10px); /* 5px padding left + 5px padding right */
padding: 15px 5px 5px 5px;
}
.media-controls-outside {
display: flex;
justify-content: space-between;
padding: 10px;
}
.media-controls {
display: flex;
justify-content: space-between;
}
ul.media-controls {
margin: 0;
padding: 0;
list-style: none;
}
li.media-control {
margin: 0;
padding: 0;
display: flex;
}
:global(.media-pinch-zoom) {
width: 100%;
height: 100%;
}
:global(.media-control-button-dummy-spacer) {
visibility: hidden;
}
:global(.icon-button.media-control-button) {
margin: 0 5px;
}
:global(.media-control-button-svg) {
/* ensure that click events do not fall on these svgs */
pointer-events: none;
}
@media (max-width: 767px) {
:global(.icon-button.media-control-button) {
margin: 0;
padding-left: 5px;
padding-right: 5px;
}
}
@media (max-width: 320px) {
:global(.icon-button.media-control-button) {
padding-left: 2px;
padding-right: 2px;
}
}
@supports (scroll-snap-align: start) {
/* modern scroll snap points */
.media-scroll {
scroll-snap-type: x mandatory;
}
.media-scroll-item {
scroll-snap-align: center;
}
}
@supports not (scroll-snap-align: start) {
/* old scroll snap points spec */
.media-scroll {
-webkit-scroll-snap-type: mandatory;
scroll-snap-type: mandatory;
-webkit-scroll-snap-destination: 0% center;
scroll-snap-destination: 0% center;
-webkit-scroll-snap-points-x: repeat(100%);
scroll-snap-points-x: repeat(100%);
}
.media-scroll-item {
scroll-snap-coordinate: 0 0;
}
}
@media (max-width: 767px) {
.media-container {
height: calc(100vh - 64px);
}
}
</style>
<script>
import ModalDialog from './ModalDialog.html'
import MediaInDialog from './MediaInDialog.html'
import IconButton from '../../IconButton.html'
import Shortcut from '../../shortcut/Shortcut.html'
import PinchZoomable from './PinchZoomable.html'
import { show } from '../helpers/showDialog'
import { oncreate as onCreateDialog } from '../helpers/onCreateDialog'
import { close } from '../helpers/closeDialog'
import debounce from 'lodash-es/debounce'
import times from 'lodash-es/times'
import { smoothScroll, hasNativeSmoothScroll } from '../../../_utils/smoothScroll'
import { store } from '../../../_store/store'
import { intrinsicScale } from '../../../_thirdparty/intrinsic-scale/intrinsicScale'
import { get } from '../../../_utils/lodash-lite'
import { formatIntl } from '../../../_utils/formatIntl'
// padding for .media-scroll-item-image-area
const IMAGE_AREA_PADDING = {
top: 15,
left: 5,
right: 5,
bottom: 5
}
export default {
oncreate () {
onCreateDialog.call(this)
this.onScroll = debounce(this.onScroll.bind(this), 50, { leading: false, trailing: true })
},
ondestroy () {
this.teardownScroll()
},
store: () => store,
data: () => ({
pinchZoomMode: false
}),
computed: {
length: ({ mediaItems }) => mediaItems.length,
dots: ({ length }) => times(length, i => ({ i })),
canPinchZoom: ({ mediaItems }) => !mediaItems.some(media => ['video', 'audio'].includes(media.type)),
mediaItem: ({ mediaItems, scrolledItem }) => mediaItems[scrolledItem],
nativeWidth: ({ mediaItem }) => get(mediaItem, ['meta', 'original', 'width'], 300), // TODO: Pleroma placeholder
nativeHeight: ({ mediaItem }) => get(mediaItem, ['meta', 'original', 'height'], 200) // TODO: Pleroma placeholder
},
components: {
ModalDialog,
MediaInDialog,
IconButton,
Shortcut,
PinchZoomable
},
helpers: {
createLabel (i, current) {
return formatIntl('intl.showMedia', { index: i + 1, current })
}
},
methods: {
show,
close,
setupScroll () {
this.refs.scroller.addEventListener('scroll', this.onScroll)
},
teardownScroll () {
this.refs.scroller.removeEventListener('scroll', this.onScroll)
},
onScroll () {
const { length } = this.get()
const { scroller } = this.refs
if (!scroller) {
return
}
const { scrollWidth, scrollLeft } = scroller
const scrolledItem = Math.round((scrollLeft / scrollWidth) * length)
this.set({ scrolledItem })
},
onButtonClick (i) {
const { scrolledItem } = this.get()
if (scrolledItem !== i) {
this.scrollToItem(i, true)
}
},
next () {
const { scrolledItem, length } = this.get()
if (scrolledItem < length - 1) {
this.scrollToItem(scrolledItem + 1, true)
}
},
prev () {
const { scrolledItem } = this.get()
if (scrolledItem > 0) {
this.scrollToItem(scrolledItem - 1, true)
}
},
onShow () {
const { scrolledItem } = this.get()
if (scrolledItem) {
requestAnimationFrame(() => {
this.scrollToItem(scrolledItem, false)
this.setupScroll()
})
} else {
this.setupScroll()
}
},
scrollToItem (scrolledItem, smooth) {
this.set({ scrolledItem: scrolledItem })
const { length } = this.get()
const { scroller } = this.refs
const { scrollWidth } = scroller
const scrollLeft = Math.floor(scrollWidth * (scrolledItem / length))
if (smooth) {
if (!hasNativeSmoothScroll && 'StyleMedia' in window) {
// Edge has a weird bug where it changes the height if we try to
// smooth scroll, so disable smooth scrolling
scroller.scrollLeft = scrollLeft
} else {
smoothScroll(scroller, scrollLeft, /* horizontal */ true, /* preferFast */ false)
}
} else {
console.log('setting scrollLeft', scrollLeft)
scroller.scrollLeft = scrollLeft
}
},
togglePinchZoomMode () {
this.set({ pinchZoomMode: !this.get().pinchZoomMode })
},
onImageClick (e) {
const { nativeWidth, nativeHeight, pinchZoomMode } = this.get()
if (pinchZoomMode) {
return
}
let rect = this.refs.scroller.getBoundingClientRect()
// apply padding
rect = {
width: rect.width - IMAGE_AREA_PADDING.left - IMAGE_AREA_PADDING.right,
height: rect.height - IMAGE_AREA_PADDING.top - IMAGE_AREA_PADDING.bottom,
left: rect.left + IMAGE_AREA_PADDING.left,
top: rect.top + IMAGE_AREA_PADDING.top
}
const scale = intrinsicScale(rect.width, rect.height, nativeWidth, nativeHeight)
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const insideImage = x >= scale.x && x <= (scale.x + scale.width) && y >= scale.y && y <= (scale.y + scale.height)
if (!insideImage) {
// close dialog when clicking outside of image
e.preventDefault()
e.stopPropagation()
this.close()
}
},
onMediaControlsClick (e) {
const { pinchZoomMode } = this.get()
if (pinchZoomMode) {
return
}
const { target } = e
if (target.tagName !== 'BUTTON' && !target.classList.contains('media-controls')) {
e.preventDefault()
e.stopPropagation()
// close dialog when clicking on the controls but not on a button inside the controls,
// or between the buttons
this.close()
}
}
}
}
</script>