400 lines
12 KiB
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>
|