react-photo-album
Advanced tools
Comparing version 2.4.1 to 3.0.0-rc.1
@@ -1,228 +0,11 @@ | ||
import * as React from 'react'; | ||
type LayoutType = "columns" | "rows" | "masonry"; | ||
type PhotoAlbumProps<T extends Photo = Photo> = { | ||
/** An array of photos to display in the photo album. */ | ||
photos: Array<T>; | ||
/** Photo album layout type. */ | ||
layout: LayoutType; | ||
/** A number of columns in the `columns` or `masonry` layout. */ | ||
columns?: ResponsiveParameter; | ||
/** Spacing between images. */ | ||
spacing?: ResponsiveParameter; | ||
/** Padding around each image in the photo album. */ | ||
padding?: ResponsiveParameter; | ||
/** Target row height in the 'rows' layout. */ | ||
targetRowHeight?: ResponsiveParameter; | ||
/** Additional row constraints in the `rows` layout. */ | ||
rowConstraints?: ResponsiveParameter<RowConstraints>; | ||
/** Photo album container width at various viewport sizes. */ | ||
sizes?: ResponsiveSizes; | ||
/** Photo click callback function. */ | ||
onClick?: ClickHandler<T>; | ||
/** Responsive breakpoints. */ | ||
breakpoints?: number[]; | ||
/** Default container width in SSR. */ | ||
defaultContainerWidth?: number; | ||
/** Additional HTML attributes to be passed to the rendered elements. */ | ||
componentsProps?: ComponentsPropsParameter; | ||
/** Custom photo rendering function. */ | ||
renderPhoto?: RenderPhoto<T>; | ||
/** Custom container rendering function. */ | ||
renderContainer?: RenderContainer; | ||
/** Custom row container rendering function. */ | ||
renderRowContainer?: RenderRowContainer<T>; | ||
/** Custom column container rendering function. */ | ||
renderColumnContainer?: RenderColumnContainer<T>; | ||
}; | ||
interface Image { | ||
/** Image source. */ | ||
src: string; | ||
/** Image width in pixels. */ | ||
width: number; | ||
/** Image height in pixels. */ | ||
height: number; | ||
} | ||
interface Photo extends Image { | ||
/** Optional `key` attribute. */ | ||
key?: string; | ||
/** Optional image `alt` attribute. */ | ||
alt?: string; | ||
/** Optional image `title` attribute. */ | ||
title?: string; | ||
/** @deprecated - use `srcSet` instead */ | ||
images?: Image[]; | ||
/** Optional array of alternative images to be included in the `srcset` attribute. */ | ||
srcSet?: Image[]; | ||
} | ||
type RenderPhotoProps<T extends Photo = Photo> = { | ||
/** photo object */ | ||
photo: T; | ||
/** computed photo layout */ | ||
layout: PhotoLayout; | ||
/** photo album layout options */ | ||
layoutOptions: LayoutOptions<T>; | ||
/** pre-populated 'img' element attributes */ | ||
imageProps: NonOptional<ImageElementAttributes, "src" | "alt" | "style">; | ||
/** A callback to render the default photo implementation. If `wrapped` is `true`, the image is styled with `width` | ||
* and `height` set to 100%. Use this option when rendering image wrapper styled with wrapperStyle. */ | ||
renderDefaultPhoto: RenderFunction<{ | ||
wrapped?: boolean; | ||
} | void>; | ||
/** CSS styles to properly size image wrapper (i.e. <div> wrapper) */ | ||
wrapperStyle: React.CSSProperties; | ||
}; | ||
type RenderPhoto<T extends Photo = Photo> = RenderFunction<RenderPhotoProps<T>>; | ||
type ClickHandlerProps<T extends Photo = Photo> = { | ||
event: React.MouseEvent; | ||
photo: T; | ||
index: number; | ||
}; | ||
type ClickHandler<T extends Photo = Photo> = (props: ClickHandlerProps<T>) => void; | ||
type ResponsiveParameterProvider<T = number> = (containerWidth: number) => T; | ||
type ResponsiveParameter<T = number> = T | ResponsiveParameterProvider<T>; | ||
type ResponsiveSizes = { | ||
/** default size e.g. 100vw or calc(100vw - 200px) */ | ||
size: string; | ||
/** array of sizes at various breakpoint */ | ||
sizes?: { | ||
/** viewport size media query e.g. (max-width: 600px) */ | ||
viewport: string; | ||
/** photo album width at given viewport size e.g. calc(100vw - 50px) */ | ||
size: string; | ||
}[]; | ||
}; | ||
type PhotoLayout = { | ||
/** rendered photo width */ | ||
width: number; | ||
/** rendered photo height */ | ||
height: number; | ||
/** photo index in the original `photos` array */ | ||
index: number; | ||
/** photo index in a given row/column */ | ||
photoIndex: number; | ||
/** number of photos in a given row/column */ | ||
photosCount: number; | ||
}; | ||
type GenericLayoutOptions<T extends Photo = Photo> = { | ||
/** layout spacing (gaps between photos) */ | ||
spacing: number; | ||
/** padding around each photo */ | ||
padding: number; | ||
/** current photo album container width */ | ||
containerWidth: number; | ||
/** photo click handler */ | ||
onClick?: ClickHandler<T>; | ||
/** photo album size at various viewport sizes */ | ||
sizes?: ResponsiveSizes; | ||
}; | ||
type RowsLayoutOptions<T extends Photo = Photo> = GenericLayoutOptions<T> & { | ||
/** layout type */ | ||
layout: Extract<LayoutType, "rows">; | ||
/** target row height in 'rows' layout */ | ||
targetRowHeight: number; | ||
/** Additional row constraints */ | ||
rowConstraints?: RowConstraints; | ||
}; | ||
type ColumnsLayoutOptions<T extends Photo = Photo> = GenericLayoutOptions<T> & { | ||
/** layout type */ | ||
layout: Extract<LayoutType, "columns" | "masonry">; | ||
/** number of columns in 'columns' or 'masonry' layout */ | ||
columns: number; | ||
}; | ||
type LayoutOptions<T extends Photo = Photo> = ColumnsLayoutOptions<T> | RowsLayoutOptions<T>; | ||
type RowConstraints = { | ||
/** minimum number of photos per row in 'rows' layout */ | ||
minPhotos?: number; | ||
/** maximum number of photos per row in 'rows' layout */ | ||
maxPhotos?: number; | ||
/** maximum row height when there is not enough photos to fill more than one row */ | ||
singleRowMaxHeight?: number; | ||
}; | ||
type ComponentsProps = { | ||
/** Additional HTML attributes to be passed to the outer container `div` element */ | ||
containerProps?: DivElementAttributes; | ||
/** Additional HTML attributes to be passed to the row container `div` element */ | ||
rowContainerProps?: DivElementAttributes; | ||
/** Additional HTML attributes to be passed to the column container `div` element */ | ||
columnContainerProps?: DivElementAttributes; | ||
/** Additional HTML attributes to be passed to the photo `img` element */ | ||
imageProps?: ImageElementAttributes; | ||
}; | ||
type ComponentsPropsParameter = ComponentsProps | ((containerWidth?: number) => ComponentsProps); | ||
type RenderContainerProps = React.PropsWithChildren<{ | ||
/** layout type */ | ||
layout: LayoutType; | ||
/** pre-populated default container attributes */ | ||
containerProps: DivElementAttributes; | ||
/** container ref callback */ | ||
containerRef: React.RefCallback<HTMLDivElement>; | ||
}>; | ||
type RenderContainer = RenderFunction<RenderContainerProps>; | ||
type RenderRowContainerProps<T extends Photo = Photo> = React.PropsWithChildren<{ | ||
/** layout options */ | ||
layoutOptions: RowsLayoutOptions<T>; | ||
/** row number */ | ||
rowIndex: number; | ||
/** total number of rows */ | ||
rowsCount: number; | ||
/** pre-populated default row container attributes */ | ||
rowContainerProps: DivElementAttributes; | ||
}>; | ||
type RenderRowContainer<T extends Photo = Photo> = RenderFunction<RenderRowContainerProps<T>>; | ||
type RenderColumnContainerProps<T extends Photo = Photo> = React.PropsWithChildren<{ | ||
layoutOptions: ColumnsLayoutOptions<T>; | ||
/** column number */ | ||
columnIndex: number; | ||
/** total number of columns */ | ||
columnsCount: number; | ||
/** sum of spacings and paddings in each column */ | ||
columnsGaps?: number[]; | ||
/** width adjustment ratios of each column */ | ||
columnsRatios?: number[]; | ||
/** pre-populated default column container attributes */ | ||
columnContainerProps: DivElementAttributes; | ||
}>; | ||
type RenderColumnContainer<T extends Photo = Photo> = RenderFunction<RenderColumnContainerProps<T>>; | ||
type RenderFunction<T = void> = (props: T) => React.ReactNode; | ||
type DivElementAttributes = React.HTMLAttributes<HTMLDivElement>; | ||
type ImageElementAttributes = React.ImgHTMLAttributes<HTMLImageElement>; | ||
type Optional<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>; | ||
type NonOptional<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; | ||
declare function PhotoAlbum<T extends Photo>(props: PhotoAlbumProps<T>): React.JSX.Element | null; | ||
type RowsLayoutModel<T extends Photo = Photo> = { | ||
photo: T; | ||
layout: PhotoLayout; | ||
}[][] | undefined; | ||
declare function computeRowsLayout<T extends Photo = Photo>({ photos, layoutOptions, }: { | ||
photos: T[]; | ||
layoutOptions: RowsLayoutOptions<T>; | ||
}): RowsLayoutModel<T>; | ||
type ComputeColumnsLayoutProps<T extends Photo = Photo> = { | ||
photos: T[]; | ||
layoutOptions: ColumnsLayoutOptions<T>; | ||
}; | ||
type ColumnsLayoutModel<T extends Photo = Photo> = { | ||
columnsModel: { | ||
photo: T; | ||
layout: PhotoLayout; | ||
}[][]; | ||
columnsRatios: number[]; | ||
columnsGaps: number[]; | ||
} | undefined; | ||
declare function computeColumnsLayout<T extends Photo = Photo>({ photos, layoutOptions, }: ComputeColumnsLayoutProps<T>): ColumnsLayoutModel<T>; | ||
type ComputeMasonryLayoutProps<T extends Photo = Photo> = { | ||
photos: T[]; | ||
layoutOptions: ColumnsLayoutOptions<T>; | ||
}; | ||
type MasonryColumnsModel<T extends Photo = Photo> = { | ||
photo: T; | ||
layout: PhotoLayout; | ||
}[][] | undefined; | ||
declare function computeMasonryLayout<T extends Photo = Photo>(props: ComputeMasonryLayoutProps<T>): MasonryColumnsModel<T>; | ||
export { type ClickHandler, type ClickHandlerProps, type ColumnsLayoutOptions, type ComponentsProps, type ComponentsPropsParameter, type DivElementAttributes, type GenericLayoutOptions, type Image, type ImageElementAttributes, type LayoutOptions, type LayoutType, type NonOptional, type Optional, type Photo, PhotoAlbum, type PhotoAlbumProps, type PhotoLayout, type RenderColumnContainer, type RenderColumnContainerProps, type RenderContainer, type RenderContainerProps, type RenderFunction, type RenderPhoto, type RenderPhotoProps, type RenderRowContainer, type RenderRowContainerProps, type ResponsiveParameter, type ResponsiveParameterProvider, type ResponsiveSizes, type RowConstraints, type RowsLayoutOptions, PhotoAlbum as default, computeColumnsLayout as unstable_computeColumnsLayout, computeMasonryLayout as unstable_computeMasonryLayout, computeRowsLayout as unstable_computeRowsLayout }; | ||
export { ClickHandlerProps, ColumnsPhotoAlbumProps, CommonPhotoAlbumProps, ComponentsProps, ContextAware, Image, LayoutModel, LayoutVariables, MasonryPhotoAlbumProps, NonOptional, Photo, Render, RenderButtonContext, RenderButtonProps, RenderContainerProps, RenderFunction, RenderImageContext, RenderImageProps, RenderLinkContext, RenderLinkProps, RenderPhotoContext, RenderPhotoProps, RenderTrackProps, RenderWrapperContext, RenderWrapperProps, ResponsiveParameter, ResponsiveSizes, RowConstraints, RowsPhotoAlbumProps } from './types.js'; | ||
export { default } from './client/aggregate.js'; | ||
export { default as RowsPhotoAlbum } from './client/rows.js'; | ||
export { default as ColumnsPhotoAlbum } from './client/columns.js'; | ||
export { default as MasonryPhotoAlbum } from './client/masonry.js'; | ||
export { default as UnstableStaticPhotoAlbum } from './core/static.js'; | ||
export { default as unstable_computeRowsLayout } from './layouts/rows.js'; | ||
export { default as unstable_computeColumnsLayout } from './layouts/columns.js'; | ||
export { default as unstable_computeMasonryLayout } from './layouts/masonry.js'; | ||
import 'react'; | ||
import 'react/jsx-runtime'; |
@@ -1,888 +0,18 @@ | ||
"use client"; | ||
"use strict"; | ||
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); | ||
const React = require("react"); | ||
function _interopNamespaceDefault(e) { | ||
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } }); | ||
if (e) { | ||
for (const k in e) { | ||
if (k !== "default") { | ||
const d = Object.getOwnPropertyDescriptor(e, k); | ||
Object.defineProperty(n, k, d.get ? d : { | ||
enumerable: true, | ||
get: () => e[k] | ||
}); | ||
} | ||
} | ||
} | ||
n.default = e; | ||
return Object.freeze(n); | ||
} | ||
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React); | ||
function ratio({ width, height }) { | ||
return width / height; | ||
} | ||
function round(value, decimals = 0) { | ||
const factor = 10 ** decimals; | ||
return Math.round((value + Number.EPSILON) * factor) / factor; | ||
} | ||
function rankingFunctionComparator(rank) { | ||
return (a, b) => rank(b) - rank(a); | ||
} | ||
class MinHeap { | ||
constructor(comparator) { | ||
this.comparator = comparator; | ||
this.heap = []; | ||
this.n = 0; | ||
} | ||
greater(i, j) { | ||
return this.comparator(this.heap[i], this.heap[j]) < 0; | ||
} | ||
swap(i, j) { | ||
const temp = this.heap[i]; | ||
this.heap[i] = this.heap[j]; | ||
this.heap[j] = temp; | ||
} | ||
swim(i) { | ||
let k = i; | ||
let k2 = Math.floor(k / 2); | ||
while (k > 1 && this.greater(k2, k)) { | ||
this.swap(k2, k); | ||
k = k2; | ||
k2 = Math.floor(k / 2); | ||
} | ||
} | ||
sink(i) { | ||
let k = i; | ||
let k2 = k * 2; | ||
while (k2 <= this.n) { | ||
if (k2 < this.n && this.greater(k2, k2 + 1)) k2 += 1; | ||
if (!this.greater(k, k2)) break; | ||
this.swap(k, k2); | ||
k = k2; | ||
k2 = k * 2; | ||
} | ||
} | ||
push(element) { | ||
this.n += 1; | ||
this.heap[this.n] = element; | ||
this.swim(this.n); | ||
} | ||
pop() { | ||
if (this.n === 0) return void 0; | ||
this.swap(1, this.n); | ||
this.n -= 1; | ||
const max = this.heap.pop(); | ||
this.sink(1); | ||
return max; | ||
} | ||
size() { | ||
return this.n; | ||
} | ||
} | ||
function buildPrecedentsMap(graph, startNode, endNode) { | ||
const precedentsMap = /* @__PURE__ */ new Map(); | ||
const visited = /* @__PURE__ */ new Set(); | ||
const storedShortestPaths = /* @__PURE__ */ new Map(); | ||
storedShortestPaths.set(startNode, 0); | ||
const queue = new MinHeap(rankingFunctionComparator((el) => el.weight)); | ||
queue.push({ id: startNode, weight: 0 }); | ||
while (queue.size() > 0) { | ||
const { id, weight } = queue.pop(); | ||
if (!visited.has(id)) { | ||
const neighboringNodes = graph(id); | ||
visited.add(id); | ||
neighboringNodes.forEach((neighborWeight, neighbor) => { | ||
const newWeight = weight + neighborWeight; | ||
const currentId = precedentsMap.get(neighbor); | ||
const currentWeight = storedShortestPaths.get(neighbor); | ||
if (currentWeight === void 0 || currentWeight > newWeight && (currentWeight / newWeight > 1.005 || currentId !== void 0 && currentId < id)) { | ||
storedShortestPaths.set(neighbor, newWeight); | ||
queue.push({ id: neighbor, weight: newWeight }); | ||
precedentsMap.set(neighbor, id); | ||
} | ||
}); | ||
} | ||
} | ||
return storedShortestPaths.has(endNode) ? precedentsMap : void 0; | ||
} | ||
function getPathFromPrecedentsMap(precedentsMap, endNode) { | ||
const nodes = []; | ||
for (let node = endNode; node !== void 0; node = precedentsMap.get(node)) { | ||
nodes.push(node); | ||
} | ||
return nodes.reverse(); | ||
} | ||
function findShortestPath(graph, startNode, endNode) { | ||
const precedentsMap = buildPrecedentsMap(graph, startNode, endNode); | ||
return precedentsMap ? getPathFromPrecedentsMap(precedentsMap, endNode) : void 0; | ||
} | ||
function findIdealNodeSearch({ | ||
photos, | ||
targetRowHeight, | ||
containerWidth | ||
}) { | ||
const minRatio = photos.reduce((acc, photo) => Math.min(ratio(photo), acc), Number.MAX_VALUE); | ||
return round(containerWidth / targetRowHeight / minRatio) + 2; | ||
} | ||
function getCommonHeight(row, containerWidth, spacing, padding) { | ||
const rowWidth = containerWidth - (row.length - 1) * spacing - 2 * padding * row.length; | ||
const totalAspectRatio = row.reduce((acc, photo) => acc + ratio(photo), 0); | ||
return rowWidth / totalAspectRatio; | ||
} | ||
function cost(photos, i, j, width, targetRowHeight, spacing, padding) { | ||
const row = photos.slice(i, j); | ||
const commonHeight = getCommonHeight(row, width, spacing, padding); | ||
return commonHeight > 0 ? (commonHeight - targetRowHeight) ** 2 * row.length : void 0; | ||
} | ||
function makeGetRowNeighbors({ | ||
photos, | ||
layoutOptions, | ||
targetRowHeight, | ||
limitNodeSearch, | ||
rowConstraints | ||
}) { | ||
return (node) => { | ||
var _a, _b; | ||
const { containerWidth, spacing, padding } = layoutOptions; | ||
const results = /* @__PURE__ */ new Map(); | ||
results.set(node, 0); | ||
const startOffset = (_a = rowConstraints == null ? void 0 : rowConstraints.minPhotos) != null ? _a : 1; | ||
const endOffset = Math.min(limitNodeSearch, (_b = rowConstraints == null ? void 0 : rowConstraints.maxPhotos) != null ? _b : Infinity); | ||
for (let i = node + startOffset; i < photos.length + 1; i += 1) { | ||
if (i - node > endOffset) break; | ||
const currentCost = cost(photos, node, i, containerWidth, targetRowHeight, spacing, padding); | ||
if (currentCost === void 0) break; | ||
results.set(i, currentCost); | ||
} | ||
return results; | ||
}; | ||
} | ||
function computeRowsLayout({ | ||
photos, | ||
layoutOptions | ||
}) { | ||
const { spacing, padding, containerWidth, targetRowHeight, rowConstraints } = layoutOptions; | ||
const limitNodeSearch = findIdealNodeSearch({ photos, containerWidth, targetRowHeight }); | ||
const getNeighbors = makeGetRowNeighbors({ | ||
photos, | ||
layoutOptions, | ||
targetRowHeight, | ||
limitNodeSearch, | ||
rowConstraints | ||
}); | ||
const path = findShortestPath(getNeighbors, 0, photos.length); | ||
if (path === void 0) return void 0; | ||
const layout = []; | ||
for (let i = 1; i < path.length; i += 1) { | ||
const row = photos.map((photo, index) => ({ photo, index })).slice(path[i - 1], path[i]); | ||
const height = getCommonHeight( | ||
row.map(({ photo }) => photo), | ||
containerWidth, | ||
spacing, | ||
padding | ||
); | ||
layout.push( | ||
row.map(({ photo, index }, photoIndex) => ({ | ||
photo, | ||
layout: { | ||
height, | ||
width: height * ratio(photo), | ||
index, | ||
photoIndex, | ||
photosCount: row.length | ||
} | ||
})) | ||
); | ||
} | ||
return layout; | ||
} | ||
function clsx(...classes) { | ||
return [...classes].filter((cls) => Boolean(cls)).join(" "); | ||
} | ||
function calcWidth(base, photoLayout, layoutOptions) { | ||
const { width, photosCount } = photoLayout; | ||
const { layout, spacing, padding, containerWidth } = layoutOptions; | ||
const count = layout === "rows" ? photosCount : layoutOptions.columns; | ||
const gaps = spacing * (count - 1) + 2 * padding * count; | ||
return `calc((${base} - ${gaps}px) / ${round((containerWidth - gaps) / width, 5)})`; | ||
} | ||
function cssPhotoWidth(layout, layoutOptions) { | ||
return layoutOptions.layout !== "rows" ? `calc(100% - ${2 * layoutOptions.padding}px)` : calcWidth("100%", layout, layoutOptions); | ||
} | ||
function calculateSizesValue(size, layout, layoutOptions) { | ||
var _a, _b; | ||
return calcWidth((_b = (_a = size.match(/^\s*calc\((.*)\)\s*$/)) == null ? void 0 : _a[1]) != null ? _b : size, layout, layoutOptions); | ||
} | ||
function srcSetAndSizes(photo, layout, layoutOptions) { | ||
var _a; | ||
let srcSet; | ||
let sizes; | ||
const images = photo.srcSet || photo.images; | ||
if (images && images.length > 0) { | ||
srcSet = images.concat( | ||
!images.find(({ width }) => width === photo.width) ? [{ src: photo.src, width: photo.width, height: photo.height }] : [] | ||
).sort((first, second) => first.width - second.width).map((image) => `${image.src} ${image.width}w`).join(", "); | ||
} | ||
if ((_a = layoutOptions.sizes) == null ? void 0 : _a.size) { | ||
sizes = (layoutOptions.sizes.sizes || []).map(({ viewport, size }) => `${viewport} ${calculateSizesValue(size, layout, layoutOptions)}`).concat(calculateSizesValue(layoutOptions.sizes.size, layout, layoutOptions)).join(", "); | ||
} else { | ||
sizes = `${Math.ceil(layout.width / layoutOptions.containerWidth * 100)}vw`; | ||
} | ||
return { srcSet, sizes }; | ||
} | ||
function PhotoRenderer(props) { | ||
var _a, _b; | ||
const { photo, layout, layoutOptions, imageProps: { style, className, ...restImageProps } = {}, renderPhoto } = props; | ||
const { onClick } = layoutOptions; | ||
const imageStyle = { | ||
display: "block", | ||
boxSizing: "content-box", | ||
width: cssPhotoWidth(layout, layoutOptions), | ||
height: "auto", | ||
aspectRatio: `${photo.width} / ${photo.height}`, | ||
...layoutOptions.padding ? { padding: `${layoutOptions.padding}px` } : null, | ||
...(layoutOptions.layout === "columns" || layoutOptions.layout === "masonry") && layout.photoIndex < layout.photosCount - 1 ? { marginBottom: `${layoutOptions.spacing}px` } : null, | ||
...onClick ? { cursor: "pointer" } : null, | ||
...style | ||
}; | ||
const handleClick = onClick ? (event) => { | ||
onClick({ event, photo, index: layout.index }); | ||
} : void 0; | ||
const imageProps = { | ||
src: photo.src, | ||
alt: (_a = photo.alt) != null ? _a : "", | ||
title: photo.title, | ||
onClick: handleClick, | ||
style: imageStyle, | ||
className: clsx("react-photo-album--photo", className), | ||
loading: "lazy", | ||
decoding: "async", | ||
...srcSetAndSizes(photo, layout, layoutOptions), | ||
...restImageProps | ||
}; | ||
const renderDefaultPhoto = (options) => { | ||
const { src, alt, srcSet, sizes, style: unwrappedStyle, ...rest } = imageProps; | ||
return React__namespace.createElement( | ||
"img", | ||
{ | ||
alt, | ||
...srcSet ? { srcSet, sizes } : null, | ||
src, | ||
style: (options == null ? void 0 : options.wrapped) ? { display: "block", width: "100%", height: "100%" } : unwrappedStyle, | ||
...rest | ||
} | ||
); | ||
}; | ||
const wrapperStyle = (({ display, boxSizing, width, aspectRatio, padding, marginBottom, cursor }) => ({ | ||
display, | ||
boxSizing, | ||
width, | ||
aspectRatio, | ||
padding, | ||
marginBottom, | ||
cursor | ||
}))(imageStyle); | ||
return React__namespace.createElement(React__namespace.Fragment, null, (_b = renderPhoto == null ? void 0 : renderPhoto({ | ||
photo, | ||
layout, | ||
layoutOptions, | ||
imageProps, | ||
renderDefaultPhoto, | ||
wrapperStyle | ||
})) != null ? _b : renderDefaultPhoto()); | ||
} | ||
function defaultRenderRowContainer({ | ||
rowContainerProps, | ||
children | ||
}) { | ||
return React__namespace.createElement("div", { ...rowContainerProps }, children); | ||
} | ||
function RowContainerRenderer(props) { | ||
const { | ||
layoutOptions, | ||
rowIndex, | ||
rowsCount, | ||
renderRowContainer, | ||
rowContainerProps: { style, className, ...restRowContainerProps } = {}, | ||
children | ||
} = props; | ||
const rowContainerProps = { | ||
className: clsx("react-photo-album--row", className), | ||
style: { | ||
display: "flex", | ||
flexDirection: "row", | ||
flexWrap: "nowrap", | ||
alignItems: "flex-start", | ||
justifyContent: "space-between", | ||
...rowIndex < rowsCount - 1 ? { marginBottom: `${layoutOptions.spacing}px` } : null, | ||
...style | ||
}, | ||
...restRowContainerProps | ||
}; | ||
return React__namespace.createElement(React__namespace.Fragment, null, (renderRowContainer != null ? renderRowContainer : defaultRenderRowContainer)({ | ||
layoutOptions, | ||
rowIndex, | ||
rowsCount, | ||
rowContainerProps, | ||
children | ||
})); | ||
} | ||
function RowsLayout(props) { | ||
const { | ||
photos, | ||
layoutOptions, | ||
renderPhoto, | ||
renderRowContainer, | ||
componentsProps: { imageProps, rowContainerProps } | ||
} = props; | ||
const rowsLayout = computeRowsLayout({ photos, layoutOptions }); | ||
if (!rowsLayout) return null; | ||
return React__namespace.createElement(React__namespace.Fragment, null, rowsLayout.map((row, rowIndex) => React__namespace.createElement( | ||
RowContainerRenderer, | ||
{ | ||
key: `row-${rowIndex}`, | ||
layoutOptions, | ||
rowIndex, | ||
rowsCount: rowsLayout.length, | ||
renderRowContainer, | ||
rowContainerProps | ||
}, | ||
row.map(({ photo, layout }) => React__namespace.createElement( | ||
PhotoRenderer, | ||
{ | ||
key: photo.key || photo.src, | ||
photo, | ||
layout, | ||
layoutOptions, | ||
renderPhoto, | ||
imageProps | ||
} | ||
)) | ||
))); | ||
} | ||
function computeShortestPath(graph, pathLength, startNode, endNode) { | ||
const matrix = /* @__PURE__ */ new Map(); | ||
const queue = /* @__PURE__ */ new Set(); | ||
queue.add(startNode); | ||
for (let length = 0; length < pathLength; length += 1) { | ||
const currentQueue = [...queue.keys()]; | ||
queue.clear(); | ||
currentQueue.forEach((node) => { | ||
const accumulatedWeight = length > 0 ? matrix.get(node)[length].weight : 0; | ||
graph(node).forEach(({ neighbor, weight }) => { | ||
let paths = matrix.get(neighbor); | ||
if (!paths) { | ||
paths = []; | ||
matrix.set(neighbor, paths); | ||
} | ||
const newWeight = accumulatedWeight + weight; | ||
const nextPath = paths[length + 1]; | ||
if (!nextPath || nextPath.weight > newWeight && (nextPath.weight / newWeight > 1.0001 || node < nextPath.node)) { | ||
paths[length + 1] = { node, weight: newWeight }; | ||
} | ||
if (length < pathLength - 1 && neighbor !== endNode) { | ||
queue.add(neighbor); | ||
} | ||
}); | ||
}); | ||
} | ||
return matrix; | ||
} | ||
function reconstructShortestPath(matrix, pathLength, endNode) { | ||
const path = [endNode]; | ||
for (let node = endNode, length = pathLength; length > 0; length -= 1) { | ||
node = matrix.get(node)[length].node; | ||
path.push(node); | ||
} | ||
return path.reverse(); | ||
} | ||
function findShortestPathLengthN(graph, pathLength, startNode, endNode) { | ||
return reconstructShortestPath(computeShortestPath(graph, pathLength, startNode, endNode), pathLength, endNode); | ||
} | ||
function makeGetColumnNeighbors({ | ||
photos, | ||
spacing, | ||
padding, | ||
targetColumnWidth, | ||
targetColumnHeight | ||
}) { | ||
return (node) => { | ||
const results = []; | ||
const cutOffHeight = targetColumnHeight * 1.5; | ||
let height = targetColumnWidth / ratio(photos[node]) + 2 * padding; | ||
for (let i = node + 1; i < photos.length + 1; i += 1) { | ||
results.push({ neighbor: i, weight: (targetColumnHeight - height) ** 2 }); | ||
if (height > cutOffHeight || i === photos.length) { | ||
break; | ||
} | ||
height += targetColumnWidth / ratio(photos[i]) + spacing + 2 * padding; | ||
} | ||
return results; | ||
}; | ||
} | ||
function buildColumnsModel({ | ||
path, | ||
photos, | ||
containerWidth, | ||
columnsGaps, | ||
columnsRatios, | ||
spacing, | ||
padding | ||
}) { | ||
const columnsModel = []; | ||
const totalRatio = columnsRatios.reduce((total, columnRatio) => total + columnRatio, 0); | ||
for (let i = 0; i < path.length - 1; i += 1) { | ||
const column = photos.map((photo, index) => ({ photo, index })).slice(path[i], path[i + 1]); | ||
const totalAdjustedGaps = columnsRatios.reduce( | ||
(total, columnRatio, index) => total + (columnsGaps[i] - columnsGaps[index]) * columnRatio, | ||
0 | ||
); | ||
const columnWidth = (containerWidth - (path.length - 2) * spacing - 2 * (path.length - 1) * padding - totalAdjustedGaps) * columnsRatios[i] / totalRatio; | ||
columnsModel.push( | ||
column.map(({ photo, index }, photoIndex) => ({ | ||
photo, | ||
layout: { | ||
width: columnWidth, | ||
height: columnWidth / ratio(photo), | ||
index, | ||
photoIndex, | ||
photosCount: column.length | ||
} | ||
})) | ||
); | ||
} | ||
return columnsModel; | ||
} | ||
function computeColumnsModel({ | ||
photos, | ||
layoutOptions, | ||
targetColumnWidth | ||
}) { | ||
const { columns, spacing, padding, containerWidth } = layoutOptions; | ||
const columnsGaps = []; | ||
const columnsRatios = []; | ||
if (photos.length <= columns) { | ||
const averageRatio = photos.length > 0 ? photos.reduce((acc, photo) => acc + ratio(photo), 0) / photos.length : 1; | ||
for (let i = 0; i < columns; i += 1) { | ||
columnsGaps[i] = 2 * padding; | ||
columnsRatios[i] = i < photos.length ? ratio(photos[i]) : averageRatio; | ||
} | ||
const columnsModel2 = buildColumnsModel({ | ||
path: Array.from({ length: columns + 1 }).map((_, index) => Math.min(index, photos.length)), | ||
photos, | ||
columnsRatios, | ||
columnsGaps, | ||
containerWidth, | ||
spacing, | ||
padding | ||
}); | ||
return { columnsGaps, columnsRatios, columnsModel: columnsModel2 }; | ||
} | ||
const targetColumnHeight = (photos.reduce((acc, photo) => acc + targetColumnWidth / ratio(photo), 0) + spacing * (photos.length - columns) + 2 * padding * photos.length) / columns; | ||
const getNeighbors = makeGetColumnNeighbors({ | ||
photos, | ||
targetColumnWidth, | ||
targetColumnHeight, | ||
spacing, | ||
padding | ||
}); | ||
const path = findShortestPathLengthN(getNeighbors, columns, 0, photos.length); | ||
for (let i = 0; i < path.length - 1; i += 1) { | ||
const column = photos.slice(path[i], path[i + 1]); | ||
columnsGaps[i] = spacing * (column.length - 1) + 2 * padding * column.length; | ||
columnsRatios[i] = 1 / column.reduce((acc, photo) => acc + 1 / ratio(photo), 0); | ||
} | ||
const columnsModel = buildColumnsModel({ | ||
path, | ||
photos, | ||
columnsRatios, | ||
columnsGaps, | ||
containerWidth, | ||
spacing, | ||
padding | ||
}); | ||
return { columnsGaps, columnsRatios, columnsModel }; | ||
} | ||
function computeLayout(props) { | ||
const { photos, layoutOptions } = props; | ||
const { columns, spacing, padding, containerWidth } = layoutOptions; | ||
const targetColumnWidth = (containerWidth - spacing * (columns - 1) - 2 * padding * columns) / columns; | ||
const { columnsGaps, columnsRatios, columnsModel } = computeColumnsModel({ | ||
photos, | ||
layoutOptions, | ||
targetColumnWidth | ||
}); | ||
if (columnsModel.findIndex( | ||
(columnModel) => columnModel.findIndex(({ layout: { width, height } }) => width < 0 || height < 0) >= 0 | ||
) >= 0) { | ||
if (columns > 1) { | ||
return computeLayout({ photos, layoutOptions: { ...layoutOptions, columns: columns - 1 } }); | ||
} | ||
return void 0; | ||
} | ||
return { columnsModel, columnsGaps, columnsRatios }; | ||
} | ||
function computeColumnsLayout({ | ||
photos, | ||
layoutOptions | ||
}) { | ||
return computeLayout({ photos, layoutOptions }); | ||
} | ||
function defaultRenderColumnContainer({ | ||
columnContainerProps, | ||
children | ||
}) { | ||
return React__namespace.createElement("div", { ...columnContainerProps }, children); | ||
} | ||
function cssColumnWidth(props) { | ||
const { layoutOptions, columnIndex, columnsCount, columnsGaps, columnsRatios } = props; | ||
const { layout, spacing, padding } = layoutOptions; | ||
if (layout === "masonry" || !columnsGaps || !columnsRatios) { | ||
return `calc((100% - ${spacing * (columnsCount - 1)}px) / ${columnsCount})`; | ||
} | ||
const totalRatio = columnsRatios.reduce((acc, ratio2) => acc + ratio2, 0); | ||
const totalAdjustedGaps = columnsRatios.reduce( | ||
(acc, ratio2, index) => acc + (columnsGaps[columnIndex] - columnsGaps[index]) * ratio2, | ||
0 | ||
); | ||
return `calc((100% - ${round( | ||
(columnsCount - 1) * spacing + 2 * columnsCount * padding + totalAdjustedGaps, | ||
3 | ||
)}px) * ${round(columnsRatios[columnIndex] / totalRatio, 5)} + ${2 * padding}px)`; | ||
} | ||
function ColumnContainerRenderer(props) { | ||
const { | ||
layoutOptions, | ||
renderColumnContainer, | ||
children, | ||
columnContainerProps: { style, className, ...restColumnContainerProps } = {}, | ||
...rest | ||
} = props; | ||
const columnContainerProps = { | ||
className: clsx("react-photo-album--column", className), | ||
style: { | ||
display: "flex", | ||
flexDirection: "column", | ||
flexWrap: "nowrap", | ||
alignItems: "flex-start", | ||
width: cssColumnWidth(props), | ||
justifyContent: layoutOptions.layout === "columns" ? "space-between" : "flex-start", | ||
...style | ||
}, | ||
...restColumnContainerProps | ||
}; | ||
return React__namespace.createElement(React__namespace.Fragment, null, (renderColumnContainer != null ? renderColumnContainer : defaultRenderColumnContainer)({ | ||
layoutOptions, | ||
columnContainerProps, | ||
children, | ||
...rest | ||
})); | ||
} | ||
function ColumnsLayout(props) { | ||
const { | ||
photos, | ||
layoutOptions, | ||
renderPhoto, | ||
renderColumnContainer, | ||
componentsProps: { imageProps, columnContainerProps } | ||
} = props; | ||
const columnsLayout = computeColumnsLayout({ photos, layoutOptions }); | ||
if (!columnsLayout) return null; | ||
const { columnsModel, columnsRatios, columnsGaps } = columnsLayout; | ||
return React__namespace.createElement(React__namespace.Fragment, null, columnsModel.map((column, columnIndex) => React__namespace.createElement( | ||
ColumnContainerRenderer, | ||
{ | ||
key: `column-${columnIndex}`, | ||
layoutOptions, | ||
columnIndex, | ||
columnsCount: columnsModel.length, | ||
columnsGaps, | ||
columnsRatios, | ||
renderColumnContainer, | ||
columnContainerProps | ||
}, | ||
column.map(({ photo, layout }) => React__namespace.createElement( | ||
PhotoRenderer, | ||
{ | ||
key: photo.key || photo.src, | ||
photo, | ||
layout, | ||
layoutOptions, | ||
renderPhoto, | ||
imageProps | ||
} | ||
)) | ||
))); | ||
} | ||
function computeMasonryLayout(props) { | ||
const { photos, layoutOptions } = props; | ||
const { columns, spacing, padding, containerWidth } = layoutOptions; | ||
const columnWidth = (containerWidth - spacing * (columns - 1) - 2 * padding * columns) / columns; | ||
if (columnWidth <= 0) { | ||
return columns > 1 ? computeMasonryLayout({ | ||
...props, | ||
layoutOptions: { ...layoutOptions, columns: columns - 1 } | ||
}) : void 0; | ||
} | ||
const columnsCurrentTopPositions = []; | ||
for (let i = 0; i < columns; i += 1) { | ||
columnsCurrentTopPositions[i] = 0; | ||
} | ||
const columnsModel = photos.reduce( | ||
(model, photo, index) => { | ||
const shortestColumn = columnsCurrentTopPositions.reduce( | ||
(currentShortestColumn, item, i) => item < columnsCurrentTopPositions[currentShortestColumn] - 1 ? i : currentShortestColumn, | ||
0 | ||
); | ||
columnsCurrentTopPositions[shortestColumn] = columnsCurrentTopPositions[shortestColumn] + columnWidth / ratio(photo) + spacing + 2 * padding; | ||
model[shortestColumn].push({ photo, index }); | ||
return model; | ||
}, | ||
Array.from({ length: columns }).map(() => []) | ||
); | ||
return columnsModel.map( | ||
(column) => column.map(({ photo, index }, photoIndex) => ({ | ||
photo, | ||
layout: { | ||
width: columnWidth, | ||
height: columnWidth / ratio(photo), | ||
index, | ||
photoIndex, | ||
photosCount: column.length | ||
} | ||
})) | ||
); | ||
} | ||
function MasonryLayout(props) { | ||
const { | ||
photos, | ||
layoutOptions, | ||
renderPhoto, | ||
renderColumnContainer, | ||
componentsProps: { imageProps, columnContainerProps } | ||
} = props; | ||
const masonryLayout = computeMasonryLayout({ photos, layoutOptions }); | ||
if (!masonryLayout) return null; | ||
return React__namespace.createElement(React__namespace.Fragment, null, masonryLayout.map((column, columnIndex) => React__namespace.createElement( | ||
ColumnContainerRenderer, | ||
{ | ||
key: `masonry-column-${columnIndex}`, | ||
layoutOptions, | ||
columnsCount: masonryLayout.length, | ||
columnIndex, | ||
renderColumnContainer, | ||
columnContainerProps | ||
}, | ||
column.map(({ photo, layout }) => React__namespace.createElement( | ||
PhotoRenderer, | ||
{ | ||
key: photo.key || photo.src, | ||
photo, | ||
layout, | ||
layoutOptions, | ||
renderPhoto, | ||
imageProps | ||
} | ||
)) | ||
))); | ||
} | ||
function defaultRenderContainer({ containerProps, children, containerRef }) { | ||
return React__namespace.createElement("div", { ref: containerRef, ...containerProps }, children); | ||
} | ||
function ContainerRenderer(props) { | ||
const { | ||
layout, | ||
renderContainer, | ||
children, | ||
containerRef, | ||
containerProps: { style, className, ...restContainerProps } = {} | ||
} = props; | ||
const containerProps = { | ||
className: clsx("react-photo-album", `react-photo-album--${layout}`, className), | ||
style: { | ||
display: "flex", | ||
flexWrap: "nowrap", | ||
justifyContent: "space-between", | ||
flexDirection: layout === "rows" ? "column" : "row", | ||
...style | ||
}, | ||
...restContainerProps | ||
}; | ||
return React__namespace.createElement(React__namespace.Fragment, null, (renderContainer != null ? renderContainer : defaultRenderContainer)({ | ||
containerProps, | ||
containerRef, | ||
layout, | ||
children | ||
})); | ||
} | ||
function useArray(array) { | ||
const ref = React__namespace.useRef(array); | ||
if (!array || !ref.current || array.join() !== ref.current.join()) { | ||
ref.current = array; | ||
} | ||
return ref.current; | ||
} | ||
function containerWidthReducer(state, { newContainerWidth, newScrollbarWidth }) { | ||
const { containerWidth, scrollbarWidth } = state; | ||
if (containerWidth !== void 0 && scrollbarWidth !== void 0 && newContainerWidth !== void 0 && newScrollbarWidth !== void 0 && newContainerWidth > containerWidth && newContainerWidth - containerWidth <= 20 && newScrollbarWidth < scrollbarWidth) { | ||
return { containerWidth, scrollbarWidth: newScrollbarWidth }; | ||
} | ||
return containerWidth !== newContainerWidth || scrollbarWidth !== newScrollbarWidth ? { containerWidth: newContainerWidth, scrollbarWidth: newScrollbarWidth } : state; | ||
} | ||
function resolveContainerWidth(el, breakpoints2) { | ||
let width = el == null ? void 0 : el.clientWidth; | ||
if (width !== void 0 && breakpoints2 && breakpoints2.length > 0) { | ||
const sorted = [...breakpoints2.filter((x) => x > 0)].sort((a, b) => b - a); | ||
sorted.push(Math.floor(sorted[sorted.length - 1] / 2)); | ||
const threshold = width; | ||
width = sorted.find((breakpoint, index) => breakpoint <= threshold || index === sorted.length - 1); | ||
} | ||
return width; | ||
} | ||
function useContainerWidth(breakpoints2, defaultContainerWidth) { | ||
const [{ containerWidth }, dispatch] = React__namespace.useReducer(containerWidthReducer, { | ||
containerWidth: defaultContainerWidth | ||
}); | ||
const ref = React__namespace.useRef(null); | ||
const observerRef = React__namespace.useRef(); | ||
const containerRef = React__namespace.useCallback( | ||
(node) => { | ||
var _a; | ||
(_a = observerRef.current) == null ? void 0 : _a.disconnect(); | ||
observerRef.current = void 0; | ||
ref.current = node; | ||
const updateWidth = () => dispatch({ | ||
newContainerWidth: resolveContainerWidth(ref.current, breakpoints2), | ||
newScrollbarWidth: window.innerWidth - document.documentElement.clientWidth | ||
}); | ||
updateWidth(); | ||
if (node && typeof ResizeObserver !== "undefined") { | ||
observerRef.current = new ResizeObserver(updateWidth); | ||
observerRef.current.observe(node); | ||
} | ||
}, | ||
[breakpoints2] | ||
); | ||
return { containerRef, containerWidth }; | ||
} | ||
const breakpoints = Object.freeze([1200, 600, 300, 0]); | ||
function unwrap(value, arg) { | ||
return typeof value === "function" ? value(arg) : value; | ||
} | ||
function unwrapParameter(value, containerWidth) { | ||
return typeof value !== "undefined" ? unwrap(value, containerWidth) : void 0; | ||
} | ||
function selectResponsiveValue(values, containerWidth) { | ||
const index = breakpoints.findIndex((breakpoint) => breakpoint <= containerWidth); | ||
return unwrap(values[index >= 0 ? index : 0], containerWidth); | ||
} | ||
function resolveResponsiveParameter(parameter, containerWidth, values, minValue = 0) { | ||
const value = unwrapParameter(parameter, containerWidth); | ||
return Math.round(Math.max(value === void 0 ? selectResponsiveValue(values, containerWidth) : value, minValue)); | ||
} | ||
function resolveLayoutOptions({ | ||
layout, | ||
onClick, | ||
containerWidth, | ||
targetRowHeight, | ||
rowConstraints, | ||
columns, | ||
spacing, | ||
padding, | ||
sizes | ||
}) { | ||
return { | ||
layout, | ||
onClick, | ||
containerWidth, | ||
columns: resolveResponsiveParameter(columns, containerWidth, [5, 4, 3, 2], 1), | ||
spacing: resolveResponsiveParameter(spacing, containerWidth, [20, 15, 10, 5]), | ||
padding: resolveResponsiveParameter(padding, containerWidth, [0, 0, 0, 0, 0]), | ||
targetRowHeight: resolveResponsiveParameter(targetRowHeight, containerWidth, [ | ||
(w) => w / 5, | ||
(w) => w / 4, | ||
(w) => w / 3, | ||
(w) => w / 2 | ||
]), | ||
rowConstraints: unwrapParameter(rowConstraints, containerWidth), | ||
sizes | ||
}; | ||
} | ||
function resolveComponentsProps(props, containerWidth, layoutOptions) { | ||
const { photos, componentsProps: componentsPropsProp } = props; | ||
const componentsProps = unwrap(componentsPropsProp, containerWidth) || {}; | ||
if (layoutOptions) { | ||
const { layout, spacing, padding, rowConstraints } = layoutOptions; | ||
if (layout === "rows") { | ||
const { singleRowMaxHeight } = rowConstraints || {}; | ||
if (singleRowMaxHeight) { | ||
const maxWidth = Math.floor( | ||
photos.reduce( | ||
(acc, { width, height }) => acc + width / height * singleRowMaxHeight - 2 * padding, | ||
padding * photos.length * 2 + spacing * (photos.length - 1) | ||
) | ||
); | ||
if (maxWidth > 0) { | ||
componentsProps.containerProps = componentsProps.containerProps || {}; | ||
componentsProps.containerProps.style = { maxWidth, ...componentsProps.containerProps.style }; | ||
} | ||
} | ||
} | ||
} | ||
return componentsProps; | ||
} | ||
function renderLayout(props, componentsProps, layoutOptions) { | ||
const { photos, layout, renderPhoto, renderRowContainer, renderColumnContainer } = props; | ||
const commonLayoutProps = { photos, renderPhoto, componentsProps }; | ||
if (layout === "rows") { | ||
return React__namespace.createElement( | ||
RowsLayout, | ||
{ | ||
layoutOptions, | ||
renderRowContainer, | ||
...commonLayoutProps | ||
} | ||
); | ||
} | ||
if (layout === "columns") { | ||
return React__namespace.createElement( | ||
ColumnsLayout, | ||
{ | ||
layoutOptions, | ||
renderColumnContainer, | ||
...commonLayoutProps | ||
} | ||
); | ||
} | ||
return React__namespace.createElement( | ||
MasonryLayout, | ||
{ | ||
layoutOptions, | ||
renderColumnContainer, | ||
...commonLayoutProps | ||
} | ||
); | ||
} | ||
function PhotoAlbum(props) { | ||
const { photos, layout, renderContainer, defaultContainerWidth, breakpoints: breakpoints2 } = props; | ||
const { containerRef, containerWidth } = useContainerWidth(useArray(breakpoints2), defaultContainerWidth); | ||
if (!layout || !["rows", "columns", "masonry"].includes(layout) || !Array.isArray(photos)) return null; | ||
const layoutOptions = containerWidth ? resolveLayoutOptions({ containerWidth, ...props }) : void 0; | ||
const componentsProps = resolveComponentsProps(props, containerWidth, layoutOptions); | ||
return React__namespace.createElement( | ||
ContainerRenderer, | ||
{ | ||
layout, | ||
containerRef, | ||
renderContainer, | ||
containerProps: componentsProps.containerProps | ||
}, | ||
layoutOptions && renderLayout(props, componentsProps, layoutOptions) | ||
); | ||
} | ||
exports.PhotoAlbum = PhotoAlbum; | ||
exports.default = PhotoAlbum; | ||
exports.unstable_computeColumnsLayout = computeColumnsLayout; | ||
exports.unstable_computeMasonryLayout = computeMasonryLayout; | ||
exports.unstable_computeRowsLayout = computeRowsLayout; | ||
import { default as default2 } from "./client/aggregate.js"; | ||
import { default as default3 } from "./client/rows.js"; | ||
import { default as default4 } from "./client/columns.js"; | ||
import { default as default5 } from "./client/masonry.js"; | ||
import { default as default6 } from "./core/static.js"; | ||
import { default as default7 } from "./layouts/rows.js"; | ||
import { default as default8 } from "./layouts/columns.js"; | ||
import { default as default9 } from "./layouts/masonry.js"; | ||
export { | ||
default4 as ColumnsPhotoAlbum, | ||
default5 as MasonryPhotoAlbum, | ||
default3 as RowsPhotoAlbum, | ||
default6 as UnstableStaticPhotoAlbum, | ||
default2 as default, | ||
default8 as unstable_computeColumnsLayout, | ||
default9 as unstable_computeMasonryLayout, | ||
default7 as unstable_computeRowsLayout | ||
}; |
{ | ||
"name": "react-photo-album", | ||
"version": "2.4.1", | ||
"version": "3.0.0-rc.1", | ||
"description": "Responsive photo gallery component for React", | ||
"author": "Igor Danchenko", | ||
"license": "MIT", | ||
"main": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"type": "module", | ||
"module": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"exports": { | ||
"import": { | ||
"types": "./dist/index.d.mts", | ||
"default": "./dist/index.mjs" | ||
}, | ||
"require": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
}, | ||
"./styles.css": { | ||
"types": "./dist/styles/styles.css.d.ts", | ||
"default": "./dist/styles/styles.css" | ||
}, | ||
"./rows.css": { | ||
"types": "./dist/styles/rows.css.d.ts", | ||
"default": "./dist/styles/rows.css" | ||
}, | ||
"./columns.css": { | ||
"types": "./dist/styles/columns.css.d.ts", | ||
"default": "./dist/styles/columns.css" | ||
}, | ||
"./masonry.css": { | ||
"types": "./dist/styles/masonry.css.d.ts", | ||
"default": "./dist/styles/masonry.css" | ||
} | ||
@@ -23,3 +35,5 @@ }, | ||
], | ||
"sideEffects": false, | ||
"sideEffects": [ | ||
"*.css" | ||
], | ||
"homepage": "https://react-photo-album.com", | ||
@@ -34,3 +48,3 @@ "repository": { | ||
"engines": { | ||
"node": ">=12" | ||
"node": ">=18" | ||
}, | ||
@@ -42,4 +56,10 @@ "publishConfig": { | ||
"peerDependencies": { | ||
"react": ">=16.8.0" | ||
"@types/react": ">=18", | ||
"react": ">=18" | ||
}, | ||
"peerDependenciesMeta": { | ||
"@types/react": { | ||
"optional": true | ||
} | ||
}, | ||
"keywords": [ | ||
@@ -46,0 +66,0 @@ "react", |
113
README.md
@@ -14,3 +14,3 @@ # React Photo Album | ||
- **Built for React:** works with React 18, 17 and 16.8.0+ | ||
- **Built for React:** works with React 18+ | ||
- **SSR friendly:** produces server-side rendered markup that looks pixel | ||
@@ -25,3 +25,3 @@ perfect on the client even before hydration | ||
- **Performance:** it was built with performance in mind in order to support | ||
large photo albums and silky smooth layout adjustments | ||
large photo albums | ||
@@ -60,14 +60,21 @@ ## Layouts | ||
## Requirements | ||
- React 18+ | ||
- Node 18+ | ||
- modern ESM-compatible bundler | ||
## Minimal Setup Example | ||
```tsx | ||
import PhotoAlbum from "react-photo-album"; | ||
import { RowsPhotoAlbum } from "react-photo-album"; | ||
import "react-photo-album/rows.css"; | ||
const photos = [ | ||
{ src: "/images/image1.jpg", width: 800, height: 600 }, | ||
{ src: "/images/image2.jpg", width: 1600, height: 900 }, | ||
{ src: "/image1.jpg", width: 800, height: 600 }, | ||
{ src: "/image2.jpg", width: 1600, height: 900 }, | ||
]; | ||
export default function Gallery() { | ||
return <PhotoAlbum layout="rows" photos={photos} />; | ||
return <RowsPhotoAlbum photos={photos} />; | ||
} | ||
@@ -80,23 +87,21 @@ ``` | ||
Rows layout fills the rectangular container space by arranging photos into rows | ||
that are similar in size, with the height of each row being as close to the | ||
Rows layout fills the container space by arranging photos into rows that are | ||
similar in height, with the height of each row being as close to the | ||
`targetRowHeight` as possible. This layout uses an algorithm adapted from the | ||
Knuth and Plass line breaking algorithm. To calculate the single best layout, it | ||
uses Dijkstra's algorithm to find the shortest past in a graph where each photo | ||
Knuth and Plass line-breaking algorithm. To calculate the optimal layout, it | ||
uses Dijkstra's algorithm to find the shortest path in a graph where each photo | ||
to break on represents a node, and each row represents an edge. The cost of each | ||
edge is calculated as the squared deviation from the `targetRowHeight`. This | ||
algorithm produces rows that are similar in height and photos that are not being | ||
stretched or shrunken abnormally (as is what happens in a naive implementation). | ||
It solves the issue of panoramas shrinking rows or having stragglers or | ||
stretched images in the last row, instead creating a justified grid. The graph | ||
is being built as the shortest path is being calculated to improve algorithm's | ||
performance, so the entire adjacency list is not calculated ahead of time. | ||
edge is calculated as a squared deviation from the `targetRowHeight`. This | ||
algorithm produces rows that are similar in height and photos that are not | ||
stretched or abnormally shrunk (as what happens in a naive implementation). It | ||
solves the issue of panoramas shrinking rows or having stragglers or stretched | ||
images in the last row. | ||
### Columns Layout | ||
Columns layout fills the rectangular container space by arranging photos into a | ||
predefined number of columns, determined by the `columns` parameter. This layout | ||
uses an algorithm very similar to the one described above, but instead of | ||
Dijkstra's algorithm, it uses a dynamic programming algorithm to find the | ||
shortest path of length N in a directed weighted graph. | ||
Columns layout fills the container space by arranging photos into a predefined | ||
number of columns, determined by the `columns` parameter. This layout uses an | ||
algorithm very similar to the one described above, with the only difference | ||
being that instead of Dijkstra's algorithm, it uses a dynamic programming | ||
algorithm to find the shortest path of length N in a directed weighted graph. | ||
@@ -106,4 +111,4 @@ ### Masonry Layout | ||
Masonry layout arranges photos into columns of equal width by placing each photo | ||
into the shortest column. This layout does not completely fill the rectangular | ||
container space, but the columns end up being as close in height to each other | ||
into the shortest column. This layout does not fill the container space flush to | ||
its bottom edge, but the columns end up being as close in height to each other | ||
as possible. | ||
@@ -113,29 +118,32 @@ | ||
React Photo Album automatically generates `sizes` and `srcset` image attributes. | ||
In the case of SSR, React Photo Album includes `sizes` and `srcset` image | ||
attributes in the server-rendered markup, allowing browsers to pick images of | ||
the most appropriate resolution depending on their viewport size. To enable | ||
images with automatic resolution switching, simply provide smaller images in the | ||
photo `srcSet` attribute. | ||
React Photo Album can automatically produce `sizes` and `srcset` image | ||
attributes. In the case of SSR, React Photo Album includes `sizes` and `srcset` | ||
image attributes in the server-rendered markup, allowing browsers to pick images | ||
of the most appropriate resolution depending on the end-user viewport size. To | ||
utilize images with automatic resolution switching, provide images of different | ||
resolutions in the photo `srcSet` attribute. To further improve app | ||
responsiveness and bandwidth utilization, you can specify the `sizes` prop that | ||
describes the width of the photo album in various viewports. | ||
```tsx | ||
import PhotoAlbum from "react-photo-album"; | ||
import { RowsPhotoAlbum } from "react-photo-album"; | ||
import "react-photo-album/rows.css"; | ||
const photos = [ | ||
{ | ||
src: "/images/image1_800x600.jpg", | ||
src: "/image1_800x600.jpg", | ||
width: 800, | ||
height: 600, | ||
srcSet: [ | ||
{ src: "/images/image1_400x300.jpg", width: 400, height: 300 }, | ||
{ src: "/images/image1_200x150.jpg", width: 200, height: 150 }, | ||
{ src: "/image1_400x300.jpg", width: 400, height: 300 }, | ||
{ src: "/image1_200x150.jpg", width: 200, height: 150 }, | ||
], | ||
}, | ||
{ | ||
src: "/images/image2_1600x900.jpg", | ||
src: "/image2_1600x900.jpg", | ||
width: 1600, | ||
height: 900, | ||
srcSet: [ | ||
{ src: "/images/image2_800x450.jpg", width: 800, height: 450 }, | ||
{ src: "/images/image2_400x225.jpg", width: 400, height: 225 }, | ||
{ src: "/image2_800x450.jpg", width: 800, height: 450 }, | ||
{ src: "/image2_400x225.jpg", width: 400, height: 225 }, | ||
], | ||
@@ -146,3 +154,16 @@ }, | ||
export default function Gallery() { | ||
return <PhotoAlbum layout="rows" photos={photos} />; | ||
return ( | ||
<RowsPhotoAlbum | ||
photos={photos} | ||
sizes={{ | ||
size: "1168px", | ||
sizes: [ | ||
{ | ||
viewport: "(max-width: 1200px)", | ||
size: "calc(100vw - 32px)", | ||
}, | ||
], | ||
}} | ||
/> | ||
); | ||
} | ||
@@ -153,10 +174,10 @@ ``` | ||
React Photo Album extensively uses CSS flexbox and CSS `calc` function to | ||
calculate the dimensions of images on the client. Unlike its predecessor, React | ||
Photo Album avoids setting the exact dimensions of images in pixels. Thanks to | ||
this approach, server-side rendered markup looks pixel-perfect on the client | ||
even before hydration (or even when JavaScript is completely disabled in the | ||
browser). To enable server-side rendering, be sure to specify | ||
`defaultContainerWidth` prop. Otherwise, React Photo Album produces empty markup | ||
on the server and renders on the client only after hydration. | ||
React Photo Album extensively uses CSS flexbox and CSS `calc` functions to | ||
calculate images' dimensions on the client. Thanks to this approach, server-side | ||
rendered markup looks pixel-perfect on the client even before hydration. To | ||
enable server-side rendering, specify the `defaultContainerWidth` prop. | ||
Otherwise, React Photo Album produces an empty markup on the server and renders | ||
on the client only after hydration. Please note that unless your photo album is | ||
of constant width that always matches the `defaultContainerWidth` value, you | ||
will most likely see a layout shift immediately after hydration. | ||
@@ -163,0 +184,0 @@ ## Credits |
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
34
187
Yes
56713
2
1018
1
1
1