Skip to content

Commit

Permalink
feat: add VMediaViewer (#231)
Browse files Browse the repository at this point in the history
* feat: add VMediaViewer

* feat(VMediaViewer): improved code

* feat : css warnings

* feat: add VCarouselProgress
  • Loading branch information
Illawind authored Jan 14, 2025
1 parent b04e981 commit 47586c8
Show file tree
Hide file tree
Showing 15 changed files with 725 additions and 87 deletions.
15 changes: 15 additions & 0 deletions assets/stories/fixtures/documents/image-portrait.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"@type": "Document",
"@id": "/api/documents/2",
"mimeType": "image/jpeg",
"imageWidth": "2043",
"imageHeight": "3040",
"mediaDuration": 0,
"imageAverageColor": "#533c28",
"alt": "img_test.jpg",
"relativePath": "02.jpg",
"processable": true,
"type": "image",
"copyright": "Sous licence protégée",
"folders": []
}
4 changes: 2 additions & 2 deletions components/molecules/VCarouselControls/Default.stories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ const index = ref(0)
<NuxtStoryVariant title="default">
<VCarouselControls
v-model:index="index"
:length="3"
:snap-length="3"
display-numbers
/>
</NuxtStoryVariant>
<NuxtStoryVariant title="without numbers">
<VCarouselControls
v-model:index="index"
:length="3"
:snap-length="3"
/>
</NuxtStoryVariant>
</NuxtStory>
Expand Down
128 changes: 46 additions & 82 deletions components/molecules/VCarouselControls/VCarouselControls.vue
Original file line number Diff line number Diff line change
@@ -1,98 +1,52 @@
<script setup lang="ts">
import type { ThemeProps } from '#imports'
import { useCarouselControls } from '~/composables/use-carousel-controls'
interface VCarouselControlsProps extends ThemeProps {
length: number
type VCarouselControlsProps = ThemeProps & {
snapLength: number
displayNumbers?: boolean
isEnd?: boolean
}
const props = defineProps<VCarouselControlsProps>()
const scroll = ref<HTMLInputElement | null>(null)
const thumb = ref<HTMLInputElement | null>(null)
const index = defineModel<number>({ default: 0 })
onMounted(() => {
setIndicatorWidth()
setIndicatorPosition(index.value)
})
watch(() => props.length, () => {
setIndicatorWidth()
setIndicatorPosition(index.value)
})
watch(index, (newIndex) => {
setIndicatorPosition(newIndex)
})
const props = defineProps<VCarouselControlsProps>()
const slidePosition = computed(() => {
if (!props.displayNumbers) return
const snapLength = ref(props.snapLength)
watch(() => props.snapLength, v => snapLength.value = v)
return `${formatValue(index.value)} / ${formatValue(props.length - 1)}`
const { numbersOutput, prevButtonAttrs, isCarouselDraggable, nextButtonAttrs } = useCarouselControls({
displayNumbers: props.displayNumbers,
snapLength,
index,
})
function formatValue(n: number) {
return (n < 9 ? '0' : '') + (n + 1)
}
function setIndicatorWidth() {
if (!scroll.value || !thumb.value) return // can be undefined in SSR
thumb.value?.style.setProperty('--v-carousel-controls-thumb-width', (scroll.value?.offsetWidth / props.length / scroll.value?.offsetWidth) * 100 + '%')
}
function setIndicatorPosition(index: number) {
if (!scroll.value || !thumb.value) return
const percent = index / (props.length - 1)
const translate = percent * (scroll.value?.offsetWidth - thumb.value?.getBoundingClientRect().width)
thumb.value.style.translate = translate + 'px 0 '
}
function onClick(event: Event) {
const direction = (event.currentTarget as HTMLButtonElement).name === 'next' ? 1 : -1
index.value = index.value + direction
}
const ariaHidden = ref(false)
onMounted(() => ariaHidden.value = !isCarouselDraggable.value)
watch(isCarouselDraggable, value => ariaHidden.value = !value)
</script>

<template>
<div :class="$style.root">
<div
:class="[$style.root, isCarouselDraggable && $style['root--carousel-draggable']]"
:aria-hidden="ariaHidden || undefined"
>
<div
ref="scroll"
:class="$style.scroll"
v-if="numbersOutput"
class="text-body-xs"
:class="$style.numbers"
>
<div
ref="thumb"
:class="$style.thumb"
/>
<span>{{ numbersOutput }}</span>
</div>
<VButton
name="previous"
icon-name="arrow-left"
:aria-label="$t('carousel.previous_slide_label')"
:disabled="index === 0"
:theme="theme"
:class="$style.button"
@click="onClick"
v-bind="prevButtonAttrs"
:class="$style['button-prev']"
/>
<VCarouselProgress
:index="index"
:snap-length="snapLength"
/>
<div
v-if="slidePosition"
class="text-body-xs"
:class="$style.number"
>
<span>{{ slidePosition }}</span>
</div>
<VButton
name="next"
icon-name="arrow-right"
:aria-label="$t('carousel.next_slide_label')"
:disabled="index === length - 1 || isEnd"
:theme="theme"
:class="$style.button"
@click="onClick"
v-bind="nextButtonAttrs"
:class="$style['button-next']"
/>
</div>
</template>
Expand All @@ -106,6 +60,20 @@ function onClick(event: Event) {
align-items: center;
justify-content: var(--v-carousel-controls-justify-content, center);
gap: rem(8);
opacity: 0;
transition: opacity 0.3s;
&[aria-hidden="true"] {
display: var(--v-carousel-controls-hidden-display, none);
}
&--carousel-draggable {
opacity: var(--v-carousel-controls-opacity, 1);
}
&[aria-hidden="true"] {
pointer-events: none;
}
}
.scroll {
Expand All @@ -121,7 +89,7 @@ function onClick(event: Event) {
&::before {
position: absolute;
background-color: var(--theme-color-controls-selected, color-mix(in srgb, currentColor, transparent 70%));
background-color: color-mix(in srgb, currentcolor, transparent 70%);
content: '';
inset: 0;
opacity: 0.2;
Expand All @@ -132,19 +100,15 @@ function onClick(event: Event) {
position: relative;
width: clamp(#{rem(22)}, var(--v-carousel-controls-thumb-width), 100%);
height: 100%;
background-color: var(--theme-color-controls-selected, currentColor);
background-color: currentcolor;
content: '';
transform-origin: left;
transition: 0.2s linear, 0.5s;
transition-property: translate, width;
}
.number {
.numbers {
display: var(--v-carousel-controls-numbers-display, block);
@include media('>=lg') {
display: var(--v-carousel-controls-numbers-display, none);
}
}
.button {
Expand Down
12 changes: 12 additions & 0 deletions components/molecules/VCarouselProgress/Default.stories.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
const index = ref(0)
</script>

<template>
<NuxtStory>
<VCarouselProgress
v-model:index="index"
:snap-length="3"
/>
</NuxtStory>
</template>
94 changes: 94 additions & 0 deletions components/molecules/VCarouselProgress/VCarouselProgress.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { ThemeProps } from '~/composables/use-theme'
const props = defineProps<{
index: number
snapLength: number
} & ThemeProps>()
const { themeClass } = useTheme({ props })
const root = ref<HTMLElement | null>(null)
const thumb = ref<HTMLElement | null>(null)
onMounted(() => {
setIndicatorWidth()
setIndicatorPosition(props.index)
})
watch(() => props.snapLength, () => {
setIndicatorWidth()
setIndicatorPosition(props.index)
}, { flush: 'post' })
watch(() => props.index, (newIndex) => {
setIndicatorPosition(newIndex)
})
function setIndicatorWidth() {
if (!root.value?.offsetWidth || !thumb.value) return // can be undefined in SSR
const value = (root.value?.offsetWidth / Math.max(props.snapLength, 1) / root.value?.offsetWidth) * 100
thumb.value?.style.setProperty('--v-carousel-controls-thumb-width', value + '%')
}
function setIndicatorPosition(index: number) {
if (!root.value || !thumb.value) return
const percent = index / Math.max(props.snapLength - 1, 1)
const translate = percent * (root.value?.offsetWidth - thumb.value?.getBoundingClientRect().width)
thumb.value.style.translate = translate + 'px 0 '
}
</script>

<template>
<div
ref="root"
:class="[$style.root, themeClass]"
>
<div
ref="thumb"
:class="$style.thumb"
/>
</div>
</template>

<style lang="scss" module>
@use 'assets/scss/functions/rem' as *;
@use 'assets/scss/mixins/include-media' as *;
@use 'assets/scss/mixins/theme' as *;
.root {
position: relative;
overflow: hidden;
width: rem(48);
height: rem(2);
flex-shrink: 0;
@include theme-variants('colors-control');
&::before {
position: absolute;
background-color: var(--theme-colors-control-content-disabled, #ccc);
content: '';
inset: 0;
}
@include media('>=lg') {
width: rem(64);
}
}
.thumb {
position: relative;
width: clamp(#{rem(22)}, var(--v-carousel-controls-thumb-width), 100%);
height: 100%;
background-color: var(--theme-colors-control-content, currentColor);
content: '';
transform-origin: left;
transition: 0.2s linear, 0.5s;
transition-property: translate, width;
}
</style>
2 changes: 1 addition & 1 deletion components/organisms/VCarousel/AsyncSlides.stories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const snapGridLength = ref(numSlides)
</VCarousel>
<VCarouselControls
v-model="slideIndex"
:length="snapGridLength"
:snap-length="snapGridLength"
/>
</div>
</NuxtStory>
Expand Down
2 changes: 1 addition & 1 deletion components/organisms/VCarousel/Default.stories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const snapGridLength = ref(numSlides)
</VCarousel>
<VCarouselControls
v-model="slideIndex"
:length="snapGridLength"
:snap-length="snapGridLength"
/>
</div>
</NuxtStory>
Expand Down
2 changes: 1 addition & 1 deletion components/organisms/VCarousel/VCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const slideIndex = defineModel<number>('index', { default: 0 })
const snapLength = defineModel<number>('snapLength')
const carouselEnabled = defineModel<boolean>('enabled', { default: false })
const props = withDefaults(defineProps<VCarouselProps & { index?: number, enabled: boolean } >(), { lazy: true })
const props = withDefaults(defineProps<VCarouselProps & { index?: number, enabled?: boolean } >(), { lazy: true })
const emit = defineEmits<{
progress: [number]
Expand Down
46 changes: 46 additions & 0 deletions components/organisms/VMediaViewer/Default.stories.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import type { RoadizDocument } from '@roadiz/types'
import { useMediaViewer } from '~/composables/use-media-viewer'
import image from '~/assets/stories/fixtures/documents/image-01.json'
import imagePortrait from '~/assets/stories/fixtures/documents/image-portrait.json'
import video from '~/assets/stories/fixtures/documents/video-01.json'
const documents = [image, imagePortrait, video, image, video, image] as RoadizDocument[]
const { isOpen, open } = useMediaViewer()
function onOpenClick() {
open(documents, 0)
}
</script>

<template>
<NuxtStory>
<VButton
v-if="!isOpen"
label="open"
filled
@click="onOpenClick"
/>
<Transition :name="$style['media-viewer']">
<VMediaViewer v-if="isOpen" />
</Transition>
</NuxtStory>
</template>

<style lang="scss" module>
.media-viewer {
&:global(#{'-enter-active'}),
&:global(#{'-leave-active'}) {
transition: translate 1s ease(out-quart);
}
&:global(#{'-enter-from'}),
&:global(#{'-leave-to'}) {
translate: 0 100%;
}
}
.controls {
position: fixed;
z-index: 2000;
}
</style>
Loading

0 comments on commit 47586c8

Please sign in to comment.