@ecodev/natural-gallery-js
Version:
A lazy load, infinite scroll, natural and masonry layout gallery
552 lines (542 loc) • 18.4 kB
TypeScript
import { PhotoSwipeOptions } from 'photoswipe';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
declare enum LabelVisibility {
HOVER = "hover",
NEVER = "never",
ALWAYS = "always"
}
declare interface ItemOptions {
/**
* Enables ability to zoom images in photoswipe
*/
lightbox?: boolean;
/**
* Add a checkbox to select image
*/
selectable?: boolean;
/**
* Activable emits 'activate' event when link is not provided. It takes place where the link used to be :
* in caption or in image if lightbox is false
*/
activable?: boolean;
gap?: number;
labelVisibility?: LabelVisibility;
}
declare class Item<Model extends ModelAttributes> {
private readonly document;
private readonly options;
readonly model: Model;
/**
* Cleaned title, used for label / button
*/
readonly sanitizedTitle: string;
/**
* Reference to the select button
*/
private _checkbox;
/**
* Element referring the "button" containing the label
*/
private figcaption;
/**
*
* @param model Contains the source data given for an item (e.g. object instance from database with id etc...)
*/
constructor(document: Document, options: ItemOptions, model: Model);
/**
* Actual row index in the list
*/
private _row;
get row(): number;
set row(value: number);
/**
* Computed size (real used size)
*/
private _width;
get width(): number;
set width(value: number);
private _height;
get height(): number;
set height(value: number);
private _cropped;
get cropped(): boolean;
set cropped(value: boolean);
/**
* Wherever item is selected or not
*/
private _selected;
get selected(): boolean;
/**
* Item root element reference (figure)
*/
private _rootElement;
get rootElement(): HTMLElement | null;
get checkbox(): HTMLButtonElement | null;
get enlargedWidth(): number;
get enlargedHeight(): number;
/**
* Create DOM elements according to element raw data (thumbnail and enlarged urls)
* Also apply border-radius at this level because it never changed threw time
*
*
* Base structure is always the same :
*
* <figure>
* <img>
* <figcaption>
* </figure>
*
* But depending on settings, we can add <a> inside <figcaption> or wrap around <figure>
*/
init(): HTMLElement;
setLabelHover(activate: boolean): void;
/**
* Use computed (organized) data to apply style (size and margin) to elements on DOM
* Does not apply border-radius because is used to restyle data on browser resize, and border-radius don't change.
*/
style(): void;
private emitSelectEvent;
toggleSelect(): void;
private throwNotSelectableError;
select(): void;
unselect(): void;
remove(): void;
private updateAriaSelectedStatus;
private getEmptyLinkOrButton;
/**
* Label is visible if options mention hover or always
* @private
*/
private showLabel;
private getFigure;
private getImage;
private getEmptyCaption;
private getCheckbox;
private handleActivation;
private handleZoom;
}
type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | 'inherit' | 'initial' | 'unset';
type ObjectPosition = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'top left' | 'top right' | 'top center' | 'bottom left' | 'bottom right' | 'bottom center' | 'center left' | 'center right' | string;
/**
* A map of all possible event and the structure of their details
*/
interface CustomEventDetailMap<T extends ModelAttributes> {
activate: {
item: Item<T>;
event: MouseEvent | KeyboardEvent;
};
'item-added-to-dom': Item<T>;
'item-displayed': Item<T>;
pagination: {
offset: number;
limit: number;
};
select: Item<T>[];
}
/**
* Augment the global namespace with our custom events
* See: https://github.com/Microsoft/TypeScript/issues/28357
*/
declare global {
interface HTMLElementEventMap {
activate: CustomEvent;
'item-added-to-dom': CustomEvent;
'item-displayed': CustomEvent;
pagination: CustomEvent;
select: CustomEvent;
}
}
interface SizedModel {
/**
* Height in pixels of the enlarged version the image
* If photoswipe is used, the size of the photoswipe enlarged image is required
* If photoswipe is not used, any size that match the ratio is enough
*/
enlargedWidth: number;
/**
* Width in pixels of the enlarged version the image
* If photoswipe is used, the size of the photoswipe enlarged image is required
* If photoswipe is not used, any size that match the ratio is enough
*/
enlargedHeight: number;
}
interface ModelAttributes extends SizedModel {
/**
* Source link for thumbnail image
*/
thumbnailSrc: string;
/**
* Source link for enlarged (photoswipe) image
*/
enlargedSrc?: string;
/**
* Label of item (or button)
*/
title?: string;
/**
* Href link
*/
link?: string;
/**
* a href target attribute
*/
linkTarget?: '_blank' | '_self' | '_parent' | '_top';
/**
* Hex color
*/
color?: string;
/**
* If item is selected
*/
selected?: boolean;
/**
* Background size, default : cover
*/
objectFit?: ObjectFit;
/**
* Background position, default : center
*/
objectPosition?: ObjectPosition;
/**
* Short text describing specifically the image
*/
alt?: string;
}
interface GalleryOptions extends ItemOptions {
rowsPerPage?: number;
minRowsAtStart?: number;
infiniteScrollOffset?: number;
photoSwipeOptions?: PhotoSwipeOptions;
photoSwipePluginsInitFn?: ((lighbox: PhotoSwipeLightbox) => void) | null;
ssr?: {
/**
* In SSR mode, if the gallery width cannot be computed, it will fallback to this value
*/
galleryWidth: number;
};
}
declare abstract class AbstractGallery<Model extends ModelAttributes = ModelAttributes> {
protected elementRef: HTMLElement;
protected scrollElementRef?: (HTMLElement | null) | undefined;
/**
* Default options
*/
protected options: Required<GalleryOptions>;
/**
* Images wrapper container
*/
protected bodyElementRef: HTMLElement;
/**
* Items for which container has been added to dom, but image has not been queries yet
*/
protected scrollBufferedItems: Item<Model>[];
/**
* Debounce function
* Runs a small delay after last image has been added to dom
* When it runs, images are loaded (appear with fade) and more images are queries to preserve a buffer of
* out-of-dom items
*/
protected flushBufferedItems: () => void;
/**
* Number of items to query on buffer flushing
*/
protected requiredItems: number;
protected readonly document: Document;
/**
* PhotoSwipe Lightbox object
*/
protected psLightbox: PhotoSwipeLightbox | null;
/**
* Used to test the scroll direction
* Avoid to load more images when scrolling up
*/
private old_scroll_top;
/**
* Stores page index that have been emitted
* Keeps a log of pages already asked to prevent to ask them multiple times
*/
private requestedIndexesLog;
/**
* Reference to next button element
*/
private nextButton;
/**
*
* @param elementRef
* @param options
* @param scrollElementRef
*/
constructor(elementRef: HTMLElement, options: GalleryOptions, scrollElementRef?: (HTMLElement | null) | undefined);
/**
* Get PhotoSwipe Lightbox
*/
get photoSwipe(): PhotoSwipeLightbox | null;
/**
* Get currently selected PhotoSwipe image
*/
get photoSwipeCurrentItem(): Model | null;
/**
* Complete collection of images
* @type {Array}
*/
protected _collection: Item<Model>[];
get collection(): Item<Model>[];
/**
* Partial set of items that represent the visible items
* @type {Item[]}
* @private
*/
protected _domCollection: Item<Model>[];
get domCollection(): Item<Model>[];
get selectedItems(): Item<Model>[];
get width(): number;
addItemToPhotoSwipeCollection(item: Item<Model>): void;
/**
* Add items to collection
* Transform given list of models into inner Items
* @param models list of models
*/
addItems(models: Model[]): void;
setLabelHover(activate: boolean): void;
/**
* Select all items given to the gallery, whenever they are in the DOM or not
*/
selectCollection(): Item<Model>[];
/**
* Select all items in the DOM
* Ignores buffered items
*/
selectDomCollection(): Item<Model>[];
private selectItems;
/**
* Unselect all selected elements
*/
unselectAllItems(): void;
/**
* Allows to use the same approach and method name to listen as gallery events on DOM or on javascript gallery
* object
*
* Gallery requests items when it's instantiated. But user may subscribe after creation, so we need to request
* again if user subscribes by this function.
*
* @param name
* @param callback
* @param options An object that specifies characteristics about the event listener. The available options are, see
* addEventListener official documentation
*/
addEventListener<K extends keyof CustomEventDetailMap<Model>>(name: K, callback: (evt: CustomEvent<CustomEventDetailMap<Model>[K]>) => void, options?: boolean | AddEventListenerOptions): void;
/**
* Public api for empty function
* Emits a pagination event
*/
clear(): void;
/**
* Return copy of options to prevent modification
*/
getOptions(): GalleryOptions;
/**
* Override current collection
* @param {Item[]} items
*/
setItems(items: Model[]): void;
/**
*
*/
abstract organizeItems(items: Item<Model>[], fromRow?: number, toRow?: number): void;
/**
* Initializes PhotoSwipe
*/
protected photoSwipeInit(): void;
/**
*
*/
protected abstract getEstimatedColumnsPerRow(): number;
/**
* AbstractRowGallery + Masonry
*/
protected abstract onScroll(): void;
/**
* AbstractRowGallery + Masonry
*/
protected abstract onPageAdd(): void;
protected abstract getFormatName(): string;
/**
* Return number of rows to show per page to fill the empty space until the bottom of the screen
* Should grant all the space is used or more, but not less.
* @returns {number}
*/
protected abstract getEstimatedRowsPerPage(): number;
/**
* Fire pagination event
* Information provided in the event allows to retrieve items from the server using given data :
* "offset" and "limit" that have the same semantic that respective attributes in mySQL.
*
* The gallery asks for items it needs, including some buffer items that are not displayed when given but are
* available to be added immediately to DOM when user scrolls.
*
*/
protected requestItems(): void;
/**
* Returns option.rowsPerPage is specified.
* If not returns the estimated number of rows to fill the rest of the vertical space in the screen
* @returns {number}
*/
protected getRowsPerPage(): number;
/**
* Add given item to DOM and to domCollection
* @param {Item} item
* @param destination
*/
protected addItemToDOM(item: Item<Model>, destination?: HTMLElement): void;
protected updateNextButtonVisibility(): void;
/**
* If infinite scroll (no option.rowsPerPage provided), a minimum height is setted to force gallery to overflow
* from viewport. This activates the scroll before adding items to dom. This prevents the scroll to fire new resize
* event and recompute all gallery twice on start.
*/
protected extendToFreeViewport(): void;
/**
* Space between the top of the gallery wrapper (parent of gallery root elementRef) and the bottom of the window
*/
protected getGalleryVisibleHeight(): number;
protected startResize(): void;
protected endResize(): void;
protected dispatchEvent<K extends keyof CustomEventDetailMap<Model>>(name: K, data: CustomEventDetailMap<Model>[K]): void;
/**
* Effectively empty gallery, and should prepare container to receive new items
*/
protected empty(): void;
/**
* Listen to scroll event and manages rows additions for lazy load
* @param {HTMLElement | Document} element
*/
private bindScroll;
get rootElement(): HTMLElement;
get bodyElement(): HTMLElement;
}
interface RatioLimits {
min?: number;
max?: number;
}
declare abstract class AbstractRowGallery<Model extends ModelAttributes = ModelAttributes> extends AbstractGallery<Model> {
protected elementRef: HTMLElement;
protected scrollElementRef?: (HTMLElement | null) | undefined;
protected constructor(elementRef: HTMLElement, options: GalleryOptions, scrollElementRef?: (HTMLElement | null) | undefined);
protected onScroll(): void;
protected onPageAdd(): void;
/**
* Add given number of rows to DOM
* @param rows
*/
protected addRows(rows: number): void;
protected endResize(): void;
}
interface NaturalGalleryOptions extends GalleryOptions {
rowHeight: number;
ratioLimit?: RatioLimits;
}
declare class Natural<Model extends ModelAttributes = ModelAttributes> extends AbstractRowGallery<Model> {
/**
* Options after having been defaulted
*/
protected options: Required<NaturalGalleryOptions>;
constructor(elementRef: HTMLElement, options: NaturalGalleryOptions, scrollElementRef?: HTMLElement | null);
static organizeItems<T extends ModelAttributes>(gallery: Natural<T>, items: Item<T>[], fromRow?: number, toRow?: number | null, currentRow?: number | null): void;
/**
* Compute sizes for given images to fit in given row width
* Items are updated
*/
static computeSizes<T extends ModelAttributes>(chunk: Item<T>[], containerWidth: number | null, margin: number, row: number, maxRowHeight?: number | null, ratioLimits?: RatioLimits): void;
static getRowWidth(models: SizedModel[], maxRowHeight: number, margin: number, ratioLimits?: RatioLimits): number;
static getRowHeight(models: SizedModel[], containerWidth: number, margin: number, ratioLimits?: RatioLimits): number;
/**
* Return the ratio format of models as if they were a single image
*/
static getRatios(models: SizedModel[], ratioLimits?: RatioLimits): number;
addRows(rows: number): void;
organizeItems(items: Item<Model>[], fromRow?: number, toRow?: number): void;
protected getFormatName(): string;
protected endResize(): void;
protected getEstimatedColumnsPerRow(): number;
protected getEstimatedRowsPerPage(): number;
private completeLastRow;
}
declare interface ColumnOptions {
width: number;
gap: number;
}
declare class Column<Model extends ModelAttributes> {
private options;
private readonly collection;
private readonly _elementRef;
constructor(document: Document, options: ColumnOptions);
get height(): number;
get elementRef(): HTMLElement;
addItem(item: Item<Model>): void;
}
interface MasonryGalleryOptions extends GalleryOptions {
columnWidth: number;
ratioLimit?: RatioLimits;
}
declare class Masonry<Model extends ModelAttributes = ModelAttributes> extends AbstractGallery<Model> {
/**
* Options after having been defaulted
*/
protected options: Required<MasonryGalleryOptions>;
/**
* Regroup the list of columns
*/
protected columns: Column<Model>[];
constructor(elementRef: HTMLElement, options: MasonryGalleryOptions, scrollElementRef?: HTMLElement | null);
/**
* Compute sides with 1:1 ratio
*/
static organizeItems<T extends ModelAttributes>(gallery: Masonry<T>, items: Item<T>[], fromIndex?: number, toIndex?: number | null): void;
organizeItems(items: Item<Model>[], fromRow?: number, toRow?: number): void;
protected onScroll(): void;
protected onPageAdd(): void;
protected getFormatName(): string;
protected getEstimatedColumnsPerRow(): number;
protected getEstimatedRowsPerPage(): number;
/**
* Use current gallery height as reference. To fill free space it add images until the gallery height changes, then are one more row
*/
protected addUntilFill(): void;
protected addItemToDOM(item: Item<Model>): void;
protected endResize(): void;
protected addColumns(): void;
protected empty(): void;
/**
* Returns true if at least one column doesn't overflow on the bottom of the viewport
*/
private viewportIsNotFilled;
private addItemsToDom;
/**
* Return square side size
*/
private getColumnWidth;
private getShortestColumn;
}
interface SquareGalleryOptions extends GalleryOptions {
itemsPerRow: number;
}
declare class Square<Model extends ModelAttributes = ModelAttributes> extends AbstractRowGallery<Model> {
/**
* Options after having been defaulted
*/
protected options: Required<SquareGalleryOptions>;
constructor(elementRef: HTMLElement, options: SquareGalleryOptions, scrollElementRef?: HTMLElement | null);
/**
* Compute sides with 1:1 ratio
*/
static organizeItems<T extends ModelAttributes>(gallery: Square<T>, items: Item<T>[], firstRowIndex?: number, toRow?: number | null): void;
organizeItems(items: Item<Model>[], fromRow?: number, toRow?: number): void;
protected getEstimatedColumnsPerRow(): number;
protected getEstimatedRowsPerPage(): number;
/**
* Return square side size
*/
protected getItemSideSize(): number;
protected getFormatName(): string;
}
export { type CustomEventDetailMap, Item, type ItemOptions, LabelVisibility, Masonry, type MasonryGalleryOptions, type ModelAttributes, Natural, type NaturalGalleryOptions, Square, type SquareGalleryOptions };