From ba71cbfdefe7e27275f0f453f9967c7473a86189 Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:05:29 +0300 Subject: [PATCH] Mobile view slider (#324) * Mobile view slider --------- Co-authored-by: kvakazyambra --- src/components/Views/Edit.tsx | 5 +- .../_shared/SolidSwiper/ArticleCardSwiper.tsx | 6 +- .../_shared/SolidSwiper/EditorSwiper.tsx | 326 ++++++++++++++++++ .../_shared/SolidSwiper/ImageSwiper.tsx | 265 +++----------- .../_shared/SolidSwiper/Swiper.module.scss | 51 ++- src/components/_shared/SolidSwiper/index.ts | 1 + 6 files changed, 423 insertions(+), 231 deletions(-) create mode 100644 src/components/_shared/SolidSwiper/EditorSwiper.tsx diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx index 5c497d13..7e8cafc6 100644 --- a/src/components/Views/Edit.tsx +++ b/src/components/Views/Edit.tsx @@ -16,7 +16,7 @@ import { slugify } from '../../utils/slugify' import { DropArea } from '../_shared/DropArea' import { Icon } from '../_shared/Icon' import { Popover } from '../_shared/Popover' -import { ImageSwiper } from '../_shared/SolidSwiper' +import { EditorSwiper } from '../_shared/SolidSwiper' import { Editor, Panel } from '../Editor' import { AudioUploader } from '../Editor/AudioUploader' import { AutoSaveNotice } from '../Editor/AutoSaveNotice' @@ -369,8 +369,7 @@ export const EditView = (props: Props) => { - handleMediaDelete(index)} diff --git a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx index ef63085e..9df151cf 100644 --- a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx @@ -36,7 +36,7 @@ export const ArticleCardSwiper = (props: Props) => { ref={(el) => (mainSwipeRef.current = el)} centered-slides={true} observer={true} - space-between={20} + space-between={10} breakpoints={{ 576: { spaceBetween: 20, slidesPerView: 1.5 }, 992: { spaceBetween: 52, slidesPerView: 1.5 }, @@ -44,13 +44,11 @@ export const ArticleCardSwiper = (props: Props) => { round-lengths={true} loop={true} speed={800} - /* autoplay={{ disableOnInteraction: false, delay: 6000, - pauseOnMouseEnter: true + pauseOnMouseEnter: true, }} -*/ > {(slide, index) => ( diff --git a/src/components/_shared/SolidSwiper/EditorSwiper.tsx b/src/components/_shared/SolidSwiper/EditorSwiper.tsx new file mode 100644 index 00000000..dc9f06e6 --- /dev/null +++ b/src/components/_shared/SolidSwiper/EditorSwiper.tsx @@ -0,0 +1,326 @@ +import { createFileUploader } from '@solid-primitives/upload' +import { clsx } from 'clsx' +import { createEffect, createSignal, For, Show, on, onMount, lazy } from 'solid-js' +import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper' + +import { useLocalize } from '../../../context/localize' +import { useSnackbar } from '../../../context/snackbar' +import { MediaItem, UploadedFile } from '../../../pages/types' +import { composeMediaItems } from '../../../utils/composeMediaItems' +import { getImageUrl } from '../../../utils/getImageUrl' +import { handleImageUpload } from '../../../utils/handleImageUpload' +import { validateFiles } from '../../../utils/validateFile' +import { DropArea } from '../DropArea' +import { Icon } from '../Icon' +import { Image } from '../Image' +import { Loading } from '../Loading' +import { Popover } from '../Popover' + +import { SwiperRef } from './swiper' + +import styles from './Swiper.module.scss' + +const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) + +type Props = { + images: MediaItem[] + onImagesAdd?: (value: MediaItem[]) => void + onImagesSorted?: (value: MediaItem[]) => void + onImageDelete?: (mediaItemIndex: number) => void + onImageChange?: (index: number, value: MediaItem) => void +} + +export const EditorSwiper = (props: Props) => { + const { t } = useLocalize() + const [loading, setLoading] = createSignal(false) + const [slideIndex, setSlideIndex] = createSignal(0) + const [slideBody, setSlideBody] = createSignal() + + const mainSwipeRef: { current: SwiperRef } = { current: null } + const thumbSwipeRef: { current: SwiperRef } = { current: null } + + const { + actions: { showSnackbar }, + } = useSnackbar() + + const handleSlideDescriptionChange = (index: number, field: string, value) => { + if (props.onImageChange) { + props.onImageChange(index, { ...props.images[index], [field]: value }) + } + } + const swipeToUploaded = () => { + setTimeout(() => { + mainSwipeRef.current.swiper.slideTo(props.images.length - 1) + }, 0) + } + const handleSlideChange = () => { + thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex) + setSlideIndex(mainSwipeRef.current.swiper.activeIndex) + } + + createEffect( + on( + () => props.images.length, + () => { + mainSwipeRef.current?.swiper.update() + thumbSwipeRef.current?.swiper.update() + }, + { defer: true }, + ), + ) + const handleDropAreaUpload = (value: UploadedFile[]) => { + props.onImagesAdd(composeMediaItems(value)) + swipeToUploaded() + } + + const handleDelete = (index: number) => { + props.onImageDelete(index) + + if (index === 0) { + mainSwipeRef.current.swiper.update() + } else { + mainSwipeRef.current.swiper.slideTo(index - 1) + } + } + + const { selectFiles } = createFileUploader({ + multiple: true, + accept: `image/*`, + }) + + const initUpload = async (selectedFiles) => { + const isValid = validateFiles('image', selectedFiles) + + if (!isValid) { + await showSnackbar({ type: 'error', body: t('Invalid file type') }) + setLoading(false) + return + } + + try { + setLoading(true) + const results: UploadedFile[] = [] + for (const file of selectedFiles) { + const result = await handleImageUpload(file) + results.push(result) + } + props.onImagesAdd(composeMediaItems(results)) + setLoading(false) + swipeToUploaded() + } catch (error) { + console.error('[runUpload]', error) + showSnackbar({ type: 'error', body: t('Error') }) + setLoading(false) + } + } + const handleUploadThumb = async () => { + selectFiles((selectedFiles) => { + initUpload(selectedFiles) + }) + } + + const handleChangeIndex = (direction: 'left' | 'right', index: number) => { + const images = [...props.images] + if (direction === 'left' && index > 0) { + const copy = images.splice(index, 1)[0] + images.splice(index - 1, 0, copy) + } else if (direction === 'right' && index < images.length - 1) { + const copy = images.splice(index, 1)[0] + images.splice(index + 1, 0, copy) + } + props.onImagesSorted(images) + setTimeout(() => { + mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1) + }, 0) + } + + const handleSaveBeforeSlideChange = () => { + handleSlideDescriptionChange(slideIndex(), 'body', slideBody()) + } + + onMount(async () => { + const { register } = await import('swiper/element/bundle') + register() + SwiperCore.use([Pagination, Navigation, Manipulation]) + }) + + return ( +
+
+ + + {t('You can upload up to 100 images in .jpg, .png format.')} +
+ {t('Each image must be no larger than 5 MB.')} +
+ } + /> + + 0}> +
+ (mainSwipeRef.current = el)} + slides-per-view={1} + thumbs-swiper={'.thumbSwiper'} + observer={true} + onSlideChange={handleSlideChange} + onBeforeSlideChangeStart={handleSaveBeforeSlideChange} + space-between={20} + > + + {(slide, index) => ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + +
+ {slide.title} + + + {(triggerRef: (el) => void) => ( +
handleDelete(index())} class={styles.action}> + +
+ )} +
+
+
+ )} +
+
+
mainSwipeRef.current.swiper.slidePrev()} + > + +
+
mainSwipeRef.current.swiper.slideNext()} + > + +
+
+ {slideIndex() + 1} / {props.images.length} +
+
+
+
+ (thumbSwipeRef.current = el)} + slides-per-view={'auto'} + space-between={20} + auto-scroll-offset={1} + watch-overflow={true} + watch-slides-visibility={true} + direction={'horizontal'} + slides-offset-after={160} + slides-offset-before={30} + > + + {(slide, index) => ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + +
+
+
handleDelete(index())}> + +
+
handleChangeIndex('left', index())} + > + +
+
handleChangeIndex('right', index())} + > + +
+
+
+
+ )} +
+ +
+
+ }> + + +
+
+
+
thumbSwipeRef.current.swiper.slidePrev()} + > + +
+
thumbSwipeRef.current.swiper.slideNext()} + > + +
+
+
+
+
+ + 0}> +
+ handleSlideDescriptionChange(slideIndex(), 'title', event.target.value)} + /> + handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)} + /> + setSlideBody(value)} + /> +
+
+ + ) +} diff --git a/src/components/_shared/SolidSwiper/ImageSwiper.tsx b/src/components/_shared/SolidSwiper/ImageSwiper.tsx index 695b0d6d..d8861f32 100644 --- a/src/components/_shared/SolidSwiper/ImageSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ImageSwiper.tsx @@ -1,59 +1,34 @@ -import { createFileUploader } from '@solid-primitives/upload' import { clsx } from 'clsx' -import { createEffect, createSignal, For, Show, on, onMount, lazy } from 'solid-js' +import { createEffect, createSignal, For, Show, on, onMount, lazy, onCleanup } from 'solid-js' import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper' +import { throttle } from 'throttle-debounce' -import { useLocalize } from '../../../context/localize' -import { useSnackbar } from '../../../context/snackbar' -import { MediaItem, UploadedFile } from '../../../pages/types' -import { composeMediaItems } from '../../../utils/composeMediaItems' +import { MediaItem } from '../../../pages/types' import { getImageUrl } from '../../../utils/getImageUrl' -import { handleImageUpload } from '../../../utils/handleImageUpload' -import { validateFiles } from '../../../utils/validateFile' -import { DropArea } from '../DropArea' import { Icon } from '../Icon' import { Image } from '../Image' -import { Loading } from '../Loading' -import { Popover } from '../Popover' import { SwiperRef } from './swiper' import styles from './Swiper.module.scss' -const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) - type Props = { images: MediaItem[] - editorMode?: boolean onImagesAdd?: (value: MediaItem[]) => void onImagesSorted?: (value: MediaItem[]) => void onImageDelete?: (mediaItemIndex: number) => void onImageChange?: (index: number, value: MediaItem) => void } -export const ImageSwiper = (props: Props) => { - const { t } = useLocalize() - const [loading, setLoading] = createSignal(false) - const [slideIndex, setSlideIndex] = createSignal(0) - const [slideBody, setSlideBody] = createSignal() +const MIN_WIDTH = 540 +export const ImageSwiper = (props: Props) => { + const [slideIndex, setSlideIndex] = createSignal(0) + const [isMobileView, setIsMobileView] = createSignal(false) const mainSwipeRef: { current: SwiperRef } = { current: null } const thumbSwipeRef: { current: SwiperRef } = { current: null } + const swiperMainContainer: { current: HTMLDivElement } = { current: null } - const { - actions: { showSnackbar }, - } = useSnackbar() - - const handleSlideDescriptionChange = (index: number, field: string, value) => { - if (props.onImageChange) { - props.onImageChange(index, { ...props.images[index], [field]: value }) - } - } - const swipeToUploaded = () => { - setTimeout(() => { - mainSwipeRef.current.swiper.slideTo(props.images.length - 1) - }, 0) - } const handleSlideChange = () => { thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex) setSlideIndex(mainSwipeRef.current.swiper.activeIndex) @@ -69,99 +44,41 @@ export const ImageSwiper = (props: Props) => { { defer: true }, ), ) - const handleDropAreaUpload = (value: UploadedFile[]) => { - props.onImagesAdd(composeMediaItems(value)) - swipeToUploaded() - } - - const handleDelete = (index: number) => { - props.onImageDelete(index) - - if (index === 0) { - mainSwipeRef.current.swiper.update() - } else { - mainSwipeRef.current.swiper.slideTo(index - 1) - } - } - - const { selectFiles } = createFileUploader({ - multiple: true, - accept: `image/*`, - }) - - const initUpload = async (selectedFiles) => { - const isValid = validateFiles('image', selectedFiles) - if (isValid) { - try { - setLoading(true) - const results: UploadedFile[] = [] - for (const file of selectedFiles) { - const result = await handleImageUpload(file) - results.push(result) - } - props.onImagesAdd(composeMediaItems(results)) - setLoading(false) - swipeToUploaded() - } catch (error) { - await showSnackbar({ type: 'error', body: t('Error') }) - console.error('[runUpload]', error) - setLoading(false) - } - } else { - await showSnackbar({ type: 'error', body: t('Invalid file type') }) - setLoading(false) - return false - } - } - const handleUploadThumb = async () => { - selectFiles((selectedFiles) => { - initUpload(selectedFiles) - }) - } - - const handleChangeIndex = (direction: 'left' | 'right', index: number) => { - const images = [...props.images] - if (direction === 'left' && index > 0) { - const copy = images.splice(index, 1)[0] - images.splice(index - 1, 0, copy) - } else if (direction === 'right' && index < images.length - 1) { - const copy = images.splice(index, 1)[0] - images.splice(index + 1, 0, copy) - } - props.onImagesSorted(images) - setTimeout(() => { - mainSwipeRef.current.swiper.slideTo(direction === 'left' ? index - 1 : index + 1) - }, 0) - } - - const handleSaveBeforeSlideChange = () => { - handleSlideDescriptionChange(slideIndex(), 'body', slideBody()) - } onMount(async () => { const { register } = await import('swiper/element/bundle') register() - SwiperCore.use([Pagination, Navigation, Manipulation]) + SwiperCore.use([Pagination, Navigation, Manipulation, ResizeObserver]) + }) + + onMount(() => { + const updateDirection = () => { + const width = window.innerWidth + const direction = width > MIN_WIDTH ? 'vertical' : 'horizontal' + if (direction === 'horizontal') { + setIsMobileView(true) + } else { + setIsMobileView(false) + } + thumbSwipeRef.current?.swiper?.changeDirection(direction) + } + + updateDirection() + + const handleResize = throttle(100, () => { + updateDirection() + }) + + window.addEventListener('resize', handleResize) + + onCleanup(() => { + window.removeEventListener('resize', handleResize) + }) }) return ( -
-
- - - {t('You can upload up to 100 images in .jpg, .png format.')} -
- {t('Each image must be no larger than 5 MB.')} -
- } - /> - +
+
(swiperMainContainer.current = el)}> 0}>
{ thumbs-swiper={'.thumbSwiper'} observer={true} onSlideChange={handleSlideChange} - onBeforeSlideChangeStart={handleSaveBeforeSlideChange} - space-between={20} + space-between={isMobileView() ? 20 : 10} > {(slide, index) => ( @@ -180,19 +96,6 @@ export const ImageSwiper = (props: Props) => {
{slide.title} - - - {(triggerRef: (el) => void) => ( -
handleDelete(index())} - class={styles.action} - > - -
- )} -
-
)} @@ -224,13 +127,10 @@ export const ImageSwiper = (props: Props) => { class={'thumbSwiper'} ref={(el) => (thumbSwipeRef.current = el)} slides-per-view={'auto'} - space-between={20} + space-between={isMobileView() ? 20 : 10} auto-scroll-offset={1} watch-overflow={true} watch-slides-visibility={true} - direction={props.editorMode ? 'horizontal' : 'vertical'} - slides-offset-after={props.editorMode && 160} - slides-offset-before={props.editorMode && 30} > {(slide, index) => ( @@ -242,47 +142,10 @@ export const ImageSwiper = (props: Props) => { style={{ 'background-image': `url(${getImageUrl(slide.url, { width: 110, height: 75 })})`, }} - > - -
-
handleDelete(index())}> - -
-
handleChangeIndex('left', index())} - > - -
-
handleChangeIndex('right', index())} - > - -
-
-
-
+ /> )} - -
-
- }> - - -
-
-
{
- - -
{props.images[slideIndex()].title}
-
- -
{props.images[slideIndex()].source}
-
- -
- -
- } - > - 0}> -
- handleSlideDescriptionChange(slideIndex(), 'title', event.target.value)} - /> - handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)} - /> - setSlideBody(value)} - /> -
+
+ +
{props.images[slideIndex()].title}
- + +
{props.images[slideIndex()].source}
+
+ +
+ +
) } diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss index e953b565..6ae644b0 100644 --- a/src/components/_shared/SolidSwiper/Swiper.module.scss +++ b/src/components/_shared/SolidSwiper/Swiper.module.scss @@ -38,7 +38,6 @@ } .container { - // max-width: 800px; margin: auto; position: relative; padding: 24px 0; @@ -48,6 +47,7 @@ width: 100%; .thumbsHolder { + min-width: 110px; width: auto; } @@ -60,13 +60,6 @@ margin: 0; position: relative; - & > swiper-container { - position: absolute; - top: 52px; - bottom: 52px; - left: 0; - } - .thumbsNav { height: 52px; padding: 14px 0; @@ -92,6 +85,48 @@ } } } + &.mobileView { + .container { + flex-direction: column-reverse; + padding: 0; + + .thumbsHolder { + min-width: unset; + } + + .thumbs { + width: 100%; + height: 80px; + padding: 0; + + & swiper-slide { + //bind to html element + width: unset !important; + } + + .thumbsNav { + height: 100%; + padding: 0; + width: 40px; + + .icon { + transform: none; + } + + &.prev { + top: 0; + left: 0; + } + + &.next { + top: 0; + right: 0; + left: unset; + } + } + } + } + } } &.editorMode { diff --git a/src/components/_shared/SolidSwiper/index.ts b/src/components/_shared/SolidSwiper/index.ts index 54c84efc..b8d29439 100644 --- a/src/components/_shared/SolidSwiper/index.ts +++ b/src/components/_shared/SolidSwiper/index.ts @@ -1 +1,2 @@ export { ImageSwiper } from './ImageSwiper' +export { EditorSwiper } from './EditorSwiper'