2023-07-30 12:31:54 +00:00
|
|
|
import { createEffect, createSignal, For, Show, on } from 'solid-js'
|
2023-07-14 13:06:21 +00:00
|
|
|
import { MediaItem, UploadedFile } from '../../../pages/types'
|
2023-07-02 05:08:42 +00:00
|
|
|
import { Icon } from '../Icon'
|
|
|
|
import { Popover } from '../Popover'
|
|
|
|
import { useLocalize } from '../../../context/localize'
|
|
|
|
import { register } from 'swiper/element/bundle'
|
|
|
|
import { DropArea } from '../DropArea'
|
|
|
|
import { createFileUploader } from '@solid-primitives/upload'
|
|
|
|
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
|
|
|
import { SwiperRef } from './swiper'
|
|
|
|
import { validateFiles } from '../../../utils/validateFile'
|
|
|
|
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
|
|
|
import { useSnackbar } from '../../../context/snackbar'
|
|
|
|
import { Loading } from '../Loading'
|
|
|
|
import { imageProxy } from '../../../utils/imageProxy'
|
|
|
|
import { clsx } from 'clsx'
|
|
|
|
import styles from './Swiper.module.scss'
|
2023-07-14 13:06:21 +00:00
|
|
|
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
2023-07-24 08:58:07 +00:00
|
|
|
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
|
2023-07-02 05:08:42 +00:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
images: MediaItem[]
|
|
|
|
editorMode?: boolean
|
|
|
|
onImagesAdd?: (value: MediaItem[]) => void
|
|
|
|
onImagesSorted?: (value: MediaItem[]) => void
|
|
|
|
onImageDelete?: (mediaItemIndex: number) => void
|
|
|
|
onImageChange?: (index: number, value: MediaItem) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
register()
|
|
|
|
|
|
|
|
SwiperCore.use([Pagination, Navigation, Manipulation])
|
|
|
|
|
|
|
|
export const SolidSwiper = (props: Props) => {
|
|
|
|
const { t } = useLocalize()
|
|
|
|
const [loading, setLoading] = createSignal(false)
|
|
|
|
const [slideIndex, setSlideIndex] = createSignal(0)
|
|
|
|
|
|
|
|
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
|
|
|
const thumbSwipeRef: { current: SwiperRef } = { current: null }
|
|
|
|
|
|
|
|
const {
|
|
|
|
actions: { showSnackbar }
|
|
|
|
} = useSnackbar()
|
|
|
|
|
|
|
|
const handleSlideDescriptionChange = (index: number, field: string, value) => {
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
const handleDropAreaUpload = (value: UploadedFile[]) => {
|
|
|
|
props.onImagesAdd(composeMediaItems(value))
|
2023-07-02 05:08:42 +00:00
|
|
|
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)
|
2023-07-15 21:57:52 +00:00
|
|
|
const results: UploadedFile[] = []
|
2023-07-02 05:08:42 +00:00
|
|
|
for (const file of selectedFiles) {
|
|
|
|
const result = await handleFileUpload(file)
|
2023-07-13 13:19:52 +00:00
|
|
|
results.push(result.url)
|
2023-07-02 05:08:42 +00:00
|
|
|
}
|
2023-07-14 13:06:21 +00:00
|
|
|
props.onImagesAdd(composeMediaItems(results))
|
2023-07-02 05:08:42 +00:00
|
|
|
setLoading(false)
|
|
|
|
swipeToUploaded()
|
|
|
|
} catch (error) {
|
|
|
|
await showSnackbar({ type: 'error', body: t('Error') })
|
|
|
|
console.error('[runUpload]', error)
|
2023-07-28 09:47:19 +00:00
|
|
|
setLoading(false)
|
2023-07-02 05:08:42 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
await showSnackbar({ type: 'error', body: t('Invalid file type') })
|
2023-07-28 09:47:19 +00:00
|
|
|
setLoading(false)
|
2023-07-02 05:08:42 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div class={clsx(styles.Swiper, props.editorMode ? styles.editorMode : styles.articleMode)}>
|
|
|
|
<div class={styles.container}>
|
|
|
|
<Show when={props.editorMode && props.images.length === 0}>
|
|
|
|
<DropArea
|
|
|
|
fileType="image"
|
|
|
|
isMultiply={true}
|
|
|
|
placeholder={t('Add images')}
|
|
|
|
onUpload={handleDropAreaUpload}
|
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
{t('You can upload up to 100 images in .jpg, .png format.')}
|
|
|
|
<br />
|
|
|
|
{t('Each image must be no larger than 5 MB.')}
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Show>
|
|
|
|
<Show when={props.images.length > 0}>
|
|
|
|
<div class={styles.holder}>
|
|
|
|
<swiper-container
|
|
|
|
ref={(el) => (mainSwipeRef.current = el)}
|
|
|
|
slides-per-view={1}
|
|
|
|
thumbs-swiper={'.thumbSwiper'}
|
|
|
|
observer={true}
|
|
|
|
onSlideChange={handleSlideChange}
|
2023-07-17 22:24:37 +00:00
|
|
|
space-between={20}
|
2023-07-02 05:08:42 +00:00
|
|
|
>
|
|
|
|
<For each={props.images}>
|
|
|
|
{(slide, index) => (
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
<swiper-slide lazy="true" virtual-index={index()}>
|
|
|
|
<div class={styles.image}>
|
|
|
|
<img src={imageProxy(slide.url)} alt={slide.title} />
|
|
|
|
<Show when={props.editorMode}>
|
|
|
|
<Popover content={t('Delete')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<div
|
|
|
|
ref={triggerRef}
|
|
|
|
onClick={() => handleDelete(index())}
|
|
|
|
class={styles.action}
|
|
|
|
>
|
|
|
|
<Icon class={styles.icon} name="delete-white" />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</swiper-slide>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</swiper-container>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.navigation, styles.prev, {
|
|
|
|
[styles.disabled]: slideIndex() === 0
|
|
|
|
})}
|
|
|
|
onClick={() => mainSwipeRef.current.swiper.slidePrev()}
|
|
|
|
>
|
|
|
|
<Icon name="swiper-l-arr" class={styles.icon} />
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.navigation, styles.next, {
|
|
|
|
[styles.disabled]: slideIndex() + 1 === props.images.length
|
|
|
|
})}
|
|
|
|
onClick={() => mainSwipeRef.current.swiper.slideNext()}
|
|
|
|
>
|
|
|
|
<Icon name="swiper-r-arr" class={styles.icon} />
|
|
|
|
</div>
|
|
|
|
<div class={styles.counter}>
|
|
|
|
{slideIndex() + 1} / {props.images.length}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class={clsx(styles.holder, styles.thumbsHolder)}>
|
|
|
|
<div class={styles.thumbs}>
|
|
|
|
<swiper-container
|
|
|
|
class={'thumbSwiper'}
|
|
|
|
ref={(el) => (thumbSwipeRef.current = el)}
|
|
|
|
slides-per-view={'auto'}
|
|
|
|
space-between={20}
|
|
|
|
auto-scroll-offset={1}
|
|
|
|
watch-overflow={true}
|
|
|
|
watch-slides-visibility={true}
|
|
|
|
direction={props.editorMode ? 'horizontal' : 'vertical'}
|
2023-07-28 09:47:19 +00:00
|
|
|
slides-offset-after={props.editorMode && 160}
|
|
|
|
slides-offset-before={props.editorMode && 30}
|
2023-07-02 05:08:42 +00:00
|
|
|
>
|
|
|
|
<For each={props.images}>
|
|
|
|
{(slide, index) => (
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
<swiper-slide virtual-index={index()} style={{ width: 'auto', height: 'auto' }}>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.imageThumb)}
|
|
|
|
style={{ 'background-image': `url(${imageProxy(slide.url)})` }}
|
|
|
|
>
|
|
|
|
<Show when={props.editorMode}>
|
|
|
|
<div class={styles.thumbAction}>
|
|
|
|
<div class={clsx(styles.action)} onClick={() => handleDelete(index())}>
|
|
|
|
<Icon class={styles.icon} name="delete-white" />
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.action, {
|
|
|
|
[styles.hidden]: index() === 0
|
|
|
|
})}
|
|
|
|
onClick={() => handleChangeIndex('left', index())}
|
|
|
|
>
|
|
|
|
<Icon
|
|
|
|
class={styles.icon}
|
|
|
|
name="arrow-right-white"
|
|
|
|
style={{ transform: 'rotate(-180deg)' }}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.action, {
|
2023-07-18 19:11:00 +00:00
|
|
|
[styles.hidden]: index() === props.images.length - 1
|
2023-07-02 05:08:42 +00:00
|
|
|
})}
|
|
|
|
onClick={() => handleChangeIndex('right', index())}
|
|
|
|
>
|
|
|
|
<Icon class={styles.icon} name="arrow-right-white" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</swiper-slide>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
<Show when={props.editorMode}>
|
|
|
|
<div class={styles.upload}>
|
|
|
|
<div class={styles.inner} onClick={handleUploadThumb}>
|
|
|
|
<Show when={!loading()} fallback={<Loading size="small" />}>
|
|
|
|
<Icon name="swiper-plus" />
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
</swiper-container>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.navigation, styles.thumbsNav, styles.prev, {
|
|
|
|
[styles.disabled]: slideIndex() === 0
|
|
|
|
})}
|
|
|
|
onClick={() => thumbSwipeRef.current.swiper.slidePrev()}
|
|
|
|
>
|
2023-07-17 22:24:37 +00:00
|
|
|
<Icon name="swiper-l-arr" class={styles.icon} />
|
2023-07-02 05:08:42 +00:00
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
class={clsx(styles.navigation, styles.thumbsNav, styles.next, {
|
|
|
|
[styles.disabled]: slideIndex() + 1 === props.images.length
|
|
|
|
})}
|
|
|
|
onClick={() => thumbSwipeRef.current.swiper.slideNext()}
|
|
|
|
>
|
2023-07-17 22:24:37 +00:00
|
|
|
<Icon name="swiper-r-arr" class={styles.icon} />
|
2023-07-02 05:08:42 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
</div>
|
2023-07-28 09:47:19 +00:00
|
|
|
<Show
|
|
|
|
when={props.editorMode}
|
|
|
|
fallback={
|
|
|
|
<div class={styles.slideDescription}>
|
|
|
|
<Show when={props.images[slideIndex()]?.title}>
|
|
|
|
<div class={styles.articleTitle}>{props.images[slideIndex()].title}</div>
|
|
|
|
</Show>
|
|
|
|
<Show when={props.images[slideIndex()]?.source}>
|
|
|
|
<div class={styles.source}>{props.images[slideIndex()].source}</div>
|
|
|
|
</Show>
|
|
|
|
<Show when={props.images[slideIndex()]?.body}>
|
|
|
|
<div class={styles.body} innerHTML={props.images[slideIndex()].body} />
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
>
|
2023-07-28 19:53:21 +00:00
|
|
|
<Show when={props.images.length > 0}>
|
|
|
|
<div class={styles.description}>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
class={clsx(styles.input, styles.title)}
|
|
|
|
placeholder={t('Enter image title')}
|
|
|
|
value={props.images[slideIndex()].title}
|
|
|
|
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'title', event.target.value)}
|
|
|
|
/>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
class={styles.input}
|
|
|
|
placeholder={t('Specify the source and the name of the author')}
|
|
|
|
value={props.images[slideIndex()].source}
|
|
|
|
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)}
|
|
|
|
/>
|
|
|
|
<SimplifiedEditor
|
|
|
|
initialContent={props.images[slideIndex()].body}
|
|
|
|
smallHeight={true}
|
|
|
|
placeholder={t('Enter image description')}
|
2023-08-13 21:26:40 +00:00
|
|
|
onAutoSave={(value) => handleSlideDescriptionChange(slideIndex(), 'body', value)}
|
2023-07-28 19:53:21 +00:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</Show>
|
2023-07-28 09:47:19 +00:00
|
|
|
</Show>
|
2023-07-02 05:08:42 +00:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|