Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add VMediaViewer #231

Merged
merged 5 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Illawind marked this conversation as resolved.
Show resolved Hide resolved
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
Loading