Skip to content

Commit

Permalink
improvement: implement hash navigation
Browse files Browse the repository at this point in the history
closes #19
  • Loading branch information
irudoy committed Apr 25, 2020
1 parent 3514d0f commit bde7d7e
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 22 deletions.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,38 @@ const MyGallery = () => {
}
```

## Hash Navigation

You should pass a unique `id` prop to `<Gallery />` or `<CustomGallery />` component, to enable hash navigation.

Optionally, you can also pass the `id` to `<Item />` component. Otherwise, the index will be used.

```javascript
const MyGallery = () => {
<Gallery id="my-gallery">
<Item
id="first-pic"
{/*...*/}
/>
<Item
id="second-pic"
{/*...*/}
/>
</Gallery>
}
```

## Props

### Gallery

> You can pass any of `DefaultLayout` [props](#default-layout-props) to `Gallery`.
> All props are optional.
<a name="gallery-props"></a>

| Prop | Type | Description |
| - | - | - |
| Prop | Type | Required | Description |
| - | - | - | - |
| `id` | Number or String | ✓ (for hash navigation) | Item ID, for hash navigation |
| `options` | Object | PhotoSwipe [options](https://photoswipe.com/documentation/options.html) |

### Item
Expand All @@ -119,6 +139,7 @@ const MyGallery = () => {
| `height` | Number or String | | Height of original image |
| `title` | String | | Title for Default UI |
| `html` | String | | Html content, if you need to use it as modal |
| `id` | Number or String | | Item ID, for hash navigation |

#### Note about Item's `children` render prop.

Expand Down Expand Up @@ -160,6 +181,7 @@ type RenderItem = (props: {
| - | - | - | - |
| `layoutRef` | React.MutableRefObject<HTMLElement> || Ref to your layout element |
| `ui` | PhotoSwipeUI || PhotoSwipe UI class |
| `id` | Number or String | ✓ (for hash navigation) | Item ID, for hash navigation |
| `options` | Object | | PhotoSwipe [options](https://photoswipe.com/documentation/options.html) |

### DefaultLayout
Expand Down
37 changes: 34 additions & 3 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,28 @@ jest.mock('photoswipe', () => {

beforeEach(() => PhotoSwipeMocked.mockClear())

const photoswipeArgsMock = (items: InternalItem[] | null, index: number) => [
const photoswipeArgsMock = (
items: InternalItem[] | null,
index: number,
galleryUID?: string,
) => [
expect.anything(),
expect.anything(),
items === null
? expect.anything()
: items.map(({ original, thumbnail, width, height, title }) => ({
: items.map(({ original, thumbnail, width, height, title, id }) => ({
src: original,
msrc: thumbnail,
w: width,
h: height,
title,
el: expect.anything(),
pid: id,
})),
{
getThumbBoundsFn: expect.anything(),
history: false,
...(galleryUID !== undefined ? { history: true, galleryUID } : {}),
index,
},
]
Expand All @@ -60,14 +67,15 @@ const TestGallery: React.FC<{ items: InternalItem[] } & GalleryProps> = ({
...rest
}) => (
<Gallery {...rest}>
{items.map(({ original, thumbnail, width, height, title }, i) => (
{items.map(({ original, thumbnail, width, height, title, id }) => (
<Item
key={original}
original={original}
thumbnail={thumbnail}
width={width}
height={height}
title={title}
id={id}
>
{({ ref, open }) => (
<img
Expand Down Expand Up @@ -279,4 +287,27 @@ describe('gallery', () => {
...photoswipeArgsMock(null, 0),
)
})

test('should init photoswipe when location.hash contains valid gid and pid', () => {
const items = createItems(3)
const galleryID = 'my-gallery'
window.location.hash = `&gid=${galleryID}&pid=2`
mount(<TestGallery id={galleryID} items={items} />)
expect(PhotoSwipeMocked).toHaveBeenCalledWith(
...photoswipeArgsMock(items, 1, galleryID),
)
})

test('should init photoswipe when location.hash contains valid gid and custom pid, passed via Item id prop', () => {
const items = createItems(3).map((item, index) => ({
...item,
id: `picture-${index + 1}`,
}))
const galleryID = 'my-gallery'
window.location.hash = `&gid=${galleryID}&pid=picture-3`
mount(<TestGallery id={galleryID} items={items} />)
expect(PhotoSwipeMocked).toHaveBeenCalledWith(
...photoswipeArgsMock(items, 2, galleryID),
)
})
})
62 changes: 54 additions & 8 deletions src/gallery-custom.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import PhotoSwipe from 'photoswipe'
import { Options as PhotoswipeUiDefaultOptions } from 'photoswipe/dist/photoswipe-ui-default'
import React, { useRef, useCallback, FC } from 'react'
import React, { useRef, useCallback, useEffect, FC } from 'react'
import PropTypes from 'prop-types'
import { getElBounds, sortNodes } from './helpers'
import { Context } from './context'
import { ItemRef, InternalItem } from './types'

interface PhotoSwipeItem extends PhotoSwipe.Item {
el: HTMLElement
pid?: string | number
}

type PhotoSwipeUI =
Expand All @@ -32,21 +33,27 @@ export interface CustomGalleryProps {
* PhotoSwipe options
*/
options?: PhotoSwipe.Options & PhotoswipeUiDefaultOptions

/**
* Gallery ID, for hash navigation
*/
id?: string | number
}

/**
* Gallery component with ability to use specific UI and Layout
*/
export const CustomGallery: FC<CustomGalleryProps> = ({
children,
layoutRef,
ui,
options,
layoutRef,
id: galleryUID,
}) => {
const items = useRef(new Map<ItemRef, InternalItem>())

const handleClick = useCallback((targetRef: ItemRef) => {
let index = 0
const open = useCallback((targetRef?: ItemRef, targetId?: string) => {
let index: number | null = null

const normalized: PhotoSwipeItem[] = []

Expand All @@ -55,10 +62,13 @@ export const CustomGallery: FC<CustomGalleryProps> = ({
const prepare = (entry: [ItemRef, InternalItem], i: number) => {
const [
ref,
{ width, height, title, original, thumbnail, ...rest },
{ width, height, title, original, thumbnail, id: pid, ...rest },
] = entry

if (targetRef === ref) {
if (
targetRef === ref ||
(pid !== undefined && String(pid) === targetId)
) {
index = i
}

Expand All @@ -69,6 +79,7 @@ export const CustomGallery: FC<CustomGalleryProps> = ({
src: original,
msrc: thumbnail,
el: ref.current,
...(pid !== undefined ? { pid } : {}),
...rest,
})
}
Expand All @@ -85,16 +96,50 @@ export const CustomGallery: FC<CustomGalleryProps> = ({

if (layoutEl) {
new PhotoSwipe(layoutEl, ui, normalized, {
index,
index: index === null ? parseInt(targetId, 10) - 1 : index,
getThumbBoundsFn: (thumbIndex) => {
const { el } = normalized[thumbIndex]
return el ? getElBounds(el) : { x: 0, y: 0, w: 0 }
},
history: false,
...(galleryUID !== undefined
? { galleryUID: galleryUID as number, history: true }
: {}),
...(options || {}),
}).init()
}
}, [])

useEffect(() => {
if (galleryUID === undefined) {
return
}

const hash = window.location.hash.substring(1)
const params: { [key: string]: string } = {}

if (hash.length < 5) {
return
}

const vars = hash.split('&')

for (let i = 0; i < vars.length; i++) {
if (vars[i]) {
const [key, value] = vars[i].split('=')
if (key && value) {
params[key] = value
}
}
}

const { pid, gid } = params

if (pid && gid === String(galleryUID)) {
open(null, pid)
}
}, [])

const remove = useCallback((ref) => {
items.current.delete(ref)
}, [])
Expand All @@ -104,7 +149,7 @@ export const CustomGallery: FC<CustomGalleryProps> = ({
}, [])

return (
<Context.Provider value={{ remove, set, handleClick }}>
<Context.Provider value={{ remove, set, handleClick: open }}>
{children}
</Context.Provider>
)
Expand All @@ -120,6 +165,7 @@ CustomGallery.propTypes = {
),
}).isRequired,
ui: PropTypes.any.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}

CustomGallery.defaultProps = {
Expand Down
8 changes: 8 additions & 0 deletions src/gallery-default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export interface GalleryProps extends LayoutProps {
* PhotoSwipe options
*/
options?: PhotoSwipe.Options & PhotoswipeUIDefault.Options

/**
* Gallery ID, for hash navigation
*/
id?: string | number
}

/**
Expand All @@ -16,6 +21,7 @@ export interface GalleryProps extends LayoutProps {
export const Gallery: FC<GalleryProps> = ({
children,
options,
id,
...restProps
}) => {
const defaultLayoutRef = useRef<HTMLElement>()
Expand All @@ -24,6 +30,7 @@ export const Gallery: FC<GalleryProps> = ({
layoutRef={defaultLayoutRef}
ui={PhotoswipeUIDefault}
options={options}
id={id}
>
{children}
<DefaultLayout {...restProps} ref={defaultLayoutRef} />
Expand All @@ -33,5 +40,6 @@ export const Gallery: FC<GalleryProps> = ({

Gallery.propTypes = {
options: PropTypes.object,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
...layoutPropTypes,
}
17 changes: 10 additions & 7 deletions src/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ImageItem: FC<InternalItem> = ({
width,
height,
title,
id,
}) => {
const [fullTitle, setFullTitle] = useState(title)
return (
Expand All @@ -34,6 +35,7 @@ const ImageItem: FC<InternalItem> = ({
width={width}
height={height}
title={fullTitle}
id={id}
>
{({ ref, open }) => (
<div style={{ display: 'inline-block', margin: 5 }}>
Expand Down Expand Up @@ -63,7 +65,7 @@ export const simple = () => {
maxHeight: '100%',
}
return (
<Gallery>
<Gallery id="simple-gallery">
<div
style={{
display: 'grid',
Expand All @@ -78,6 +80,7 @@ export const simple = () => {
width="1600"
height="1600"
title="Author: Folkert Gorter"
id="so-first"
>
{({ ref, open }) => (
<img
Expand Down Expand Up @@ -186,15 +189,15 @@ export const sharedLayout = () => {
return (
<>
<h1>First Gallery</h1>
<CustomGallery ui={PhotoswipeUIDefault} layoutRef={layoutRef}>
{shuffle(items).map((props) => (
<ImageItem {...props} key={props.original} />
<CustomGallery ui={PhotoswipeUIDefault} layoutRef={layoutRef} id="first">
{shuffle(items).map((props, i) => (
<ImageItem {...props} key={props.original} id={i} />
))}
</CustomGallery>
<h1>Second Gallery</h1>
<CustomGallery ui={PhotoswipeUIDefault} layoutRef={layoutRef}>
{shuffle(items).map((props) => (
<ImageItem {...props} key={props.original} />
<CustomGallery ui={PhotoswipeUIDefault} layoutRef={layoutRef} id={2}>
{shuffle(items).map((props, i) => (
<ImageItem {...props} key={props.original} id={`kitten-${i}`} />
))}
</CustomGallery>
<DefaultLayout
Expand Down
6 changes: 6 additions & 0 deletions src/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export interface ItemProps {
* Html content, if you need to use it as modal
*/
html?: string

/**
* Item ID, for hash navigation
*/
id?: string | number
}

/**
Expand Down Expand Up @@ -80,4 +85,5 @@ Item.propTypes = {
title: PropTypes.string,
html: PropTypes.string,
children: PropTypes.func.isRequired,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}

0 comments on commit bde7d7e

Please sign in to comment.