Repository: dimsemenov/PhotoSwipe
Branch: master
Commit: cd41cb587a46
Files: 159
Total size: 1000.8 KB
Directory structure:
gitextract_0u2e7_9t/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── build/
│ ├── config-builder.js
│ ├── rollup.config.js
│ └── rollup.config.watch.js
├── demo-docs-website/
│ ├── .gitignore
│ ├── babel.config.js
│ ├── docusaurus.config.js
│ ├── package.json
│ ├── repo-data.json
│ ├── sidebars.js
│ ├── src/
│ │ ├── components/
│ │ │ ├── HomepageFeatures/
│ │ │ │ ├── index.js
│ │ │ │ └── styles.module.css
│ │ │ └── PswpCodePreview/
│ │ │ ├── demo-images.js
│ │ │ ├── gallery-templates/
│ │ │ │ ├── basic--badges.js
│ │ │ │ ├── basic--cropped.js
│ │ │ │ ├── basic.js
│ │ │ │ ├── caption.js
│ │ │ │ ├── content-types.js
│ │ │ │ ├── custom-html-markup-data-source.js
│ │ │ │ ├── getting-started.js
│ │ │ │ └── srcset-test.js
│ │ │ └── index.js
│ │ ├── css/
│ │ │ ├── custom.css
│ │ │ ├── docs-page.css
│ │ │ ├── example-code-block.css
│ │ │ ├── header.css
│ │ │ ├── home.css
│ │ │ ├── scrollbar.css
│ │ │ └── sidebar-menu.css
│ │ ├── pages/
│ │ │ ├── _index-deep-zoom-demo.js
│ │ │ ├── _index-gallery-header.js
│ │ │ ├── index.js
│ │ │ └── index.module.css
│ │ └── theme/
│ │ ├── CodeBlock/
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ ├── DocItem/
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ ├── DocItemFooter/
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ ├── DocPage/
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ ├── DocSidebar/
│ │ │ ├── index.js
│ │ │ ├── is-same-path.ts
│ │ │ └── styles.module.css
│ │ ├── EditThisPage/
│ │ │ └── index.js
│ │ ├── Footer/
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ ├── Logo/
│ │ │ └── index.js
│ │ ├── MDXComponents/
│ │ │ ├── index.js
│ │ │ └── styles.css
│ │ └── Navbar/
│ │ ├── github-stars.js
│ │ ├── index.js
│ │ └── styles.module.css
│ └── static/
│ ├── .nojekyll
│ └── photoswipe/
│ ├── photoswipe-lightbox.esm.js
│ ├── photoswipe.css
│ └── photoswipe.esm.js
├── dist/
│ ├── photoswipe-lightbox.esm.js
│ ├── photoswipe.css
│ ├── photoswipe.esm.js
│ ├── types/
│ │ ├── core/
│ │ │ ├── base.d.ts
│ │ │ └── eventable.d.ts
│ │ ├── gestures/
│ │ │ ├── drag-handler.d.ts
│ │ │ ├── gestures.d.ts
│ │ │ ├── tap-handler.d.ts
│ │ │ └── zoom-handler.d.ts
│ │ ├── keyboard.d.ts
│ │ ├── lightbox/
│ │ │ └── lightbox.d.ts
│ │ ├── main-scroll.d.ts
│ │ ├── opener.d.ts
│ │ ├── photoswipe.d.ts
│ │ ├── scroll-wheel.d.ts
│ │ ├── slide/
│ │ │ ├── content.d.ts
│ │ │ ├── get-thumb-bounds.d.ts
│ │ │ ├── loader.d.ts
│ │ │ ├── pan-bounds.d.ts
│ │ │ ├── placeholder.d.ts
│ │ │ ├── slide.d.ts
│ │ │ └── zoom-level.d.ts
│ │ ├── types.d.ts
│ │ ├── ui/
│ │ │ ├── button-arrow.d.ts
│ │ │ ├── button-close.d.ts
│ │ │ ├── button-zoom.d.ts
│ │ │ ├── counter-indicator.d.ts
│ │ │ ├── loading-indicator.d.ts
│ │ │ ├── ui-element.d.ts
│ │ │ └── ui.d.ts
│ │ └── util/
│ │ ├── animations.d.ts
│ │ ├── css-animation.d.ts
│ │ ├── dom-events.d.ts
│ │ ├── spring-animation.d.ts
│ │ ├── spring-easer.d.ts
│ │ ├── util.d.ts
│ │ └── viewport-size.d.ts
│ └── umd/
│ └── README.md
├── docs/
│ ├── adding-ui-elements.md
│ ├── adjusting-zoom-level.md
│ ├── caption.md
│ ├── click-and-tap-actions.md
│ ├── custom-content.md
│ ├── data-sources.md
│ ├── events.md
│ ├── filters.md
│ ├── getting-started.md
│ ├── methods.md
│ ├── native-fullscreen-on-open.md
│ ├── opening-or-closing-transition.md
│ ├── options.md
│ ├── properties.md
│ ├── react-image-gallery.md
│ ├── styling.md
│ ├── svelte-image-gallery.md
│ └── vue-image-gallery.md
├── global.d.ts
├── package.json
├── src/
│ ├── js/
│ │ ├── core/
│ │ │ ├── base.js
│ │ │ └── eventable.js
│ │ ├── gestures/
│ │ │ ├── drag-handler.js
│ │ │ ├── gestures.js
│ │ │ ├── tap-handler.js
│ │ │ └── zoom-handler.js
│ │ ├── keyboard.js
│ │ ├── lightbox/
│ │ │ └── lightbox.js
│ │ ├── main-scroll.js
│ │ ├── opener.js
│ │ ├── photoswipe.js
│ │ ├── scroll-wheel.js
│ │ ├── slide/
│ │ │ ├── content.js
│ │ │ ├── get-thumb-bounds.js
│ │ │ ├── loader.js
│ │ │ ├── pan-bounds.js
│ │ │ ├── placeholder.js
│ │ │ ├── slide.js
│ │ │ └── zoom-level.js
│ │ ├── types.ts
│ │ ├── ui/
│ │ │ ├── button-arrow.js
│ │ │ ├── button-close.js
│ │ │ ├── button-zoom.js
│ │ │ ├── counter-indicator.js
│ │ │ ├── loading-indicator.js
│ │ │ ├── ui-element.js
│ │ │ └── ui.js
│ │ └── util/
│ │ ├── animations.js
│ │ ├── css-animation.js
│ │ ├── dom-events.js
│ │ ├── spring-animation.js
│ │ ├── spring-easer.js
│ │ ├── util.js
│ │ └── viewport-size.js
│ └── photoswipe.css
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"plugins": [
"@babel/plugin-transform-optional-chaining",
"@babel/plugin-transform-nullish-coalescing-operator"
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
================================================
FILE: .eslintignore
================================================
node_modules
!.*.js
.eslintrc.js
*.d.ts
*.ts
================================================
FILE: .eslintrc.json
================================================
{
"env": {
"browser": true,
"es6": true
},
"extends": "airbnb-base",
"parser": "@babel/eslint-parser",
"parserOptions": {
"sourceType": "module",
"requireConfigFile": false,
"babelOptions": {
"plugins": ["@babel/plugin-syntax-class-properties"]
}
},
"overrides": [
{
"files": [
"website/static/es6/**/*.js"
]
}
],
"rules": {
"max-len": ["error", { "code": 100, "comments": 140 }],
"comma-dangle": "off",
"no-param-reassign": 0,
"import/extensions": 0,
"no-underscore-dangle": 0,
"consistent-return": 0,
"no-trailing-spaces": "warn",
"spaced-comment": 0,
"no-plusplus": 0,
"prefer-template": 0,
"object-curly-newline": 0,
"arrow-body-style": 0,
"import/prefer-default-export": 0,
"no-else-return": 0,
"no-lonely-if": 0,
"default-case": 0,
"linebreak-style": 0,
"class-methods-use-this": 0,
"max-classes-per-file": 0,
"arrow-parens": 0,
"no-multiple-empty-lines": 0,
"no-mixed-operators": 0,
"function-paren-newline": 0
}
}
================================================
FILE: .github/FUNDING.yml
================================================
open_collective: photoswipe
================================================
FILE: .github/workflows/build.yml
================================================
name: PhotoSwipe Jobs
on: [workflow_dispatch]
jobs:
build:
name: Publish docs
runs-on: ubuntu-latest
steps:
- name: "checkout repository"
uses: actions/checkout@v2
- name: "setup node"
uses: actions/setup-node@v2
with:
node-version: 16
- name: "github api fetch (for stars count)"
working-directory: demo-docs-website
run: curl -s https://api.github.com/repos/dimsemenov/photoswipe -o ./repo-data.json
- name: "npm install & build docs"
working-directory: demo-docs-website
run: |
npm install
npm run build
- name: "install ssh key"
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
- name: "deploy with rsync"
run: rsync -avz ./demo-docs-website/build/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.RSYNC_DIR }}
================================================
FILE: .gitignore
================================================
node_modules/
release/
Gemfile.lock
test/dest
*.gem
pkg/
*.swp
*~
_site/
.bundle/
website/static/photoswipe/
.DS_Store
bbin/
sftp-config*
_site
.htaccess
private-*
__article/
_site/*
website/build/
node_modules
_production
all.min.css
aws-keys.json
*.sublime-project
*.sublime-workspace
website/dist/
*.idea
/.idea
.vscode
.node-version
.sass-cache/
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2014-2022 Dmitry Semenov, https://dimsemenov.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
**FEEDBACK NEEDED** I am developing the new version - PhotoSwipe v6, please [read about upcoming changes and leave feedback](https://github.com/dimsemenov/PhotoSwipe/discussions/2170).
PhotoSwipe v5 — JavaScript image gallery and lightbox
**[Demo](https://photoswipe.com)** | **[Documentation](https://photoswipe.com/getting-started/)**
[](https://opencollective.com/photoswipe)
[](https://twitter.com/intent/user?screen_name=photoswipe)
### Repo structure
- `dist/` - main JS and CSS
- `src/` - source JS and CSS.
- `src/js/photoswipe.js` - entry for PhotoSwipe Core.
- `src/js/lightbox/lightbox.js` - entry for PhotoSwipe Lightbox.
- `docs/` - documentation markdown files.
- `demo-docs-website/` - website with documentation, demos and manual tests.
- `build/` - rollup build config.
To build JS and CSS in `dist/` directory, run `npm run build`.
To run the demo website and automatically rebuild files during development, run `npm install` in `demo-docs-website/` and `npm run watch` in the root directory.
### Older versions
Documentation for the old version (v4) can be found [here](https://photoswipe.com/v4-docs/getting-started.html) and [the code for 4.1.3 is here](https://github.com/dimsemenov/PhotoSwipe/tree/v4.1.3).
[](https://savelife.in.ua/en/)
---
This project is tested with [BrowserStack](https://www.browserstack.com/).
================================================
FILE: build/config-builder.js
================================================
// eslint-disable-next-line import/no-extraneous-dependencies
import { terser } from 'rollup-plugin-terser';
import { babel } from '@rollup/plugin-babel';
import pkg from '../package.json';
const year = new Date().getFullYear();
function getBanner(name) {
return `/*!
* ${name} ${pkg.version} - https://photoswipe.com
* (c) ${year} Dmytro Semenov
*/`;
}
function getMinifyPlugin() {
return terser({
output: {
comments: /^\**!/i,
},
mangle: {
properties: {
// mangle properties and func names that start with underscore
regex: /^_/,
}
}
});
}
function getBabelPlugin() {
return babel({
exclude: 'node_modules/**'
})
}
const baseOutputDir = 'demo-docs-website/static/photoswipe/';
export const lightboxJS = {
input: 'src/js/lightbox/lightbox.js',
output: {
banner: getBanner('PhotoSwipe Lightbox'),
file: baseOutputDir + 'photoswipe-lightbox.esm.js',
format: 'esm',
sourcemap: true
},
plugins: [getBabelPlugin()]
};
export const coreJS = {
input: 'src/js/photoswipe.js',
output: {
banner: getBanner('PhotoSwipe'),
file: baseOutputDir + 'photoswipe.esm.js',
format: 'esm',
sourcemap: true
},
plugins: [getBabelPlugin()]
};
export const minLightboxJS = {
input: 'src/js/lightbox/lightbox.js',
output: {
banner: getBanner('PhotoSwipe Lightbox'),
file: baseOutputDir + 'photoswipe-lightbox.esm.min.js',
format: 'esm'
},
plugins: [getBabelPlugin(), getMinifyPlugin()]
};
export const minCoreJS = {
input: 'src/js/photoswipe.js',
output: {
banner: getBanner('PhotoSwipe'),
file: baseOutputDir + 'photoswipe.esm.min.js',
format: 'esm',
},
plugins: [getBabelPlugin(), getMinifyPlugin()]
};
// UMD config
const umdBaseOutputDir = 'demo-docs-website/static/photoswipe/umd/';
export const umdMinLightboxJS = {
input: 'src/js/lightbox/lightbox.js',
output: {
name: 'PhotoSwipeLightbox',
banner: getBanner('PhotoSwipe Lightbox'),
file: umdBaseOutputDir + 'photoswipe-lightbox.umd.min.js',
format: 'umd'
},
plugins: [getBabelPlugin(), getMinifyPlugin()]
};
export const umdMinCoreJS = {
input: 'src/js/photoswipe.js',
output: {
name: 'PhotoSwipe',
banner: getBanner('PhotoSwipe'),
file: umdBaseOutputDir + 'photoswipe.umd.min.js',
format: 'umd',
},
plugins: [getBabelPlugin(), getMinifyPlugin()]
};
================================================
FILE: build/rollup.config.js
================================================
import {
lightboxJS,
coreJS,
minLightboxJS,
minCoreJS,
umdMinLightboxJS,
umdMinCoreJS
} from './config-builder';
export default [lightboxJS, coreJS, minLightboxJS, minCoreJS, umdMinLightboxJS, umdMinCoreJS];
================================================
FILE: build/rollup.config.watch.js
================================================
import { lightboxJS, coreJS } from './config-builder';
export default [lightboxJS, coreJS];
================================================
FILE: demo-docs-website/.gitignore
================================================
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: demo-docs-website/babel.config.js
================================================
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
================================================
FILE: demo-docs-website/docusaurus.config.js
================================================
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const path = require('path');
const lightCodeTheme = require('prism-react-renderer/themes/github');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'PhotoSwipe',
tagline: '',
url: 'https://photoswipe.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon-16x16.png',
organizationName: 'dimsemenov', // Usually your GitHub org/user name.
projectName: 'photoswipe', // Usually your repo name.
// scripts: [
// 'https://docusaurus.io/slash.js',
// {
// src:
// 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js',
// async: true,
// },
// ],
stylesheets: [
{
href: '/photoswipe/photoswipe.css',
type: 'text/css',
},
],
plugins: [
[
'@docusaurus/plugin-google-gtag',
{
trackingID: 'G-57MLE6HBT9',
},
],
],
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
path: '../docs/',
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/dimsemenov/PhotoSwipe/tree/master/docs',
breadcrumbs: false
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
colorMode: {
defaultMode: 'light',
disableSwitch: true,
respectPrefersColorScheme: false,
},
navbar: {
title: 'PhotoSwipe',
// logo: {
// alt: 'My Site Logo',
// src: 'img/logo.svg',
// },
items: [
{
type: 'doc',
docId: 'getting-started',
position: 'left',
label: 'Docs',
}
],
},
footer: {
style: 'dark',
links: [
{
},
{
title: 'Community',
items: [
{
label: 'Twitter',
href: 'https://twitter.com/photoswipe',
},
],
},
{
title: 'More',
items: [
{
label: 'Blog',
to: '/blog',
},
{
label: 'GitHub',
href: 'https://github.com/dimsemenov/photoswipe',
},
],
},
],
copyright: 'Made in Ukraine by Dmytro Semenov ',
},
prism: {
theme: lightCodeTheme,
},
}),
};
module.exports = config;
================================================
FILE: demo-docs-website/package.json
================================================
{
"name": "demo-docs-website",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"build-and-analyze": "docusaurus build --bundle-analyzer",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docsearch/react": "^3.0.0",
"@docusaurus/core": "2.0.0-beta.17",
"@docusaurus/plugin-google-gtag": "^2.0.0-beta.17",
"@docusaurus/preset-classic": "2.0.0-beta.17",
"@docusaurus/theme-live-codeblock": "^2.0.0-beta.17",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1",
"photoswipe-deep-zoom-plugin": "^1.1.1",
"photoswipe-dynamic-caption-plugin": "^1.1.1",
"prism-react-renderer": "^1.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: demo-docs-website/repo-data.json
================================================
{
"id": 1580851,
"name": "PhotoSwipe",
"full_name": "dimsemenov/PhotoSwipe",
"private": false,
"owner": {
"login": "dimsemenov",
"id": 1061115,
"avatar_url": "https://avatars.githubusercontent.com/u/1061115?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/dimsemenov",
"html_url": "https://github.com/dimsemenov",
"followers_url": "https://api.github.com/users/dimsemenov/followers",
"following_url": "https://api.github.com/users/dimsemenov/following{/other_user}",
"gists_url": "https://api.github.com/users/dimsemenov/gists{/gist_id}",
"starred_url": "https://api.github.com/users/dimsemenov/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dimsemenov/subscriptions",
"organizations_url": "https://api.github.com/users/dimsemenov/orgs",
"repos_url": "https://api.github.com/users/dimsemenov/repos",
"events_url": "https://api.github.com/users/dimsemenov/events{/privacy}",
"received_events_url": "https://api.github.com/users/dimsemenov/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/dimsemenov/PhotoSwipe",
"description": "JavaScript image gallery for mobile and desktop, modular, framework independent",
"fork": false,
"url": "https://api.github.com/repos/dimsemenov/PhotoSwipe",
"forks_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/forks",
"keys_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/teams",
"hooks_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/hooks",
"issue_events_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/issues/events{/number}",
"events_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/events",
"assignees_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/assignees{/user}",
"branches_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/branches{/branch}",
"tags_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/tags",
"blobs_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/statuses/{sha}",
"languages_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/languages",
"stargazers_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/stargazers",
"contributors_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/contributors",
"subscribers_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/subscribers",
"subscription_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/subscription",
"commits_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/contents/{+path}",
"compare_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/merges",
"archive_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/downloads",
"issues_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/issues{/number}",
"pulls_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/pulls{/number}",
"milestones_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/milestones{/number}",
"notifications_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/labels{/name}",
"releases_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/releases{/id}",
"deployments_url": "https://api.github.com/repos/dimsemenov/PhotoSwipe/deployments",
"created_at": "2011-04-07T05:46:29Z",
"updated_at": "2022-03-27T01:05:17Z",
"pushed_at": "2022-03-26T14:18:32Z",
"git_url": "git://github.com/dimsemenov/PhotoSwipe.git",
"ssh_url": "git@github.com:dimsemenov/PhotoSwipe.git",
"clone_url": "https://github.com/dimsemenov/PhotoSwipe.git",
"svn_url": "https://github.com/dimsemenov/PhotoSwipe",
"homepage": "http://photoswipe.com",
"size": 31451,
"stargazers_count": 21653,
"watchers_count": 21653,
"language": "JavaScript",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 3271,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 564,
"license": {
"key": "mit",
"name": "MIT License",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit",
"node_id": "MDc6TGljZW5zZTEz"
},
"allow_forking": true,
"is_template": false,
"topics": [
"gallery",
"image",
"javascript",
"lightbox"
],
"visibility": "public",
"forks": 3271,
"open_issues": 564,
"watchers": 21655,
"default_branch": "master",
"temp_clone_token": null,
"network_count": 3271,
"subscribers_count": 556
}
================================================
FILE: demo-docs-website/sidebars.js
================================================
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
// tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
mainSidebar: [
{
type: 'doc',
id: 'getting-started',
},
{
type: 'category',
label: 'Examples',
items: [
{
type: 'doc',
id: 'styling',
},
{
type: 'doc',
id: 'opening-or-closing-transition',
},
{
type: 'doc',
id: 'adding-ui-elements',
},
{
type: 'doc',
id: 'adjusting-zoom-level',
},
{
type: 'doc',
id: 'caption',
},
{
type: 'doc',
id: 'click-and-tap-actions',
},
{
type: 'doc',
id: 'custom-content',
},
{
type: 'doc',
id: 'data-sources',
},
{
type: 'doc',
id: 'native-fullscreen-on-open',
},
],
},
{
type: 'category',
label: 'API',
items: [
{
type: 'doc',
id: 'options',
},
{
type: 'doc',
id: 'events',
},
{
type: 'doc',
id: 'filters',
},
{
type: 'doc',
id: 'methods',
},
],
},
{
type: 'category',
label: 'Frameworks',
items: [
{
type: 'doc',
id: 'react-image-gallery',
},
{
type: 'doc',
id: 'vue-image-gallery',
},
{
type: 'doc',
id: 'svelte-image-gallery',
},
],
},
]
// But you can create a sidebar manually
/*
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial',
items: ['hello'],
},
],
*/
};
module.exports = sidebars;
================================================
FILE: demo-docs-website/src/components/HomepageFeatures/index.js
================================================
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
const FeatureList = [
{
title: 'Easy to Use',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
Docusaurus was designed from the ground up to be easily installed and
used to get your website up and running quickly.
>
),
},
{
title: 'Focus on What Matters',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
Docusaurus lets you focus on your docs, and we'll do the chores. Go
ahead and move your docs into the docs directory.
>
),
},
{
title: 'Powered by React',
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can
be extended while reusing the same header and footer.
>
),
},
];
function Feature({Svg, title, description}) {
return (
);
}
export default function HomepageFeatures() {
return (
{FeatureList.map((props, idx) => (
))}
);
}
================================================
FILE: demo-docs-website/src/components/HomepageFeatures/styles.module.css
================================================
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/demo-images.js
================================================
export const pswpDemoImages = [{"sizes":[{"height":2500,"width":1875,"type":"jpg","src":"1/img-2500.jpg"},{"height":2000,"width":1500,"type":"jpg","src":"1/img-2000.jpg"},{"height":1500,"width":1125,"type":"jpg","src":"1/img-1500.jpg"},{"height":1000,"width":750,"type":"jpg","src":"1/img-1000.jpg"},{"height":700,"width":525,"type":"jpg","src":"1/img-700.jpg"},{"height":400,"width":300,"type":"jpg","src":"1/img-400.jpg"},{"height":200,"width":150,"type":"jpg","src":"1/img-200.jpg"}]},{"sizes":[{"height":2500,"width":1669,"type":"jpg","src":"2/img-2500.jpg"},{"height":2000,"width":1335,"type":"jpg","src":"2/img-2000.jpg"},{"height":1500,"width":1001,"type":"jpg","src":"2/img-1500.jpg"},{"height":1000,"width":668,"type":"jpg","src":"2/img-1000.jpg"},{"height":700,"width":467,"type":"jpg","src":"2/img-700.jpg"},{"height":400,"width":267,"type":"jpg","src":"2/img-400.jpg"},{"height":200,"width":134,"type":"jpg","src":"2/img-200.jpg"}]},{"sizes":[{"height":1666,"width":2500,"type":"jpg","src":"3/img-2500.jpg"},{"height":1333,"width":2000,"type":"jpg","src":"3/img-2000.jpg"},{"height":1000,"width":1500,"type":"jpg","src":"3/img-1500.jpg"},{"height":667,"width":1000,"type":"jpg","src":"3/img-1000.jpg"},{"height":467,"width":700,"type":"jpg","src":"3/img-700.jpg"},{"height":267,"width":400,"type":"jpg","src":"3/img-400.jpg"},{"height":133,"width":200,"type":"jpg","src":"3/img-200.jpg"}]},{"sizes":[{"height":1667,"width":2500,"type":"jpg","src":"4/img-2500.jpg"},{"height":1333,"width":2000,"type":"jpg","src":"4/img-2000.jpg"},{"height":1000,"width":1500,"type":"jpg","src":"4/img-1500.jpg"},{"height":667,"width":1000,"type":"jpg","src":"4/img-1000.jpg"},{"height":467,"width":700,"type":"jpg","src":"4/img-700.jpg"},{"height":267,"width":400,"type":"jpg","src":"4/img-400.jpg"},{"height":133,"width":200,"type":"jpg","src":"4/img-200.jpg"}]},{"sizes":[{"height":1668,"width":2500,"type":"jpg","src":"5/img-2500.jpg"},{"height":1334,"width":2000,"type":"jpg","src":"5/img-2000.jpg"},{"height":1001,"width":1500,"type":"jpg","src":"5/img-1500.jpg"},{"height":667,"width":1000,"type":"jpg","src":"5/img-1000.jpg"},{"height":467,"width":700,"type":"jpg","src":"5/img-700.jpg"},{"height":267,"width":400,"type":"jpg","src":"5/img-400.jpg"},{"height":133,"width":200,"type":"jpg","src":"5/img-200.jpg"}]},{"sizes":[{"height":1667,"width":2500,"type":"jpg","src":"6/img-2500.jpg"},{"height":1333,"width":2000,"type":"jpg","src":"6/img-2000.jpg"},{"height":1000,"width":1500,"type":"jpg","src":"6/img-1500.jpg"},{"height":667,"width":1000,"type":"jpg","src":"6/img-1000.jpg"},{"height":467,"width":700,"type":"jpg","src":"6/img-700.jpg"},{"height":267,"width":400,"type":"jpg","src":"6/img-400.jpg"},{"height":133,"width":200,"type":"jpg","src":"6/img-200.jpg"}]},{"sizes":[{"height":2500,"width":1875,"type":"jpg","src":"7/img-2500.jpg"},{"height":2000,"width":1500,"type":"jpg","src":"7/img-2000.jpg"},{"height":1500,"width":1125,"type":"jpg","src":"7/img-1500.jpg"},{"height":1000,"width":750,"type":"jpg","src":"7/img-1000.jpg"},{"height":700,"width":525,"type":"jpg","src":"7/img-700.jpg"},{"height":400,"width":300,"type":"jpg","src":"7/img-400.jpg"},{"height":200,"width":150,"type":"jpg","src":"7/img-200.jpg"}]},{"sizes":[{"height":1667,"width":2500,"type":"jpg","src":"8/img-2500.jpg"},{"height":1334,"width":2000,"type":"jpg","src":"8/img-2000.jpg"},{"height":1000,"width":1500,"type":"jpg","src":"8/img-1500.jpg"},{"height":667,"width":1000,"type":"jpg","src":"8/img-1000.jpg"},{"height":467,"width":700,"type":"jpg","src":"8/img-700.jpg"},{"height":267,"width":400,"type":"jpg","src":"8/img-400.jpg"},{"height":133,"width":200,"type":"jpg","src":"8/img-200.jpg"}]},{"sizes":[{"height":1667,"width":2500,"type":"jpg","src":"9/img-2500.jpg"},{"height":1333,"width":2000,"type":"jpg","src":"9/img-2000.jpg"},{"height":1000,"width":1500,"type":"jpg","src":"9/img-1500.jpg"},{"height":667,"width":1000,"type":"jpg","src":"9/img-1000.jpg"},{"height":467,"width":700,"type":"jpg","src":"9/img-700.jpg"},{"height":267,"width":400,"type":"jpg","src":"9/img-400.jpg"},{"height":133,"width":200,"type":"jpg","src":"9/img-200.jpg"}]},{"sizes":[{"height":2500,"width":2000,"type":"jpg","src":"10/img-2500.jpg"},{"height":2000,"width":1600,"type":"jpg","src":"10/img-2000.jpg"},{"height":1500,"width":1200,"type":"jpg","src":"10/img-1500.jpg"},{"height":1000,"width":800,"type":"jpg","src":"10/img-1000.jpg"},{"height":700,"width":560,"type":"jpg","src":"10/img-700.jpg"},{"height":400,"width":320,"type":"jpg","src":"10/img-400.jpg"},{"height":200,"width":160,"type":"jpg","src":"10/img-200.jpg"}]},{"sizes":[{"height":2500,"width":2000,"type":"jpg","src":"11/img-2500.jpg"},{"height":2000,"width":1600,"type":"jpg","src":"11/img-2000.jpg"},{"height":1500,"width":1200,"type":"jpg","src":"11/img-1500.jpg"},{"height":1000,"width":800,"type":"jpg","src":"11/img-1000.jpg"},{"height":700,"width":560,"type":"jpg","src":"11/img-700.jpg"},{"height":400,"width":320,"type":"jpg","src":"11/img-400.jpg"},{"height":200,"width":160,"type":"jpg","src":"11/img-200.jpg"}]}];
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/basic--badges.js
================================================
export function basicBadgesTemplate(props) {
let out = ``;
for (let i = 0; i < (props.numItems || 4); i++) {
out += `
Badge ${i + 1}
`;
}
out += '
';
return out;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/basic--cropped.js
================================================
export function basicCroppedTemplate(props) {
let out = ``;
for (let i = 0; i < (props.numItems || 3); i++) {
out += `
`;
}
out += '
';
return out;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/basic.js
================================================
export function basicTemplate(props) {
let out = `\n`;
for (let i = 0; i < (props.numItems || 3); i++) {
out += `
\n`;
}
out += '
';
return out;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/caption.js
================================================
export function captionTemplate(props) {
return `
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Test link →
`;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/content-types.js
================================================
export function contentTypesTemplate(props) {
return ``;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/custom-html-markup-data-source.js
================================================
export function customHTMLDataSourceTemplate(props) {
let out = `\n`;
for (let i = 0; i < (props.numItems || 3); i++) {
out += `
Test ${i + 1} \n`;
}
out += '
';
return out;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/getting-started.js
================================================
export function gettingStartedTemplate(props) {
return ``;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/gallery-templates/srcset-test.js
================================================
export function srcsetTemplate(props) {
const items = [{ thumbSrc: '1-300x200.png', largeSrc: '1-1500x1000.png', width: 1500, height: 1000, srcset: '%1-600x400.png 600w, %1-1200x800.png 1200w, %1-1500x1000.png 1500w' }, { thumbSrc: '2-300x200.png', largeSrc: '2-1500x1000.png', width: 1500, height: 1000, srcset: '%2-600x400.png 600w, %2-1200x800.png 1200w, %2-1500x1000.png 1500w' }, { thumbSrc: '3-300x300.png', largeSrc: '3-1500x1500.png', width: 1500, height: 1500, srcset: '%3-600x600.png 600w, %3-1200x1200.png 1200w, %3-1500x1500.png 1500w' }, { thumbSrc: '4-200x300.png', largeSrc: '4-1000x1500.png', width: 1000, height: 1500, srcset: '%4-400x600.png 400w, %4-800x1200.png 800w, %4-1000x1500.png 1000w' }];
let out = `\n`;
const baseURL = props.cdnURL + 'srcset-test/';
items.forEach((item) => {
const srcset = item.srcset.replace(/%/g, baseURL);
out += `
\n`;
});
out += '
';
return out;
}
================================================
FILE: demo-docs-website/src/components/PswpCodePreview/index.js
================================================
import React from 'react';
import CodeBlock from '../../theme/CodeBlock';
import { pswpDemoImages } from './demo-images';
import { basicTemplate } from './gallery-templates/basic';
let uidCounter = 1;
/**
* Get the smallest size (but not smaller than minSize)
*
* @param {Array} sizes
* @param {Integer} minSize
*/
const getSmallestImageSize = (sizes, minSize) => {
sizes = sizes.filter(size => size.width >= minSize);
return sizes.reduce((a, b) => (a.width < b.width ? a : b));
};
/**
* Generates gallery for demo
*
* Supported params:
* displayHTML: false|true (whether HTML code block should be visible)
* numItems: Integer (number of images to display)
* galleryID: String (ID attribute)
*/
function generateGallery(galleryData) {
const thumbnailSize = 70;
const cdnURL = 'https://cdn.photoswipe.com/photoswipe-demo-images/photos/';
const templateProps = {
numItems: Math.min(parseInt(galleryData.numItems, 10) || 3, 11),
id: galleryData.galleryID || (uidCounter++),
img: [],
cdnURL
};
const { orientation } = galleryData;
let demoImages = [ ...pswpDemoImages ];
if (orientation === 'landscape') {
demoImages = demoImages.filter((imageData) => {
return imageData.sizes[0].width >= imageData.sizes[0].height;
});
} else if (orientation === 'portrait') {
demoImages = demoImages.filter((imageData) => {
return imageData.sizes[0].width <= imageData.sizes[0].height;
});
}
demoImages.forEach((imageData, index) => {
const largest = imageData.sizes[0];
const thumbnail = getSmallestImageSize(imageData.sizes, thumbnailSize);
templateProps.img.push({
index,
width: largest.width,
height: largest.height,
src: cdnURL + largest.src,
thumbSrc: cdnURL + thumbnail.src
});
});
if (galleryData.templateFn) {
return galleryData.templateFn(templateProps);
}
return basicTemplate(templateProps);
}
export default function PswpCodePreview(props) {
return (
{ props.children }
{ props.galleryID && {generateGallery(props)} }
);
}
================================================
FILE: demo-docs-website/src/css/custom.css
================================================
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--ifm-toc-border-color: rgba(0, 0, 0, 0.065);
--ifm-code-background: rgb(250, 250, 250);
--ifm-link-color: #3169B3;
--ifm-link-hover-color: #C00;
--ifm-link-decoration: underline;
--ifm-link-hover-decoration: underline;
--ifm-navbar-link-hover-color: var(--ifm-link-hover-color);
--ifm-footer-background-color: none;
--ifm-footer-color: auto;
--ifm-footer-padding-horizontal: calc(var(--ifm-spacing-horizontal) * 2);
--ifm-footer-padding-vertical: calc(var(--ifm-spacing-vertical) * 2);
--pswp-docs-main-content-width: 1100px;
--ifm-heading-font-weight: var(--ifm-font-weight-semibold);
}
a {
transition: none;
}
a:hover {
color: var(--ifm-link-hover-color);
/* autoprefixer: ignore next */
text-decoration: var(--ifm-link-hover-decoration);
}
pre code,
.pswp-example__preview {
background: var(--ifm-code-background);
}
code {
border: 0;
}
.docusaurus-highlight-code-line {
background-color: rgba(0, 0, 0, 0.1);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}
.ukraine-flag {
width: 21px;
height: 14px;
position: relative;
background: #ffcc00;
top: 2px;
display: inline-block;
margin-left: 3px;
margin-right: 2px;
}
.ukraine-flag:before {
content:'';
position: absolute;
width: 21px;
height: 7px;
left:0;
top:0;
background: #0066cc;
}
a.hash-link {
text-decoration: none;
transition: none;
padding: 0.5rem 0.7rem;
}
.theme-code-block--hidden {
display: none;
}
.navbar,
.footer,
.main-wrapper {
width: 100%;
max-width: var(--pswp-docs-main-content-width);
margin: 0 auto;
}
.navbar {
z-index: 20;
box-shadow: none;
border-bottom: 1px solid var(--ifm-toc-border-color);
}
.footer {
margin-top: 60px;
border-top: 1px solid var(--ifm-toc-border-color);
}
.docs-wrapper {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
}
.pswp-docs__main-column > article {
max-width: 700px;
}
.pswp-docs__sidebar-menu {
width: 30%;
width: 250px;
}
/*.pswp-docs__main-column {
width: 70%;
} */
.theme-doc-markdown {
/* max-width: 670px; */
}
@import './sidebar-menu.css';
@import './docs-page.css';
@import './example-code-block.css';
@import './scrollbar.css';
@import './header.css';
@import './home.css';
================================================
FILE: demo-docs-website/src/css/docs-page.css
================================================
/* .theme-doc-footer,
.pagination-nav,
.theme-doc-markdown > * {
max-width: 650px;
} */
.theme-doc-markdown iframe {
width: 100%;
height: 450px;
}
.theme-doc-footer {
border-top: 1px solid var(--ifm-toc-border-color);
padding: var(--ifm-global-spacing) 0;
}
nav.pagination-nav {
border-top: 1px solid var(--ifm-toc-border-color);
margin-top: 0;
}
a.pagination-nav__link {
border: 0;
text-decoration: none;
padding: var(--ifm-global-spacing) 0;
}
a.pagination-nav__link .pagination-nav__sublabel {
text-decoration: none;
}
.pagination-nav__label {
font-size: var(--ifm-h3-font-size);
}
.pagination-nav__label::after,
.pagination-nav__label::before {
content: none !important;
}
================================================
FILE: demo-docs-website/src/css/example-code-block.css
================================================
.pswp-example {
width: 100%;
position: relative;
margin-bottom: 1rem;
}
span.docusaurus-highlight-code-line {
background: rgba(255, 217, 109, .3);
}
.pswp-example .theme-code-block {
width: calc(100% - 204px);
margin-bottom: 4px;
}
.pswp-example__preview {
width: 200px;
position: absolute;
right: 0;
top: 0;
height: 100%;
overflow-y: auto;
margin-left: 4px;
}
.pswp-example__preview {
padding: var(--ifm-pre-padding);
}
.pswp-example .pswp-gallery {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
position: relative;
width: 155px;
}
.pswp-example .pswp-gallery img {
display: block;
}
.pswp-gallery {
display: flex;
}
.pswp-gallery > * {
display: block;
position: relative;
margin-bottom: 4px;
margin-right: 4px;
width: 70px;
}
.pswp-example .pswp-gallery > *:nth-child(2n) {
margin-right: 0;
}
.pswp-example .pswp-gallery--single-column > * {
width: 120px;
margin-bottom: 18px;
font-size: 14px;
}
button.pswp-example__hide-html-btn {
position: absolute;
cursor: pointer;
z-index: 10;
font-size: 12px;
right: 0;
bottom: 0;
color: var(--ifm-link-color);
background: none;
border: 0;
padding: 2px 5px;
background: var(--ifm-pre-background);
}
button.pswp-example__hide-html-btn:hover {
color: var(--ifm-link-hover-color);
}
[data-cropped] img {
width: 100%;
height: 70px;
object-fit: cover;
}
@media only screen and (max-width: 600px) {
.pswp-example .theme-code-block {
width: 100%;
}
.pswp-example__preview {
width: 100%;
height: auto;
position: relative;
}
.pswp-example__preview .pswp-gallery {
max-width: 150px;
}
}
.pswp-example__code--hidden {
display: none;
}
input[type="checkbox"].hidden-cb {
width: 1px;
height: 1px;
opacity: 0.01;
overflow: hidden;
position: absolute;
right: 0;
bottom: 0;
}
label.pswp-example__toggle {
position: absolute;
right: 0;
bottom: -25px;
padding: 0 0;
border-radius: 0;
font-size: 14px;
color: #1B57A5;
cursor: pointer;
z-index: 20;
}
label.pswp-example__toggle:hover {
color: #c00;
}
input[type="checkbox"]:focus + label {
outline: 1px dotted #212121;
outline: 5px auto -webkit-focus-ring-color;
}
input[type="checkbox"]:checked {
display: none;
}
input[type="checkbox"]:checked + label {
display: none;
}
input[type="checkbox"]:checked + label + .pswp-example__code--hidden {
display: block !important;
}
================================================
FILE: demo-docs-website/src/css/header.css
================================================
a.navbar__brand {
text-decoration: none;
font-size: 20px;
line-height: 1;
white-space: nowrap;
transform: translate(0, -2px);
}
a.navbar__link {
color: var(--ifm-link-color);
text-decoration: none;
}
a.navbar__link--active {
color: var(--ifm-font-color-base);
}
.navbar__link svg {
display: none;
}
a.pswp-docs__github-link {
display: flex;
flex-direction: row;
align-items: flex-start;
text-decoration: none;
font-weight: var(--ifm-font-weight-semibold);
}
a.pswp-docs__github-link:hover {
text-decoration: none;
}
.pswp-docs__github-link svg {
transform: translate(0, 2px);
margin-right: 3px;
}
.pswp-docs__github-link-left {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
padding: 2px 8px;
border: 1px solid #D5D5D5;
}
.pswp-docs__github-link-right {
background: #fff;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
padding: 2px 8px;
border: 1px solid #D5D5D5;
border-left: 0;
color: #222;
}
.navbar__brand span {
font-size: 12px;
margin-left: 4px;
margin-top: -12px;
line-height: 1;
}
button.DocSearch {
--docsearch-searchbox-background: #e9e9e9;
--docsearch-primary-color: var(--ifm-link-color);
--docsearch-text-color: #222;
--docsearch-muted-color: #222;
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
--docsearch-highlight-color: var(--ifm-link-color);
height: 35px;
}
button .DocSearch-Button-Placeholder {
font-size: 16px;
font-weight: 400;
}
button .DocSearch-Button-Keys {
display: none !important;
}
================================================
FILE: demo-docs-website/src/css/home.css
================================================
.pswp-docs__home .navbar__brand {
display: none;
}
.pswp-docs__header-title-text {
margin: 60px 0px 50px;
font-size: 22px;
text-align: center;
}
.pswp-docs__whats-new {
margin-top: 50px;
}
.pswp-docs__whats-new h4 {
margin: 50px auto 12px;
font-size: 24px;
}
.pswp-docs__home-block-main-col > * {
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.pswp-docs__home-block-full-width-col {
max-width: 1068px;
}
.pswp-docs__header-title-text a svg {
display: inline-block;
width: 16px;
fill: var(--ifm-link-color);
transform: translate(0px, 3px);
margin-left: 0.2em;
}
.pswp-docs__header-title-text a:hover svg {
fill: var(--ifm-link-hover-color);
}
.pswp-docs__header-title-text a {
font-weight: bold;
text-decoration: none;
}
.pswp-docs__header-title-text h1 {
font-weight: bold;
font-size: 64px;
line-height: 1;
margin-bottom: 14px;
}
.pswp-docs__header-title-text h1 span {
position: absolute;
vertical-align: super;
font-size: 16px;
text-decoration: none;
}
.pswp-docs__header-title-text p {
margin-bottom: 0;
}
.pswp-docs__home-gallery {
position: relative;
display: grid;
width: 100%;
grid-template-columns: 1fr 0.5fr 0.5fr;
grid-gap: 10px;
}
.pswp-docs__home-gallery-credit {
position: absolute;
right: 0;
bottom: -21px;
font-size: 12px;
}
.pswp-docs__home-gallery .pswp-docs__home-gallery-item {
position: relative;
}
figure.pswp-docs__home-gallery-item {
display: block;
margin: 0;
padding: 0;
}
.pswp-docs__home-gallery-item figcaption {
display: none;
}
.pswp-docs__home-gallery .pswp-docs__home-gallery-item img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.pswp-docs__home-gallery .pswp-docs__home-gallery-item:first-child {
grid-row: span 2;
}
.pswp-docs__home-gallery .pswp-docs__home-gallery-item a {
display: block;
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
padding-bottom: 100%;
}
.pswp-docs__home-gallery-example {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
width: 100%;
}
.pswp-docs__home-gallery-example a {
position: relative;
margin: 0 4px 4px 0;
line-height: 0;
display: block;
}
.pswp-docs__home-gallery-example img {
position: absolute;
left: 0;
top: 0;
width: 100%;
max-width: none;
}
.pswp-docs__home-block h2 {
font-weight: bold;
font-size: 40px;
line-height: 1.1;
margin: 70px auto 36px;
}
.pswp__dynamic-caption {
font-size: 14px;
line-height: 1.5;
}
#gallery--deep-zoom {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
}
#gallery--deep-zoom figure {
display: block;
margin: 0;
padding: 0;
margin-right: 4px;
min-width: 100px;
max-width: 180px;
}
@media (max-width: 900px) {
.pswp__dynamic-caption.pswp__dynamic-caption--aside {
margin-top: 0;
}
}
@media (max-width: 700px) {
#gallery--deep-zoom {
max-width: 296px;
}
}
#gallery--deep-zoom figure:last-child {
margin-right: 0;
}
#gallery--deep-zoom figure > a {
display: block;
position: relative;
}
#gallery--deep-zoom img {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
figcaption.caption {
font-size: 12px;
margin-top: 6px;
}
@media (max-width: 650px) {
.pswp-docs__home-gallery {
grid-gap: 5px;
}
.pswp-docs__header-title-text h1 {
font-size: 36px;
}
.pswp-docs__header-title-text {
margin: 40px 0px 30px;
font-size: 18px;
text-align: center;
}
.pswp-docs__home-block h2 {
font-size: 26px;
margin: 40px auto 12px;
}
.pswp-docs__whats-new h4 {
margin: 24px auto 12px;
font-size: var(--ifm-font-size-base);
}
#gallery--deep-zoom {
max-width: 296px;
}
}
================================================
FILE: demo-docs-website/src/css/scrollbar.css
================================================
/* style scrollbar */
.docs-styled-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.docs-styled-scrollbar::-webkit-scrollbar-button {
width: 0px;
height: 0px;
}
.docs-styled-scrollbar::-webkit-scrollbar-thumb {
background: #C2C2C2;
border: 0;
border-radius: 0;
}
.docs-styled-scrollbar::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
.docs-styled-scrollbar::-webkit-scrollbar-thumb:active {
background: #999;
}
.docs-styled-scrollbar::-webkit-scrollbar-track {
background: none;
border: 0;
border-radius: 0;
background: #eee;
}
.docs-styled-scrollbar::-webkit-scrollbar-track:hover {
background: #ddd;
}
.docs-styled-scrollbar::-webkit-scrollbar-corner {
background: none;
}
================================================
FILE: demo-docs-website/src/css/sidebar-menu.css
================================================
aside.theme-doc-sidebar-container {
border: 0;
margin-top: 0;
}
aside.theme-doc-sidebar-container {
position: sticky;
margin-top: 30px;
top: 30px;
}
.pswp-docs__sidebar-menu {
position: relative;
border-right: 1px solid var(--ifm-toc-border-color);
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0 var(--ifm-navbar-padding-horizontal)
}
.pswp-docs__sidebar-menu--mobile {
position: relative;
margin-top: 0;
top: auto;
}
.pswp-docs__sidebar-menu-item {
text-decoration: none;
font-size: 16px;
line-height: 22px;
padding: 4px 0;
}
.pswp-docs__sidebar-menu-item--category {
color: #393939;
opacity: 0.5;
margin-top: 16px;
}
.pswp-docs__sidebar-menu-item--category:first-child {
margin-top: 0;;
}
a.pswp-docs__sidebar-menu-item--active {
color: var(--ifm-font-color-base);
}
.docSidebarContainer {
display: none;
}
@media (max-width: 996px) {
.pswp-docs__sidebar-menu {
border-right: 0;
}
.theme-doc-sidebar-container > .pswp-docs__sidebar-menu {
display: none;
}
}
@media (min-width: 997px) {
}
================================================
FILE: demo-docs-website/src/pages/_index-deep-zoom-demo.js
================================================
import React, { useEffect } from 'react';
import Lightbox from '../../static/photoswipe/photoswipe-lightbox.esm.js';
//import PhotoSwipeDeepZoom from 'photoswipe-deep-zoom-plugin';
const galleryHTML = `
Cambriae Typus
Humphrey Llwyd
5,832px x 4,409px
A sergeant of the Light Horse
George Lambert
4,578px x 5,736px
The Starry Night
Vincent van Gogh
30,000px x 23,756px
Magnolia and Erect Rock
Chen Hongshou
1,820px x 1,948px (not tiled)
`;
export default function DeepZoomGalleryDemo() {
useEffect(() => {
let deepZoomPlugin;
let lightbox = new Lightbox({
gallery: '#gallery--deep-zoom',
children: 'figure > a',
pswpModule: () => import('../../static/photoswipe/photoswipe.esm.js'),
// dynamically load deep zoom plugin
openPromise: () => {
// make sure it's initialized only once per lightbox
if (!deepZoomPlugin) {
return import('photoswipe-deep-zoom-plugin').then((deepZoomPluginModule) => {
deepZoomPlugin = new deepZoomPluginModule.default(lightbox, {
// deep zoom plugin options
});
})
}
},
// Recommended PhotoSwipe options for this plugin
allowPanToNext: false, // prevent swiping to the next slide when image is zoomed
allowMouseDrag: true, // display dragging cursor at max zoom level
wheelToZoom: true, // enable wheel-based zoom
zoom: false // disable default zoom button
});
lightbox.init();
return function cleanup() {
if (lightbox) {
lightbox.destroy();
lightbox = null;
}
if (deepZoomPlugin) {
deepZoomPlugin.destroy();
deepZoomPlugin = null;
}
};
}, []);
return (
)
}
================================================
FILE: demo-docs-website/src/pages/_index-gallery-header.js
================================================
import React, { useEffect } from 'react';
import Lightbox from '../../static/photoswipe/photoswipe-lightbox.esm.js';
import PhotoSwipe from '../../static/photoswipe/photoswipe.esm.js'
import PhotoSwipeDynamicCaption from 'photoswipe-dynamic-caption-plugin';
import 'photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css';
const baseCdnUrl = 'https://cdn.photoswipe.com/photoswipe-demo-images/photos/home-demo/';
const imagesData = [
{
caption: `Test Caption
Dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.`,
width: 2500,
height: 3125,
src: `${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_2500.jpg`,
thumbSrc: `${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_664.jpg`,
thumbSrcset: `${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/thumb.jpg 524w,
${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_664.jpg 664w,
${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_932.jpg 932w`,
srcset: `${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_664.jpg 664w,
${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_932.jpg 932w,
${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_1355.jpg 1355w,
${baseCdnUrl}luca-bravo-ny6qxqv_m04-unsplash_snrzpf/luca-bravo-ny6qxqv_m04-unsplash_snrzpf_c_scale,w_2500.jpg 2500w`
},
{
width: 2500,
height: 1667,
caption: `Another Test Caption
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.`,
src: `${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_2500.jpg`,
thumbSrc: `${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_300.jpg`,
thumbSrcset: `${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/thumb.jpg 393w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_612.jpg 612w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_826.jpg 826w`,
srcset: `${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_612.jpg 612w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_826.jpg 826w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_1115.jpg 1115w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_1400.jpg 1400w,
${baseCdnUrl}luca-bravo-O453M2Liufs-unsplash_qqt53u/luca-bravo-O453M2Liufs-unsplash_qqt53u_c_scale,w_2500.jpg 2500w`
},
{
width: 2500,
height: 1667,
caption: `Long caption
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore.Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?`,
src: `${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_2500.jpg`,
thumbSrc: `${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_300.jpg`,
thumbSrcset: `${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/thumb.jpg 393w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_639.jpg 639w`,
srcset: `${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_639.jpg 639w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_1176.jpg 1176w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_1200.jpg 1200w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_1385.jpg 1385w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_1834.jpg 1834w,
${baseCdnUrl}luca-bravo-VowIFDxogG4-unsplash_ibrktu/luca-bravo-VowIFDxogG4-unsplash_ibrktu_c_scale,w_2500.jpg 2500w`
},
{
width: 2500,
height: 1667,
caption: `Lorem Ipsum Unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.`,
src: `${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_2500.jpg`,
thumbSrc: `${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_300.jpg`,
thumbSrcset: `${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/thumb.jpg 393w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_627.jpg 627w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_861.jpg 861w`,
srcset: `${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_627.jpg 627w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_861.jpg 861w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_1057.jpg 1057w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_1441.jpg 1441w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_1881.jpg 1881w,
${baseCdnUrl}luca-bravo-zAjdgNXsMeg-unsplash_q6zdih/luca-bravo-zAjdgNXsMeg-unsplash_q6zdih_c_scale,w_2500.jpg 2500w`
},
{
width: 2500,
height: 1667,
caption: `Test Caption
Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? `,
src: `${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_2500.jpg`,
thumbSrc: `${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_300.jpg`,
thumbSrcset: `${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/thumb.jpg 393w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_649.jpg 649w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_996.jpg 996w`,
srcset: `${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_300.jpg 300w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_649.jpg 649w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_996.jpg 996w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_1188.jpg 1188w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_1303.jpg 1303w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_1840.jpg 1840w,
${baseCdnUrl}luca-bravo-A-fubu9QJxE-unsplash_jxy5p8/luca-bravo-A-fubu9QJxE-unsplash_jxy5p8_c_scale,w_2500.jpg 2500w`
},
];
function GalleryItem(props) {
const attributes = {
style: {
paddingBottom: props.cropped ? 1 / props.aspectRatio * 100 + '%' : props.height / props.width * 100 + '%'
},
href: props.src,
'data-pswp-srcset': props.srcset,
'data-pswp-width': props.width,
'data-pswp-height': props.height,
target: '_blank',
...( props.cropped && { 'data-cropped': true } )
};
const figureAttributes = {
style: {},
className: 'pswp-docs__home-gallery-item'
};
let sizes = props.sizes;
if (props.justifiedRow) {
figureAttributes.style.flex = props.width / props.height * 100;
const widthRatio = ( props.width / props.height) / props.aspectRatioSumm;
sizes = `(min-width: 1124px) ${Math.ceil(600 * widthRatio)}px, ${Math.ceil(100 * widthRatio)}vw`;
}
return (
{ props.caption && }
);
}
export function GalleryExampleOpenZoomed(props) {
useEffect(() => {
let lightbox = new Lightbox({
gallery: '#gallery--open-zooomed figure > a',
initialZoomLevel: 'fill',
secondaryZoomLevel: 'fill',
maxZoomLevel: 3,
pswpModule: PhotoSwipe
});
lightbox.init();
return function cleanup() {
lightbox.destroy();
lightbox = null;
};
}, []);
return (
)
}
export function GalleryExampleDynamicCaptionPlugin(props) {
useEffect(() => {
const smallScreenPadding = {
top: 0, bottom: 0, left: 0, right: 0
};
const largeScreenPadding = {
top: 20, bottom: 20, left: 50, right: 50
};
let lightbox = new Lightbox({
gallery: '#gallery--dynamic-caption',
children: '.pswp-docs__home-gallery-item > a',
paddingFn: (viewportSize) => {
return viewportSize.x < 700 ? smallScreenPadding : largeScreenPadding
},
pswpModule: () => import('../../static/photoswipe/photoswipe.esm.js')
});
lightbox.init();
let captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {
type: 'auto',
captionContent: 'figcaption'
});
return function cleanup() {
lightbox.destroy();
lightbox = null;
captionPlugin = null;
};
}, []);
return (
)
}
export function GalleryExample(props) {
const aspectRatioSumm = imagesData.reduce((summ, image, index) => {
if (props.items && !props.items.includes(index)) {
return summ;
}
return summ + image.width / image.height;
}, 0);
const galleryItems = imagesData.map((image, index) => {
if (props.items && !props.items.includes(index)) {
return;
}
return
});
return (
{galleryItems}
);
}
export default function GalleryHeader() {
useEffect(() => {
let lightbox = new Lightbox({
gallery: '#gallery--header-home',
children: '.pswp-docs__home-gallery-item > a',
pswpModule: () => import('../../static/photoswipe/photoswipe.esm.js')
});
lightbox.init();
return function cleanup() {
lightbox.destroy();
lightbox = null;
};
}, []);
return (
);
}
================================================
FILE: demo-docs-website/src/pages/index.js
================================================
import React, { useEffect } from 'react';
import Layout from '@theme/Layout';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import GalleryHeader, { GalleryExampleDynamicCaptionPlugin, GalleryExampleOpenZoomed } from './_index-gallery-header';
import CodeBlock from '../theme/CodeBlock';
import packageInfo from '../../../package.json';
import DeepZoomGalleryDemo from './_index-deep-zoom-demo';
import Head from '@docusaurus/Head';
function HomepageHeader() {
return (
PhotoSwipe: Responsive JavaScript Image Gallery
);
}
function WhatsNew() {
const initCodeExample = ``;
return (
What’s new in v5
Code quality and rewrite in ES6
The script is now distributed as an ES module and does not require a build step to use. The documentation is also updated and now includes more examples.
Simpler initialization and dynamic import support
PhotoSwipe now supports dynamic import and does not block page rendering.
{initCodeExample}
Animation and gesture engine update
Improved performance of most animations, touch gestures should feel more fluid now.
The initial opening or closing transition can be run from a CSS-cropped thumbnail , as you can see on the top of this page.
Single CSS file and no external assets
Using CSS variables, default icons are dynamically generated and tiny.Styling guide →
Built-in responsive images support
PhotoSwipe also dynamically loads larger images as a user zooms via srcset.
Open images in a zoomed state
It's now much easier to control zoom level, refer to the Adjusting Zoom Level section of docs for more info. The example below opens images in a zoomed state and individually.
Removed features from the core
Some built-in features were removed in v5, either because they are using outdated technology or just rarely used. Some of them are or will be replaced by a plugin. These include:
History API (#hash-based navigation is outdated)
Social sharing (unreliable URL, lack of Opengraph support)
Fullscreen button (rarely used, double fullscreen). Related example in docs →
Caption (accessibility problems). Refer to the caption section of docs .
Inline gallery support (v5 is mainly designed to be used as a dialog).
Plugins
A plugin that dynamically positions the caption below or aside
depending on the available free space.
Tile-based image viewer that allows displaying of extremely large images.
Unlike conventional tile-viewers (such as Leaflet or OpenSeaDragon)
it displays tiles only after the user zooms beyond the primary image,
and keeps all default PhotoSwipe navigation between slides.
License
PhotoSwipe is free for personal or commercial projects (MIT license). Please support the development on Open Collective if you find it useful.
);
}
export default function Home() {
const {siteConfig} = useDocusaurusContext();
return (
);
}
================================================
FILE: demo-docs-website/src/pages/index.module.css
================================================
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
================================================
FILE: demo-docs-website/src/theme/CodeBlock/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {isValidElement, useEffect, useState} from 'react';
import clsx from 'clsx';
import Highlight, {defaultProps} from 'prism-react-renderer';
import {
useThemeConfig,
parseCodeBlockTitle,
parseLanguage,
parseLines,
ThemeClassNames,
usePrismTheme,
} from '@docusaurus/theme-common';
import styles from './styles.module.css';
export default function CodeBlock({
children,
className: blockClassName = '',
metastring,
title,
pswpcode,
pswpdisplayhtml,
displayHTML,
language: languageProp,
}) {
const {prism} = useThemeConfig();
const [mounted, setMounted] = useState(false); // The Prism theme on SSR is always the default theme but the site theme
// can be in a different mode. React hydration doesn't update DOM styles
// that come from SSR. Hence force a re-render after mounting to apply the
// current relevant styles. There will be a flash seen of the original
// styles seen using this current approach but that's probably ok. Fixing
// the flash will require changing the theming approach and is not worth it
// at this point.
let esModuleScript;
useEffect(() => {
setMounted(true);
if (pswpcode && language === 'js' && !esModuleScript) {
esModuleScript = document.createElement('script');
esModuleScript.type = 'module';
esModuleScript.innerHTML = children;
document.body.appendChild(esModuleScript);
}
return () => {
if (esModuleScript) {
esModuleScript.remove();
esModuleScript = null;
}
};
}, []); // We still parse the metastring in case we want to support more syntax in the
// future. Note that MDX doesn't strip quotes when parsing metastring:
// "title=\"xyz\"" => title: "\"xyz\""
const codeBlockTitle = parseCodeBlockTitle(metastring) || title;
const prismTheme = usePrismTheme(); // tags in markdown map to CodeBlocks and they may contain JSX children.
// When the children is not a simple string, we just return a styled block
// without actually highlighting.
if (React.Children.toArray(children).some((el) => isValidElement(el))) {
return (
{({className, style}) => (
{children}
)}
);
} // The children is now guaranteed to be one/more plain strings
const content = Array.isArray(children) ? children.join('') : children;
const language =
languageProp ?? parseLanguage(blockClassName) ?? prism.defaultLanguage;
const {highlightLines, code} = parseLines(content, metastring, language);
let hideCodeBlock = false;
if (language === 'html' && pswpcode && !displayHTML && !pswpdisplayhtml) {
hideCodeBlock = true;
}
return (
{!hideCodeBlock &&
{({className, style, tokens, getLineProps, getTokenProps}) => (
{codeBlockTitle && (
{codeBlockTitle}
)}
{tokens.map((line, i) => {
if (line.length === 1 && line[0].content === '\n') {
line[0].content = '';
}
const lineProps = getLineProps({
line,
key: i,
});
if (highlightLines.includes(i)) {
lineProps.className += ' docusaurus-highlight-code-line';
}
return (
{line.map((token, key) => (
))}
);
})}
)}
}
{language === 'css' && pswpcode &&
}
{language === 'html' && pswpcode &&
}
);
}
================================================
FILE: demo-docs-website/src/theme/CodeBlock/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.codeBlockContainer {
margin-bottom: var(--ifm-leading);
}
.codeBlockContent {
position: relative;
/* rtl:ignore */
direction: ltr;
}
.codeBlockContent pre {
border-radius: 0;
}
.codeBlockTitle {
border-bottom: 1px solid var(--ifm-color-emphasis-300);
font-size: var(--ifm-code-font-size);
font-weight: 500;
padding: 0.75rem var(--ifm-pre-padding);
border-top-left-radius: var(--ifm-global-radius);
border-top-right-radius: var(--ifm-global-radius);
}
.codeBlock {
margin: 0;
padding: 0;
}
.codeBlockTitle + .codeBlockContent .codeBlock {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.codeBlockStandalone {
padding: 0;
border-radius: var(--ifm-global-radius);
}
.codeBlockLines {
font: inherit;
/* rtl:ignore */
float: left;
min-width: 100%;
padding: var(--ifm-pre-padding);
}
@media print {
.codeBlockLines {
white-space: pre-wrap;
}
}
================================================
FILE: demo-docs-website/src/theme/DocItem/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import DocPaginator from '@theme/DocPaginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import Seo from '@theme/Seo';
import DocItemFooter from '@theme/DocItemFooter';
import TOC from '@theme/TOC';
import TOCCollapsible from '@theme/TOCCollapsible';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
import {ThemeClassNames, useWindowSize} from '@docusaurus/theme-common';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
export default function DocItem(props) {
const {content: DocContent} = props;
const {metadata, frontMatter, assets} = DocContent;
const {
keywords,
hide_title: hideTitle,
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
const {description, title} = metadata;
const image = assets.image ?? frontMatter.image; // We only add a title if:
// - user asks to hide it with front matter
// - the markdown content does not already contain a top-level h1 heading
const shouldAddTitle =
!hideTitle && typeof DocContent.contentTitle === 'undefined';
const windowSize = useWindowSize();
const canRenderTOC =
!hideTableOfContents && DocContent.toc && DocContent.toc.length > 0;
const renderTocDesktop =
canRenderTOC && (windowSize === 'desktop' || windowSize === 'ssr');
return (
<>
{/*
Title can be declared inside md content or declared through
front matter and added manually. To make both cases consistent,
the added title is added under the same div.markdown block
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
*/}
{shouldAddTitle && (
)}
>
);
}
================================================
FILE: demo-docs-website/src/theme/DocItem/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.docItemContainer header + *,
.docItemContainer article > *:first-child {
margin-top: 0;
}
@media (min-width: 997px) {
/* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */
.tocMobile {
display: none;
}
}
================================================
FILE: demo-docs-website/src/theme/DocItemFooter/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import LastUpdated from '@theme/LastUpdated';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline from '@theme/TagsListInline';
import styles from './styles.module.css';
import {ThemeClassNames} from '@docusaurus/theme-common';
function TagsRow(props) {
return (
);
}
function EditMetaRow({
editUrl,
lastUpdatedAt,
lastUpdatedBy,
formattedLastUpdatedAt,
}) {
return (
{(lastUpdatedAt || lastUpdatedBy) && (
)}
);
}
export default function DocItemFooter(props) {
const {content: DocContent} = props;
const {metadata} = DocContent;
const {editUrl, lastUpdatedAt, formattedLastUpdatedAt, lastUpdatedBy, tags} =
metadata;
const canDisplayTagsRow = tags.length > 0;
const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow;
if (!canDisplayFooter) {
return null;
}
return (
{canDisplayTagsRow && }
{canDisplayEditMetaRow && (
)}
);
}
================================================
FILE: demo-docs-website/src/theme/DocItemFooter/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.lastUpdated {
margin-top: 0.2rem;
font-style: italic;
font-size: smaller;
}
@media (min-width: 997px) {
.lastUpdated {
text-align: right;
}
}
================================================
FILE: demo-docs-website/src/theme/DocPage/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {useState, useCallback} from 'react';
import {MDXProvider} from '@mdx-js/react';
import renderRoutes from '@docusaurus/renderRoutes';
import Layout from '@theme/Layout';
import DocSidebar from '@theme/DocSidebar';
import MDXComponents from '@theme/MDXComponents';
import NotFound from '@theme/NotFound';
import IconArrow from '@theme/IconArrow';
import {matchPath} from '@docusaurus/router';
import {translate} from '@docusaurus/Translate';
import clsx from 'clsx';
import styles from './styles.module.css';
import {
ThemeClassNames,
docVersionSearchTag,
DocsSidebarProvider,
useDocsSidebar,
DocsVersionProvider,
} from '@docusaurus/theme-common';
import Head from '@docusaurus/Head';
function DocPageContent({
currentDocRoute,
versionMetadata,
children,
sidebarName,
}) {
const sidebar = useDocsSidebar();
const {pluginId, version} = versionMetadata;
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
const [hiddenSidebar, setHiddenSidebar] = useState(false);
const toggleSidebar = useCallback(() => {
if (hiddenSidebar) {
setHiddenSidebar(false);
}
setHiddenSidebarContainer((value) => !value);
}, [hiddenSidebar]);
return (
{sidebar && (
{
if (
!e.currentTarget.classList.contains(styles.docSidebarContainer)
) {
return;
}
if (hiddenSidebarContainer) {
setHiddenSidebar(true);
}
}}>
{hiddenSidebar && (
)}
)}
{children}
);
}
export default function DocPage(props) {
const {
route: {routes: docRoutes},
versionMetadata,
location,
} = props;
const currentDocRoute = docRoutes.find((docRoute) =>
matchPath(location.pathname, docRoute),
);
if (!currentDocRoute) {
return ;
} // For now, the sidebarName is added as route config: not ideal!
const sidebarName = currentDocRoute.sidebar;
const sidebar = sidebarName
? versionMetadata.docsSidebars[sidebarName]
: null;
return (
<>
{/* TODO we should add a core addRoute({htmlClassName}) action */}
{renderRoutes(docRoutes, {
versionMetadata,
})}
>
);
}
================================================
FILE: demo-docs-website/src/theme/DocPage/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
================================================
FILE: demo-docs-website/src/theme/DocSidebar/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {useState} from 'react';
import clsx from 'clsx';
import {useLocation} from '@docusaurus/router';
import {
useThemeConfig,
useAnnouncementBar,
MobileSecondaryMenuFiller,
ThemeClassNames,
useScrollPosition,
useWindowSize,
} from '@docusaurus/theme-common';
import styles from './styles.module.css';
function useShowAnnouncementBar() {
const {isActive} = useAnnouncementBar();
const [showAnnouncementBar, setShowAnnouncementBar] = useState(isActive);
useScrollPosition(
({scrollY}) => {
if (isActive) {
setShowAnnouncementBar(scrollY === 0);
}
},
[isActive],
);
return isActive && showAnnouncementBar;
}
function SimpleSidebarCategory(props) {
return (
{props.label}
);
}
function SimpleSidebarItem(props) {
const location = useLocation();
const isActive = (props.href.toLowerCase() === location.pathname.toLowerCase());
return (
{props.label}
);
}
function SimpleSidebar(props) {
const sidebarItems = [];
let index = 0;
props.sidebar.forEach((sidebarItem) => {
if (sidebarItem.type === 'category') {
sidebarItems.push( );
index++;
sidebarItem.items.forEach((subItem) => {
sidebarItems.push( );
index++;
});
} else if (sidebarItem.type === 'link') {
sidebarItems.push( );
index++;
}
});
return (
<>
{sidebarItems}
>
);
}
function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}) {
const showAnnouncementBar = useShowAnnouncementBar();
const {
navbar: {hideOnScroll},
hideableSidebar,
} = useThemeConfig();
return (
);
} // eslint-disable-next-line react/function-component-definition
const DocSidebarMobileSecondaryMenu = ({toggleSidebar, sidebar, path}) => (
);
function DocSidebarMobile(props) {
return (
);
}
const DocSidebarDesktopMemo = React.memo(DocSidebarDesktop);
const DocSidebarMobileMemo = React.memo(DocSidebarMobile);
export default function DocSidebar(props) {
const windowSize = useWindowSize(); // Desktop sidebar visible on hydration: need SSR rendering
const shouldRenderSidebarDesktop =
windowSize === 'desktop' || windowSize === 'ssr'; // Mobile sidebar not visible on hydration: can avoid SSR rendering
const shouldRenderSidebarMobile = windowSize === 'mobile';
return (
<>
{shouldRenderSidebarDesktop && }
{shouldRenderSidebarMobile && }
>
);
}
================================================
FILE: demo-docs-website/src/theme/DocSidebar/is-same-path.ts
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// Compare the 2 paths, case insensitive and ignoring trailing slash
export const isSamePath = (
path1: string | undefined,
path2: string | undefined,
): boolean => {
const normalize = (pathname: string | undefined) =>
(!pathname || pathname?.endsWith('/')
? pathname
: `${pathname}/`
)?.toLowerCase();
return normalize(path1) === normalize(path2);
};
================================================
FILE: demo-docs-website/src/theme/DocSidebar/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
:root {
--collapse-button-bg-color-dark: #2e333a;
}
@media (min-width: 997px) {
.sidebar {
display: flex;
flex-direction: column;
max-height: 100vh;
height: 100%;
position: sticky;
top: 0;
padding-top: var(--ifm-navbar-height);
transition: opacity 50ms ease;
}
.sidebarWithHideableNavbar {
padding-top: 0;
}
.sidebarHidden {
opacity: 0;
height: 0;
overflow: hidden;
visibility: hidden;
}
.sidebarLogo {
display: flex !important;
align-items: center;
margin: 0 var(--ifm-navbar-padding-horizontal);
min-height: var(--ifm-navbar-height);
max-height: var(--ifm-navbar-height);
color: inherit !important;
text-decoration: none !important;
}
.sidebarLogo img {
margin-right: 0.5rem;
height: 2rem;
}
.menu {
flex-grow: 1;
padding: 0.5rem;
}
.menuWithAnnouncementBar {
margin-bottom: var(--docusaurus-announcement-bar-height);
}
.collapseSidebarButton {
display: block !important;
background-color: var(--ifm-button-background-color);
height: 40px;
position: sticky;
bottom: 0;
border-radius: 0;
border: 1px solid var(--ifm-toc-border-color);
}
.collapseSidebarButtonIcon {
transform: rotate(180deg);
margin-top: 4px;
}
[dir='rtl'] .collapseSidebarButtonIcon {
transform: rotate(0);
}
[data-theme='dark'] .collapseSidebarButton {
background-color: var(--collapse-button-bg-color-dark);
}
[data-theme='dark'] .collapseSidebarButton:hover,
[data-theme='dark'] .collapseSidebarButton:focus {
background-color: var(--ifm-color-emphasis-200);
}
}
.sidebarLogo,
.collapseSidebarButton {
display: none;
}
.sidebarMenuIcon {
vertical-align: middle;
}
.sidebarMenuCloseIcon {
display: inline-flex;
justify-content: center;
align-items: center;
height: 24px;
font-size: 1.5rem;
font-weight: var(--ifm-font-weight-bold);
line-height: 0.9;
width: 24px;
}
================================================
FILE: demo-docs-website/src/theme/EditThisPage/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import {ThemeClassNames} from '@docusaurus/theme-common';
export default function EditThisPage({editUrl}) {
return (
<>Found a typo? Edit this page >
);
}
================================================
FILE: demo-docs-website/src/theme/Footer/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import {useThemeConfig} from '@docusaurus/theme-common';
import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './styles.module.css';
import ThemedImage from '@theme/ThemedImage';
function FooterLogo({sources, alt, width, height}) {
return (
);
}
function Footer() {
const {footer} = useThemeConfig();
const {copyright, links = [], logo = {}} = footer || {};
const sources = {
light: useBaseUrl(logo.src),
dark: useBaseUrl(logo.srcDark || logo.src),
};
if (!footer) {
return null;
}
return (
);
}
export default React.memo(Footer);
================================================
FILE: demo-docs-website/src/theme/Footer/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.footerLogoLink {
opacity: 0.5;
transition: opacity var(--ifm-transition-fast)
var(--ifm-transition-timing-default);
}
.footerLogoLink:hover {
opacity: 1;
}
================================================
FILE: demo-docs-website/src/theme/Logo/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import ThemedImage from '@theme/ThemedImage';
import useBaseUrl from '@docusaurus/useBaseUrl';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useThemeConfig} from '@docusaurus/theme-common';
import packageInfo from '../../../../package.json';
export default function Logo(props) {
const {
siteConfig: {title},
} = useDocusaurusContext();
const {
navbar: {
title: navbarTitle,
logo = {
src: '',
},
},
} = useThemeConfig();
const {imageClassName, titleClassName, ...propsRest} = props;
const logoLink = useBaseUrl(logo.href || '/');
const sources = {
light: useBaseUrl(logo.src),
dark: useBaseUrl(logo.srcDark || logo.src),
};
const themedImage = (
);
return (
{logo.src &&
(imageClassName ? (
{themedImage}
) : (
themedImage
))}
{navbarTitle != null && {navbarTitle} }
{packageInfo.version}
);
}
================================================
FILE: demo-docs-website/src/theme/MDXComponents/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {isValidElement} from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import CodeBlock from '@theme/CodeBlock';
import Heading from '@theme/Heading';
import Details from '@theme/Details';
import PswpCodePreview from '@site/src/components/PswpCodePreview';
import './styles.css'; // MDX elements are wrapped through the MDX pragma. In some cases (notably usage
// with Head/Helmet) we need to unwrap those elements.
function unwrapMDXElement(element) {
if (element?.props?.mdxType && element?.props?.originalType) {
const {mdxType, originalType, ...newProps} = element.props;
return React.createElement(element.props.originalType, newProps);
}
return element;
}
const MDXComponents = {
head: (props) => {
const unwrappedChildren = React.Children.map(props.children, (child) =>
unwrapMDXElement(child),
);
return {unwrappedChildren};
},
code: (props) => {
const inlineElements = [
'a',
'b',
'big',
'i',
'span',
'em',
'strong',
'sup',
'sub',
'small',
];
const shouldBeInline = React.Children.toArray(props.children).every(
(el) =>
(typeof el === 'string' && !el.includes('\n')) ||
(React.isValidElement(el) && inlineElements.includes(el.props.mdxType)),
);
return shouldBeInline ? : ;
},
a: (props) => ,
pre: (props) => (
),
details: (props) => {
const items = React.Children.toArray(props.children); // Split summary item from the rest to pass it as a separate prop to the
// Details theme component
const summary = items.find((item) => item?.props?.mdxType === 'summary');
const children = <>{items.filter((item) => item !== summary)}>;
return (
{children}
);
},
h1: (props) => ,
h2: (props) => ,
h3: (props) => ,
h4: (props) => ,
h5: (props) => ,
h6: (props) => ,
PswpCodePreview: (props) =>
};
export default MDXComponents;
================================================
FILE: demo-docs-website/src/theme/MDXComponents/styles.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
ul.contains-task-list {
padding-left: 0;
list-style: none;
}
img {
height: auto;
}
================================================
FILE: demo-docs-website/src/theme/Navbar/github-stars.js
================================================
import React from 'react'
import repoData from '../../../repo-data.json';
export default function ReactGithubStars() {
return (
Github
{
(repoData
&& repoData.stargazers_count
&& parseInt(repoData.stargazers_count, 10) > 100) ? parseInt(repoData.stargazers_count, 10).toLocaleString() : ''
}
)
}
================================================
FILE: demo-docs-website/src/theme/Navbar/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { useCallback, useState, useEffect } from "react";
import clsx from "clsx";
import Translate from "@docusaurus/Translate";
import ColorModeToggle from "@theme/ColorModeToggle";
import {
useThemeConfig,
useMobileSecondaryMenuRenderer,
usePrevious,
useHistoryPopHandler,
useHideableNavbar,
useLockBodyScroll,
useWindowSize,
useColorMode,
} from "@docusaurus/theme-common";
import { useActivePlugin } from "@docusaurus/plugin-content-docs/client";
import NavbarItem from "@theme/NavbarItem";
import Logo from "@theme/Logo";
import IconMenu from "@theme/IconMenu";
import IconClose from "@theme/IconClose";
import styles from "./styles.module.css"; // retrocompatible with v1
import ReactGithubStars from "./github-stars";
import { DocSearch } from "@docsearch/react";
import "@docsearch/css";
const DefaultNavItemPosition = "right";
function useNavbarItems() {
// TODO temporary casting until ThemeConfig type is improved
return useThemeConfig().navbar.items;
} // If split links by left/right
// if position is unspecified, fallback to right (as v1)
function splitNavItemsByPosition(items) {
const leftItems = items.filter(
(item) => (item.position ?? DefaultNavItemPosition) === "left"
);
const rightItems = items.filter(
(item) => (item.position ?? DefaultNavItemPosition) === "right"
);
return {
leftItems,
rightItems,
};
}
function useMobileSidebar() {
const windowSize = useWindowSize(); // Mobile sidebar not visible on hydration: can avoid SSR rendering
const shouldRender = windowSize === "mobile"; // || windowSize === 'ssr';
const [shown, setShown] = useState(false); // Close mobile sidebar on navigation pop
// Most likely firing when using the Android back button (but not only)
useHistoryPopHandler(() => {
if (shown) {
setShown(false); // Should we prevent the navigation here?
// See https://github.com/facebook/docusaurus/pull/5462#issuecomment-911699846
return false; // prevent pop navigation
}
return undefined;
});
const toggle = useCallback(() => {
setShown((s) => !s);
}, []);
useEffect(() => {
if (windowSize === "desktop") {
setShown(false);
}
}, [windowSize]);
return {
shouldRender,
toggle,
shown,
};
}
function useColorModeToggle() {
const {
colorMode: { disableSwitch },
} = useThemeConfig();
const { isDarkTheme, setLightTheme, setDarkTheme } = useColorMode();
const toggle = useCallback(
(e) => (e.target.checked ? setDarkTheme() : setLightTheme()),
[setLightTheme, setDarkTheme]
);
return {
isDarkTheme,
toggle,
disabled: disableSwitch,
};
}
const TwitterLink = () => {
return (
Twitter
);
};
function useSecondaryMenu({ sidebarShown, toggleSidebar }) {
const content = useMobileSecondaryMenuRenderer()?.({
toggleSidebar,
});
const previousContent = usePrevious(content);
const [shown, setShown] = useState(
() =>
// /!\ content is set with useEffect,
// so it's not available on mount anyway
// "return !!content" => always returns false
false
); // When content is become available for the first time (set in useEffect)
// we set this content to be shown!
useEffect(() => {
const contentBecameAvailable = content && !previousContent;
if (contentBecameAvailable) {
setShown(true);
}
}, [content, previousContent]);
const hasContent = !!content; // On sidebar close, secondary menu is set to be shown on next re-opening
// (if any secondary menu content available)
useEffect(() => {
if (!hasContent) {
setShown(false);
return;
}
if (!sidebarShown) {
setShown(true);
}
}, [sidebarShown, hasContent]);
const hide = useCallback(() => {
setShown(false);
}, []);
return {
shown,
hide,
content,
};
}
function NavbarMobileSidebar({ sidebarShown, toggleSidebar }) {
useLockBodyScroll(sidebarShown);
const items = useNavbarItems();
const colorModeToggle = useColorModeToggle();
const secondaryMenu = useSecondaryMenu({
sidebarShown,
toggleSidebar,
});
return (
{!colorModeToggle.disabled && (
)}
{items.map((item, i) => (
))}
{items.length > 0 && (
)}
{secondaryMenu.content}
);
}
export default function Navbar() {
const {
navbar: { hideOnScroll, style },
} = useThemeConfig();
const mobileSidebar = useMobileSidebar();
const colorModeToggle = useColorModeToggle();
const activeDocPlugin = useActivePlugin();
const { navbarRef, isNavbarVisible } = useHideableNavbar(hideOnScroll);
const items = useNavbarItems();
const { leftItems, rightItems } = splitNavItemsByPosition(items);
return (
{(items?.length > 0 || activeDocPlugin) && (
)}
{leftItems.map((item, i) => (
))}
{rightItems.map((item, i) => (
))}
{!colorModeToggle.disabled && (
)}
{mobileSidebar.shouldRender && (
)}
);
}
================================================
FILE: demo-docs-website/src/theme/Navbar/styles.module.css
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*
Hide toggle in small viewports
*/
@media (max-width: 996px) {
.toggle {
display: none;
}
}
.navbarHideable {
transition: transform var(--ifm-transition-fast) ease;
}
.navbarHidden {
transform: translate3d(0, calc(-100% - 2px), 0);
}
.navbarSidebarToggle {
margin-right: 1rem;
}
================================================
FILE: demo-docs-website/static/.nojekyll
================================================
================================================
FILE: demo-docs-website/static/photoswipe/photoswipe-lightbox.esm.js
================================================
/*!
* PhotoSwipe Lightbox 5.4.4 - https://photoswipe.com
* (c) 2024 Dmytro Semenov
*/
/** @typedef {import('../photoswipe.js').Point} Point */
/**
* @template {keyof HTMLElementTagNameMap} T
* @param {string} className
* @param {T} tagName
* @param {Node} [appendToEl]
* @returns {HTMLElementTagNameMap[T]}
*/
function createElement(className, tagName, appendToEl) {
const el = document.createElement(tagName);
if (className) {
el.className = className;
}
if (appendToEl) {
appendToEl.appendChild(el);
}
return el;
}
/**
* Get transform string
*
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
* @returns {string}
*/
function toTransformString(x, y, scale) {
let propValue = `translate3d(${x}px,${y || 0}px,0)`;
if (scale !== undefined) {
propValue += ` scale3d(${scale},${scale},1)`;
}
return propValue;
}
/**
* Apply width and height CSS properties to element
*
* @param {HTMLElement} el
* @param {string | number} w
* @param {string | number} h
*/
function setWidthHeight(el, w, h) {
el.style.width = typeof w === 'number' ? `${w}px` : w;
el.style.height = typeof h === 'number' ? `${h}px` : h;
}
/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
const LOAD_STATE = {
IDLE: 'idle',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error'
};
/**
* Check if click or keydown event was dispatched
* with a special key or via mouse wheel.
*
* @param {MouseEvent | KeyboardEvent} e
* @returns {boolean}
*/
function specialKeyUsed(e) {
return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
}
/**
* Parse `gallery` or `children` options.
*
* @param {import('../photoswipe.js').ElementProvider} [option]
* @param {string} [legacySelector]
* @param {HTMLElement | Document} [parent]
* @returns HTMLElement[]
*/
function getElementsFromOption(option, legacySelector, parent = document) {
/** @type {HTMLElement[]} */
let elements = [];
if (option instanceof Element) {
elements = [option];
} else if (option instanceof NodeList || Array.isArray(option)) {
elements = Array.from(option);
} else {
const selector = typeof option === 'string' ? option : legacySelector;
if (selector) {
elements = Array.from(parent.querySelectorAll(selector));
}
}
return elements;
}
/**
* Check if variable is PhotoSwipe class
*
* @param {any} fn
* @returns {boolean}
*/
function isPswpClass(fn) {
return typeof fn === 'function' && fn.prototype && fn.prototype.goTo;
}
/**
* Check if browser is Safari
*
* @returns {boolean}
*/
function isSafari() {
return !!(navigator.vendor && navigator.vendor.match(/apple/i));
}
/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../slide/content.js').default} ContentDefault */
/** @typedef {import('../slide/slide.js').default} Slide */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
/**
* Allow adding an arbitrary props to the Content
* https://photoswipe.com/custom-content/#using-webp-image-format
* @typedef {ContentDefault & Record} Content
*/
/** @typedef {{ x?: number; y?: number }} Point */
/**
* @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
*
*
* https://photoswipe.com/adding-ui-elements/
*
* @prop {undefined} uiRegister
* @prop {{ data: UIElementData }} uiElementCreate
*
*
* https://photoswipe.com/events/#initialization-events
*
* @prop {undefined} beforeOpen
* @prop {undefined} firstUpdate
* @prop {undefined} initialLayout
* @prop {undefined} change
* @prop {undefined} afterInit
* @prop {undefined} bindEvents
*
*
* https://photoswipe.com/events/#opening-or-closing-transition-events
*
* @prop {undefined} openingAnimationStart
* @prop {undefined} openingAnimationEnd
* @prop {undefined} closingAnimationStart
* @prop {undefined} closingAnimationEnd
*
*
* https://photoswipe.com/events/#closing-events
*
* @prop {undefined} close
* @prop {undefined} destroy
*
*
* https://photoswipe.com/events/#pointer-and-gesture-events
*
* @prop {{ originalEvent: PointerEvent }} pointerDown
* @prop {{ originalEvent: PointerEvent }} pointerMove
* @prop {{ originalEvent: PointerEvent }} pointerUp
* @prop {{ bgOpacity: number }} pinchClose can be default prevented
* @prop {{ panY: number }} verticalDrag can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*
* @prop {{ content: Content }} contentInit
* @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
* @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
* @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
* @prop {{ content: Content; slide: Slide }} loadError
* @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
* @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
* @prop {{ content: Content }} contentLazyLoad can be default prevented
* @prop {{ content: Content }} contentAppend can be default prevented
* @prop {{ content: Content }} contentActivate can be default prevented
* @prop {{ content: Content }} contentDeactivate can be default prevented
* @prop {{ content: Content }} contentRemove can be default prevented
* @prop {{ content: Content }} contentDestroy can be default prevented
*
*
* undocumented
*
* @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
*
* @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
* @prop {{ x: number; dragging: boolean }} moveMainScroll
* @prop {{ slide: Slide }} firstZoomPan
* @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
* @prop {undefined} beforeResize
* @prop {undefined} resize
* @prop {undefined} viewportSize
* @prop {undefined} updateScrollOffset
* @prop {{ slide: Slide }} slideInit
* @prop {{ slide: Slide }} afterSetContent
* @prop {{ slide: Slide }} slideLoad
* @prop {{ slide: Slide }} appendHeavy can be default prevented
* @prop {{ slide: Slide }} appendHeavyContent
* @prop {{ slide: Slide }} slideActivate
* @prop {{ slide: Slide }} slideDeactivate
* @prop {{ slide: Slide }} slideDestroy
* @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
* @prop {{ slide: Slide }} zoomPanUpdate
* @prop {{ slide: Slide }} initialZoomPan
* @prop {{ slide: Slide }} calcSlideSize
* @prop {undefined} resolutionChanged
* @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
* @prop {{ content: Content }} contentAppendImage can be default prevented
* @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
* @prop {undefined} lazyLoad
* @prop {{ slide: Slide }} calcBounds
* @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
*
*
* legacy
*
* @prop {undefined} init
* @prop {undefined} initialZoomIn
* @prop {undefined} initialZoomOut
* @prop {undefined} initialZoomInEnd
* @prop {undefined} initialZoomOutEnd
* @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
* @prop {{ itemData: SlideData; index: number }} itemData
* @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
*/
/**
* @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
*
* @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*
* @prop {(itemData: SlideData, index: number) => SlideData} itemData
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*
* @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*
* @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*
* @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*
* @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*
* @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*
* @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*
* @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*
*
* @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*
* @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*
* @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*
* @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*
* @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
*
* @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
*
*/
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent : PhotoSwipeEvent & PhotoSwipeEventsMap[T]} AugmentedEvent
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {(event: AugmentedEvent) => void} EventCallback
*/
/**
* Base PhotoSwipe event object
*
* @template {keyof PhotoSwipeEventsMap} T
*/
class PhotoSwipeEvent {
/**
* @param {T} type
* @param {PhotoSwipeEventsMap[T]} [details]
*/
constructor(type, details) {
this.type = type;
this.defaultPrevented = false;
if (details) {
Object.assign(this, details);
}
}
preventDefault() {
this.defaultPrevented = true;
}
}
/**
* PhotoSwipe base class that can listen and dispatch for events.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
*/
class Eventable {
constructor() {
/**
* @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent) => void)[] }}
*/
this._listeners = {};
/**
* @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter[] }}
*/
this._filters = {};
/** @type {PhotoSwipe | undefined} */
this.pswp = undefined;
/** @type {PhotoSwipeOptions | undefined} */
this.options = undefined;
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
* @param {number} priority
*/
addFilter(name, fn, priority = 100) {
var _this$_filters$name, _this$_filters$name2, _this$pswp;
if (!this._filters[name]) {
this._filters[name] = [];
}
(_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
fn,
priority
});
(_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
(_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
*/
removeFilter(name, fn) {
if (this._filters[name]) {
// @ts-expect-error
this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
}
if (this.pswp) {
this.pswp.removeFilter(name, fn);
}
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {Parameters} args
* @returns {Parameters[0]}
*/
applyFilters(name, ...args) {
var _this$_filters$name3;
(_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
// @ts-expect-error
args[0] = filter.fn.apply(this, args);
});
return args[0];
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
on(name, fn) {
var _this$_listeners$name, _this$pswp2;
if (!this._listeners[name]) {
this._listeners[name] = [];
}
(_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
// also bind events to PhotoSwipe Core,
// if it's open.
(_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
off(name, fn) {
var _this$pswp3;
if (this._listeners[name]) {
// @ts-expect-error
this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
}
(_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {PhotoSwipeEventsMap[T]} [details]
* @returns {AugmentedEvent}
*/
dispatch(name, details) {
var _this$_listeners$name2;
if (this.pswp) {
return this.pswp.dispatch(name, details);
}
const event =
/** @type {AugmentedEvent} */
new PhotoSwipeEvent(name, details);
(_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
listener.call(this, event);
});
return event;
}
}
class Placeholder {
/**
* @param {string | false} imageSrc
* @param {HTMLElement} container
*/
constructor(imageSrc, container) {
// Create placeholder
// (stretched thumbnail or simple div behind the main image)
/** @type {HTMLImageElement | HTMLDivElement | null} */
this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
if (imageSrc) {
const imgEl =
/** @type {HTMLImageElement} */
this.element;
imgEl.decoding = 'async';
imgEl.alt = '';
imgEl.src = imageSrc;
imgEl.setAttribute('role', 'presentation');
}
this.element.setAttribute('aria-hidden', 'true');
}
/**
* @param {number} width
* @param {number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.element.tagName === 'IMG') {
// Use transform scale() to modify img placeholder size
// (instead of changing width/height directly).
// This helps with performance, specifically in iOS15 Safari.
setWidthHeight(this.element, 250, 'auto');
this.element.style.transformOrigin = '0 0';
this.element.style.transform = toTransformString(0, 0, width / 250);
} else {
setWidthHeight(this.element, width, height);
}
}
destroy() {
var _this$element;
if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
this.element.remove();
}
this.element = null;
}
}
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../util/util.js').LoadState} LoadState */
class Content {
/**
* @param {SlideData} itemData Slide data
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
*/
constructor(itemData, instance, index) {
this.instance = instance;
this.data = itemData;
this.index = index;
/** @type {HTMLImageElement | HTMLDivElement | undefined} */
this.element = undefined;
/** @type {Placeholder | undefined} */
this.placeholder = undefined;
/** @type {Slide | undefined} */
this.slide = undefined;
this.displayedImageWidth = 0;
this.displayedImageHeight = 0;
this.width = Number(this.data.w) || Number(this.data.width) || 0;
this.height = Number(this.data.h) || Number(this.data.height) || 0;
this.isAttached = false;
this.hasSlide = false;
this.isDecoding = false;
/** @type {LoadState} */
this.state = LOAD_STATE.IDLE;
if (this.data.type) {
this.type = this.data.type;
} else if (this.data.src) {
this.type = 'image';
} else {
this.type = 'html';
}
this.instance.dispatch('contentInit', {
content: this
});
}
removePlaceholder() {
if (this.placeholder && !this.keepPlaceholder()) {
// With delay, as image might be loaded, but not rendered
setTimeout(() => {
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
}, 1000);
}
}
/**
* Preload content
*
* @param {boolean} isLazy
* @param {boolean} [reload]
*/
load(isLazy, reload) {
if (this.slide && this.usePlaceholder()) {
if (!this.placeholder) {
const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide,
// as rendering (even small stretched thumbnail) is an expensive operation
this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
} else {
const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
if (placeholderEl && !placeholderEl.parentElement) {
this.slide.container.prepend(placeholderEl);
}
}
}
if (this.element && !reload) {
return;
}
if (this.instance.dispatch('contentLoad', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
if (this.isImageContent()) {
this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
// Due to Safari feature, we must define sizes before srcset.
if (this.displayedImageWidth) {
this.loadImage(isLazy);
}
} else {
this.element = createElement('pswp__content', 'div');
this.element.innerHTML = this.data.html || '';
}
if (reload && this.slide) {
this.slide.updateContentSize(true);
}
}
/**
* Preload image
*
* @param {boolean} isLazy
*/
loadImage(isLazy) {
var _this$data$src, _this$data$alt;
if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
const imageElement =
/** @type HTMLImageElement */
this.element;
this.updateSrcsetSizes();
if (this.data.srcset) {
imageElement.srcset = this.data.srcset;
}
imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
this.state = LOAD_STATE.LOADING;
if (imageElement.complete) {
this.onLoaded();
} else {
imageElement.onload = () => {
this.onLoaded();
};
imageElement.onerror = () => {
this.onError();
};
}
}
/**
* Assign slide to content
*
* @param {Slide} slide
*/
setSlide(slide) {
this.slide = slide;
this.hasSlide = true;
this.instance = slide.pswp; // todo: do we need to unset slide?
}
/**
* Content load success handler
*/
onLoaded() {
this.state = LOAD_STATE.LOADED;
if (this.slide && this.element) {
this.instance.dispatch('loadComplete', {
slide: this.slide,
content: this
}); // if content is reloaded
if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
this.append();
this.slide.updateContentSize(true);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/**
* Content load error handler
*/
onError() {
this.state = LOAD_STATE.ERROR;
if (this.slide) {
this.displayError();
this.instance.dispatch('loadComplete', {
slide: this.slide,
isError: true,
content: this
});
this.instance.dispatch('loadError', {
slide: this.slide,
content: this
});
}
}
/**
* @returns {Boolean} If the content is currently loading
*/
isLoading() {
return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
}
/**
* @returns {Boolean} If the content is in error state
*/
isError() {
return this.state === LOAD_STATE.ERROR;
}
/**
* @returns {boolean} If the content is image
*/
isImageContent() {
return this.type === 'image';
}
/**
* Update content size
*
* @param {Number} width
* @param {Number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.placeholder) {
this.placeholder.setDisplayedSize(width, height);
}
if (this.instance.dispatch('contentResize', {
content: this,
width,
height
}).defaultPrevented) {
return;
}
setWidthHeight(this.element, width, height);
if (this.isImageContent() && !this.isError()) {
const isInitialSizeUpdate = !this.displayedImageWidth && width;
this.displayedImageWidth = width;
this.displayedImageHeight = height;
if (isInitialSizeUpdate) {
this.loadImage(false);
} else {
this.updateSrcsetSizes();
}
if (this.slide) {
this.instance.dispatch('imageSizeChange', {
slide: this.slide,
width,
height,
content: this
});
}
}
}
/**
* @returns {boolean} If the content can be zoomed
*/
isZoomable() {
return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
}
/**
* Update image srcset sizes attribute based on width and height
*/
updateSrcsetSizes() {
// Handle srcset sizes attribute.
//
// Never lower quality, if it was increased previously.
// Chrome does this automatically, Firefox and Safari do not,
// so we store largest used size in dataset.
if (!this.isImageContent() || !this.element || !this.data.srcset) {
return;
}
const image =
/** @type HTMLImageElement */
this.element;
const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
image.sizes = sizesWidth + 'px';
image.dataset.largestUsedSize = String(sizesWidth);
}
}
/**
* @returns {boolean} If content should use a placeholder (from msrc by default)
*/
usePlaceholder() {
return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
}
/**
* Preload content with lazy-loading param
*/
lazyLoad() {
if (this.instance.dispatch('contentLazyLoad', {
content: this
}).defaultPrevented) {
return;
}
this.load(true);
}
/**
* @returns {boolean} If placeholder should be kept after content is loaded
*/
keepPlaceholder() {
return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
}
/**
* Destroy the content
*/
destroy() {
this.hasSlide = false;
this.slide = undefined;
if (this.instance.dispatch('contentDestroy', {
content: this
}).defaultPrevented) {
return;
}
this.remove();
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
if (this.isImageContent() && this.element) {
this.element.onload = null;
this.element.onerror = null;
this.element = undefined;
}
}
/**
* Display error message
*/
displayError() {
if (this.slide) {
var _this$instance$option, _this$instance$option2;
let errorMsgEl = createElement('pswp__error-msg', 'div');
errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
errorMsgEl =
/** @type {HTMLDivElement} */
this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
this.element = createElement('pswp__content pswp__error-msg-container', 'div');
this.element.appendChild(errorMsgEl);
this.slide.container.innerText = '';
this.slide.container.appendChild(this.element);
this.slide.updateContentSize(true);
this.removePlaceholder();
}
}
/**
* Append the content
*/
append() {
if (this.isAttached || !this.element) {
return;
}
this.isAttached = true;
if (this.state === LOAD_STATE.ERROR) {
this.displayError();
return;
}
if (this.instance.dispatch('contentAppend', {
content: this
}).defaultPrevented) {
return;
}
const supportsDecode = ('decode' in this.element);
if (this.isImageContent()) {
// Use decode() on nearby slides
//
// Nearby slide images are in DOM and not hidden via display:none.
// However, they are placed offscreen (to the left and right side).
//
// Some browsers do not composite the image until it's actually visible,
// using decode() helps.
//
// You might ask "why dont you just decode() and then append all images",
// that's because I want to show image before it's fully loaded,
// as browser can render parts of image while it is loading.
// We do not do this in Safari due to partial loading bug.
if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
this.isDecoding = true; // purposefully using finally instead of then,
// as if srcset sizes changes dynamically - it may cause decode error
/** @type {HTMLImageElement} */
this.element.decode().catch(() => {}).finally(() => {
this.isDecoding = false;
this.appendImage();
});
} else {
this.appendImage();
}
} else if (this.slide && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
}
/**
* Activate the slide,
* active slide is generally the current one,
* meaning the user can see it.
*/
activate() {
if (this.instance.dispatch('contentActivate', {
content: this
}).defaultPrevented || !this.slide) {
return;
}
if (this.isImageContent() && this.isDecoding && !isSafari()) {
// add image to slide when it becomes active,
// even if it's not finished decoding
this.appendImage();
} else if (this.isError()) {
this.load(false, true); // try to reload
}
if (this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'false');
}
}
/**
* Deactivate the content
*/
deactivate() {
this.instance.dispatch('contentDeactivate', {
content: this
});
if (this.slide && this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'true');
}
}
/**
* Remove the content from DOM
*/
remove() {
this.isAttached = false;
if (this.instance.dispatch('contentRemove', {
content: this
}).defaultPrevented) {
return;
}
if (this.element && this.element.parentNode) {
this.element.remove();
}
if (this.placeholder && this.placeholder.element) {
this.placeholder.element.remove();
}
}
/**
* Append the image content to slide container
*/
appendImage() {
if (!this.isAttached) {
return;
}
if (this.instance.dispatch('contentAppendImage', {
content: this
}).defaultPrevented) {
return;
} // ensure that element exists and is not already appended
if (this.slide && this.element && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/**
* @param {PhotoSwipeOptions} options
* @param {PhotoSwipeBase} pswp
* @returns {Point}
*/
function getViewportSize(options, pswp) {
if (options.getViewportSizeFn) {
const newViewportSize = options.getViewportSizeFn(options, pswp);
if (newViewportSize) {
return newViewportSize;
}
}
return {
x: document.documentElement.clientWidth,
// TODO: height on mobile is very incosistent due to toolbar
// find a way to improve this
//
// document.documentElement.clientHeight - doesn't seem to work well
y: window.innerHeight
};
}
/**
* Parses padding option.
* Supported formats:
*
* // Object
* padding: {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* }
*
* // A function that returns the object
* paddingFn: (viewportSize, itemData, index) => {
* return {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* };
* }
*
* // Legacy variant
* paddingLeft: 0,
* paddingRight: 0,
* paddingTop: 0,
* paddingBottom: 0,
*
* @param {'left' | 'top' | 'bottom' | 'right'} prop
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
* @param {SlideData} itemData Data about the slide
* @param {number} index Slide index
* @returns {number}
*/
function parsePaddingOption(prop, options, viewportSize, itemData, index) {
let paddingValue = 0;
if (options.paddingFn) {
paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
} else if (options.padding) {
paddingValue = options.padding[prop];
} else {
const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
if (options[legacyPropName]) {
// @ts-expect-error
paddingValue = options[legacyPropName];
}
}
return Number(paddingValue) || 0;
}
/**
* @param {PhotoSwipeOptions} options
* @param {Point} viewportSize
* @param {SlideData} itemData
* @param {number} index
* @returns {Point}
*/
function getPanAreaSize(options, viewportSize, itemData, index) {
return {
x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
};
}
const MAX_IMAGE_WIDTH = 4000;
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
/**
* Calculates zoom levels for specific slide.
* Depends on viewport size and image size.
*/
class ZoomLevel {
/**
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {SlideData} itemData Slide data
* @param {number} index Slide index
* @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
*/
constructor(options, itemData, index, pswp) {
this.pswp = pswp;
this.options = options;
this.itemData = itemData;
this.index = index;
/** @type { Point | null } */
this.panAreaSize = null;
/** @type { Point | null } */
this.elementSize = null;
this.fit = 1;
this.fill = 1;
this.vFill = 1;
this.initial = 1;
this.secondary = 1;
this.max = 1;
this.min = 1;
}
/**
* Calculate initial, secondary and maximum zoom level for the specified slide.
*
* It should be called when either image or viewport size changes.
*
* @param {number} maxWidth
* @param {number} maxHeight
* @param {Point} panAreaSize
*/
update(maxWidth, maxHeight, panAreaSize) {
/** @type {Point} */
const elementSize = {
x: maxWidth,
y: maxHeight
};
this.elementSize = elementSize;
this.panAreaSize = panAreaSize;
const hRatio = panAreaSize.x / elementSize.x;
const vRatio = panAreaSize.y / elementSize.y;
this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
// when it has 100% of viewport vertical space (height)
this.vFill = Math.min(1, vRatio);
this.initial = this._getInitial();
this.secondary = this._getSecondary();
this.max = Math.max(this.initial, this.secondary, this._getMax());
this.min = Math.min(this.fit, this.initial, this.secondary);
if (this.pswp) {
this.pswp.dispatch('zoomLevelsUpdate', {
zoomLevels: this,
slideData: this.itemData
});
}
}
/**
* Parses user-defined zoom option.
*
* @private
* @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
* @returns { number | undefined }
*/
_parseZoomLevelOption(optionPrefix) {
const optionName =
/** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
optionPrefix + 'ZoomLevel';
const optionValue = this.options[optionName];
if (!optionValue) {
return;
}
if (typeof optionValue === 'function') {
return optionValue(this);
}
if (optionValue === 'fill') {
return this.fill;
}
if (optionValue === 'fit') {
return this.fit;
}
return Number(optionValue);
}
/**
* Get zoom level to which image will be zoomed after double-tap gesture,
* or when user clicks on zoom icon,
* or mouse-click on image itself.
* If you return 1 image will be zoomed to its original size.
*
* @private
* @return {number}
*/
_getSecondary() {
let currZoomLevel = this._parseZoomLevelOption('secondary');
if (currZoomLevel) {
return currZoomLevel;
} // 3x of "fit" state, but not larger than original
currZoomLevel = Math.min(1, this.fit * 3);
if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
}
return currZoomLevel;
}
/**
* Get initial image zoom level.
*
* @private
* @return {number}
*/
_getInitial() {
return this._parseZoomLevelOption('initial') || this.fit;
}
/**
* Maximum zoom level when user zooms
* via zoom/pinch gesture,
* via cmd/ctrl-wheel or via trackpad.
*
* @private
* @return {number}
*/
_getMax() {
// max zoom level is x4 from "fit state",
// used for zoom gesture and ctrl/trackpad zoom
return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
}
}
/**
* Lazy-load an image
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* @param {SlideData} itemData Data about the slide
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
function lazyLoadData(itemData, instance, index) {
const content = instance.createContentFromData(itemData, index);
/** @type {ZoomLevel | undefined} */
let zoomLevel;
const {
options
} = instance; // We need to know dimensions of the image to preload it,
// as it might use srcset, and we need to define sizes
if (options) {
zoomLevel = new ZoomLevel(options, itemData, -1);
let viewportSize;
if (instance.pswp) {
viewportSize = instance.pswp.viewportSize;
} else {
viewportSize = getViewportSize(options, instance);
}
const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
zoomLevel.update(content.width, content.height, panAreaSize);
}
content.lazyLoad();
if (zoomLevel) {
content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
}
return content;
}
/**
* Lazy-loads specific slide.
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* By default, it loads image based on viewport size and initial zoom level.
*
* @param {number} index Slide index
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
* @returns {Content | undefined}
*/
function lazyLoadSlide(index, instance) {
const itemData = instance.getItemData(index);
if (instance.dispatch('lazyLoadSlide', {
index,
itemData
}).defaultPrevented) {
return;
}
return lazyLoadData(itemData, instance, index);
}
/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
/** @typedef {import("../slide/slide.js").SlideData} SlideData */
/**
* PhotoSwipe base class that can retrieve data about every slide.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
*/
class PhotoSwipeBase extends Eventable {
/**
* Get total number of slides
*
* @returns {number}
*/
getNumItems() {
var _this$options;
let numItems = 0;
const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
if (dataSource && 'length' in dataSource) {
// may be an array or just object with length property
numItems = dataSource.length;
} else if (dataSource && 'gallery' in dataSource) {
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
if (dataSource.items) {
numItems = dataSource.items.length;
}
} // legacy event, before filters were introduced
const event = this.dispatch('numItems', {
dataSource,
numItems
});
return this.applyFilters('numItems', event.numItems, dataSource);
}
/**
* @param {SlideData} slideData
* @param {number} index
* @returns {Content}
*/
createContentFromData(slideData, index) {
return new Content(slideData, this, index);
}
/**
* Get item data by index.
*
* "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
* For example, it may contain properties like
* `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
*
* @param {number} index
* @returns {SlideData}
*/
getItemData(index) {
var _this$options2;
const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
/** @type {SlideData | HTMLElement} */
let dataSourceItem = {};
if (Array.isArray(dataSource)) {
// Datasource is an array of elements
dataSourceItem = dataSource[index];
} else if (dataSource && 'gallery' in dataSource) {
// dataSource has gallery property,
// thus it was created by Lightbox, based on
// gallery and children options
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
dataSourceItem = dataSource.items[index];
}
let itemData = dataSourceItem;
if (itemData instanceof Element) {
itemData = this._domElementToItemData(itemData);
} // Dispatching the itemData event,
// it's a legacy verion before filters were introduced
const event = this.dispatch('itemData', {
itemData: itemData || {},
index
});
return this.applyFilters('itemData', event.itemData, index);
}
/**
* Get array of gallery DOM elements,
* based on childSelector and gallery element.
*
* @param {HTMLElement} galleryElement
* @returns {HTMLElement[]}
*/
_getGalleryDOMElements(galleryElement) {
var _this$options3, _this$options4;
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
}
return [galleryElement];
}
/**
* Converts DOM element to item data object.
*
* @param {HTMLElement} element DOM element
* @returns {SlideData}
*/
_domElementToItemData(element) {
/** @type {SlideData} */
const itemData = {
element
};
const linkEl =
/** @type {HTMLAnchorElement} */
element.tagName === 'A' ? element : element.querySelector('a');
if (linkEl) {
// src comes from data-pswp-src attribute,
// if it's empty link href is used
itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
if (linkEl.dataset.pswpSrcset) {
itemData.srcset = linkEl.dataset.pswpSrcset;
}
itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
itemData.w = itemData.width;
itemData.h = itemData.height;
if (linkEl.dataset.pswpType) {
itemData.type = linkEl.dataset.pswpType;
}
const thumbnailEl = element.querySelector('img');
if (thumbnailEl) {
var _thumbnailEl$getAttri;
// msrc is URL to placeholder image that's displayed before large image is loaded
// by default it's displayed only for the first slide
itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
}
if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
itemData.thumbCropped = true;
}
}
return this.applyFilters('domItemData', itemData, element, linkEl);
}
/**
* Lazy-load by slide data
*
* @param {SlideData} itemData Data about the slide
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
lazyLoadData(itemData, index) {
return lazyLoadData(itemData, this, index);
}
}
/**
* @template T
* @typedef {import('../types.js').Type} Type
*/
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/content.js').default} Content */
/** @typedef {import('../core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
/** @typedef {import('../core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('../core/eventable.js').EventCallback} EventCallback
*/
/**
* PhotoSwipe Lightbox
*
* - If user has unsupported browser it falls back to default browser action (just opens URL)
* - Binds click event to links that should open PhotoSwipe
* - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes)
* - Initializes PhotoSwipe
*
*
* Loader options use the same object as PhotoSwipe, and supports such options:
*
* gallery - Element | Element[] | NodeList | string selector for the gallery element
* children - Element | Element[] | NodeList | string selector for the gallery children
*
*/
class PhotoSwipeLightbox extends PhotoSwipeBase {
/**
* @param {PhotoSwipeOptions} [options]
*/
constructor(options) {
super();
/** @type {PhotoSwipeOptions} */
this.options = options || {};
this._uid = 0;
this.shouldOpen = false;
/**
* @private
* @type {Content | undefined}
*/
this._preloadedContent = undefined;
this.onThumbnailsClick = this.onThumbnailsClick.bind(this);
}
/**
* Initialize lightbox, should be called only once.
* It's not included in the main constructor, so you may bind events before it.
*/
init() {
// Bind click events to each gallery
getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => {
galleryElement.addEventListener('click', this.onThumbnailsClick, false);
});
}
/**
* @param {MouseEvent} e
*/
onThumbnailsClick(e) {
// Exit and allow default browser action if:
if (specialKeyUsed(e) // ... if clicked with a special key (ctrl/cmd...)
|| window.pswp) {
// ... if PhotoSwipe is already open
return;
} // If both clientX and clientY are 0 or not defined,
// the event is likely triggered by keyboard,
// so we do not pass the initialPoint
//
// Note that some screen readers emulate the mouse position,
// so it's not the ideal way to detect them.
//
/** @type {Point | null} */
let initialPoint = {
x: e.clientX,
y: e.clientY
};
if (!initialPoint.x && !initialPoint.y) {
initialPoint = null;
}
let clickedIndex = this.getClickedIndex(e);
clickedIndex = this.applyFilters('clickedIndex', clickedIndex, e, this);
/** @type {DataSource} */
const dataSource = {
gallery:
/** @type {HTMLElement} */
e.currentTarget
};
if (clickedIndex >= 0) {
e.preventDefault();
this.loadAndOpen(clickedIndex, dataSource, initialPoint);
}
}
/**
* Get index of gallery item that was clicked.
*
* @param {MouseEvent} e click event
* @returns {number}
*/
getClickedIndex(e) {
// legacy option
if (this.options.getClickedIndexFn) {
return this.options.getClickedIndexFn.call(this, e);
}
const clickedTarget =
/** @type {HTMLElement} */
e.target;
const childElements = getElementsFromOption(this.options.children, this.options.childSelector,
/** @type {HTMLElement} */
e.currentTarget);
const clickedChildIndex = childElements.findIndex(child => child === clickedTarget || child.contains(clickedTarget));
if (clickedChildIndex !== -1) {
return clickedChildIndex;
} else if (this.options.children || this.options.childSelector) {
// click wasn't on a child element
return -1;
} // There is only one item (which is the gallery)
return 0;
}
/**
* Load and open PhotoSwipe
*
* @param {number} index
* @param {DataSource} [dataSource]
* @param {Point | null} [initialPoint]
* @returns {boolean}
*/
loadAndOpen(index, dataSource, initialPoint) {
// Check if the gallery is already open
if (window.pswp || !this.options) {
return false;
} // Use the first gallery element if dataSource is not provided
if (!dataSource && this.options.gallery && this.options.children) {
const galleryElements = getElementsFromOption(this.options.gallery);
if (galleryElements[0]) {
dataSource = {
gallery: galleryElements[0]
};
}
} // set initial index
this.options.index = index; // define options for PhotoSwipe constructor
this.options.initialPointerPos = initialPoint;
this.shouldOpen = true;
this.preload(index, dataSource);
return true;
}
/**
* Load the main module and the slide content by index
*
* @param {number} index
* @param {DataSource} [dataSource]
*/
preload(index, dataSource) {
const {
options
} = this;
if (dataSource) {
options.dataSource = dataSource;
} // Add the main module
/** @type {Promise>[]} */
const promiseArray = [];
const pswpModuleType = typeof options.pswpModule;
if (isPswpClass(options.pswpModule)) {
promiseArray.push(Promise.resolve(
/** @type {Type} */
options.pswpModule));
} else if (pswpModuleType === 'string') {
throw new Error('pswpModule as string is no longer supported');
} else if (pswpModuleType === 'function') {
promiseArray.push(
/** @type {() => Promise>} */
options.pswpModule());
} else {
throw new Error('pswpModule is not valid');
} // Add custom-defined promise, if any
if (typeof options.openPromise === 'function') {
// allow developers to perform some task before opening
promiseArray.push(options.openPromise());
}
if (options.preloadFirstSlide !== false && index >= 0) {
this._preloadedContent = lazyLoadSlide(index, this);
} // Wait till all promises resolve and open PhotoSwipe
const uid = ++this._uid;
Promise.all(promiseArray).then(iterableModules => {
if (this.shouldOpen) {
const mainModule = iterableModules[0];
this._openPhotoswipe(mainModule, uid);
}
});
}
/**
* @private
* @param {Type | { default: Type }} module
* @param {number} uid
*/
_openPhotoswipe(module, uid) {
// Cancel opening if UID doesn't match the current one
// (if user clicked on another gallery item before current was loaded).
//
// Or if shouldOpen flag is set to false
// (developer may modify it via public API)
if (uid !== this._uid && this.shouldOpen) {
return;
}
this.shouldOpen = false; // PhotoSwipe is already open
if (window.pswp) {
return;
}
/**
* Pass data to PhotoSwipe and open init
*
* @type {PhotoSwipe}
*/
const pswp = typeof module === 'object' ? new module.default(this.options) // eslint-disable-line
: new module(this.options); // eslint-disable-line
this.pswp = pswp;
window.pswp = pswp; // map listeners from Lightbox to PhotoSwipe Core
/** @type {(keyof PhotoSwipeEventsMap)[]} */
Object.keys(this._listeners).forEach(name => {
var _this$_listeners$name;
(_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.forEach(fn => {
pswp.on(name,
/** @type {EventCallback} */
fn);
});
}); // same with filters
/** @type {(keyof PhotoSwipeFiltersMap)[]} */
Object.keys(this._filters).forEach(name => {
var _this$_filters$name;
(_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.forEach(filter => {
pswp.addFilter(name, filter.fn, filter.priority);
});
});
if (this._preloadedContent) {
pswp.contentLoader.addToCache(this._preloadedContent);
this._preloadedContent = undefined;
}
pswp.on('destroy', () => {
// clean up public variables
this.pswp = undefined;
delete window.pswp;
});
pswp.init();
}
/**
* Unbinds all events, closes PhotoSwipe if it's open.
*/
destroy() {
var _this$pswp;
(_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.destroy();
this.shouldOpen = false;
this._listeners = {};
getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => {
galleryElement.removeEventListener('click', this.onThumbnailsClick, false);
});
}
}
export { PhotoSwipeLightbox as default };
//# sourceMappingURL=photoswipe-lightbox.esm.js.map
================================================
FILE: demo-docs-website/static/photoswipe/photoswipe.css
================================================
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
.pswp {
--pswp-bg: #000;
--pswp-placeholder-bg: #222;
--pswp-root-z-index: 100000;
--pswp-preloader-color: rgba(79, 79, 79, 0.4);
--pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
/* defined via js:
--pswp-transition-duration: 333ms; */
--pswp-icon-color: #fff;
--pswp-icon-color-secondary: #4f4f4f;
--pswp-icon-stroke-color: #4f4f4f;
--pswp-icon-stroke-width: 2px;
--pswp-error-text-color: var(--pswp-icon-color);
}
/*
Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
*/
.pswp {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: var(--pswp-root-z-index);
display: none;
touch-action: none;
outline: 0;
opacity: 0.003;
contain: layout style size;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Prevents focus outline on the root element,
(it may be focused initially) */
.pswp:focus {
outline: 0;
}
.pswp * {
box-sizing: border-box;
}
.pswp img {
max-width: none;
}
.pswp--open {
display: block;
}
.pswp,
.pswp__bg {
transform: translateZ(0);
will-change: opacity;
}
.pswp__bg {
opacity: 0.005;
background: var(--pswp-bg);
}
.pswp,
.pswp__scroll-wrap {
overflow: hidden;
}
.pswp__scroll-wrap,
.pswp__bg,
.pswp__container,
.pswp__item,
.pswp__content,
.pswp__img,
.pswp__zoom-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pswp__img,
.pswp__zoom-wrap {
width: auto;
height: auto;
}
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
cursor: -webkit-zoom-in;
cursor: -moz-zoom-in;
cursor: zoom-in;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* :active to override grabbing cursor */
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
.pswp__img {
cursor: -webkit-zoom-out;
cursor: -moz-zoom-out;
cursor: zoom-out;
}
/* Prevent selection and tap highlights */
.pswp__container,
.pswp__img,
.pswp__button,
.pswp__counter {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pswp__item {
/* z-index for fade transition */
z-index: 1;
overflow: hidden;
}
.pswp__hidden {
display: none !important;
}
/* Allow to click through pswp__content element, but not its children */
.pswp__content {
pointer-events: none;
}
.pswp__content > * {
pointer-events: auto;
}
/*
PhotoSwipe UI
*/
/*
Error message appears when image is not loaded
(JS option errorMsg controls markup)
*/
.pswp__error-msg-container {
display: grid;
}
.pswp__error-msg {
margin: auto;
font-size: 1em;
line-height: 1;
color: var(--pswp-error-text-color);
}
/*
class pswp__hide-on-close is applied to elements that
should hide (for example fade out) when PhotoSwipe is closed
and show (for example fade in) when PhotoSwipe is opened
*/
.pswp .pswp__hide-on-close {
opacity: 0.005;
will-change: opacity;
transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
z-index: 10; /* always overlap slide content */
pointer-events: none; /* hidden elements should not be clickable */
}
/* class pswp--ui-visible is added when opening or closing transition starts */
.pswp--ui-visible .pswp__hide-on-close {
opacity: 1;
pointer-events: auto;
}
/* styles, including css reset */
.pswp__button {
position: relative;
display: block;
width: 50px;
height: 60px;
padding: 0;
margin: 0;
overflow: hidden;
cursor: pointer;
background: none;
border: 0;
box-shadow: none;
opacity: 0.85;
-webkit-appearance: none;
-webkit-touch-callout: none;
}
.pswp__button:hover,
.pswp__button:active,
.pswp__button:focus {
transition: none;
padding: 0;
background: none;
border: 0;
box-shadow: none;
opacity: 1;
}
.pswp__button:disabled {
opacity: 0.3;
cursor: auto;
}
.pswp__icn {
fill: var(--pswp-icon-color);
color: var(--pswp-icon-color-secondary);
}
.pswp__icn {
position: absolute;
top: 14px;
left: 9px;
width: 32px;
height: 32px;
overflow: hidden;
pointer-events: none;
}
.pswp__icn-shadow {
stroke: var(--pswp-icon-stroke-color);
stroke-width: var(--pswp-icon-stroke-width);
fill: none;
}
.pswp__icn:focus {
outline: 0;
}
/*
div element that matches size of large image,
large image loads on top of it,
used when msrc is not provided
*/
div.pswp__img--placeholder,
.pswp__img--with-bg {
background: var(--pswp-placeholder-bg);
}
.pswp__top-bar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 60px;
display: flex;
flex-direction: row;
justify-content: flex-end;
z-index: 10;
/* allow events to pass through top bar itself */
pointer-events: none !important;
}
.pswp__top-bar > * {
pointer-events: auto;
/* this makes transition significantly more smooth,
even though inner elements are not animated */
will-change: opacity;
}
/*
Close button
*/
.pswp__button--close {
margin-right: 6px;
}
/*
Arrow buttons
*/
.pswp__button--arrow {
position: absolute;
top: 0;
width: 75px;
height: 100px;
top: 50%;
margin-top: -50px;
}
.pswp__button--arrow:disabled {
display: none;
cursor: default;
}
.pswp__button--arrow .pswp__icn {
top: 50%;
margin-top: -30px;
width: 60px;
height: 60px;
background: none;
border-radius: 0;
}
.pswp--one-slide .pswp__button--arrow {
display: none;
}
/* hide arrows on touch screens */
.pswp--touch .pswp__button--arrow {
visibility: hidden;
}
/* show arrows only after mouse was used */
.pswp--has_mouse .pswp__button--arrow {
visibility: visible;
}
.pswp__button--arrow--prev {
right: auto;
left: 0px;
}
.pswp__button--arrow--next {
right: 0px;
}
.pswp__button--arrow--next .pswp__icn {
left: auto;
right: 14px;
/* flip horizontally */
transform: scale(-1, 1);
}
/*
Zoom button
*/
.pswp__button--zoom {
display: none;
}
.pswp--zoom-allowed .pswp__button--zoom {
display: block;
}
/* "+" => "-" */
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
display: none;
}
/*
Loading indicator
*/
.pswp__preloader {
position: relative;
overflow: hidden;
width: 50px;
height: 60px;
margin-right: auto;
}
.pswp__preloader .pswp__icn {
opacity: 0;
transition: opacity 0.2s linear;
animation: pswp-clockwise 600ms linear infinite;
}
.pswp__preloader--active .pswp__icn {
opacity: 0.85;
}
@keyframes pswp-clockwise {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/*
"1 of 10" counter
*/
.pswp__counter {
height: 30px;
margin-top: 15px;
margin-inline-start: 20px;
font-size: 14px;
line-height: 30px;
color: var(--pswp-icon-color);
text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
opacity: 0.85;
}
.pswp--one-slide .pswp__counter {
display: none;
}
================================================
FILE: demo-docs-website/static/photoswipe/photoswipe.esm.js
================================================
/*!
* PhotoSwipe 5.4.4 - https://photoswipe.com
* (c) 2024 Dmytro Semenov
*/
/** @typedef {import('../photoswipe.js').Point} Point */
/**
* @template {keyof HTMLElementTagNameMap} T
* @param {string} className
* @param {T} tagName
* @param {Node} [appendToEl]
* @returns {HTMLElementTagNameMap[T]}
*/
function createElement(className, tagName, appendToEl) {
const el = document.createElement(tagName);
if (className) {
el.className = className;
}
if (appendToEl) {
appendToEl.appendChild(el);
}
return el;
}
/**
* @param {Point} p1
* @param {Point} p2
* @returns {Point}
*/
function equalizePoints(p1, p2) {
p1.x = p2.x;
p1.y = p2.y;
if (p2.id !== undefined) {
p1.id = p2.id;
}
return p1;
}
/**
* @param {Point} p
*/
function roundPoint(p) {
p.x = Math.round(p.x);
p.y = Math.round(p.y);
}
/**
* Returns distance between two points.
*
* @param {Point} p1
* @param {Point} p2
* @returns {number}
*/
function getDistanceBetween(p1, p2) {
const x = Math.abs(p1.x - p2.x);
const y = Math.abs(p1.y - p2.y);
return Math.sqrt(x * x + y * y);
}
/**
* Whether X and Y positions of points are equal
*
* @param {Point} p1
* @param {Point} p2
* @returns {boolean}
*/
function pointsEqual(p1, p2) {
return p1.x === p2.x && p1.y === p2.y;
}
/**
* The float result between the min and max values.
*
* @param {number} val
* @param {number} min
* @param {number} max
* @returns {number}
*/
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
/**
* Get transform string
*
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
* @returns {string}
*/
function toTransformString(x, y, scale) {
let propValue = `translate3d(${x}px,${y || 0}px,0)`;
if (scale !== undefined) {
propValue += ` scale3d(${scale},${scale},1)`;
}
return propValue;
}
/**
* Apply transform:translate(x, y) scale(scale) to element
*
* @param {HTMLElement} el
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
*/
function setTransform(el, x, y, scale) {
el.style.transform = toTransformString(x, y, scale);
}
const defaultCSSEasing = 'cubic-bezier(.4,0,.22,1)';
/**
* Apply CSS transition to element
*
* @param {HTMLElement} el
* @param {string} [prop] CSS property to animate
* @param {number} [duration] in ms
* @param {string} [ease] CSS easing function
*/
function setTransitionStyle(el, prop, duration, ease) {
// inOut: 'cubic-bezier(.4, 0, .22, 1)', // for "toggle state" transitions
// out: 'cubic-bezier(0, 0, .22, 1)', // for "show" transitions
// in: 'cubic-bezier(.4, 0, 1, 1)'// for "hide" transitions
el.style.transition = prop ? `${prop} ${duration}ms ${ease || defaultCSSEasing}` : 'none';
}
/**
* Apply width and height CSS properties to element
*
* @param {HTMLElement} el
* @param {string | number} w
* @param {string | number} h
*/
function setWidthHeight(el, w, h) {
el.style.width = typeof w === 'number' ? `${w}px` : w;
el.style.height = typeof h === 'number' ? `${h}px` : h;
}
/**
* @param {HTMLElement} el
*/
function removeTransitionStyle(el) {
setTransitionStyle(el);
}
/**
* @param {HTMLImageElement} img
* @returns {Promise}
*/
function decodeImage(img) {
if ('decode' in img) {
return img.decode().catch(() => {});
}
if (img.complete) {
return Promise.resolve(img);
}
return new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});
}
/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
const LOAD_STATE = {
IDLE: 'idle',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error'
};
/**
* Check if click or keydown event was dispatched
* with a special key or via mouse wheel.
*
* @param {MouseEvent | KeyboardEvent} e
* @returns {boolean}
*/
function specialKeyUsed(e) {
return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
}
/**
* Parse `gallery` or `children` options.
*
* @param {import('../photoswipe.js').ElementProvider} [option]
* @param {string} [legacySelector]
* @param {HTMLElement | Document} [parent]
* @returns HTMLElement[]
*/
function getElementsFromOption(option, legacySelector, parent = document) {
/** @type {HTMLElement[]} */
let elements = [];
if (option instanceof Element) {
elements = [option];
} else if (option instanceof NodeList || Array.isArray(option)) {
elements = Array.from(option);
} else {
const selector = typeof option === 'string' ? option : legacySelector;
if (selector) {
elements = Array.from(parent.querySelectorAll(selector));
}
}
return elements;
}
/**
* Check if browser is Safari
*
* @returns {boolean}
*/
function isSafari() {
return !!(navigator.vendor && navigator.vendor.match(/apple/i));
}
// Detect passive event listener support
let supportsPassive = false;
/* eslint-disable */
try {
/* @ts-ignore */
window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
get: () => {
supportsPassive = true;
}
}));
} catch (e) {}
/* eslint-enable */
/**
* @typedef {Object} PoolItem
* @prop {HTMLElement | Window | Document | undefined | null} target
* @prop {string} type
* @prop {EventListenerOrEventListenerObject} listener
* @prop {boolean} [passive]
*/
class DOMEvents {
constructor() {
/**
* @type {PoolItem[]}
* @private
*/
this._pool = [];
}
/**
* Adds event listeners
*
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type Can be multiple, separated by space.
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
*/
add(target, type, listener, passive) {
this._toggleListener(target, type, listener, passive);
}
/**
* Removes event listeners
*
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
*/
remove(target, type, listener, passive) {
this._toggleListener(target, type, listener, passive, true);
}
/**
* Removes all bound events
*/
removeAll() {
this._pool.forEach(poolItem => {
this._toggleListener(poolItem.target, poolItem.type, poolItem.listener, poolItem.passive, true, true);
});
this._pool = [];
}
/**
* Adds or removes event
*
* @private
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
* @param {boolean} [unbind] Whether the event should be added or removed
* @param {boolean} [skipPool] Whether events pool should be skipped
*/
_toggleListener(target, type, listener, passive, unbind, skipPool) {
if (!target) {
return;
}
const methodName = unbind ? 'removeEventListener' : 'addEventListener';
const types = type.split(' ');
types.forEach(eType => {
if (eType) {
// Events pool is used to easily unbind all events when PhotoSwipe is closed,
// so developer doesn't need to do this manually
if (!skipPool) {
if (unbind) {
// Remove from the events pool
this._pool = this._pool.filter(poolItem => {
return poolItem.type !== eType || poolItem.listener !== listener || poolItem.target !== target;
});
} else {
// Add to the events pool
this._pool.push({
target,
type: eType,
listener,
passive
});
}
} // most PhotoSwipe events call preventDefault,
// and we do not need browser to scroll the page
const eventOptions = supportsPassive ? {
passive: passive || false
} : false;
target[methodName](eType, listener, eventOptions);
}
});
}
}
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/**
* @param {PhotoSwipeOptions} options
* @param {PhotoSwipeBase} pswp
* @returns {Point}
*/
function getViewportSize(options, pswp) {
if (options.getViewportSizeFn) {
const newViewportSize = options.getViewportSizeFn(options, pswp);
if (newViewportSize) {
return newViewportSize;
}
}
return {
x: document.documentElement.clientWidth,
// TODO: height on mobile is very incosistent due to toolbar
// find a way to improve this
//
// document.documentElement.clientHeight - doesn't seem to work well
y: window.innerHeight
};
}
/**
* Parses padding option.
* Supported formats:
*
* // Object
* padding: {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* }
*
* // A function that returns the object
* paddingFn: (viewportSize, itemData, index) => {
* return {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* };
* }
*
* // Legacy variant
* paddingLeft: 0,
* paddingRight: 0,
* paddingTop: 0,
* paddingBottom: 0,
*
* @param {'left' | 'top' | 'bottom' | 'right'} prop
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
* @param {SlideData} itemData Data about the slide
* @param {number} index Slide index
* @returns {number}
*/
function parsePaddingOption(prop, options, viewportSize, itemData, index) {
let paddingValue = 0;
if (options.paddingFn) {
paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
} else if (options.padding) {
paddingValue = options.padding[prop];
} else {
const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
if (options[legacyPropName]) {
// @ts-expect-error
paddingValue = options[legacyPropName];
}
}
return Number(paddingValue) || 0;
}
/**
* @param {PhotoSwipeOptions} options
* @param {Point} viewportSize
* @param {SlideData} itemData
* @param {number} index
* @returns {Point}
*/
function getPanAreaSize(options, viewportSize, itemData, index) {
return {
x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
};
}
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {Record} Point */
/** @typedef {'x' | 'y'} Axis */
/**
* Calculates minimum, maximum and initial (center) bounds of a slide
*/
class PanBounds {
/**
* @param {Slide} slide
*/
constructor(slide) {
this.slide = slide;
this.currZoomLevel = 1;
this.center =
/** @type {Point} */
{
x: 0,
y: 0
};
this.max =
/** @type {Point} */
{
x: 0,
y: 0
};
this.min =
/** @type {Point} */
{
x: 0,
y: 0
};
}
/**
* _getItemBounds
*
* @param {number} currZoomLevel
*/
update(currZoomLevel) {
this.currZoomLevel = currZoomLevel;
if (!this.slide.width) {
this.reset();
} else {
this._updateAxis('x');
this._updateAxis('y');
this.slide.pswp.dispatch('calcBounds', {
slide: this.slide
});
}
}
/**
* _calculateItemBoundsForAxis
*
* @param {Axis} axis
*/
_updateAxis(axis) {
const {
pswp
} = this.slide;
const elSize = this.slide[axis === 'x' ? 'width' : 'height'] * this.currZoomLevel;
const paddingProp = axis === 'x' ? 'left' : 'top';
const padding = parsePaddingOption(paddingProp, pswp.options, pswp.viewportSize, this.slide.data, this.slide.index);
const panAreaSize = this.slide.panAreaSize[axis]; // Default position of element.
// By default, it is center of viewport:
this.center[axis] = Math.round((panAreaSize - elSize) / 2) + padding; // maximum pan position
this.max[axis] = elSize > panAreaSize ? Math.round(panAreaSize - elSize) + padding : this.center[axis]; // minimum pan position
this.min[axis] = elSize > panAreaSize ? padding : this.center[axis];
} // _getZeroBounds
reset() {
this.center.x = 0;
this.center.y = 0;
this.max.x = 0;
this.max.y = 0;
this.min.x = 0;
this.min.y = 0;
}
/**
* Correct pan position if it's beyond the bounds
*
* @param {Axis} axis x or y
* @param {number} panOffset
* @returns {number}
*/
correctPan(axis, panOffset) {
// checkPanBounds
return clamp(panOffset, this.max[axis], this.min[axis]);
}
}
const MAX_IMAGE_WIDTH = 4000;
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
/**
* Calculates zoom levels for specific slide.
* Depends on viewport size and image size.
*/
class ZoomLevel {
/**
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {SlideData} itemData Slide data
* @param {number} index Slide index
* @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
*/
constructor(options, itemData, index, pswp) {
this.pswp = pswp;
this.options = options;
this.itemData = itemData;
this.index = index;
/** @type { Point | null } */
this.panAreaSize = null;
/** @type { Point | null } */
this.elementSize = null;
this.fit = 1;
this.fill = 1;
this.vFill = 1;
this.initial = 1;
this.secondary = 1;
this.max = 1;
this.min = 1;
}
/**
* Calculate initial, secondary and maximum zoom level for the specified slide.
*
* It should be called when either image or viewport size changes.
*
* @param {number} maxWidth
* @param {number} maxHeight
* @param {Point} panAreaSize
*/
update(maxWidth, maxHeight, panAreaSize) {
/** @type {Point} */
const elementSize = {
x: maxWidth,
y: maxHeight
};
this.elementSize = elementSize;
this.panAreaSize = panAreaSize;
const hRatio = panAreaSize.x / elementSize.x;
const vRatio = panAreaSize.y / elementSize.y;
this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
// when it has 100% of viewport vertical space (height)
this.vFill = Math.min(1, vRatio);
this.initial = this._getInitial();
this.secondary = this._getSecondary();
this.max = Math.max(this.initial, this.secondary, this._getMax());
this.min = Math.min(this.fit, this.initial, this.secondary);
if (this.pswp) {
this.pswp.dispatch('zoomLevelsUpdate', {
zoomLevels: this,
slideData: this.itemData
});
}
}
/**
* Parses user-defined zoom option.
*
* @private
* @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
* @returns { number | undefined }
*/
_parseZoomLevelOption(optionPrefix) {
const optionName =
/** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
optionPrefix + 'ZoomLevel';
const optionValue = this.options[optionName];
if (!optionValue) {
return;
}
if (typeof optionValue === 'function') {
return optionValue(this);
}
if (optionValue === 'fill') {
return this.fill;
}
if (optionValue === 'fit') {
return this.fit;
}
return Number(optionValue);
}
/**
* Get zoom level to which image will be zoomed after double-tap gesture,
* or when user clicks on zoom icon,
* or mouse-click on image itself.
* If you return 1 image will be zoomed to its original size.
*
* @private
* @return {number}
*/
_getSecondary() {
let currZoomLevel = this._parseZoomLevelOption('secondary');
if (currZoomLevel) {
return currZoomLevel;
} // 3x of "fit" state, but not larger than original
currZoomLevel = Math.min(1, this.fit * 3);
if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
}
return currZoomLevel;
}
/**
* Get initial image zoom level.
*
* @private
* @return {number}
*/
_getInitial() {
return this._parseZoomLevelOption('initial') || this.fit;
}
/**
* Maximum zoom level when user zooms
* via zoom/pinch gesture,
* via cmd/ctrl-wheel or via trackpad.
*
* @private
* @return {number}
*/
_getMax() {
// max zoom level is x4 from "fit state",
// used for zoom gesture and ctrl/trackpad zoom
return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
}
}
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/**
* Renders and allows to control a single slide
*/
class Slide {
/**
* @param {SlideData} data
* @param {number} index
* @param {PhotoSwipe} pswp
*/
constructor(data, index, pswp) {
this.data = data;
this.index = index;
this.pswp = pswp;
this.isActive = index === pswp.currIndex;
this.currentResolution = 0;
/** @type {Point} */
this.panAreaSize = {
x: 0,
y: 0
};
/** @type {Point} */
this.pan = {
x: 0,
y: 0
};
this.isFirstSlide = this.isActive && !pswp.opener.isOpen;
this.zoomLevels = new ZoomLevel(pswp.options, data, index, pswp);
this.pswp.dispatch('gettingData', {
slide: this,
data: this.data,
index
});
this.content = this.pswp.contentLoader.getContentBySlide(this);
this.container = createElement('pswp__zoom-wrap', 'div');
/** @type {HTMLElement | null} */
this.holderElement = null;
this.currZoomLevel = 1;
/** @type {number} */
this.width = this.content.width;
/** @type {number} */
this.height = this.content.height;
this.heavyAppended = false;
this.bounds = new PanBounds(this);
this.prevDisplayedWidth = -1;
this.prevDisplayedHeight = -1;
this.pswp.dispatch('slideInit', {
slide: this
});
}
/**
* If this slide is active/current/visible
*
* @param {boolean} isActive
*/
setIsActive(isActive) {
if (isActive && !this.isActive) {
// slide just became active
this.activate();
} else if (!isActive && this.isActive) {
// slide just became non-active
this.deactivate();
}
}
/**
* Appends slide content to DOM
*
* @param {HTMLElement} holderElement
*/
append(holderElement) {
this.holderElement = holderElement;
this.container.style.transformOrigin = '0 0'; // Slide appended to DOM
if (!this.data) {
return;
}
this.calculateSize();
this.load();
this.updateContentSize();
this.appendHeavy();
this.holderElement.appendChild(this.container);
this.zoomAndPanToInitial();
this.pswp.dispatch('firstZoomPan', {
slide: this
});
this.applyCurrentZoomPan();
this.pswp.dispatch('afterSetContent', {
slide: this
});
if (this.isActive) {
this.activate();
}
}
load() {
this.content.load(false);
this.pswp.dispatch('slideLoad', {
slide: this
});
}
/**
* Append "heavy" DOM elements
*
* This may depend on a type of slide,
* but generally these are large images.
*/
appendHeavy() {
const {
pswp
} = this;
const appendHeavyNearby = true; // todo
// Avoid appending heavy elements during animations
if (this.heavyAppended || !pswp.opener.isOpen || pswp.mainScroll.isShifted() || !this.isActive && !appendHeavyNearby) {
return;
}
if (this.pswp.dispatch('appendHeavy', {
slide: this
}).defaultPrevented) {
return;
}
this.heavyAppended = true;
this.content.append();
this.pswp.dispatch('appendHeavyContent', {
slide: this
});
}
/**
* Triggered when this slide is active (selected).
*
* If it's part of opening/closing transition -
* activate() will trigger after the transition is ended.
*/
activate() {
this.isActive = true;
this.appendHeavy();
this.content.activate();
this.pswp.dispatch('slideActivate', {
slide: this
});
}
/**
* Triggered when this slide becomes inactive.
*
* Slide can become inactive only after it was active.
*/
deactivate() {
this.isActive = false;
this.content.deactivate();
if (this.currZoomLevel !== this.zoomLevels.initial) {
// allow filtering
this.calculateSize();
} // reset zoom level
this.currentResolution = 0;
this.zoomAndPanToInitial();
this.applyCurrentZoomPan();
this.updateContentSize();
this.pswp.dispatch('slideDeactivate', {
slide: this
});
}
/**
* The slide should destroy itself, it will never be used again.
* (unbind all events and destroy internal components)
*/
destroy() {
this.content.hasSlide = false;
this.content.remove();
this.container.remove();
this.pswp.dispatch('slideDestroy', {
slide: this
});
}
resize() {
if (this.currZoomLevel === this.zoomLevels.initial || !this.isActive) {
// Keep initial zoom level if it was before the resize,
// as well as when this slide is not active
// Reset position and scale to original state
this.calculateSize();
this.currentResolution = 0;
this.zoomAndPanToInitial();
this.applyCurrentZoomPan();
this.updateContentSize();
} else {
// readjust pan position if it's beyond the bounds
this.calculateSize();
this.bounds.update(this.currZoomLevel);
this.panTo(this.pan.x, this.pan.y);
}
}
/**
* Apply size to current slide content,
* based on the current resolution and scale.
*
* @param {boolean} [force] if size should be updated even if dimensions weren't changed
*/
updateContentSize(force) {
// Use initial zoom level
// if resolution is not defined (user didn't zoom yet)
const scaleMultiplier = this.currentResolution || this.zoomLevels.initial;
if (!scaleMultiplier) {
return;
}
const width = Math.round(this.width * scaleMultiplier) || this.pswp.viewportSize.x;
const height = Math.round(this.height * scaleMultiplier) || this.pswp.viewportSize.y;
if (!this.sizeChanged(width, height) && !force) {
return;
}
this.content.setDisplayedSize(width, height);
}
/**
* @param {number} width
* @param {number} height
*/
sizeChanged(width, height) {
if (width !== this.prevDisplayedWidth || height !== this.prevDisplayedHeight) {
this.prevDisplayedWidth = width;
this.prevDisplayedHeight = height;
return true;
}
return false;
}
/** @returns {HTMLImageElement | HTMLDivElement | null | undefined} */
getPlaceholderElement() {
var _this$content$placeho;
return (_this$content$placeho = this.content.placeholder) === null || _this$content$placeho === void 0 ? void 0 : _this$content$placeho.element;
}
/**
* Zoom current slide image to...
*
* @param {number} destZoomLevel Destination zoom level.
* @param {Point} [centerPoint]
* Transform origin center point, or false if viewport center should be used.
* @param {number | false} [transitionDuration] Transition duration, may be set to 0.
* @param {boolean} [ignoreBounds] Minimum and maximum zoom levels will be ignored.
*/
zoomTo(destZoomLevel, centerPoint, transitionDuration, ignoreBounds) {
const {
pswp
} = this;
if (!this.isZoomable() || pswp.mainScroll.isShifted()) {
return;
}
pswp.dispatch('beforeZoomTo', {
destZoomLevel,
centerPoint,
transitionDuration
}); // stop all pan and zoom transitions
pswp.animations.stopAllPan(); // if (!centerPoint) {
// centerPoint = pswp.getViewportCenterPoint();
// }
const prevZoomLevel = this.currZoomLevel;
if (!ignoreBounds) {
destZoomLevel = clamp(destZoomLevel, this.zoomLevels.min, this.zoomLevels.max);
} // if (transitionDuration === undefined) {
// transitionDuration = this.pswp.options.zoomAnimationDuration;
// }
this.setZoomLevel(destZoomLevel);
this.pan.x = this.calculateZoomToPanOffset('x', centerPoint, prevZoomLevel);
this.pan.y = this.calculateZoomToPanOffset('y', centerPoint, prevZoomLevel);
roundPoint(this.pan);
const finishTransition = () => {
this._setResolution(destZoomLevel);
this.applyCurrentZoomPan();
};
if (!transitionDuration) {
finishTransition();
} else {
pswp.animations.startTransition({
isPan: true,
name: 'zoomTo',
target: this.container,
transform: this.getCurrentTransform(),
onComplete: finishTransition,
duration: transitionDuration,
easing: pswp.options.easing
});
}
}
/**
* @param {Point} [centerPoint]
*/
toggleZoom(centerPoint) {
this.zoomTo(this.currZoomLevel === this.zoomLevels.initial ? this.zoomLevels.secondary : this.zoomLevels.initial, centerPoint, this.pswp.options.zoomAnimationDuration);
}
/**
* Updates zoom level property and recalculates new pan bounds,
* unlike zoomTo it does not apply transform (use applyCurrentZoomPan)
*
* @param {number} currZoomLevel
*/
setZoomLevel(currZoomLevel) {
this.currZoomLevel = currZoomLevel;
this.bounds.update(this.currZoomLevel);
}
/**
* Get pan position after zoom at a given `point`.
*
* Always call setZoomLevel(newZoomLevel) beforehand to recalculate
* pan bounds according to the new zoom level.
*
* @param {'x' | 'y'} axis
* @param {Point} [point]
* point based on which zoom is performed, usually refers to the current mouse position,
* if false - viewport center will be used.
* @param {number} [prevZoomLevel] Zoom level before new zoom was applied.
* @returns {number}
*/
calculateZoomToPanOffset(axis, point, prevZoomLevel) {
const totalPanDistance = this.bounds.max[axis] - this.bounds.min[axis];
if (totalPanDistance === 0) {
return this.bounds.center[axis];
}
if (!point) {
point = this.pswp.getViewportCenterPoint();
}
if (!prevZoomLevel) {
prevZoomLevel = this.zoomLevels.initial;
}
const zoomFactor = this.currZoomLevel / prevZoomLevel;
return this.bounds.correctPan(axis, (this.pan[axis] - point[axis]) * zoomFactor + point[axis]);
}
/**
* Apply pan and keep it within bounds.
*
* @param {number} panX
* @param {number} panY
*/
panTo(panX, panY) {
this.pan.x = this.bounds.correctPan('x', panX);
this.pan.y = this.bounds.correctPan('y', panY);
this.applyCurrentZoomPan();
}
/**
* If the slide in the current state can be panned by the user
* @returns {boolean}
*/
isPannable() {
return Boolean(this.width) && this.currZoomLevel > this.zoomLevels.fit;
}
/**
* If the slide can be zoomed
* @returns {boolean}
*/
isZoomable() {
return Boolean(this.width) && this.content.isZoomable();
}
/**
* Apply transform and scale based on
* the current pan position (this.pan) and zoom level (this.currZoomLevel)
*/
applyCurrentZoomPan() {
this._applyZoomTransform(this.pan.x, this.pan.y, this.currZoomLevel);
if (this === this.pswp.currSlide) {
this.pswp.dispatch('zoomPanUpdate', {
slide: this
});
}
}
zoomAndPanToInitial() {
this.currZoomLevel = this.zoomLevels.initial; // pan according to the zoom level
this.bounds.update(this.currZoomLevel);
equalizePoints(this.pan, this.bounds.center);
this.pswp.dispatch('initialZoomPan', {
slide: this
});
}
/**
* Set translate and scale based on current resolution
*
* @param {number} x
* @param {number} y
* @param {number} zoom
* @private
*/
_applyZoomTransform(x, y, zoom) {
zoom /= this.currentResolution || this.zoomLevels.initial;
setTransform(this.container, x, y, zoom);
}
calculateSize() {
const {
pswp
} = this;
equalizePoints(this.panAreaSize, getPanAreaSize(pswp.options, pswp.viewportSize, this.data, this.index));
this.zoomLevels.update(this.width, this.height, this.panAreaSize);
pswp.dispatch('calcSlideSize', {
slide: this
});
}
/** @returns {string} */
getCurrentTransform() {
const scale = this.currZoomLevel / (this.currentResolution || this.zoomLevels.initial);
return toTransformString(this.pan.x, this.pan.y, scale);
}
/**
* Set resolution and re-render the image.
*
* For example, if the real image size is 2000x1500,
* and resolution is 0.5 - it will be rendered as 1000x750.
*
* Image with zoom level 2 and resolution 0.5 is
* the same as image with zoom level 1 and resolution 1.
*
* Used to optimize animations and make
* sure that browser renders image in the highest quality.
* Also used by responsive images to load the correct one.
*
* @param {number} newResolution
*/
_setResolution(newResolution) {
if (newResolution === this.currentResolution) {
return;
}
this.currentResolution = newResolution;
this.updateContentSize();
this.pswp.dispatch('resolutionChanged');
}
}
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('./gestures.js').default} Gestures */
const PAN_END_FRICTION = 0.35;
const VERTICAL_DRAG_FRICTION = 0.6; // 1 corresponds to the third of viewport height
const MIN_RATIO_TO_CLOSE = 0.4; // Minimum speed required to navigate
// to next or previous slide
const MIN_NEXT_SLIDE_SPEED = 0.5;
/**
* @param {number} initialVelocity
* @param {number} decelerationRate
* @returns {number}
*/
function project(initialVelocity, decelerationRate) {
return initialVelocity * decelerationRate / (1 - decelerationRate);
}
/**
* Handles single pointer dragging
*/
class DragHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures) {
this.gestures = gestures;
this.pswp = gestures.pswp;
/** @type {Point} */
this.startPan = {
x: 0,
y: 0
};
}
start() {
if (this.pswp.currSlide) {
equalizePoints(this.startPan, this.pswp.currSlide.pan);
}
this.pswp.animations.stopAll();
}
change() {
const {
p1,
prevP1,
dragAxis
} = this.gestures;
const {
currSlide
} = this.pswp;
if (dragAxis === 'y' && this.pswp.options.closeOnVerticalDrag && currSlide && currSlide.currZoomLevel <= currSlide.zoomLevels.fit && !this.gestures.isMultitouch) {
// Handle vertical drag to close
const panY = currSlide.pan.y + (p1.y - prevP1.y);
if (!this.pswp.dispatch('verticalDrag', {
panY
}).defaultPrevented) {
this._setPanWithFriction('y', panY, VERTICAL_DRAG_FRICTION);
const bgOpacity = 1 - Math.abs(this._getVerticalDragRatio(currSlide.pan.y));
this.pswp.applyBgOpacity(bgOpacity);
currSlide.applyCurrentZoomPan();
}
} else {
const mainScrollChanged = this._panOrMoveMainScroll('x');
if (!mainScrollChanged) {
this._panOrMoveMainScroll('y');
if (currSlide) {
roundPoint(currSlide.pan);
currSlide.applyCurrentZoomPan();
}
}
}
}
end() {
const {
velocity
} = this.gestures;
const {
mainScroll,
currSlide
} = this.pswp;
let indexDiff = 0;
this.pswp.animations.stopAll(); // Handle main scroll if it's shifted
if (mainScroll.isShifted()) {
// Position of the main scroll relative to the viewport
const mainScrollShiftDiff = mainScroll.x - mainScroll.getCurrSlideX(); // Ratio between 0 and 1:
// 0 - slide is not visible at all,
// 0.5 - half of the slide is visible
// 1 - slide is fully visible
const currentSlideVisibilityRatio = mainScrollShiftDiff / this.pswp.viewportSize.x; // Go next slide.
//
// - if velocity and its direction is matched,
// and we see at least tiny part of the next slide
//
// - or if we see less than 50% of the current slide
// and velocity is close to 0
//
if (velocity.x < -MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio < 0 || velocity.x < 0.1 && currentSlideVisibilityRatio < -0.5) {
// Go to next slide
indexDiff = 1;
velocity.x = Math.min(velocity.x, 0);
} else if (velocity.x > MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio > 0 || velocity.x > -0.1 && currentSlideVisibilityRatio > 0.5) {
// Go to prev slide
indexDiff = -1;
velocity.x = Math.max(velocity.x, 0);
}
mainScroll.moveIndexBy(indexDiff, true, velocity.x);
} // Restore zoom level
if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.max || this.gestures.isMultitouch) {
this.gestures.zoomLevels.correctZoomPan(true);
} else {
// we run two animations instead of one,
// as each axis has own pan boundaries and thus different spring function
// (correctZoomPan does not have this functionality,
// it animates all properties with single timing function)
this._finishPanGestureForAxis('x');
this._finishPanGestureForAxis('y');
}
}
/**
* @private
* @param {'x' | 'y'} axis
*/
_finishPanGestureForAxis(axis) {
const {
velocity
} = this.gestures;
const {
currSlide
} = this.pswp;
if (!currSlide) {
return;
}
const {
pan,
bounds
} = currSlide;
const panPos = pan[axis];
const restoreBgOpacity = this.pswp.bgOpacity < 1 && axis === 'y'; // 0.995 means - scroll view loses 0.5% of its velocity per millisecond
// Increasing this number will reduce travel distance
const decelerationRate = 0.995; // 0.99
// Pan position if there is no bounds
const projectedPosition = panPos + project(velocity[axis], decelerationRate);
if (restoreBgOpacity) {
const vDragRatio = this._getVerticalDragRatio(panPos);
const projectedVDragRatio = this._getVerticalDragRatio(projectedPosition); // If we are above and moving upwards,
// or if we are below and moving downwards
if (vDragRatio < 0 && projectedVDragRatio < -MIN_RATIO_TO_CLOSE || vDragRatio > 0 && projectedVDragRatio > MIN_RATIO_TO_CLOSE) {
this.pswp.close();
return;
}
} // Pan position with corrected bounds
const correctedPanPosition = bounds.correctPan(axis, projectedPosition); // Exit if pan position should not be changed
// or if speed it too low
if (panPos === correctedPanPosition) {
return;
} // Overshoot if the final position is out of pan bounds
const dampingRatio = correctedPanPosition === projectedPosition ? 1 : 0.82;
const initialBgOpacity = this.pswp.bgOpacity;
const totalPanDist = correctedPanPosition - panPos;
this.pswp.animations.startSpring({
name: 'panGesture' + axis,
isPan: true,
start: panPos,
end: correctedPanPosition,
velocity: velocity[axis],
dampingRatio,
onUpdate: pos => {
// Animate opacity of background relative to Y pan position of an image
if (restoreBgOpacity && this.pswp.bgOpacity < 1) {
// 0 - start of animation, 1 - end of animation
const animationProgressRatio = 1 - (correctedPanPosition - pos) / totalPanDist; // We clamp opacity to keep it between 0 and 1.
// As progress ratio can be larger than 1 due to overshoot,
// and we do not want to bounce opacity.
this.pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * animationProgressRatio, 0, 1));
}
pan[axis] = Math.floor(pos);
currSlide.applyCurrentZoomPan();
}
});
}
/**
* Update position of the main scroll,
* or/and update pan position of the current slide.
*
* Should return true if it changes (or can change) main scroll.
*
* @private
* @param {'x' | 'y'} axis
* @returns {boolean}
*/
_panOrMoveMainScroll(axis) {
const {
p1,
dragAxis,
prevP1,
isMultitouch
} = this.gestures;
const {
currSlide,
mainScroll
} = this.pswp;
const delta = p1[axis] - prevP1[axis];
const newMainScrollX = mainScroll.x + delta;
if (!delta || !currSlide) {
return false;
} // Always move main scroll if image can not be panned
if (axis === 'x' && !currSlide.isPannable() && !isMultitouch) {
mainScroll.moveTo(newMainScrollX, true);
return true; // changed main scroll
}
const {
bounds
} = currSlide;
const newPan = currSlide.pan[axis] + delta;
if (this.pswp.options.allowPanToNext && dragAxis === 'x' && axis === 'x' && !isMultitouch) {
const currSlideMainScrollX = mainScroll.getCurrSlideX(); // Position of the main scroll relative to the viewport
const mainScrollShiftDiff = mainScroll.x - currSlideMainScrollX;
const isLeftToRight = delta > 0;
const isRightToLeft = !isLeftToRight;
if (newPan > bounds.min[axis] && isLeftToRight) {
// Panning from left to right, beyond the left edge
// Wether the image was at minimum pan position (or less)
// when this drag gesture started.
// Minimum pan position refers to the left edge of the image.
const wasAtMinPanPosition = bounds.min[axis] <= this.startPan[axis];
if (wasAtMinPanPosition) {
mainScroll.moveTo(newMainScrollX, true);
return true;
} else {
this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
}
} else if (newPan < bounds.max[axis] && isRightToLeft) {
// Paning from right to left, beyond the right edge
// Maximum pan position refers to the right edge of the image.
const wasAtMaxPanPosition = this.startPan[axis] <= bounds.max[axis];
if (wasAtMaxPanPosition) {
mainScroll.moveTo(newMainScrollX, true);
return true;
} else {
this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
}
} else {
// If main scroll is shifted
if (mainScrollShiftDiff !== 0) {
// If main scroll is shifted right
if (mainScrollShiftDiff > 0
/*&& isRightToLeft*/
) {
mainScroll.moveTo(Math.max(newMainScrollX, currSlideMainScrollX), true);
return true;
} else if (mainScrollShiftDiff < 0
/*&& isLeftToRight*/
) {
// Main scroll is shifted left (Position is less than 0 comparing to the viewport 0)
mainScroll.moveTo(Math.min(newMainScrollX, currSlideMainScrollX), true);
return true;
}
} else {
// We are within pan bounds, so just pan
this._setPanWithFriction(axis, newPan);
}
}
} else {
if (axis === 'y') {
// Do not pan vertically if main scroll is shifted o
if (!mainScroll.isShifted() && bounds.min.y !== bounds.max.y) {
this._setPanWithFriction(axis, newPan);
}
} else {
this._setPanWithFriction(axis, newPan);
}
}
return false;
} // If we move above - the ratio is negative
// If we move below the ratio is positive
/**
* Relation between pan Y position and third of viewport height.
*
* When we are at initial position (center bounds) - the ratio is 0,
* if position is shifted upwards - the ratio is negative,
* if position is shifted downwards - the ratio is positive.
*
* @private
* @param {number} panY The current pan Y position.
* @returns {number}
*/
_getVerticalDragRatio(panY) {
var _this$pswp$currSlide$, _this$pswp$currSlide;
return (panY - ((_this$pswp$currSlide$ = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.bounds.center.y) !== null && _this$pswp$currSlide$ !== void 0 ? _this$pswp$currSlide$ : 0)) / (this.pswp.viewportSize.y / 3);
}
/**
* Set pan position of the current slide.
* Apply friction if the position is beyond the pan bounds,
* or if custom friction is defined.
*
* @private
* @param {'x' | 'y'} axis
* @param {number} potentialPan
* @param {number} [customFriction] (0.1 - 1)
*/
_setPanWithFriction(axis, potentialPan, customFriction) {
const {
currSlide
} = this.pswp;
if (!currSlide) {
return;
}
const {
pan,
bounds
} = currSlide;
const correctedPan = bounds.correctPan(axis, potentialPan); // If we are out of pan bounds
if (correctedPan !== potentialPan || customFriction) {
const delta = Math.round(potentialPan - pan[axis]);
pan[axis] += delta * (customFriction || PAN_END_FRICTION);
} else {
pan[axis] = potentialPan;
}
}
}
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('./gestures.js').default} Gestures */
const UPPER_ZOOM_FRICTION = 0.05;
const LOWER_ZOOM_FRICTION = 0.15;
/**
* Get center point between two points
*
* @param {Point} p
* @param {Point} p1
* @param {Point} p2
* @returns {Point}
*/
function getZoomPointsCenter(p, p1, p2) {
p.x = (p1.x + p2.x) / 2;
p.y = (p1.y + p2.y) / 2;
return p;
}
class ZoomHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures) {
this.gestures = gestures;
/**
* @private
* @type {Point}
*/
this._startPan = {
x: 0,
y: 0
};
/**
* @private
* @type {Point}
*/
this._startZoomPoint = {
x: 0,
y: 0
};
/**
* @private
* @type {Point}
*/
this._zoomPoint = {
x: 0,
y: 0
};
/** @private */
this._wasOverFitZoomLevel = false;
/** @private */
this._startZoomLevel = 1;
}
start() {
const {
currSlide
} = this.gestures.pswp;
if (currSlide) {
this._startZoomLevel = currSlide.currZoomLevel;
equalizePoints(this._startPan, currSlide.pan);
}
this.gestures.pswp.animations.stopAllPan();
this._wasOverFitZoomLevel = false;
}
change() {
const {
p1,
startP1,
p2,
startP2,
pswp
} = this.gestures;
const {
currSlide
} = pswp;
if (!currSlide) {
return;
}
const minZoomLevel = currSlide.zoomLevels.min;
const maxZoomLevel = currSlide.zoomLevels.max;
if (!currSlide.isZoomable() || pswp.mainScroll.isShifted()) {
return;
}
getZoomPointsCenter(this._startZoomPoint, startP1, startP2);
getZoomPointsCenter(this._zoomPoint, p1, p2);
let currZoomLevel = 1 / getDistanceBetween(startP1, startP2) * getDistanceBetween(p1, p2) * this._startZoomLevel; // slightly over the zoom.fit
if (currZoomLevel > currSlide.zoomLevels.initial + currSlide.zoomLevels.initial / 15) {
this._wasOverFitZoomLevel = true;
}
if (currZoomLevel < minZoomLevel) {
if (pswp.options.pinchToClose && !this._wasOverFitZoomLevel && this._startZoomLevel <= currSlide.zoomLevels.initial) {
// fade out background if zooming out
const bgOpacity = 1 - (minZoomLevel - currZoomLevel) / (minZoomLevel / 1.2);
if (!pswp.dispatch('pinchClose', {
bgOpacity
}).defaultPrevented) {
pswp.applyBgOpacity(bgOpacity);
}
} else {
// Apply the friction if zoom level is below the min
currZoomLevel = minZoomLevel - (minZoomLevel - currZoomLevel) * LOWER_ZOOM_FRICTION;
}
} else if (currZoomLevel > maxZoomLevel) {
// Apply the friction if zoom level is above the max
currZoomLevel = maxZoomLevel + (currZoomLevel - maxZoomLevel) * UPPER_ZOOM_FRICTION;
}
currSlide.pan.x = this._calculatePanForZoomLevel('x', currZoomLevel);
currSlide.pan.y = this._calculatePanForZoomLevel('y', currZoomLevel);
currSlide.setZoomLevel(currZoomLevel);
currSlide.applyCurrentZoomPan();
}
end() {
const {
pswp
} = this.gestures;
const {
currSlide
} = pswp;
if ((!currSlide || currSlide.currZoomLevel < currSlide.zoomLevels.initial) && !this._wasOverFitZoomLevel && pswp.options.pinchToClose) {
pswp.close();
} else {
this.correctZoomPan();
}
}
/**
* @private
* @param {'x' | 'y'} axis
* @param {number} currZoomLevel
* @returns {number}
*/
_calculatePanForZoomLevel(axis, currZoomLevel) {
const zoomFactor = currZoomLevel / this._startZoomLevel;
return this._zoomPoint[axis] - (this._startZoomPoint[axis] - this._startPan[axis]) * zoomFactor;
}
/**
* Correct currZoomLevel and pan if they are
* beyond minimum or maximum values.
* With animation.
*
* @param {boolean} [ignoreGesture]
* Wether gesture coordinates should be ignored when calculating destination pan position.
*/
correctZoomPan(ignoreGesture) {
const {
pswp
} = this.gestures;
const {
currSlide
} = pswp;
if (!(currSlide !== null && currSlide !== void 0 && currSlide.isZoomable())) {
return;
}
if (this._zoomPoint.x === 0) {
ignoreGesture = true;
}
const prevZoomLevel = currSlide.currZoomLevel;
/** @type {number} */
let destinationZoomLevel;
let currZoomLevelNeedsChange = true;
if (prevZoomLevel < currSlide.zoomLevels.initial) {
destinationZoomLevel = currSlide.zoomLevels.initial; // zoom to min
} else if (prevZoomLevel > currSlide.zoomLevels.max) {
destinationZoomLevel = currSlide.zoomLevels.max; // zoom to max
} else {
currZoomLevelNeedsChange = false;
destinationZoomLevel = prevZoomLevel;
}
const initialBgOpacity = pswp.bgOpacity;
const restoreBgOpacity = pswp.bgOpacity < 1;
const initialPan = equalizePoints({
x: 0,
y: 0
}, currSlide.pan);
let destinationPan = equalizePoints({
x: 0,
y: 0
}, initialPan);
if (ignoreGesture) {
this._zoomPoint.x = 0;
this._zoomPoint.y = 0;
this._startZoomPoint.x = 0;
this._startZoomPoint.y = 0;
this._startZoomLevel = prevZoomLevel;
equalizePoints(this._startPan, initialPan);
}
if (currZoomLevelNeedsChange) {
destinationPan = {
x: this._calculatePanForZoomLevel('x', destinationZoomLevel),
y: this._calculatePanForZoomLevel('y', destinationZoomLevel)
};
} // set zoom level, so pan bounds are updated according to it
currSlide.setZoomLevel(destinationZoomLevel);
destinationPan = {
x: currSlide.bounds.correctPan('x', destinationPan.x),
y: currSlide.bounds.correctPan('y', destinationPan.y)
}; // return zoom level and its bounds to initial
currSlide.setZoomLevel(prevZoomLevel);
const panNeedsChange = !pointsEqual(destinationPan, initialPan);
if (!panNeedsChange && !currZoomLevelNeedsChange && !restoreBgOpacity) {
// update resolution after gesture
currSlide._setResolution(destinationZoomLevel);
currSlide.applyCurrentZoomPan(); // nothing to animate
return;
}
pswp.animations.stopAllPan();
pswp.animations.startSpring({
isPan: true,
start: 0,
end: 1000,
velocity: 0,
dampingRatio: 1,
naturalFrequency: 40,
onUpdate: now => {
now /= 1000; // 0 - start, 1 - end
if (panNeedsChange || currZoomLevelNeedsChange) {
if (panNeedsChange) {
currSlide.pan.x = initialPan.x + (destinationPan.x - initialPan.x) * now;
currSlide.pan.y = initialPan.y + (destinationPan.y - initialPan.y) * now;
}
if (currZoomLevelNeedsChange) {
const newZoomLevel = prevZoomLevel + (destinationZoomLevel - prevZoomLevel) * now;
currSlide.setZoomLevel(newZoomLevel);
}
currSlide.applyCurrentZoomPan();
} // Restore background opacity
if (restoreBgOpacity && pswp.bgOpacity < 1) {
// We clamp opacity to keep it between 0 and 1.
// As progress ratio can be larger than 1 due to overshoot,
// and we do not want to bounce opacity.
pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * now, 0, 1));
}
},
onComplete: () => {
// update resolution after transition ends
currSlide._setResolution(destinationZoomLevel);
currSlide.applyCurrentZoomPan();
}
});
}
}
/**
* @template {string} T
* @template {string} P
* @typedef {import('../types.js').AddPostfix} AddPostfix
*/
/** @typedef {import('./gestures.js').default} Gestures */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {'imageClick' | 'bgClick' | 'tap' | 'doubleTap'} Actions */
/**
* Whether the tap was performed on the main slide
* (rather than controls or caption).
*
* @param {PointerEvent} event
* @returns {boolean}
*/
function didTapOnMainContent(event) {
return !!
/** @type {HTMLElement} */
event.target.closest('.pswp__container');
}
/**
* Tap, double-tap handler.
*/
class TapHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures) {
this.gestures = gestures;
}
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
click(point, originalEvent) {
const targetClassList =
/** @type {HTMLElement} */
originalEvent.target.classList;
const isImageClick = targetClassList.contains('pswp__img');
const isBackgroundClick = targetClassList.contains('pswp__item') || targetClassList.contains('pswp__zoom-wrap');
if (isImageClick) {
this._doClickOrTapAction('imageClick', point, originalEvent);
} else if (isBackgroundClick) {
this._doClickOrTapAction('bgClick', point, originalEvent);
}
}
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
tap(point, originalEvent) {
if (didTapOnMainContent(originalEvent)) {
this._doClickOrTapAction('tap', point, originalEvent);
}
}
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
doubleTap(point, originalEvent) {
if (didTapOnMainContent(originalEvent)) {
this._doClickOrTapAction('doubleTap', point, originalEvent);
}
}
/**
* @private
* @param {Actions} actionName
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
_doClickOrTapAction(actionName, point, originalEvent) {
var _this$gestures$pswp$e;
const {
pswp
} = this.gestures;
const {
currSlide
} = pswp;
const actionFullName =
/** @type {AddPostfix} */
actionName + 'Action';
const optionValue = pswp.options[actionFullName];
if (pswp.dispatch(actionFullName, {
point,
originalEvent
}).defaultPrevented) {
return;
}
if (typeof optionValue === 'function') {
optionValue.call(pswp, point, originalEvent);
return;
}
switch (optionValue) {
case 'close':
case 'next':
pswp[optionValue]();
break;
case 'zoom':
currSlide === null || currSlide === void 0 || currSlide.toggleZoom(point);
break;
case 'zoom-or-close':
// by default click zooms current image,
// if it can not be zoomed - gallery will be closed
if (currSlide !== null && currSlide !== void 0 && currSlide.isZoomable() && currSlide.zoomLevels.secondary !== currSlide.zoomLevels.initial) {
currSlide.toggleZoom(point);
} else if (pswp.options.clickToCloseNonZoomable) {
pswp.close();
}
break;
case 'toggle-controls':
(_this$gestures$pswp$e = this.gestures.pswp.element) === null || _this$gestures$pswp$e === void 0 || _this$gestures$pswp$e.classList.toggle('pswp--ui-visible'); // if (_controlsVisible) {
// _ui.hideControls();
// } else {
// _ui.showControls();
// }
break;
}
}
}
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').Point} Point */
// How far should user should drag
// until we can determine that the gesture is swipe and its direction
const AXIS_SWIPE_HYSTERISIS = 10; //const PAN_END_FRICTION = 0.35;
const DOUBLE_TAP_DELAY = 300; // ms
const MIN_TAP_DISTANCE = 25; // px
/**
* Gestures class bind touch, pointer or mouse events
* and emits drag to drag-handler and zoom events zoom-handler.
*
* Drag and zoom events are emited in requestAnimationFrame,
* and only when one of pointers was actually changed.
*/
class Gestures {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
/** @type {'x' | 'y' | null} */
this.dragAxis = null; // point objects are defined once and reused
// PhotoSwipe keeps track only of two pointers, others are ignored
/** @type {Point} */
this.p1 = {
x: 0,
y: 0
}; // the first pressed pointer
/** @type {Point} */
this.p2 = {
x: 0,
y: 0
}; // the second pressed pointer
/** @type {Point} */
this.prevP1 = {
x: 0,
y: 0
};
/** @type {Point} */
this.prevP2 = {
x: 0,
y: 0
};
/** @type {Point} */
this.startP1 = {
x: 0,
y: 0
};
/** @type {Point} */
this.startP2 = {
x: 0,
y: 0
};
/** @type {Point} */
this.velocity = {
x: 0,
y: 0
};
/** @type {Point}
* @private
*/
this._lastStartP1 = {
x: 0,
y: 0
};
/** @type {Point}
* @private
*/
this._intervalP1 = {
x: 0,
y: 0
};
/** @private */
this._numActivePoints = 0;
/** @type {Point[]}
* @private
*/
this._ongoingPointers = [];
/** @private */
this._touchEventEnabled = 'ontouchstart' in window;
/** @private */
this._pointerEventEnabled = !!window.PointerEvent;
this.supportsTouch = this._touchEventEnabled || this._pointerEventEnabled && navigator.maxTouchPoints > 1;
/** @private */
this._numActivePoints = 0;
/** @private */
this._intervalTime = 0;
/** @private */
this._velocityCalculated = false;
this.isMultitouch = false;
this.isDragging = false;
this.isZooming = false;
/** @type {number | null} */
this.raf = null;
/** @type {NodeJS.Timeout | null}
* @private
*/
this._tapTimer = null;
if (!this.supportsTouch) {
// disable pan to next slide for non-touch devices
pswp.options.allowPanToNext = false;
}
this.drag = new DragHandler(this);
this.zoomLevels = new ZoomHandler(this);
this.tapHandler = new TapHandler(this);
pswp.on('bindEvents', () => {
pswp.events.add(pswp.scrollWrap, 'click',
/** @type EventListener */
this._onClick.bind(this));
if (this._pointerEventEnabled) {
this._bindEvents('pointer', 'down', 'up', 'cancel');
} else if (this._touchEventEnabled) {
this._bindEvents('touch', 'start', 'end', 'cancel'); // In previous versions we also bound mouse event here,
// in case device supports both touch and mouse events,
// but newer versions of browsers now support PointerEvent.
// on iOS10 if you bind touchmove/end after touchstart,
// and you don't preventDefault touchstart (which PhotoSwipe does),
// preventDefault will have no effect on touchmove and touchend.
// Unless you bind it previously.
if (pswp.scrollWrap) {
pswp.scrollWrap.ontouchmove = () => {};
pswp.scrollWrap.ontouchend = () => {};
}
} else {
this._bindEvents('mouse', 'down', 'up');
}
});
}
/**
* @private
* @param {'mouse' | 'touch' | 'pointer'} pref
* @param {'down' | 'start'} down
* @param {'up' | 'end'} up
* @param {'cancel'} [cancel]
*/
_bindEvents(pref, down, up, cancel) {
const {
pswp
} = this;
const {
events
} = pswp;
const cancelEvent = cancel ? pref + cancel : '';
events.add(pswp.scrollWrap, pref + down,
/** @type EventListener */
this.onPointerDown.bind(this));
events.add(window, pref + 'move',
/** @type EventListener */
this.onPointerMove.bind(this));
events.add(window, pref + up,
/** @type EventListener */
this.onPointerUp.bind(this));
if (cancelEvent) {
events.add(pswp.scrollWrap, cancelEvent,
/** @type EventListener */
this.onPointerUp.bind(this));
}
}
/**
* @param {PointerEvent} e
*/
onPointerDown(e) {
// We do not call preventDefault for touch events
// to allow browser to show native dialog on longpress
// (the one that allows to save image or open it in new tab).
//
// Desktop Safari allows to drag images when preventDefault isn't called on mousedown,
// even though preventDefault IS called on mousemove. That's why we preventDefault mousedown.
const isMousePointer = e.type === 'mousedown' || e.pointerType === 'mouse'; // Allow dragging only via left mouse button.
// http://www.quirksmode.org/js/events_properties.html
// https://developer.mozilla.org/en-US/docs/Web/API/event.button
if (isMousePointer && e.button > 0) {
return;
}
const {
pswp
} = this; // if PhotoSwipe is opening or closing
if (!pswp.opener.isOpen) {
e.preventDefault();
return;
}
if (pswp.dispatch('pointerDown', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (isMousePointer) {
pswp.mouseDetected(); // preventDefault mouse event to prevent
// browser image drag feature
this._preventPointerEventBehaviour(e, 'down');
}
pswp.animations.stopAll();
this._updatePoints(e, 'down');
if (this._numActivePoints === 1) {
this.dragAxis = null; // we need to store initial point to determine the main axis,
// drag is activated only after the axis is determined
equalizePoints(this.startP1, this.p1);
}
if (this._numActivePoints > 1) {
// Tap or double tap should not trigger if more than one pointer
this._clearTapTimer();
this.isMultitouch = true;
} else {
this.isMultitouch = false;
}
}
/**
* @param {PointerEvent} e
*/
onPointerMove(e) {
this._preventPointerEventBehaviour(e, 'move');
if (!this._numActivePoints) {
return;
}
this._updatePoints(e, 'move');
if (this.pswp.dispatch('pointerMove', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (this._numActivePoints === 1 && !this.isDragging) {
if (!this.dragAxis) {
this._calculateDragDirection();
} // Drag axis was detected, emit drag.start
if (this.dragAxis && !this.isDragging) {
if (this.isZooming) {
this.isZooming = false;
this.zoomLevels.end();
}
this.isDragging = true;
this._clearTapTimer(); // Tap can not trigger after drag
// Adjust starting point
this._updateStartPoints();
this._intervalTime = Date.now(); //this._startTime = this._intervalTime;
this._velocityCalculated = false;
equalizePoints(this._intervalP1, this.p1);
this.velocity.x = 0;
this.velocity.y = 0;
this.drag.start();
this._rafStopLoop();
this._rafRenderLoop();
}
} else if (this._numActivePoints > 1 && !this.isZooming) {
this._finishDrag();
this.isZooming = true; // Adjust starting points
this._updateStartPoints();
this.zoomLevels.start();
this._rafStopLoop();
this._rafRenderLoop();
}
}
/**
* @private
*/
_finishDrag() {
if (this.isDragging) {
this.isDragging = false; // Try to calculate velocity,
// if it wasn't calculated yet in drag.change
if (!this._velocityCalculated) {
this._updateVelocity(true);
}
this.drag.end();
this.dragAxis = null;
}
}
/**
* @param {PointerEvent} e
*/
onPointerUp(e) {
if (!this._numActivePoints) {
return;
}
this._updatePoints(e, 'up');
if (this.pswp.dispatch('pointerUp', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (this._numActivePoints === 0) {
this._rafStopLoop();
if (this.isDragging) {
this._finishDrag();
} else if (!this.isZooming && !this.isMultitouch) {
//this.zoomLevels.correctZoomPan();
this._finishTap(e);
}
}
if (this._numActivePoints < 2 && this.isZooming) {
this.isZooming = false;
this.zoomLevels.end();
if (this._numActivePoints === 1) {
// Since we have 1 point left, we need to reinitiate drag
this.dragAxis = null;
this._updateStartPoints();
}
}
}
/**
* @private
*/
_rafRenderLoop() {
if (this.isDragging || this.isZooming) {
this._updateVelocity();
if (this.isDragging) {
// make sure that pointer moved since the last update
if (!pointsEqual(this.p1, this.prevP1)) {
this.drag.change();
}
} else
/* if (this.isZooming) */
{
if (!pointsEqual(this.p1, this.prevP1) || !pointsEqual(this.p2, this.prevP2)) {
this.zoomLevels.change();
}
}
this._updatePrevPoints();
this.raf = requestAnimationFrame(this._rafRenderLoop.bind(this));
}
}
/**
* Update velocity at 50ms interval
*
* @private
* @param {boolean} [force]
*/
_updateVelocity(force) {
const time = Date.now();
const duration = time - this._intervalTime;
if (duration < 50 && !force) {
return;
}
this.velocity.x = this._getVelocity('x', duration);
this.velocity.y = this._getVelocity('y', duration);
this._intervalTime = time;
equalizePoints(this._intervalP1, this.p1);
this._velocityCalculated = true;
}
/**
* @private
* @param {PointerEvent} e
*/
_finishTap(e) {
const {
mainScroll
} = this.pswp; // Do not trigger tap events if main scroll is shifted
if (mainScroll.isShifted()) {
// restore main scroll position
// (usually happens if stopped in the middle of animation)
mainScroll.moveIndexBy(0, true);
return;
} // Do not trigger tap for touchcancel or pointercancel
if (e.type.indexOf('cancel') > 0) {
return;
} // Trigger click instead of tap for mouse events
if (e.type === 'mouseup' || e.pointerType === 'mouse') {
this.tapHandler.click(this.startP1, e);
return;
} // Disable delay if there is no doubleTapAction
const tapDelay = this.pswp.options.doubleTapAction ? DOUBLE_TAP_DELAY : 0; // If tapTimer is defined - we tapped recently,
// check if the current tap is close to the previous one,
// if yes - trigger double tap
if (this._tapTimer) {
this._clearTapTimer(); // Check if two taps were more or less on the same place
if (getDistanceBetween(this._lastStartP1, this.startP1) < MIN_TAP_DISTANCE) {
this.tapHandler.doubleTap(this.startP1, e);
}
} else {
equalizePoints(this._lastStartP1, this.startP1);
this._tapTimer = setTimeout(() => {
this.tapHandler.tap(this.startP1, e);
this._clearTapTimer();
}, tapDelay);
}
}
/**
* @private
*/
_clearTapTimer() {
if (this._tapTimer) {
clearTimeout(this._tapTimer);
this._tapTimer = null;
}
}
/**
* Get velocity for axis
*
* @private
* @param {'x' | 'y'} axis
* @param {number} duration
* @returns {number}
*/
_getVelocity(axis, duration) {
// displacement is like distance, but can be negative.
const displacement = this.p1[axis] - this._intervalP1[axis];
if (Math.abs(displacement) > 1 && duration > 5) {
return displacement / duration;
}
return 0;
}
/**
* @private
*/
_rafStopLoop() {
if (this.raf) {
cancelAnimationFrame(this.raf);
this.raf = null;
}
}
/**
* @private
* @param {PointerEvent} e
* @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
*/
_preventPointerEventBehaviour(e, pointerType) {
const preventPointerEvent = this.pswp.applyFilters('preventPointerEvent', true, e, pointerType);
if (preventPointerEvent) {
e.preventDefault();
}
}
/**
* Parses and normalizes points from the touch, mouse or pointer event.
* Updates p1 and p2.
*
* @private
* @param {PointerEvent | TouchEvent} e
* @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
*/
_updatePoints(e, pointerType) {
if (this._pointerEventEnabled) {
const pointerEvent =
/** @type {PointerEvent} */
e; // Try to find the current pointer in ongoing pointers by its ID
const pointerIndex = this._ongoingPointers.findIndex(ongoingPointer => {
return ongoingPointer.id === pointerEvent.pointerId;
});
if (pointerType === 'up' && pointerIndex > -1) {
// release the pointer - remove it from ongoing
this._ongoingPointers.splice(pointerIndex, 1);
} else if (pointerType === 'down' && pointerIndex === -1) {
// add new pointer
this._ongoingPointers.push(this._convertEventPosToPoint(pointerEvent, {
x: 0,
y: 0
}));
} else if (pointerIndex > -1) {
// update existing pointer
this._convertEventPosToPoint(pointerEvent, this._ongoingPointers[pointerIndex]);
}
this._numActivePoints = this._ongoingPointers.length; // update points that PhotoSwipe uses
// to calculate position and scale
if (this._numActivePoints > 0) {
equalizePoints(this.p1, this._ongoingPointers[0]);
}
if (this._numActivePoints > 1) {
equalizePoints(this.p2, this._ongoingPointers[1]);
}
} else {
const touchEvent =
/** @type {TouchEvent} */
e;
this._numActivePoints = 0;
if (touchEvent.type.indexOf('touch') > -1) {
// Touch Event
// https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
if (touchEvent.touches && touchEvent.touches.length > 0) {
this._convertEventPosToPoint(touchEvent.touches[0], this.p1);
this._numActivePoints++;
if (touchEvent.touches.length > 1) {
this._convertEventPosToPoint(touchEvent.touches[1], this.p2);
this._numActivePoints++;
}
}
} else {
// Mouse Event
this._convertEventPosToPoint(
/** @type {PointerEvent} */
e, this.p1);
if (pointerType === 'up') {
// clear all points on mouseup
this._numActivePoints = 0;
} else {
this._numActivePoints++;
}
}
}
}
/** update points that were used during previous rAF tick
* @private
*/
_updatePrevPoints() {
equalizePoints(this.prevP1, this.p1);
equalizePoints(this.prevP2, this.p2);
}
/** update points at the start of gesture
* @private
*/
_updateStartPoints() {
equalizePoints(this.startP1, this.p1);
equalizePoints(this.startP2, this.p2);
this._updatePrevPoints();
}
/** @private */
_calculateDragDirection() {
if (this.pswp.mainScroll.isShifted()) {
// if main scroll position is shifted – direction is always horizontal
this.dragAxis = 'x';
} else {
// calculate delta of the last touchmove tick
const diff = Math.abs(this.p1.x - this.startP1.x) - Math.abs(this.p1.y - this.startP1.y);
if (diff !== 0) {
// check if pointer was shifted horizontally or vertically
const axisToCheck = diff > 0 ? 'x' : 'y';
if (Math.abs(this.p1[axisToCheck] - this.startP1[axisToCheck]) >= AXIS_SWIPE_HYSTERISIS) {
this.dragAxis = axisToCheck;
}
}
}
}
/**
* Converts touch, pointer or mouse event
* to PhotoSwipe point.
*
* @private
* @param {Touch | PointerEvent} e
* @param {Point} p
* @returns {Point}
*/
_convertEventPosToPoint(e, p) {
p.x = e.pageX - this.pswp.offset.x;
p.y = e.pageY - this.pswp.offset.y;
if ('pointerId' in e) {
p.id = e.pointerId;
} else if (e.identifier !== undefined) {
p.id = e.identifier;
}
return p;
}
/**
* @private
* @param {PointerEvent} e
*/
_onClick(e) {
// Do not allow click event to pass through after drag
if (this.pswp.mainScroll.isShifted()) {
e.preventDefault();
e.stopPropagation();
}
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/** @typedef {import('./slide/slide.js').default} Slide */
/** @typedef {{ el: HTMLDivElement; slide?: Slide }} ItemHolder */
const MAIN_SCROLL_END_FRICTION = 0.35; // const MIN_SWIPE_TRANSITION_DURATION = 250;
// const MAX_SWIPE_TRABSITION_DURATION = 500;
// const DEFAULT_SWIPE_TRANSITION_DURATION = 333;
/**
* Handles movement of the main scrolling container
* (for example, it repositions when user swipes left or right).
*
* Also stores its state.
*/
class MainScroll {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
this.x = 0;
this.slideWidth = 0;
/** @private */
this._currPositionIndex = 0;
/** @private */
this._prevPositionIndex = 0;
/** @private */
this._containerShiftIndex = -1;
/** @type {ItemHolder[]} */
this.itemHolders = [];
}
/**
* Position the scroller and slide containers
* according to viewport size.
*
* @param {boolean} [resizeSlides] Whether slides content should resized
*/
resize(resizeSlides) {
const {
pswp
} = this;
const newSlideWidth = Math.round(pswp.viewportSize.x + pswp.viewportSize.x * pswp.options.spacing); // Mobile browsers might trigger a resize event during a gesture.
// (due to toolbar appearing or hiding).
// Avoid re-adjusting main scroll position if width wasn't changed
const slideWidthChanged = newSlideWidth !== this.slideWidth;
if (slideWidthChanged) {
this.slideWidth = newSlideWidth;
this.moveTo(this.getCurrSlideX());
}
this.itemHolders.forEach((itemHolder, index) => {
if (slideWidthChanged) {
setTransform(itemHolder.el, (index + this._containerShiftIndex) * this.slideWidth);
}
if (resizeSlides && itemHolder.slide) {
itemHolder.slide.resize();
}
});
}
/**
* Reset X position of the main scroller to zero
*/
resetPosition() {
// Position on the main scroller (offset)
// it is independent from slide index
this._currPositionIndex = 0;
this._prevPositionIndex = 0; // This will force recalculation of size on next resize()
this.slideWidth = 0; // _containerShiftIndex*viewportSize will give you amount of transform of the current slide
this._containerShiftIndex = -1;
}
/**
* Create and append array of three items
* that hold data about slides in DOM
*/
appendHolders() {
this.itemHolders = []; // append our three slide holders -
// previous, current, and next
for (let i = 0; i < 3; i++) {
const el = createElement('pswp__item', 'div', this.pswp.container);
el.setAttribute('role', 'group');
el.setAttribute('aria-roledescription', 'slide');
el.setAttribute('aria-hidden', 'true'); // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
el.style.display = i === 1 ? 'block' : 'none';
this.itemHolders.push({
el //index: -1
});
}
}
/**
* Whether the main scroll can be horizontally swiped to the next or previous slide.
* @returns {boolean}
*/
canBeSwiped() {
return this.pswp.getNumItems() > 1;
}
/**
* Move main scroll by X amount of slides.
* For example:
* `-1` will move to the previous slide,
* `0` will reset the scroll position of the current slide,
* `3` will move three slides forward
*
* If loop option is enabled - index will be automatically looped too,
* (for example `-1` will move to the last slide of the gallery).
*
* @param {number} diff
* @param {boolean} [animate]
* @param {number} [velocityX]
* @returns {boolean} whether index was changed or not
*/
moveIndexBy(diff, animate, velocityX) {
const {
pswp
} = this;
let newIndex = pswp.potentialIndex + diff;
const numSlides = pswp.getNumItems();
if (pswp.canLoop()) {
newIndex = pswp.getLoopedIndex(newIndex);
const distance = (diff + numSlides) % numSlides;
if (distance <= numSlides / 2) {
// go forward
diff = distance;
} else {
// go backwards
diff = distance - numSlides;
}
} else {
if (newIndex < 0) {
newIndex = 0;
} else if (newIndex >= numSlides) {
newIndex = numSlides - 1;
}
diff = newIndex - pswp.potentialIndex;
}
pswp.potentialIndex = newIndex;
this._currPositionIndex -= diff;
pswp.animations.stopMainScroll();
const destinationX = this.getCurrSlideX();
if (!animate) {
this.moveTo(destinationX);
this.updateCurrItem();
} else {
pswp.animations.startSpring({
isMainScroll: true,
start: this.x,
end: destinationX,
velocity: velocityX || 0,
naturalFrequency: 30,
dampingRatio: 1,
//0.7,
onUpdate: x => {
this.moveTo(x);
},
onComplete: () => {
this.updateCurrItem();
pswp.appendHeavy();
}
});
let currDiff = pswp.potentialIndex - pswp.currIndex;
if (pswp.canLoop()) {
const currDistance = (currDiff + numSlides) % numSlides;
if (currDistance <= numSlides / 2) {
// go forward
currDiff = currDistance;
} else {
// go backwards
currDiff = currDistance - numSlides;
}
} // Force-append new slides during transition
// if difference between slides is more than 1
if (Math.abs(currDiff) > 1) {
this.updateCurrItem();
}
}
return Boolean(diff);
}
/**
* X position of the main scroll for the current slide
* (ignores position during dragging)
* @returns {number}
*/
getCurrSlideX() {
return this.slideWidth * this._currPositionIndex;
}
/**
* Whether scroll position is shifted.
* For example, it will return true if the scroll is being dragged or animated.
* @returns {boolean}
*/
isShifted() {
return this.x !== this.getCurrSlideX();
}
/**
* Update slides X positions and set their content
*/
updateCurrItem() {
var _this$itemHolders$;
const {
pswp
} = this;
const positionDifference = this._prevPositionIndex - this._currPositionIndex;
if (!positionDifference) {
return;
}
this._prevPositionIndex = this._currPositionIndex;
pswp.currIndex = pswp.potentialIndex;
let diffAbs = Math.abs(positionDifference);
/** @type {ItemHolder | undefined} */
let tempHolder;
if (diffAbs >= 3) {
this._containerShiftIndex += positionDifference + (positionDifference > 0 ? -3 : 3);
diffAbs = 3; // If slides are changed by 3 screens or more - clean up previous slides
this.itemHolders.forEach(itemHolder => {
var _itemHolder$slide;
(_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.destroy();
itemHolder.slide = undefined;
});
}
for (let i = 0; i < diffAbs; i++) {
if (positionDifference > 0) {
tempHolder = this.itemHolders.shift();
if (tempHolder) {
this.itemHolders[2] = tempHolder; // move first to last
this._containerShiftIndex++;
setTransform(tempHolder.el, (this._containerShiftIndex + 2) * this.slideWidth);
pswp.setContent(tempHolder, pswp.currIndex - diffAbs + i + 2);
}
} else {
tempHolder = this.itemHolders.pop();
if (tempHolder) {
this.itemHolders.unshift(tempHolder); // move last to first
this._containerShiftIndex--;
setTransform(tempHolder.el, this._containerShiftIndex * this.slideWidth);
pswp.setContent(tempHolder, pswp.currIndex + diffAbs - i - 2);
}
}
} // Reset transfrom every 50ish navigations in one direction.
//
// Otherwise transform will keep growing indefinitely,
// which might cause issues as browsers have a maximum transform limit.
// I wasn't able to reach it, but just to be safe.
// This should not cause noticable lag.
if (Math.abs(this._containerShiftIndex) > 50 && !this.isShifted()) {
this.resetPosition();
this.resize();
} // Pan transition might be running (and consntantly updating pan position)
pswp.animations.stopAllPan();
this.itemHolders.forEach((itemHolder, i) => {
if (itemHolder.slide) {
// Slide in the 2nd holder is always active
itemHolder.slide.setIsActive(i === 1);
}
});
pswp.currSlide = (_this$itemHolders$ = this.itemHolders[1]) === null || _this$itemHolders$ === void 0 ? void 0 : _this$itemHolders$.slide;
pswp.contentLoader.updateLazy(positionDifference);
if (pswp.currSlide) {
pswp.currSlide.applyCurrentZoomPan();
}
pswp.dispatch('change');
}
/**
* Move the X position of the main scroll container
*
* @param {number} x
* @param {boolean} [dragging]
*/
moveTo(x, dragging) {
if (!this.pswp.canLoop() && dragging) {
// Apply friction
let newSlideIndexOffset = (this.slideWidth * this._currPositionIndex - x) / this.slideWidth;
newSlideIndexOffset += this.pswp.currIndex;
const delta = Math.round(x - this.x);
if (newSlideIndexOffset < 0 && delta > 0 || newSlideIndexOffset >= this.pswp.getNumItems() - 1 && delta < 0) {
x = this.x + delta * MAIN_SCROLL_END_FRICTION;
}
}
this.x = x;
if (this.pswp.container) {
setTransform(this.pswp.container, x);
}
this.pswp.dispatch('moveMainScroll', {
x,
dragging: dragging !== null && dragging !== void 0 ? dragging : false
});
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/**
* @template T
* @typedef {import('./types.js').Methods} Methods
*/
const KeyboardKeyCodesMap = {
Escape: 27,
z: 90,
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40,
Tab: 9
};
/**
* @template {keyof KeyboardKeyCodesMap} T
* @param {T} key
* @param {boolean} isKeySupported
* @returns {T | number | undefined}
*/
const getKeyboardEventKey = (key, isKeySupported) => {
return isKeySupported ? key : KeyboardKeyCodesMap[key];
};
/**
* - Manages keyboard shortcuts.
* - Helps trap focus within photoswipe.
*/
class Keyboard {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
/** @private */
this._wasFocused = false;
pswp.on('bindEvents', () => {
if (pswp.options.trapFocus) {
// Dialog was likely opened by keyboard if initial point is not defined
if (!pswp.options.initialPointerPos) {
// focus causes layout,
// which causes lag during the animation,
// that's why we delay it until the opener transition ends
this._focusRoot();
}
pswp.events.add(document, 'focusin',
/** @type EventListener */
this._onFocusIn.bind(this));
}
pswp.events.add(document, 'keydown',
/** @type EventListener */
this._onKeyDown.bind(this));
});
const lastActiveElement =
/** @type {HTMLElement} */
document.activeElement;
pswp.on('destroy', () => {
if (pswp.options.returnFocus && lastActiveElement && this._wasFocused) {
lastActiveElement.focus();
}
});
}
/** @private */
_focusRoot() {
if (!this._wasFocused && this.pswp.element) {
this.pswp.element.focus();
this._wasFocused = true;
}
}
/**
* @private
* @param {KeyboardEvent} e
*/
_onKeyDown(e) {
const {
pswp
} = this;
if (pswp.dispatch('keydown', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (specialKeyUsed(e)) {
// don't do anything if special key pressed
// to prevent from overriding default browser actions
// for example, in Chrome on Mac cmd+arrow-left returns to previous page
return;
}
/** @type {Methods | undefined} */
let keydownAction;
/** @type {'x' | 'y' | undefined} */
let axis;
let isForward = false;
const isKeySupported = ('key' in e);
switch (isKeySupported ? e.key : e.keyCode) {
case getKeyboardEventKey('Escape', isKeySupported):
if (pswp.options.escKey) {
keydownAction = 'close';
}
break;
case getKeyboardEventKey('z', isKeySupported):
keydownAction = 'toggleZoom';
break;
case getKeyboardEventKey('ArrowLeft', isKeySupported):
axis = 'x';
break;
case getKeyboardEventKey('ArrowUp', isKeySupported):
axis = 'y';
break;
case getKeyboardEventKey('ArrowRight', isKeySupported):
axis = 'x';
isForward = true;
break;
case getKeyboardEventKey('ArrowDown', isKeySupported):
isForward = true;
axis = 'y';
break;
case getKeyboardEventKey('Tab', isKeySupported):
this._focusRoot();
break;
} // if left/right/top/bottom key
if (axis) {
// prevent page scroll
e.preventDefault();
const {
currSlide
} = pswp;
if (pswp.options.arrowKeys && axis === 'x' && pswp.getNumItems() > 1) {
keydownAction = isForward ? 'next' : 'prev';
} else if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.fit) {
// up/down arrow keys pan the image vertically
// left/right arrow keys pan horizontally.
// Unless there is only one image,
// or arrowKeys option is disabled
currSlide.pan[axis] += isForward ? -80 : 80;
currSlide.panTo(currSlide.pan.x, currSlide.pan.y);
}
}
if (keydownAction) {
e.preventDefault(); // @ts-ignore
pswp[keydownAction]();
}
}
/**
* Trap focus inside photoswipe
*
* @private
* @param {FocusEvent} e
*/
_onFocusIn(e) {
const {
template
} = this.pswp;
if (template && document !== e.target && template !== e.target && !template.contains(
/** @type {Node} */
e.target)) {
// focus root element
template.focus();
}
}
}
const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)';
/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
/** @typedef {Object} DefaultCssAnimationProps
*
* @prop {HTMLElement} target
* @prop {number} [duration]
* @prop {string} [easing]
* @prop {string} [transform]
* @prop {string} [opacity]
* */
/** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */
/**
* Runs CSS transition.
*/
class CSSAnimation {
/**
* onComplete can be unpredictable, be careful about current state
*
* @param {CssAnimationProps} props
*/
constructor(props) {
var _props$prop;
this.props = props;
const {
target,
onComplete,
transform,
onFinish = () => {},
duration = 333,
easing = DEFAULT_EASING
} = props;
this.onFinish = onFinish; // support only transform and opacity
const prop = transform ? 'transform' : 'opacity';
const propValue = (_props$prop = props[prop]) !== null && _props$prop !== void 0 ? _props$prop : '';
/** @private */
this._target = target;
/** @private */
this._onComplete = onComplete;
/** @private */
this._finished = false;
/** @private */
this._onTransitionEnd = this._onTransitionEnd.bind(this); // Using timeout hack to make sure that animation
// starts even if the animated property was changed recently,
// otherwise transitionend might not fire or transition won't start.
// https://drafts.csswg.org/css-transitions/#starting
//
// ¯\_(ツ)_/¯
/** @private */
this._helperTimeout = setTimeout(() => {
setTransitionStyle(target, prop, duration, easing);
this._helperTimeout = setTimeout(() => {
target.addEventListener('transitionend', this._onTransitionEnd, false);
target.addEventListener('transitioncancel', this._onTransitionEnd, false); // Safari occasionally does not emit transitionend event
// if element property was modified during the transition,
// which may be caused by resize or third party component,
// using timeout as a safety fallback
this._helperTimeout = setTimeout(() => {
this._finalizeAnimation();
}, duration + 500);
target.style[prop] = propValue;
}, 30); // Do not reduce this number
}, 0);
}
/**
* @private
* @param {TransitionEvent} e
*/
_onTransitionEnd(e) {
if (e.target === this._target) {
this._finalizeAnimation();
}
}
/**
* @private
*/
_finalizeAnimation() {
if (!this._finished) {
this._finished = true;
this.onFinish();
if (this._onComplete) {
this._onComplete();
}
}
} // Destroy is called automatically onFinish
destroy() {
if (this._helperTimeout) {
clearTimeout(this._helperTimeout);
}
removeTransitionStyle(this._target);
this._target.removeEventListener('transitionend', this._onTransitionEnd, false);
this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false);
if (!this._finished) {
this._finalizeAnimation();
}
}
}
const DEFAULT_NATURAL_FREQUENCY = 12;
const DEFAULT_DAMPING_RATIO = 0.75;
/**
* Spring easing helper
*/
class SpringEaser {
/**
* @param {number} initialVelocity Initial velocity, px per ms.
*
* @param {number} [dampingRatio]
* Determines how bouncy animation will be.
* From 0 to 1, 0 - always overshoot, 1 - do not overshoot.
* "overshoot" refers to part of animation that
* goes beyond the final value.
*
* @param {number} [naturalFrequency]
* Determines how fast animation will slow down.
* The higher value - the stiffer the transition will be,
* and the faster it will slow down.
* Recommended value from 10 to 50
*/
constructor(initialVelocity, dampingRatio, naturalFrequency) {
this.velocity = initialVelocity * 1000; // convert to "pixels per second"
// https://en.wikipedia.org/wiki/Damping_ratio
this._dampingRatio = dampingRatio || DEFAULT_DAMPING_RATIO; // https://en.wikipedia.org/wiki/Natural_frequency
this._naturalFrequency = naturalFrequency || DEFAULT_NATURAL_FREQUENCY;
this._dampedFrequency = this._naturalFrequency;
if (this._dampingRatio < 1) {
this._dampedFrequency *= Math.sqrt(1 - this._dampingRatio * this._dampingRatio);
}
}
/**
* @param {number} deltaPosition Difference between current and end position of the animation
* @param {number} deltaTime Frame duration in milliseconds
*
* @returns {number} Displacement, relative to the end position.
*/
easeFrame(deltaPosition, deltaTime) {
// Inspired by Apple Webkit and Android spring function implementation
// https://en.wikipedia.org/wiki/Oscillation
// https://en.wikipedia.org/wiki/Damping_ratio
// we ignore mass (assume that it's 1kg)
let displacement = 0;
let coeff;
deltaTime /= 1000;
const naturalDumpingPow = Math.E ** (-this._dampingRatio * this._naturalFrequency * deltaTime);
if (this._dampingRatio === 1) {
coeff = this.velocity + this._naturalFrequency * deltaPosition;
displacement = (deltaPosition + coeff * deltaTime) * naturalDumpingPow;
this.velocity = displacement * -this._naturalFrequency + coeff * naturalDumpingPow;
} else if (this._dampingRatio < 1) {
coeff = 1 / this._dampedFrequency * (this._dampingRatio * this._naturalFrequency * deltaPosition + this.velocity);
const dumpedFCos = Math.cos(this._dampedFrequency * deltaTime);
const dumpedFSin = Math.sin(this._dampedFrequency * deltaTime);
displacement = naturalDumpingPow * (deltaPosition * dumpedFCos + coeff * dumpedFSin);
this.velocity = displacement * -this._naturalFrequency * this._dampingRatio + naturalDumpingPow * (-this._dampedFrequency * deltaPosition * dumpedFSin + this._dampedFrequency * coeff * dumpedFCos);
} // Overdamped (>1) damping ratio is not supported
return displacement;
}
}
/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
/**
* @typedef {Object} DefaultSpringAnimationProps
*
* @prop {number} start
* @prop {number} end
* @prop {number} velocity
* @prop {number} [dampingRatio]
* @prop {number} [naturalFrequency]
* @prop {(end: number) => void} onUpdate
*/
/** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */
class SpringAnimation {
/**
* @param {SpringAnimationProps} props
*/
constructor(props) {
this.props = props;
this._raf = 0;
const {
start,
end,
velocity,
onUpdate,
onComplete,
onFinish = () => {},
dampingRatio,
naturalFrequency
} = props;
this.onFinish = onFinish;
const easer = new SpringEaser(velocity, dampingRatio, naturalFrequency);
let prevTime = Date.now();
let deltaPosition = start - end;
const animationLoop = () => {
if (this._raf) {
deltaPosition = easer.easeFrame(deltaPosition, Date.now() - prevTime); // Stop the animation if velocity is low and position is close to end
if (Math.abs(deltaPosition) < 1 && Math.abs(easer.velocity) < 50) {
// Finalize the animation
onUpdate(end);
if (onComplete) {
onComplete();
}
this.onFinish();
} else {
prevTime = Date.now();
onUpdate(deltaPosition + end);
this._raf = requestAnimationFrame(animationLoop);
}
}
};
this._raf = requestAnimationFrame(animationLoop);
} // Destroy is called automatically onFinish
destroy() {
if (this._raf >= 0) {
cancelAnimationFrame(this._raf);
}
this._raf = 0;
}
}
/** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */
/** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */
/** @typedef {Object} SharedAnimationProps
* @prop {string} [name]
* @prop {boolean} [isPan]
* @prop {boolean} [isMainScroll]
* @prop {VoidFunction} [onComplete]
* @prop {VoidFunction} [onFinish]
*/
/** @typedef {SpringAnimation | CSSAnimation} Animation */
/** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */
/**
* Manages animations
*/
class Animations {
constructor() {
/** @type {Animation[]} */
this.activeAnimations = [];
}
/**
* @param {SpringAnimationProps} props
*/
startSpring(props) {
this._start(props, true);
}
/**
* @param {CssAnimationProps} props
*/
startTransition(props) {
this._start(props);
}
/**
* @private
* @param {AnimationProps} props
* @param {boolean} [isSpring]
* @returns {Animation}
*/
_start(props, isSpring) {
const animation = isSpring ? new SpringAnimation(
/** @type SpringAnimationProps */
props) : new CSSAnimation(
/** @type CssAnimationProps */
props);
this.activeAnimations.push(animation);
animation.onFinish = () => this.stop(animation);
return animation;
}
/**
* @param {Animation} animation
*/
stop(animation) {
animation.destroy();
const index = this.activeAnimations.indexOf(animation);
if (index > -1) {
this.activeAnimations.splice(index, 1);
}
}
stopAll() {
// _stopAllAnimations
this.activeAnimations.forEach(animation => {
animation.destroy();
});
this.activeAnimations = [];
}
/**
* Stop all pan or zoom transitions
*/
stopAllPan() {
this.activeAnimations = this.activeAnimations.filter(animation => {
if (animation.props.isPan) {
animation.destroy();
return false;
}
return true;
});
}
stopMainScroll() {
this.activeAnimations = this.activeAnimations.filter(animation => {
if (animation.props.isMainScroll) {
animation.destroy();
return false;
}
return true;
});
}
/**
* Returns true if main scroll transition is running
*/
// isMainScrollRunning() {
// return this.activeAnimations.some((animation) => {
// return animation.props.isMainScroll;
// });
// }
/**
* Returns true if any pan or zoom transition is running
*/
isPanRunning() {
return this.activeAnimations.some(animation => {
return animation.props.isPan;
});
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/**
* Handles scroll wheel.
* Can pan and zoom current slide image.
*/
class ScrollWheel {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
pswp.events.add(pswp.element, 'wheel',
/** @type EventListener */
this._onWheel.bind(this));
}
/**
* @private
* @param {WheelEvent} e
*/
_onWheel(e) {
e.preventDefault();
const {
currSlide
} = this.pswp;
let {
deltaX,
deltaY
} = e;
if (!currSlide) {
return;
}
if (this.pswp.dispatch('wheel', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (e.ctrlKey || this.pswp.options.wheelToZoom) {
// zoom
if (currSlide.isZoomable()) {
let zoomFactor = -deltaY;
if (e.deltaMode === 1
/* DOM_DELTA_LINE */
) {
zoomFactor *= 0.05;
} else {
zoomFactor *= e.deltaMode ? 1 : 0.002;
}
zoomFactor = 2 ** zoomFactor;
const destZoomLevel = currSlide.currZoomLevel * zoomFactor;
currSlide.zoomTo(destZoomLevel, {
x: e.clientX,
y: e.clientY
});
}
} else {
// pan
if (currSlide.isPannable()) {
if (e.deltaMode === 1
/* DOM_DELTA_LINE */
) {
// 18 - average line height
deltaX *= 18;
deltaY *= 18;
}
currSlide.panTo(currSlide.pan.x - deltaX, currSlide.pan.y - deltaY);
}
}
}
}
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/**
* @template T
* @typedef {import('../types.js').Methods} Methods
*/
/**
* @typedef {Object} UIElementMarkupProps
* @prop {boolean} [isCustomSVG]
* @prop {string} inner
* @prop {string} [outlineID]
* @prop {number | string} [size]
*/
/**
* @typedef {Object} UIElementData
* @prop {DefaultUIElements | string} [name]
* @prop {string} [className]
* @prop {UIElementMarkup} [html]
* @prop {boolean} [isButton]
* @prop {keyof HTMLElementTagNameMap} [tagName]
* @prop {string} [title]
* @prop {string} [ariaLabel]
* @prop {(element: HTMLElement, pswp: PhotoSwipe) => void} [onInit]
* @prop {Methods | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void)} [onClick]
* @prop {'bar' | 'wrapper' | 'root'} [appendTo]
* @prop {number} [order]
*/
/** @typedef {'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter'} DefaultUIElements */
/** @typedef {string | UIElementMarkupProps} UIElementMarkup */
/**
* @param {UIElementMarkup} [htmlData]
* @returns {string}
*/
function addElementHTML(htmlData) {
if (typeof htmlData === 'string') {
// Allow developers to provide full svg,
// For example:
//
//
//
//
// Can also be any HTML string.
return htmlData;
}
if (!htmlData || !htmlData.isCustomSVG) {
return '';
}
const svgData = htmlData;
let out = ''; // replace all %d with size
out = out.split('%d').join(
/** @type {string} */
svgData.size || 32); // Icons may contain outline/shadow,
// to make it we "clone" base icon shape and add border to it.
// Icon itself and border are styled via CSS.
//
// Property shadowID defines ID of element that should be cloned.
if (svgData.outlineID) {
out += ' ';
}
out += svgData.inner;
out += ' ';
return out;
}
class UIElement {
/**
* @param {PhotoSwipe} pswp
* @param {UIElementData} data
*/
constructor(pswp, data) {
var _container;
const name = data.name || data.className;
let elementHTML = data.html; // @ts-expect-error lookup only by `data.name` maybe?
if (pswp.options[name] === false) {
// exit if element is disabled from options
return;
} // Allow to override SVG icons from options
// @ts-expect-error lookup only by `data.name` maybe?
if (typeof pswp.options[name + 'SVG'] === 'string') {
// arrowPrevSVG
// arrowNextSVG
// closeSVG
// zoomSVG
// @ts-expect-error lookup only by `data.name` maybe?
elementHTML = pswp.options[name + 'SVG'];
}
pswp.dispatch('uiElementCreate', {
data
});
let className = '';
if (data.isButton) {
className += 'pswp__button ';
className += data.className || `pswp__button--${data.name}`;
} else {
className += data.className || `pswp__${data.name}`;
}
let tagName = data.isButton ? data.tagName || 'button' : data.tagName || 'div';
tagName =
/** @type {keyof HTMLElementTagNameMap} */
tagName.toLowerCase();
/** @type {HTMLElement} */
const element = createElement(className, tagName);
if (data.isButton) {
if (tagName === 'button') {
/** @type {HTMLButtonElement} */
element.type = 'button';
}
let {
title
} = data;
const {
ariaLabel
} = data; // @ts-expect-error lookup only by `data.name` maybe?
if (typeof pswp.options[name + 'Title'] === 'string') {
// @ts-expect-error lookup only by `data.name` maybe?
title = pswp.options[name + 'Title'];
}
if (title) {
element.title = title;
}
const ariaText = ariaLabel || title;
if (ariaText) {
element.setAttribute('aria-label', ariaText);
}
}
element.innerHTML = addElementHTML(elementHTML);
if (data.onInit) {
data.onInit(element, pswp);
}
if (data.onClick) {
element.onclick = e => {
if (typeof data.onClick === 'string') {
// @ts-ignore
pswp[data.onClick]();
} else if (typeof data.onClick === 'function') {
data.onClick(e, element, pswp);
}
};
} // Top bar is default position
const appendTo = data.appendTo || 'bar';
/** @type {HTMLElement | undefined} root element by default */
let container = pswp.element;
if (appendTo === 'bar') {
if (!pswp.topBar) {
pswp.topBar = createElement('pswp__top-bar pswp__hide-on-close', 'div', pswp.scrollWrap);
}
container = pswp.topBar;
} else {
// element outside of top bar gets a secondary class
// that makes element fade out on close
element.classList.add('pswp__hide-on-close');
if (appendTo === 'wrapper') {
container = pswp.scrollWrap;
}
}
(_container = container) === null || _container === void 0 || _container.appendChild(pswp.applyFilters('uiElement', element, data));
}
}
/*
Backward and forward arrow buttons
*/
/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/**
*
* @param {HTMLElement} element
* @param {PhotoSwipe} pswp
* @param {boolean} [isNextButton]
*/
function initArrowButton(element, pswp, isNextButton) {
element.classList.add('pswp__button--arrow'); // TODO: this should point to a unique id for this instance
element.setAttribute('aria-controls', 'pswp__items');
pswp.on('change', () => {
if (!pswp.options.loop) {
if (isNextButton) {
/** @type {HTMLButtonElement} */
element.disabled = !(pswp.currIndex < pswp.getNumItems() - 1);
} else {
/** @type {HTMLButtonElement} */
element.disabled = !(pswp.currIndex > 0);
}
}
});
}
/** @type {UIElementData} */
const arrowPrev = {
name: 'arrowPrev',
className: 'pswp__button--arrow--prev',
title: 'Previous',
order: 10,
isButton: true,
appendTo: 'wrapper',
html: {
isCustomSVG: true,
size: 60,
inner: ' ',
outlineID: 'pswp__icn-arrow'
},
onClick: 'prev',
onInit: initArrowButton
};
/** @type {UIElementData} */
const arrowNext = {
name: 'arrowNext',
className: 'pswp__button--arrow--next',
title: 'Next',
order: 11,
isButton: true,
appendTo: 'wrapper',
html: {
isCustomSVG: true,
size: 60,
inner: ' ',
outlineID: 'pswp__icn-arrow'
},
onClick: 'next',
onInit: (el, pswp) => {
initArrowButton(el, pswp, true);
}
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const closeButton = {
name: 'close',
title: 'Close',
order: 20,
isButton: true,
html: {
isCustomSVG: true,
inner: ' ',
outlineID: 'pswp__icn-close'
},
onClick: 'close'
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const zoomButton = {
name: 'zoom',
title: 'Zoom',
order: 10,
isButton: true,
html: {
isCustomSVG: true,
// eslint-disable-next-line max-len
inner: ' ' + ' ' + ' ',
outlineID: 'pswp__icn-zoom'
},
onClick: 'toggleZoom'
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const loadingIndicator = {
name: 'preloader',
appendTo: 'bar',
order: 7,
html: {
isCustomSVG: true,
// eslint-disable-next-line max-len
inner: ' ',
outlineID: 'pswp__icn-loading'
},
onInit: (indicatorElement, pswp) => {
/** @type {boolean | undefined} */
let isVisible;
/** @type {NodeJS.Timeout | null} */
let delayTimeout = null;
/**
* @param {string} className
* @param {boolean} add
*/
const toggleIndicatorClass = (className, add) => {
indicatorElement.classList.toggle('pswp__preloader--' + className, add);
};
/**
* @param {boolean} visible
*/
const setIndicatorVisibility = visible => {
if (isVisible !== visible) {
isVisible = visible;
toggleIndicatorClass('active', visible);
}
};
const updatePreloaderVisibility = () => {
var _pswp$currSlide;
if (!((_pswp$currSlide = pswp.currSlide) !== null && _pswp$currSlide !== void 0 && _pswp$currSlide.content.isLoading())) {
setIndicatorVisibility(false);
if (delayTimeout) {
clearTimeout(delayTimeout);
delayTimeout = null;
}
return;
}
if (!delayTimeout) {
// display loading indicator with delay
delayTimeout = setTimeout(() => {
var _pswp$currSlide2;
setIndicatorVisibility(Boolean((_pswp$currSlide2 = pswp.currSlide) === null || _pswp$currSlide2 === void 0 ? void 0 : _pswp$currSlide2.content.isLoading()));
delayTimeout = null;
}, pswp.options.preloaderDelay);
}
};
pswp.on('change', updatePreloaderVisibility);
pswp.on('loadComplete', e => {
if (pswp.currSlide === e.slide) {
updatePreloaderVisibility();
}
}); // expose the method
if (pswp.ui) {
pswp.ui.updatePreloaderVisibility = updatePreloaderVisibility;
}
}
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const counterIndicator = {
name: 'counter',
order: 5,
onInit: (counterElement, pswp) => {
pswp.on('change', () => {
counterElement.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems();
});
}
};
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
/**
* Set special class on element when image is zoomed.
*
* By default, it is used to adjust
* zoom icon and zoom cursor via CSS.
*
* @param {HTMLElement} el
* @param {boolean} isZoomedIn
*/
function setZoomedIn(el, isZoomedIn) {
el.classList.toggle('pswp--zoomed-in', isZoomedIn);
}
class UI {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
this.isRegistered = false;
/** @type {UIElementData[]} */
this.uiElementsData = [];
/** @type {(UIElement | UIElementData)[]} */
this.items = [];
/** @type {() => void} */
this.updatePreloaderVisibility = () => {};
/**
* @private
* @type {number | undefined}
*/
this._lastUpdatedZoomLevel = undefined;
}
init() {
const {
pswp
} = this;
this.isRegistered = false;
this.uiElementsData = [closeButton, arrowPrev, arrowNext, zoomButton, loadingIndicator, counterIndicator];
pswp.dispatch('uiRegister'); // sort by order
this.uiElementsData.sort((a, b) => {
// default order is 0
return (a.order || 0) - (b.order || 0);
});
this.items = [];
this.isRegistered = true;
this.uiElementsData.forEach(uiElementData => {
this.registerElement(uiElementData);
});
pswp.on('change', () => {
var _pswp$element;
(_pswp$element = pswp.element) === null || _pswp$element === void 0 || _pswp$element.classList.toggle('pswp--one-slide', pswp.getNumItems() === 1);
});
pswp.on('zoomPanUpdate', () => this._onZoomPanUpdate());
}
/**
* @param {UIElementData} elementData
*/
registerElement(elementData) {
if (this.isRegistered) {
this.items.push(new UIElement(this.pswp, elementData));
} else {
this.uiElementsData.push(elementData);
}
}
/**
* Fired each time zoom or pan position is changed.
* Update classes that control visibility of zoom button and cursor icon.
*
* @private
*/
_onZoomPanUpdate() {
const {
template,
currSlide,
options
} = this.pswp;
if (this.pswp.opener.isClosing || !template || !currSlide) {
return;
}
let {
currZoomLevel
} = currSlide; // if not open yet - check against initial zoom level
if (!this.pswp.opener.isOpen) {
currZoomLevel = currSlide.zoomLevels.initial;
}
if (currZoomLevel === this._lastUpdatedZoomLevel) {
return;
}
this._lastUpdatedZoomLevel = currZoomLevel;
const currZoomLevelDiff = currSlide.zoomLevels.initial - currSlide.zoomLevels.secondary; // Initial and secondary zoom levels are almost equal
if (Math.abs(currZoomLevelDiff) < 0.01 || !currSlide.isZoomable()) {
// disable zoom
setZoomedIn(template, false);
template.classList.remove('pswp--zoom-allowed');
return;
}
template.classList.add('pswp--zoom-allowed');
const potentialZoomLevel = currZoomLevel === currSlide.zoomLevels.initial ? currSlide.zoomLevels.secondary : currSlide.zoomLevels.initial;
setZoomedIn(template, potentialZoomLevel <= currZoomLevel);
if (options.imageClickAction === 'zoom' || options.imageClickAction === 'zoom-or-close') {
template.classList.add('pswp--click-to-zoom');
}
}
}
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {{ x: number; y: number; w: number; innerRect?: { w: number; h: number; x: number; y: number } }} Bounds */
/**
* @param {HTMLElement} el
* @returns Bounds
*/
function getBoundsByElement(el) {
const thumbAreaRect = el.getBoundingClientRect();
return {
x: thumbAreaRect.left,
y: thumbAreaRect.top,
w: thumbAreaRect.width
};
}
/**
* @param {HTMLElement} el
* @param {number} imageWidth
* @param {number} imageHeight
* @returns Bounds
*/
function getCroppedBoundsByElement(el, imageWidth, imageHeight) {
const thumbAreaRect = el.getBoundingClientRect(); // fill image into the area
// (do they same as object-fit:cover does to retrieve coordinates)
const hRatio = thumbAreaRect.width / imageWidth;
const vRatio = thumbAreaRect.height / imageHeight;
const fillZoomLevel = hRatio > vRatio ? hRatio : vRatio;
const offsetX = (thumbAreaRect.width - imageWidth * fillZoomLevel) / 2;
const offsetY = (thumbAreaRect.height - imageHeight * fillZoomLevel) / 2;
/**
* Coordinates of the image,
* as if it was not cropped,
* height is calculated automatically
*
* @type {Bounds}
*/
const bounds = {
x: thumbAreaRect.left + offsetX,
y: thumbAreaRect.top + offsetY,
w: imageWidth * fillZoomLevel
}; // Coordinates of inner crop area
// relative to the image
bounds.innerRect = {
w: thumbAreaRect.width,
h: thumbAreaRect.height,
x: offsetX,
y: offsetY
};
return bounds;
}
/**
* Get dimensions of thumbnail image
* (click on which opens photoswipe or closes photoswipe to)
*
* @param {number} index
* @param {SlideData} itemData
* @param {PhotoSwipe} instance PhotoSwipe instance
* @returns {Bounds | undefined}
*/
function getThumbBounds(index, itemData, instance) {
// legacy event, before filters were introduced
const event = instance.dispatch('thumbBounds', {
index,
itemData,
instance
}); // @ts-expect-error
if (event.thumbBounds) {
// @ts-expect-error
return event.thumbBounds;
}
const {
element
} = itemData;
/** @type {Bounds | undefined} */
let thumbBounds;
/** @type {HTMLElement | null | undefined} */
let thumbnail;
if (element && instance.options.thumbSelector !== false) {
const thumbSelector = instance.options.thumbSelector || 'img';
thumbnail = element.matches(thumbSelector) ? element :
/** @type {HTMLElement | null} */
element.querySelector(thumbSelector);
}
thumbnail = instance.applyFilters('thumbEl', thumbnail, itemData, index);
if (thumbnail) {
if (!itemData.thumbCropped) {
thumbBounds = getBoundsByElement(thumbnail);
} else {
thumbBounds = getCroppedBoundsByElement(thumbnail, itemData.width || itemData.w || 0, itemData.height || itemData.h || 0);
}
}
return instance.applyFilters('thumbBounds', thumbBounds, itemData, index);
}
/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../slide/content.js').default} ContentDefault */
/** @typedef {import('../slide/slide.js').default} Slide */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
/**
* Allow adding an arbitrary props to the Content
* https://photoswipe.com/custom-content/#using-webp-image-format
* @typedef {ContentDefault & Record} Content
*/
/** @typedef {{ x?: number; y?: number }} Point */
/**
* @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
*
*
* https://photoswipe.com/adding-ui-elements/
*
* @prop {undefined} uiRegister
* @prop {{ data: UIElementData }} uiElementCreate
*
*
* https://photoswipe.com/events/#initialization-events
*
* @prop {undefined} beforeOpen
* @prop {undefined} firstUpdate
* @prop {undefined} initialLayout
* @prop {undefined} change
* @prop {undefined} afterInit
* @prop {undefined} bindEvents
*
*
* https://photoswipe.com/events/#opening-or-closing-transition-events
*
* @prop {undefined} openingAnimationStart
* @prop {undefined} openingAnimationEnd
* @prop {undefined} closingAnimationStart
* @prop {undefined} closingAnimationEnd
*
*
* https://photoswipe.com/events/#closing-events
*
* @prop {undefined} close
* @prop {undefined} destroy
*
*
* https://photoswipe.com/events/#pointer-and-gesture-events
*
* @prop {{ originalEvent: PointerEvent }} pointerDown
* @prop {{ originalEvent: PointerEvent }} pointerMove
* @prop {{ originalEvent: PointerEvent }} pointerUp
* @prop {{ bgOpacity: number }} pinchClose can be default prevented
* @prop {{ panY: number }} verticalDrag can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*
* @prop {{ content: Content }} contentInit
* @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
* @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
* @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
* @prop {{ content: Content; slide: Slide }} loadError
* @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
* @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
* @prop {{ content: Content }} contentLazyLoad can be default prevented
* @prop {{ content: Content }} contentAppend can be default prevented
* @prop {{ content: Content }} contentActivate can be default prevented
* @prop {{ content: Content }} contentDeactivate can be default prevented
* @prop {{ content: Content }} contentRemove can be default prevented
* @prop {{ content: Content }} contentDestroy can be default prevented
*
*
* undocumented
*
* @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
*
* @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
* @prop {{ x: number; dragging: boolean }} moveMainScroll
* @prop {{ slide: Slide }} firstZoomPan
* @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
* @prop {undefined} beforeResize
* @prop {undefined} resize
* @prop {undefined} viewportSize
* @prop {undefined} updateScrollOffset
* @prop {{ slide: Slide }} slideInit
* @prop {{ slide: Slide }} afterSetContent
* @prop {{ slide: Slide }} slideLoad
* @prop {{ slide: Slide }} appendHeavy can be default prevented
* @prop {{ slide: Slide }} appendHeavyContent
* @prop {{ slide: Slide }} slideActivate
* @prop {{ slide: Slide }} slideDeactivate
* @prop {{ slide: Slide }} slideDestroy
* @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
* @prop {{ slide: Slide }} zoomPanUpdate
* @prop {{ slide: Slide }} initialZoomPan
* @prop {{ slide: Slide }} calcSlideSize
* @prop {undefined} resolutionChanged
* @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
* @prop {{ content: Content }} contentAppendImage can be default prevented
* @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
* @prop {undefined} lazyLoad
* @prop {{ slide: Slide }} calcBounds
* @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
*
*
* legacy
*
* @prop {undefined} init
* @prop {undefined} initialZoomIn
* @prop {undefined} initialZoomOut
* @prop {undefined} initialZoomInEnd
* @prop {undefined} initialZoomOutEnd
* @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
* @prop {{ itemData: SlideData; index: number }} itemData
* @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
*/
/**
* @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
*
* @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*
* @prop {(itemData: SlideData, index: number) => SlideData} itemData
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*
* @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*
* @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*
* @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*
* @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*
* @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*
* @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*
* @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*
*
* @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*
* @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*
* @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*
* @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*
* @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
*
* @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
*
*/
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent : PhotoSwipeEvent & PhotoSwipeEventsMap[T]} AugmentedEvent
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {(event: AugmentedEvent) => void} EventCallback
*/
/**
* Base PhotoSwipe event object
*
* @template {keyof PhotoSwipeEventsMap} T
*/
class PhotoSwipeEvent {
/**
* @param {T} type
* @param {PhotoSwipeEventsMap[T]} [details]
*/
constructor(type, details) {
this.type = type;
this.defaultPrevented = false;
if (details) {
Object.assign(this, details);
}
}
preventDefault() {
this.defaultPrevented = true;
}
}
/**
* PhotoSwipe base class that can listen and dispatch for events.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
*/
class Eventable {
constructor() {
/**
* @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent) => void)[] }}
*/
this._listeners = {};
/**
* @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter[] }}
*/
this._filters = {};
/** @type {PhotoSwipe | undefined} */
this.pswp = undefined;
/** @type {PhotoSwipeOptions | undefined} */
this.options = undefined;
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
* @param {number} priority
*/
addFilter(name, fn, priority = 100) {
var _this$_filters$name, _this$_filters$name2, _this$pswp;
if (!this._filters[name]) {
this._filters[name] = [];
}
(_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
fn,
priority
});
(_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
(_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
*/
removeFilter(name, fn) {
if (this._filters[name]) {
// @ts-expect-error
this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
}
if (this.pswp) {
this.pswp.removeFilter(name, fn);
}
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {Parameters} args
* @returns {Parameters[0]}
*/
applyFilters(name, ...args) {
var _this$_filters$name3;
(_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
// @ts-expect-error
args[0] = filter.fn.apply(this, args);
});
return args[0];
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
on(name, fn) {
var _this$_listeners$name, _this$pswp2;
if (!this._listeners[name]) {
this._listeners[name] = [];
}
(_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
// also bind events to PhotoSwipe Core,
// if it's open.
(_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
off(name, fn) {
var _this$pswp3;
if (this._listeners[name]) {
// @ts-expect-error
this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
}
(_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {PhotoSwipeEventsMap[T]} [details]
* @returns {AugmentedEvent}
*/
dispatch(name, details) {
var _this$_listeners$name2;
if (this.pswp) {
return this.pswp.dispatch(name, details);
}
const event =
/** @type {AugmentedEvent} */
new PhotoSwipeEvent(name, details);
(_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
listener.call(this, event);
});
return event;
}
}
class Placeholder {
/**
* @param {string | false} imageSrc
* @param {HTMLElement} container
*/
constructor(imageSrc, container) {
// Create placeholder
// (stretched thumbnail or simple div behind the main image)
/** @type {HTMLImageElement | HTMLDivElement | null} */
this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
if (imageSrc) {
const imgEl =
/** @type {HTMLImageElement} */
this.element;
imgEl.decoding = 'async';
imgEl.alt = '';
imgEl.src = imageSrc;
imgEl.setAttribute('role', 'presentation');
}
this.element.setAttribute('aria-hidden', 'true');
}
/**
* @param {number} width
* @param {number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.element.tagName === 'IMG') {
// Use transform scale() to modify img placeholder size
// (instead of changing width/height directly).
// This helps with performance, specifically in iOS15 Safari.
setWidthHeight(this.element, 250, 'auto');
this.element.style.transformOrigin = '0 0';
this.element.style.transform = toTransformString(0, 0, width / 250);
} else {
setWidthHeight(this.element, width, height);
}
}
destroy() {
var _this$element;
if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
this.element.remove();
}
this.element = null;
}
}
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../util/util.js').LoadState} LoadState */
class Content {
/**
* @param {SlideData} itemData Slide data
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
*/
constructor(itemData, instance, index) {
this.instance = instance;
this.data = itemData;
this.index = index;
/** @type {HTMLImageElement | HTMLDivElement | undefined} */
this.element = undefined;
/** @type {Placeholder | undefined} */
this.placeholder = undefined;
/** @type {Slide | undefined} */
this.slide = undefined;
this.displayedImageWidth = 0;
this.displayedImageHeight = 0;
this.width = Number(this.data.w) || Number(this.data.width) || 0;
this.height = Number(this.data.h) || Number(this.data.height) || 0;
this.isAttached = false;
this.hasSlide = false;
this.isDecoding = false;
/** @type {LoadState} */
this.state = LOAD_STATE.IDLE;
if (this.data.type) {
this.type = this.data.type;
} else if (this.data.src) {
this.type = 'image';
} else {
this.type = 'html';
}
this.instance.dispatch('contentInit', {
content: this
});
}
removePlaceholder() {
if (this.placeholder && !this.keepPlaceholder()) {
// With delay, as image might be loaded, but not rendered
setTimeout(() => {
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
}, 1000);
}
}
/**
* Preload content
*
* @param {boolean} isLazy
* @param {boolean} [reload]
*/
load(isLazy, reload) {
if (this.slide && this.usePlaceholder()) {
if (!this.placeholder) {
const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide,
// as rendering (even small stretched thumbnail) is an expensive operation
this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
} else {
const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
if (placeholderEl && !placeholderEl.parentElement) {
this.slide.container.prepend(placeholderEl);
}
}
}
if (this.element && !reload) {
return;
}
if (this.instance.dispatch('contentLoad', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
if (this.isImageContent()) {
this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
// Due to Safari feature, we must define sizes before srcset.
if (this.displayedImageWidth) {
this.loadImage(isLazy);
}
} else {
this.element = createElement('pswp__content', 'div');
this.element.innerHTML = this.data.html || '';
}
if (reload && this.slide) {
this.slide.updateContentSize(true);
}
}
/**
* Preload image
*
* @param {boolean} isLazy
*/
loadImage(isLazy) {
var _this$data$src, _this$data$alt;
if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
const imageElement =
/** @type HTMLImageElement */
this.element;
this.updateSrcsetSizes();
if (this.data.srcset) {
imageElement.srcset = this.data.srcset;
}
imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
this.state = LOAD_STATE.LOADING;
if (imageElement.complete) {
this.onLoaded();
} else {
imageElement.onload = () => {
this.onLoaded();
};
imageElement.onerror = () => {
this.onError();
};
}
}
/**
* Assign slide to content
*
* @param {Slide} slide
*/
setSlide(slide) {
this.slide = slide;
this.hasSlide = true;
this.instance = slide.pswp; // todo: do we need to unset slide?
}
/**
* Content load success handler
*/
onLoaded() {
this.state = LOAD_STATE.LOADED;
if (this.slide && this.element) {
this.instance.dispatch('loadComplete', {
slide: this.slide,
content: this
}); // if content is reloaded
if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
this.append();
this.slide.updateContentSize(true);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/**
* Content load error handler
*/
onError() {
this.state = LOAD_STATE.ERROR;
if (this.slide) {
this.displayError();
this.instance.dispatch('loadComplete', {
slide: this.slide,
isError: true,
content: this
});
this.instance.dispatch('loadError', {
slide: this.slide,
content: this
});
}
}
/**
* @returns {Boolean} If the content is currently loading
*/
isLoading() {
return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
}
/**
* @returns {Boolean} If the content is in error state
*/
isError() {
return this.state === LOAD_STATE.ERROR;
}
/**
* @returns {boolean} If the content is image
*/
isImageContent() {
return this.type === 'image';
}
/**
* Update content size
*
* @param {Number} width
* @param {Number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.placeholder) {
this.placeholder.setDisplayedSize(width, height);
}
if (this.instance.dispatch('contentResize', {
content: this,
width,
height
}).defaultPrevented) {
return;
}
setWidthHeight(this.element, width, height);
if (this.isImageContent() && !this.isError()) {
const isInitialSizeUpdate = !this.displayedImageWidth && width;
this.displayedImageWidth = width;
this.displayedImageHeight = height;
if (isInitialSizeUpdate) {
this.loadImage(false);
} else {
this.updateSrcsetSizes();
}
if (this.slide) {
this.instance.dispatch('imageSizeChange', {
slide: this.slide,
width,
height,
content: this
});
}
}
}
/**
* @returns {boolean} If the content can be zoomed
*/
isZoomable() {
return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
}
/**
* Update image srcset sizes attribute based on width and height
*/
updateSrcsetSizes() {
// Handle srcset sizes attribute.
//
// Never lower quality, if it was increased previously.
// Chrome does this automatically, Firefox and Safari do not,
// so we store largest used size in dataset.
if (!this.isImageContent() || !this.element || !this.data.srcset) {
return;
}
const image =
/** @type HTMLImageElement */
this.element;
const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
image.sizes = sizesWidth + 'px';
image.dataset.largestUsedSize = String(sizesWidth);
}
}
/**
* @returns {boolean} If content should use a placeholder (from msrc by default)
*/
usePlaceholder() {
return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
}
/**
* Preload content with lazy-loading param
*/
lazyLoad() {
if (this.instance.dispatch('contentLazyLoad', {
content: this
}).defaultPrevented) {
return;
}
this.load(true);
}
/**
* @returns {boolean} If placeholder should be kept after content is loaded
*/
keepPlaceholder() {
return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
}
/**
* Destroy the content
*/
destroy() {
this.hasSlide = false;
this.slide = undefined;
if (this.instance.dispatch('contentDestroy', {
content: this
}).defaultPrevented) {
return;
}
this.remove();
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
if (this.isImageContent() && this.element) {
this.element.onload = null;
this.element.onerror = null;
this.element = undefined;
}
}
/**
* Display error message
*/
displayError() {
if (this.slide) {
var _this$instance$option, _this$instance$option2;
let errorMsgEl = createElement('pswp__error-msg', 'div');
errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
errorMsgEl =
/** @type {HTMLDivElement} */
this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
this.element = createElement('pswp__content pswp__error-msg-container', 'div');
this.element.appendChild(errorMsgEl);
this.slide.container.innerText = '';
this.slide.container.appendChild(this.element);
this.slide.updateContentSize(true);
this.removePlaceholder();
}
}
/**
* Append the content
*/
append() {
if (this.isAttached || !this.element) {
return;
}
this.isAttached = true;
if (this.state === LOAD_STATE.ERROR) {
this.displayError();
return;
}
if (this.instance.dispatch('contentAppend', {
content: this
}).defaultPrevented) {
return;
}
const supportsDecode = ('decode' in this.element);
if (this.isImageContent()) {
// Use decode() on nearby slides
//
// Nearby slide images are in DOM and not hidden via display:none.
// However, they are placed offscreen (to the left and right side).
//
// Some browsers do not composite the image until it's actually visible,
// using decode() helps.
//
// You might ask "why dont you just decode() and then append all images",
// that's because I want to show image before it's fully loaded,
// as browser can render parts of image while it is loading.
// We do not do this in Safari due to partial loading bug.
if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
this.isDecoding = true; // purposefully using finally instead of then,
// as if srcset sizes changes dynamically - it may cause decode error
/** @type {HTMLImageElement} */
this.element.decode().catch(() => {}).finally(() => {
this.isDecoding = false;
this.appendImage();
});
} else {
this.appendImage();
}
} else if (this.slide && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
}
/**
* Activate the slide,
* active slide is generally the current one,
* meaning the user can see it.
*/
activate() {
if (this.instance.dispatch('contentActivate', {
content: this
}).defaultPrevented || !this.slide) {
return;
}
if (this.isImageContent() && this.isDecoding && !isSafari()) {
// add image to slide when it becomes active,
// even if it's not finished decoding
this.appendImage();
} else if (this.isError()) {
this.load(false, true); // try to reload
}
if (this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'false');
}
}
/**
* Deactivate the content
*/
deactivate() {
this.instance.dispatch('contentDeactivate', {
content: this
});
if (this.slide && this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'true');
}
}
/**
* Remove the content from DOM
*/
remove() {
this.isAttached = false;
if (this.instance.dispatch('contentRemove', {
content: this
}).defaultPrevented) {
return;
}
if (this.element && this.element.parentNode) {
this.element.remove();
}
if (this.placeholder && this.placeholder.element) {
this.placeholder.element.remove();
}
}
/**
* Append the image content to slide container
*/
appendImage() {
if (!this.isAttached) {
return;
}
if (this.instance.dispatch('contentAppendImage', {
content: this
}).defaultPrevented) {
return;
} // ensure that element exists and is not already appended
if (this.slide && this.element && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/** @typedef {import('./content.js').default} Content */
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
const MIN_SLIDES_TO_CACHE = 5;
/**
* Lazy-load an image
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* @param {SlideData} itemData Data about the slide
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
function lazyLoadData(itemData, instance, index) {
const content = instance.createContentFromData(itemData, index);
/** @type {ZoomLevel | undefined} */
let zoomLevel;
const {
options
} = instance; // We need to know dimensions of the image to preload it,
// as it might use srcset, and we need to define sizes
if (options) {
zoomLevel = new ZoomLevel(options, itemData, -1);
let viewportSize;
if (instance.pswp) {
viewportSize = instance.pswp.viewportSize;
} else {
viewportSize = getViewportSize(options, instance);
}
const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
zoomLevel.update(content.width, content.height, panAreaSize);
}
content.lazyLoad();
if (zoomLevel) {
content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
}
return content;
}
/**
* Lazy-loads specific slide.
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* By default, it loads image based on viewport size and initial zoom level.
*
* @param {number} index Slide index
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
* @returns {Content | undefined}
*/
function lazyLoadSlide(index, instance) {
const itemData = instance.getItemData(index);
if (instance.dispatch('lazyLoadSlide', {
index,
itemData
}).defaultPrevented) {
return;
}
return lazyLoadData(itemData, instance, index);
}
class ContentLoader {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp; // Total amount of cached images
this.limit = Math.max(pswp.options.preload[0] + pswp.options.preload[1] + 1, MIN_SLIDES_TO_CACHE);
/** @type {Content[]} */
this._cachedItems = [];
}
/**
* Lazy load nearby slides based on `preload` option.
*
* @param {number} [diff] Difference between slide indexes that was changed recently, or 0.
*/
updateLazy(diff) {
const {
pswp
} = this;
if (pswp.dispatch('lazyLoad').defaultPrevented) {
return;
}
const {
preload
} = pswp.options;
const isForward = diff === undefined ? true : diff >= 0;
let i; // preload[1] - num items to preload in forward direction
for (i = 0; i <= preload[1]; i++) {
this.loadSlideByIndex(pswp.currIndex + (isForward ? i : -i));
} // preload[0] - num items to preload in backward direction
for (i = 1; i <= preload[0]; i++) {
this.loadSlideByIndex(pswp.currIndex + (isForward ? -i : i));
}
}
/**
* @param {number} initialIndex
*/
loadSlideByIndex(initialIndex) {
const index = this.pswp.getLoopedIndex(initialIndex); // try to get cached content
let content = this.getContentByIndex(index);
if (!content) {
// no cached content, so try to load from scratch:
content = lazyLoadSlide(index, this.pswp); // if content can be loaded, add it to cache:
if (content) {
this.addToCache(content);
}
}
}
/**
* @param {Slide} slide
* @returns {Content}
*/
getContentBySlide(slide) {
let content = this.getContentByIndex(slide.index);
if (!content) {
// create content if not found in cache
content = this.pswp.createContentFromData(slide.data, slide.index);
this.addToCache(content);
} // assign slide to content
content.setSlide(slide);
return content;
}
/**
* @param {Content} content
*/
addToCache(content) {
// move to the end of array
this.removeByIndex(content.index);
this._cachedItems.push(content);
if (this._cachedItems.length > this.limit) {
// Destroy the first content that's not attached
const indexToRemove = this._cachedItems.findIndex(item => {
return !item.isAttached && !item.hasSlide;
});
if (indexToRemove !== -1) {
const removedItem = this._cachedItems.splice(indexToRemove, 1)[0];
removedItem.destroy();
}
}
}
/**
* Removes an image from cache, does not destroy() it, just removes.
*
* @param {number} index
*/
removeByIndex(index) {
const indexToRemove = this._cachedItems.findIndex(item => item.index === index);
if (indexToRemove !== -1) {
this._cachedItems.splice(indexToRemove, 1);
}
}
/**
* @param {number} index
* @returns {Content | undefined}
*/
getContentByIndex(index) {
return this._cachedItems.find(content => content.index === index);
}
destroy() {
this._cachedItems.forEach(content => content.destroy());
this._cachedItems = [];
}
}
/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
/** @typedef {import("../slide/slide.js").SlideData} SlideData */
/**
* PhotoSwipe base class that can retrieve data about every slide.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
*/
class PhotoSwipeBase extends Eventable {
/**
* Get total number of slides
*
* @returns {number}
*/
getNumItems() {
var _this$options;
let numItems = 0;
const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
if (dataSource && 'length' in dataSource) {
// may be an array or just object with length property
numItems = dataSource.length;
} else if (dataSource && 'gallery' in dataSource) {
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
if (dataSource.items) {
numItems = dataSource.items.length;
}
} // legacy event, before filters were introduced
const event = this.dispatch('numItems', {
dataSource,
numItems
});
return this.applyFilters('numItems', event.numItems, dataSource);
}
/**
* @param {SlideData} slideData
* @param {number} index
* @returns {Content}
*/
createContentFromData(slideData, index) {
return new Content(slideData, this, index);
}
/**
* Get item data by index.
*
* "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
* For example, it may contain properties like
* `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
*
* @param {number} index
* @returns {SlideData}
*/
getItemData(index) {
var _this$options2;
const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
/** @type {SlideData | HTMLElement} */
let dataSourceItem = {};
if (Array.isArray(dataSource)) {
// Datasource is an array of elements
dataSourceItem = dataSource[index];
} else if (dataSource && 'gallery' in dataSource) {
// dataSource has gallery property,
// thus it was created by Lightbox, based on
// gallery and children options
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
dataSourceItem = dataSource.items[index];
}
let itemData = dataSourceItem;
if (itemData instanceof Element) {
itemData = this._domElementToItemData(itemData);
} // Dispatching the itemData event,
// it's a legacy verion before filters were introduced
const event = this.dispatch('itemData', {
itemData: itemData || {},
index
});
return this.applyFilters('itemData', event.itemData, index);
}
/**
* Get array of gallery DOM elements,
* based on childSelector and gallery element.
*
* @param {HTMLElement} galleryElement
* @returns {HTMLElement[]}
*/
_getGalleryDOMElements(galleryElement) {
var _this$options3, _this$options4;
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
}
return [galleryElement];
}
/**
* Converts DOM element to item data object.
*
* @param {HTMLElement} element DOM element
* @returns {SlideData}
*/
_domElementToItemData(element) {
/** @type {SlideData} */
const itemData = {
element
};
const linkEl =
/** @type {HTMLAnchorElement} */
element.tagName === 'A' ? element : element.querySelector('a');
if (linkEl) {
// src comes from data-pswp-src attribute,
// if it's empty link href is used
itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
if (linkEl.dataset.pswpSrcset) {
itemData.srcset = linkEl.dataset.pswpSrcset;
}
itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
itemData.w = itemData.width;
itemData.h = itemData.height;
if (linkEl.dataset.pswpType) {
itemData.type = linkEl.dataset.pswpType;
}
const thumbnailEl = element.querySelector('img');
if (thumbnailEl) {
var _thumbnailEl$getAttri;
// msrc is URL to placeholder image that's displayed before large image is loaded
// by default it's displayed only for the first slide
itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
}
if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
itemData.thumbCropped = true;
}
}
return this.applyFilters('domItemData', itemData, element, linkEl);
}
/**
* Lazy-load by slide data
*
* @param {SlideData} itemData Data about the slide
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
lazyLoadData(itemData, index) {
return lazyLoadData(itemData, this, index);
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/** @typedef {import('./slide/get-thumb-bounds.js').Bounds} Bounds */
/** @typedef {import('./util/animations.js').AnimationProps} AnimationProps */
// some browsers do not paint
// elements which opacity is set to 0,
// since we need to pre-render elements for the animation -
// we set it to the minimum amount
const MIN_OPACITY = 0.003;
/**
* Manages opening and closing transitions of the PhotoSwipe.
*
* It can perform zoom, fade or no transition.
*/
class Opener {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
this.isClosed = true;
this.isOpen = false;
this.isClosing = false;
this.isOpening = false;
/**
* @private
* @type {number | false | undefined}
*/
this._duration = undefined;
/** @private */
this._useAnimation = false;
/** @private */
this._croppedZoom = false;
/** @private */
this._animateRootOpacity = false;
/** @private */
this._animateBgOpacity = false;
/**
* @private
* @type { HTMLDivElement | HTMLImageElement | null | undefined }
*/
this._placeholder = undefined;
/**
* @private
* @type { HTMLDivElement | undefined }
*/
this._opacityElement = undefined;
/**
* @private
* @type { HTMLDivElement | undefined }
*/
this._cropContainer1 = undefined;
/**
* @private
* @type { HTMLElement | null | undefined }
*/
this._cropContainer2 = undefined;
/**
* @private
* @type {Bounds | undefined}
*/
this._thumbBounds = undefined;
this._prepareOpen = this._prepareOpen.bind(this); // Override initial zoom and pan position
pswp.on('firstZoomPan', this._prepareOpen);
}
open() {
this._prepareOpen();
this._start();
}
close() {
if (this.isClosed || this.isClosing || this.isOpening) {
// if we close during opening animation
// for now do nothing,
// browsers aren't good at changing the direction of the CSS transition
return;
}
const slide = this.pswp.currSlide;
this.isOpen = false;
this.isOpening = false;
this.isClosing = true;
this._duration = this.pswp.options.hideAnimationDuration;
if (slide && slide.currZoomLevel * slide.width >= this.pswp.options.maxWidthToAnimate) {
this._duration = 0;
}
this._applyStartProps();
setTimeout(() => {
this._start();
}, this._croppedZoom ? 30 : 0);
}
/** @private */
_prepareOpen() {
this.pswp.off('firstZoomPan', this._prepareOpen);
if (!this.isOpening) {
const slide = this.pswp.currSlide;
this.isOpening = true;
this.isClosing = false;
this._duration = this.pswp.options.showAnimationDuration;
if (slide && slide.zoomLevels.initial * slide.width >= this.pswp.options.maxWidthToAnimate) {
this._duration = 0;
}
this._applyStartProps();
}
}
/** @private */
_applyStartProps() {
const {
pswp
} = this;
const slide = this.pswp.currSlide;
const {
options
} = pswp;
if (options.showHideAnimationType === 'fade') {
options.showHideOpacity = true;
this._thumbBounds = undefined;
} else if (options.showHideAnimationType === 'none') {
options.showHideOpacity = false;
this._duration = 0;
this._thumbBounds = undefined;
} else if (this.isOpening && pswp._initialThumbBounds) {
// Use initial bounds if defined
this._thumbBounds = pswp._initialThumbBounds;
} else {
this._thumbBounds = this.pswp.getThumbBounds();
}
this._placeholder = slide === null || slide === void 0 ? void 0 : slide.getPlaceholderElement();
pswp.animations.stopAll(); // Discard animations when duration is less than 50ms
this._useAnimation = Boolean(this._duration && this._duration > 50);
this._animateZoom = Boolean(this._thumbBounds) && (slide === null || slide === void 0 ? void 0 : slide.content.usePlaceholder()) && (!this.isClosing || !pswp.mainScroll.isShifted());
if (!this._animateZoom) {
this._animateRootOpacity = true;
if (this.isOpening && slide) {
slide.zoomAndPanToInitial();
slide.applyCurrentZoomPan();
}
} else {
var _options$showHideOpac;
this._animateRootOpacity = (_options$showHideOpac = options.showHideOpacity) !== null && _options$showHideOpac !== void 0 ? _options$showHideOpac : false;
}
this._animateBgOpacity = !this._animateRootOpacity && this.pswp.options.bgOpacity > MIN_OPACITY;
this._opacityElement = this._animateRootOpacity ? pswp.element : pswp.bg;
if (!this._useAnimation) {
this._duration = 0;
this._animateZoom = false;
this._animateBgOpacity = false;
this._animateRootOpacity = true;
if (this.isOpening) {
if (pswp.element) {
pswp.element.style.opacity = String(MIN_OPACITY);
}
pswp.applyBgOpacity(1);
}
return;
}
if (this._animateZoom && this._thumbBounds && this._thumbBounds.innerRect) {
var _this$pswp$currSlide;
// Properties are used when animation from cropped thumbnail
this._croppedZoom = true;
this._cropContainer1 = this.pswp.container;
this._cropContainer2 = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.holderElement;
if (pswp.container) {
pswp.container.style.overflow = 'hidden';
pswp.container.style.width = pswp.viewportSize.x + 'px';
}
} else {
this._croppedZoom = false;
}
if (this.isOpening) {
// Apply styles before opening transition
if (this._animateRootOpacity) {
if (pswp.element) {
pswp.element.style.opacity = String(MIN_OPACITY);
}
pswp.applyBgOpacity(1);
} else {
if (this._animateBgOpacity && pswp.bg) {
pswp.bg.style.opacity = String(MIN_OPACITY);
}
if (pswp.element) {
pswp.element.style.opacity = '1';
}
}
if (this._animateZoom) {
this._setClosedStateZoomPan();
if (this._placeholder) {
// tell browser that we plan to animate the placeholder
this._placeholder.style.willChange = 'transform'; // hide placeholder to allow hiding of
// elements that overlap it (such as icons over the thumbnail)
this._placeholder.style.opacity = String(MIN_OPACITY);
}
}
} else if (this.isClosing) {
// hide nearby slides to make sure that
// they are not painted during the transition
if (pswp.mainScroll.itemHolders[0]) {
pswp.mainScroll.itemHolders[0].el.style.display = 'none';
}
if (pswp.mainScroll.itemHolders[2]) {
pswp.mainScroll.itemHolders[2].el.style.display = 'none';
}
if (this._croppedZoom) {
if (pswp.mainScroll.x !== 0) {
// shift the main scroller to zero position
pswp.mainScroll.resetPosition();
pswp.mainScroll.resize();
}
}
}
}
/** @private */
_start() {
if (this.isOpening && this._useAnimation && this._placeholder && this._placeholder.tagName === 'IMG') {
// To ensure smooth animation
// we wait till the current slide image placeholder is decoded,
// but no longer than 250ms,
// and no shorter than 50ms
// (just using requestanimationframe is not enough in Firefox,
// for some reason)
new Promise(resolve => {
let decoded = false;
let isDelaying = true;
decodeImage(
/** @type {HTMLImageElement} */
this._placeholder).finally(() => {
decoded = true;
if (!isDelaying) {
resolve(true);
}
});
setTimeout(() => {
isDelaying = false;
if (decoded) {
resolve(true);
}
}, 50);
setTimeout(resolve, 250);
}).finally(() => this._initiate());
} else {
this._initiate();
}
}
/** @private */
_initiate() {
var _this$pswp$element, _this$pswp$element2;
(_this$pswp$element = this.pswp.element) === null || _this$pswp$element === void 0 || _this$pswp$element.style.setProperty('--pswp-transition-duration', this._duration + 'ms');
this.pswp.dispatch(this.isOpening ? 'openingAnimationStart' : 'closingAnimationStart'); // legacy event
this.pswp.dispatch(
/** @type {'initialZoomIn' | 'initialZoomOut'} */
'initialZoom' + (this.isOpening ? 'In' : 'Out'));
(_this$pswp$element2 = this.pswp.element) === null || _this$pswp$element2 === void 0 || _this$pswp$element2.classList.toggle('pswp--ui-visible', this.isOpening);
if (this.isOpening) {
if (this._placeholder) {
// unhide the placeholder
this._placeholder.style.opacity = '1';
}
this._animateToOpenState();
} else if (this.isClosing) {
this._animateToClosedState();
}
if (!this._useAnimation) {
this._onAnimationComplete();
}
}
/** @private */
_onAnimationComplete() {
const {
pswp
} = this;
this.isOpen = this.isOpening;
this.isClosed = this.isClosing;
this.isOpening = false;
this.isClosing = false;
pswp.dispatch(this.isOpen ? 'openingAnimationEnd' : 'closingAnimationEnd'); // legacy event
pswp.dispatch(
/** @type {'initialZoomInEnd' | 'initialZoomOutEnd'} */
'initialZoom' + (this.isOpen ? 'InEnd' : 'OutEnd'));
if (this.isClosed) {
pswp.destroy();
} else if (this.isOpen) {
var _pswp$currSlide;
if (this._animateZoom && pswp.container) {
pswp.container.style.overflow = 'visible';
pswp.container.style.width = '100%';
}
(_pswp$currSlide = pswp.currSlide) === null || _pswp$currSlide === void 0 || _pswp$currSlide.applyCurrentZoomPan();
}
}
/** @private */
_animateToOpenState() {
const {
pswp
} = this;
if (this._animateZoom) {
if (this._croppedZoom && this._cropContainer1 && this._cropContainer2) {
this._animateTo(this._cropContainer1, 'transform', 'translate3d(0,0,0)');
this._animateTo(this._cropContainer2, 'transform', 'none');
}
if (pswp.currSlide) {
pswp.currSlide.zoomAndPanToInitial();
this._animateTo(pswp.currSlide.container, 'transform', pswp.currSlide.getCurrentTransform());
}
}
if (this._animateBgOpacity && pswp.bg) {
this._animateTo(pswp.bg, 'opacity', String(pswp.options.bgOpacity));
}
if (this._animateRootOpacity && pswp.element) {
this._animateTo(pswp.element, 'opacity', '1');
}
}
/** @private */
_animateToClosedState() {
const {
pswp
} = this;
if (this._animateZoom) {
this._setClosedStateZoomPan(true);
} // do not animate opacity if it's already at 0
if (this._animateBgOpacity && pswp.bgOpacity > 0.01 && pswp.bg) {
this._animateTo(pswp.bg, 'opacity', '0');
}
if (this._animateRootOpacity && pswp.element) {
this._animateTo(pswp.element, 'opacity', '0');
}
}
/**
* @private
* @param {boolean} [animate]
*/
_setClosedStateZoomPan(animate) {
if (!this._thumbBounds) return;
const {
pswp
} = this;
const {
innerRect
} = this._thumbBounds;
const {
currSlide,
viewportSize
} = pswp;
if (this._croppedZoom && innerRect && this._cropContainer1 && this._cropContainer2) {
const containerOnePanX = -viewportSize.x + (this._thumbBounds.x - innerRect.x) + innerRect.w;
const containerOnePanY = -viewportSize.y + (this._thumbBounds.y - innerRect.y) + innerRect.h;
const containerTwoPanX = viewportSize.x - innerRect.w;
const containerTwoPanY = viewportSize.y - innerRect.h;
if (animate) {
this._animateTo(this._cropContainer1, 'transform', toTransformString(containerOnePanX, containerOnePanY));
this._animateTo(this._cropContainer2, 'transform', toTransformString(containerTwoPanX, containerTwoPanY));
} else {
setTransform(this._cropContainer1, containerOnePanX, containerOnePanY);
setTransform(this._cropContainer2, containerTwoPanX, containerTwoPanY);
}
}
if (currSlide) {
equalizePoints(currSlide.pan, innerRect || this._thumbBounds);
currSlide.currZoomLevel = this._thumbBounds.w / currSlide.width;
if (animate) {
this._animateTo(currSlide.container, 'transform', currSlide.getCurrentTransform());
} else {
currSlide.applyCurrentZoomPan();
}
}
}
/**
* @private
* @param {HTMLElement} target
* @param {'transform' | 'opacity'} prop
* @param {string} propValue
*/
_animateTo(target, prop, propValue) {
if (!this._duration) {
target.style[prop] = propValue;
return;
}
const {
animations
} = this.pswp;
/** @type {AnimationProps} */
const animProps = {
duration: this._duration,
easing: this.pswp.options.easing,
onComplete: () => {
if (!animations.activeAnimations.length) {
this._onAnimationComplete();
}
},
target
};
animProps[prop] = propValue;
animations.startTransition(animProps);
}
}
/**
* @template T
* @typedef {import('./types.js').Type} Type
*/
/** @typedef {import('./slide/slide.js').SlideData} SlideData */
/** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */
/** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */
/** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
/** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
/** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('./core/eventable.js').EventCallback} EventCallback
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('./core/eventable.js').AugmentedEvent} AugmentedEvent
*/
/** @typedef {{ x: number; y: number; id?: string | number }} Point */
/** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */
/** @typedef {SlideData[]} DataSourceArray */
/** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */
/** @typedef {DataSourceArray | DataSourceObject} DataSource */
/** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */
/** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */
/** @typedef {Type | { default: Type }} PhotoSwipeModule */
/** @typedef {PhotoSwipeModule | Promise | (() => Promise)} PhotoSwipeModuleOption */
/**
* @typedef {string | NodeListOf | HTMLElement[] | HTMLElement} ElementProvider
*/
/** @typedef {Partial} PhotoSwipeOptions https://photoswipe.com/options/ */
/**
* @typedef {Object} PreparedPhotoSwipeOptions
*
* @prop {DataSource} [dataSource]
* Pass an array of any items via dataSource option. Its length will determine amount of slides
* (which may be modified further from numItems event).
*
* Each item should contain data that you need to generate slide
* (for image slide it would be src (image URL), width (image width), height, srcset, alt).
*
* If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter.
*
* @prop {number} bgOpacity
* Background backdrop opacity, always define it via this option and not via CSS rgba color.
*
* @prop {number} spacing
* Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport).
*
* @prop {boolean} allowPanToNext
* Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events.
*
* @prop {boolean} loop
* If set to true you'll be able to swipe from the last to the first image.
* Option is always false when there are less than 3 slides.
*
* @prop {boolean} [wheelToZoom]
* By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel.
*
* @prop {boolean} pinchToClose
* Pinch touch gesture to close the gallery.
*
* @prop {boolean} closeOnVerticalDrag
* Vertical drag gesture to close the PhotoSwipe.
*
* @prop {Padding} [padding]
* Slide area padding (in pixels).
*
* @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn]
* The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example:
*
* @prop {number | false} hideAnimationDuration
* Transition duration in milliseconds, can be 0.
*
* @prop {number | false} showAnimationDuration
* Transition duration in milliseconds, can be 0.
*
* @prop {number | false} zoomAnimationDuration
* Transition duration in milliseconds, can be 0.
*
* @prop {string} easing
* String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions.
*
* @prop {boolean} escKey
* Esc key to close.
*
* @prop {boolean} arrowKeys
* Left/right arrow keys for navigation.
*
* @prop {boolean} trapFocus
* Trap focus within PhotoSwipe element while it's open.
*
* @prop {boolean} returnFocus
* Restore focus the last active element after PhotoSwipe is closed.
*
* @prop {boolean} clickToCloseNonZoomable
* If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it.
*
* @prop {ActionType | ActionFn | false} imageClickAction
* Refer to click and tap actions page.
*
* @prop {ActionType | ActionFn | false} bgClickAction
* Refer to click and tap actions page.
*
* @prop {ActionType | ActionFn | false} tapAction
* Refer to click and tap actions page.
*
* @prop {ActionType | ActionFn | false} doubleTapAction
* Refer to click and tap actions page.
*
* @prop {number} preloaderDelay
* Delay before the loading indicator will be displayed,
* if image is loaded during it - the indicator will not be displayed at all. Can be zero.
*
* @prop {string} indexIndicatorSep
* Used for slide count indicator ("1 of 10 ").
*
* @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn]
* A function that should return slide viewport width and height, in format {x: 100, y: 100}.
*
* @prop {string} errorMsg
* Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter.
*
* @prop {[number, number]} preload
* Lazy loading of nearby slides based on direction of movement. Should be an array with two integers,
* first one - number of items to preload before the current image, second one - after the current image.
* Two nearby images are always loaded.
*
* @prop {string} [mainClass]
* Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space.
* Example on Styling page.
*
* @prop {HTMLElement} [appendToEl]
* Element to which PhotoSwipe dialog will be appended when it opens.
*
* @prop {number} maxWidthToAnimate
* Maximum width of image to animate, if initial rendered image width
* is larger than this value - the opening/closing transition will be automatically disabled.
*
* @prop {string} [closeTitle]
* Translating
*
* @prop {string} [zoomTitle]
* Translating
*
* @prop {string} [arrowPrevTitle]
* Translating
*
* @prop {string} [arrowNextTitle]
* Translating
*
* @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType]
* To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`).
* It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`.
*
* Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`.
*
* @prop {number} index
* Defines start slide index.
*
* @prop {(e: MouseEvent) => number} [getClickedIndexFn]
*
* @prop {boolean} [arrowPrev]
* @prop {boolean} [arrowNext]
* @prop {boolean} [zoom]
* @prop {boolean} [close]
* @prop {boolean} [counter]
*
* @prop {string} [arrowPrevSVG]
* @prop {string} [arrowNextSVG]
* @prop {string} [zoomSVG]
* @prop {string} [closeSVG]
* @prop {string} [counterSVG]
*
* @prop {string} [arrowPrevTitle]
* @prop {string} [arrowNextTitle]
* @prop {string} [zoomTitle]
* @prop {string} [closeTitle]
* @prop {string} [counterTitle]
*
* @prop {ZoomLevelOption} [initialZoomLevel]
* @prop {ZoomLevelOption} [secondaryZoomLevel]
* @prop {ZoomLevelOption} [maxZoomLevel]
*
* @prop {boolean} [mouseMovePan]
* @prop {Point | null} [initialPointerPos]
* @prop {boolean} [showHideOpacity]
*
* @prop {PhotoSwipeModuleOption} [pswpModule]
* @prop {() => Promise} [openPromise]
* @prop {boolean} [preloadFirstSlide]
* @prop {ElementProvider} [gallery]
* @prop {string} [gallerySelector]
* @prop {ElementProvider} [children]
* @prop {string} [childSelector]
* @prop {string | false} [thumbSelector]
*/
/** @type {PreparedPhotoSwipeOptions} */
const defaultOptions = {
allowPanToNext: true,
spacing: 0.1,
loop: true,
pinchToClose: true,
closeOnVerticalDrag: true,
hideAnimationDuration: 333,
showAnimationDuration: 333,
zoomAnimationDuration: 333,
escKey: true,
arrowKeys: true,
trapFocus: true,
returnFocus: true,
maxWidthToAnimate: 4000,
clickToCloseNonZoomable: true,
imageClickAction: 'zoom-or-close',
bgClickAction: 'close',
tapAction: 'toggle-controls',
doubleTapAction: 'zoom',
indexIndicatorSep: ' / ',
preloaderDelay: 2000,
bgOpacity: 0.8,
index: 0,
errorMsg: 'The image cannot be loaded',
preload: [1, 2],
easing: 'cubic-bezier(.4,0,.22,1)'
};
/**
* PhotoSwipe Core
*/
class PhotoSwipe extends PhotoSwipeBase {
/**
* @param {PhotoSwipeOptions} [options]
*/
constructor(options) {
super();
this.options = this._prepareOptions(options || {});
/**
* offset of viewport relative to document
*
* @type {Point}
*/
this.offset = {
x: 0,
y: 0
};
/**
* @type {Point}
* @private
*/
this._prevViewportSize = {
x: 0,
y: 0
};
/**
* Size of scrollable PhotoSwipe viewport
*
* @type {Point}
*/
this.viewportSize = {
x: 0,
y: 0
};
/**
* background (backdrop) opacity
*/
this.bgOpacity = 1;
this.currIndex = 0;
this.potentialIndex = 0;
this.isOpen = false;
this.isDestroying = false;
this.hasMouse = false;
/**
* @private
* @type {SlideData}
*/
this._initialItemData = {};
/** @type {Bounds | undefined} */
this._initialThumbBounds = undefined;
/** @type {HTMLDivElement | undefined} */
this.topBar = undefined;
/** @type {HTMLDivElement | undefined} */
this.element = undefined;
/** @type {HTMLDivElement | undefined} */
this.template = undefined;
/** @type {HTMLDivElement | undefined} */
this.container = undefined;
/** @type {HTMLElement | undefined} */
this.scrollWrap = undefined;
/** @type {Slide | undefined} */
this.currSlide = undefined;
this.events = new DOMEvents();
this.animations = new Animations();
this.mainScroll = new MainScroll(this);
this.gestures = new Gestures(this);
this.opener = new Opener(this);
this.keyboard = new Keyboard(this);
this.contentLoader = new ContentLoader(this);
}
/** @returns {boolean} */
init() {
if (this.isOpen || this.isDestroying) {
return false;
}
this.isOpen = true;
this.dispatch('init'); // legacy
this.dispatch('beforeOpen');
this._createMainStructure(); // add classes to the root element of PhotoSwipe
let rootClasses = 'pswp--open';
if (this.gestures.supportsTouch) {
rootClasses += ' pswp--touch';
}
if (this.options.mainClass) {
rootClasses += ' ' + this.options.mainClass;
}
if (this.element) {
this.element.className += ' ' + rootClasses;
}
this.currIndex = this.options.index || 0;
this.potentialIndex = this.currIndex;
this.dispatch('firstUpdate'); // starting index can be modified here
// initialize scroll wheel handler to block the scroll
this.scrollWheel = new ScrollWheel(this); // sanitize index
if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) {
this.currIndex = 0;
}
if (!this.gestures.supportsTouch) {
// enable mouse features if no touch support detected
this.mouseDetected();
} // causes forced synchronous layout
this.updateSize();
this.offset.y = window.pageYOffset;
this._initialItemData = this.getItemData(this.currIndex);
this.dispatch('gettingData', {
index: this.currIndex,
data: this._initialItemData,
slide: undefined
}); // *Layout* - calculate size and position of elements here
this._initialThumbBounds = this.getThumbBounds();
this.dispatch('initialLayout');
this.on('openingAnimationEnd', () => {
const {
itemHolders
} = this.mainScroll; // Add content to the previous and next slide
if (itemHolders[0]) {
itemHolders[0].el.style.display = 'block';
this.setContent(itemHolders[0], this.currIndex - 1);
}
if (itemHolders[2]) {
itemHolders[2].el.style.display = 'block';
this.setContent(itemHolders[2], this.currIndex + 1);
}
this.appendHeavy();
this.contentLoader.updateLazy();
this.events.add(window, 'resize', this._handlePageResize.bind(this));
this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this));
this.dispatch('bindEvents');
}); // set content for center slide (first time)
if (this.mainScroll.itemHolders[1]) {
this.setContent(this.mainScroll.itemHolders[1], this.currIndex);
}
this.dispatch('change');
this.opener.open();
this.dispatch('afterInit');
return true;
}
/**
* Get looped slide index
* (for example, -1 will return the last slide)
*
* @param {number} index
* @returns {number}
*/
getLoopedIndex(index) {
const numSlides = this.getNumItems();
if (this.options.loop) {
if (index > numSlides - 1) {
index -= numSlides;
}
if (index < 0) {
index += numSlides;
}
}
return clamp(index, 0, numSlides - 1);
}
appendHeavy() {
this.mainScroll.itemHolders.forEach(itemHolder => {
var _itemHolder$slide;
(_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.appendHeavy();
});
}
/**
* Change the slide
* @param {number} index New index
*/
goTo(index) {
this.mainScroll.moveIndexBy(this.getLoopedIndex(index) - this.potentialIndex);
}
/**
* Go to the next slide.
*/
next() {
this.goTo(this.potentialIndex + 1);
}
/**
* Go to the previous slide.
*/
prev() {
this.goTo(this.potentialIndex - 1);
}
/**
* @see slide/slide.js zoomTo
*
* @param {Parameters} args
*/
zoomTo(...args) {
var _this$currSlide;
(_this$currSlide = this.currSlide) === null || _this$currSlide === void 0 || _this$currSlide.zoomTo(...args);
}
/**
* @see slide/slide.js toggleZoom
*/
toggleZoom() {
var _this$currSlide2;
(_this$currSlide2 = this.currSlide) === null || _this$currSlide2 === void 0 || _this$currSlide2.toggleZoom();
}
/**
* Close the gallery.
* After closing transition ends - destroy it
*/
close() {
if (!this.opener.isOpen || this.isDestroying) {
return;
}
this.isDestroying = true;
this.dispatch('close');
this.events.removeAll();
this.opener.close();
}
/**
* Destroys the gallery:
* - instantly closes the gallery
* - unbinds events,
* - cleans intervals and timeouts
* - removes elements from DOM
*/
destroy() {
var _this$element;
if (!this.isDestroying) {
this.options.showHideAnimationType = 'none';
this.close();
return;
}
this.dispatch('destroy');
this._listeners = {};
if (this.scrollWrap) {
this.scrollWrap.ontouchmove = null;
this.scrollWrap.ontouchend = null;
}
(_this$element = this.element) === null || _this$element === void 0 || _this$element.remove();
this.mainScroll.itemHolders.forEach(itemHolder => {
var _itemHolder$slide2;
(_itemHolder$slide2 = itemHolder.slide) === null || _itemHolder$slide2 === void 0 || _itemHolder$slide2.destroy();
});
this.contentLoader.destroy();
this.events.removeAll();
}
/**
* Refresh/reload content of a slide by its index
*
* @param {number} slideIndex
*/
refreshSlideContent(slideIndex) {
this.contentLoader.removeByIndex(slideIndex);
this.mainScroll.itemHolders.forEach((itemHolder, i) => {
var _this$currSlide$index, _this$currSlide3;
let potentialHolderIndex = ((_this$currSlide$index = (_this$currSlide3 = this.currSlide) === null || _this$currSlide3 === void 0 ? void 0 : _this$currSlide3.index) !== null && _this$currSlide$index !== void 0 ? _this$currSlide$index : 0) - 1 + i;
if (this.canLoop()) {
potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex);
}
if (potentialHolderIndex === slideIndex) {
// set the new slide content
this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current
if (i === 1) {
var _itemHolder$slide3;
this.currSlide = itemHolder.slide;
(_itemHolder$slide3 = itemHolder.slide) === null || _itemHolder$slide3 === void 0 || _itemHolder$slide3.setIsActive(true);
}
}
});
this.dispatch('change');
}
/**
* Set slide content
*
* @param {ItemHolder} holder mainScroll.itemHolders array item
* @param {number} index Slide index
* @param {boolean} [force] If content should be set even if index wasn't changed
*/
setContent(holder, index, force) {
if (this.canLoop()) {
index = this.getLoopedIndex(index);
}
if (holder.slide) {
if (holder.slide.index === index && !force) {
// exit if holder already contains this slide
// this could be common when just three slides are used
return;
} // destroy previous slide
holder.slide.destroy();
holder.slide = undefined;
} // exit if no loop and index is out of bounds
if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) {
return;
}
const itemData = this.getItemData(index);
holder.slide = new Slide(itemData, index, this); // set current slide
if (index === this.currIndex) {
this.currSlide = holder.slide;
}
holder.slide.append(holder.el);
}
/** @returns {Point} */
getViewportCenterPoint() {
return {
x: this.viewportSize.x / 2,
y: this.viewportSize.y / 2
};
}
/**
* Update size of all elements.
* Executed on init and on page resize.
*
* @param {boolean} [force] Update size even if size of viewport was not changed.
*/
updateSize(force) {
// let item;
// let itemIndex;
if (this.isDestroying) {
// exit if PhotoSwipe is closed or closing
// (to avoid errors, as resize event might be delayed)
return;
} //const newWidth = this.scrollWrap.clientWidth;
//const newHeight = this.scrollWrap.clientHeight;
const newViewportSize = getViewportSize(this.options, this);
if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) {
// Exit if dimensions were not changed
return;
} //this._prevViewportSize.x = newWidth;
//this._prevViewportSize.y = newHeight;
equalizePoints(this._prevViewportSize, newViewportSize);
this.dispatch('beforeResize');
equalizePoints(this.viewportSize, this._prevViewportSize);
this._updatePageScrollOffset();
this.dispatch('viewportSize'); // Resize slides only after opener animation is finished
// and don't re-calculate size on inital size update
this.mainScroll.resize(this.opener.isOpen);
if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) {
this.mouseDetected();
}
this.dispatch('resize');
}
/**
* @param {number} opacity
*/
applyBgOpacity(opacity) {
this.bgOpacity = Math.max(opacity, 0);
if (this.bg) {
this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity);
}
}
/**
* Whether mouse is detected
*/
mouseDetected() {
if (!this.hasMouse) {
var _this$element2;
this.hasMouse = true;
(_this$element2 = this.element) === null || _this$element2 === void 0 || _this$element2.classList.add('pswp--has_mouse');
}
}
/**
* Page resize event handler
*
* @private
*/
_handlePageResize() {
this.updateSize(); // In iOS webview, if element size depends on document size,
// it'll be measured incorrectly in resize event
//
// https://bugs.webkit.org/show_bug.cgi?id=170595
// https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d
if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) {
setTimeout(() => {
this.updateSize();
}, 500);
}
}
/**
* Page scroll offset is used
* to get correct coordinates
* relative to PhotoSwipe viewport.
*
* @private
*/
_updatePageScrollOffset() {
this.setScrollOffset(0, window.pageYOffset);
}
/**
* @param {number} x
* @param {number} y
*/
setScrollOffset(x, y) {
this.offset.x = x;
this.offset.y = y;
this.dispatch('updateScrollOffset');
}
/**
* Create main HTML structure of PhotoSwipe,
* and add it to DOM
*
* @private
*/
_createMainStructure() {
// root DOM element of PhotoSwipe (.pswp)
this.element = createElement('pswp', 'div');
this.element.setAttribute('tabindex', '-1');
this.element.setAttribute('role', 'dialog'); // template is legacy prop
this.template = this.element; // Background is added as a separate element,
// as animating opacity is faster than animating rgba()
this.bg = createElement('pswp__bg', 'div', this.element);
this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element);
this.container = createElement('pswp__container', 'div', this.scrollWrap); // aria pattern: carousel
this.scrollWrap.setAttribute('aria-roledescription', 'carousel');
this.container.setAttribute('aria-live', 'off');
this.container.setAttribute('id', 'pswp__items');
this.mainScroll.appendHolders();
this.ui = new UI(this);
this.ui.init(); // append to DOM
(this.options.appendToEl || document.body).appendChild(this.element);
}
/**
* Get position and dimensions of small thumbnail
* {x:,y:,w:}
*
* Height is optional (calculated based on the large image)
*
* @returns {Bounds | undefined}
*/
getThumbBounds() {
return getThumbBounds(this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this);
}
/**
* If the PhotoSwipe can have continuous loop
* @returns Boolean
*/
canLoop() {
return this.options.loop && this.getNumItems() > 2;
}
/**
* @private
* @param {PhotoSwipeOptions} options
* @returns {PreparedPhotoSwipeOptions}
*/
_prepareOptions(options) {
if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) {
options.showHideAnimationType = 'none';
options.zoomAnimationDuration = 0;
}
/** @type {PreparedPhotoSwipeOptions} */
return { ...defaultOptions,
...options
};
}
}
export { PhotoSwipe as default };
//# sourceMappingURL=photoswipe.esm.js.map
================================================
FILE: dist/photoswipe-lightbox.esm.js
================================================
/*!
* PhotoSwipe Lightbox 5.4.4 - https://photoswipe.com
* (c) 2024 Dmytro Semenov
*/
/** @typedef {import('../photoswipe.js').Point} Point */
/**
* @template {keyof HTMLElementTagNameMap} T
* @param {string} className
* @param {T} tagName
* @param {Node} [appendToEl]
* @returns {HTMLElementTagNameMap[T]}
*/
function createElement(className, tagName, appendToEl) {
const el = document.createElement(tagName);
if (className) {
el.className = className;
}
if (appendToEl) {
appendToEl.appendChild(el);
}
return el;
}
/**
* Get transform string
*
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
* @returns {string}
*/
function toTransformString(x, y, scale) {
let propValue = `translate3d(${x}px,${y || 0}px,0)`;
if (scale !== undefined) {
propValue += ` scale3d(${scale},${scale},1)`;
}
return propValue;
}
/**
* Apply width and height CSS properties to element
*
* @param {HTMLElement} el
* @param {string | number} w
* @param {string | number} h
*/
function setWidthHeight(el, w, h) {
el.style.width = typeof w === 'number' ? `${w}px` : w;
el.style.height = typeof h === 'number' ? `${h}px` : h;
}
/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
const LOAD_STATE = {
IDLE: 'idle',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error'
};
/**
* Check if click or keydown event was dispatched
* with a special key or via mouse wheel.
*
* @param {MouseEvent | KeyboardEvent} e
* @returns {boolean}
*/
function specialKeyUsed(e) {
return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
}
/**
* Parse `gallery` or `children` options.
*
* @param {import('../photoswipe.js').ElementProvider} [option]
* @param {string} [legacySelector]
* @param {HTMLElement | Document} [parent]
* @returns HTMLElement[]
*/
function getElementsFromOption(option, legacySelector, parent = document) {
/** @type {HTMLElement[]} */
let elements = [];
if (option instanceof Element) {
elements = [option];
} else if (option instanceof NodeList || Array.isArray(option)) {
elements = Array.from(option);
} else {
const selector = typeof option === 'string' ? option : legacySelector;
if (selector) {
elements = Array.from(parent.querySelectorAll(selector));
}
}
return elements;
}
/**
* Check if variable is PhotoSwipe class
*
* @param {any} fn
* @returns {boolean}
*/
function isPswpClass(fn) {
return typeof fn === 'function' && fn.prototype && fn.prototype.goTo;
}
/**
* Check if browser is Safari
*
* @returns {boolean}
*/
function isSafari() {
return !!(navigator.vendor && navigator.vendor.match(/apple/i));
}
/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../slide/content.js').default} ContentDefault */
/** @typedef {import('../slide/slide.js').default} Slide */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
/**
* Allow adding an arbitrary props to the Content
* https://photoswipe.com/custom-content/#using-webp-image-format
* @typedef {ContentDefault & Record} Content
*/
/** @typedef {{ x?: number; y?: number }} Point */
/**
* @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
*
*
* https://photoswipe.com/adding-ui-elements/
*
* @prop {undefined} uiRegister
* @prop {{ data: UIElementData }} uiElementCreate
*
*
* https://photoswipe.com/events/#initialization-events
*
* @prop {undefined} beforeOpen
* @prop {undefined} firstUpdate
* @prop {undefined} initialLayout
* @prop {undefined} change
* @prop {undefined} afterInit
* @prop {undefined} bindEvents
*
*
* https://photoswipe.com/events/#opening-or-closing-transition-events
*
* @prop {undefined} openingAnimationStart
* @prop {undefined} openingAnimationEnd
* @prop {undefined} closingAnimationStart
* @prop {undefined} closingAnimationEnd
*
*
* https://photoswipe.com/events/#closing-events
*
* @prop {undefined} close
* @prop {undefined} destroy
*
*
* https://photoswipe.com/events/#pointer-and-gesture-events
*
* @prop {{ originalEvent: PointerEvent }} pointerDown
* @prop {{ originalEvent: PointerEvent }} pointerMove
* @prop {{ originalEvent: PointerEvent }} pointerUp
* @prop {{ bgOpacity: number }} pinchClose can be default prevented
* @prop {{ panY: number }} verticalDrag can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*
* @prop {{ content: Content }} contentInit
* @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
* @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
* @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
* @prop {{ content: Content; slide: Slide }} loadError
* @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
* @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
* @prop {{ content: Content }} contentLazyLoad can be default prevented
* @prop {{ content: Content }} contentAppend can be default prevented
* @prop {{ content: Content }} contentActivate can be default prevented
* @prop {{ content: Content }} contentDeactivate can be default prevented
* @prop {{ content: Content }} contentRemove can be default prevented
* @prop {{ content: Content }} contentDestroy can be default prevented
*
*
* undocumented
*
* @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
*
* @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
* @prop {{ x: number; dragging: boolean }} moveMainScroll
* @prop {{ slide: Slide }} firstZoomPan
* @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
* @prop {undefined} beforeResize
* @prop {undefined} resize
* @prop {undefined} viewportSize
* @prop {undefined} updateScrollOffset
* @prop {{ slide: Slide }} slideInit
* @prop {{ slide: Slide }} afterSetContent
* @prop {{ slide: Slide }} slideLoad
* @prop {{ slide: Slide }} appendHeavy can be default prevented
* @prop {{ slide: Slide }} appendHeavyContent
* @prop {{ slide: Slide }} slideActivate
* @prop {{ slide: Slide }} slideDeactivate
* @prop {{ slide: Slide }} slideDestroy
* @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
* @prop {{ slide: Slide }} zoomPanUpdate
* @prop {{ slide: Slide }} initialZoomPan
* @prop {{ slide: Slide }} calcSlideSize
* @prop {undefined} resolutionChanged
* @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
* @prop {{ content: Content }} contentAppendImage can be default prevented
* @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
* @prop {undefined} lazyLoad
* @prop {{ slide: Slide }} calcBounds
* @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
*
*
* legacy
*
* @prop {undefined} init
* @prop {undefined} initialZoomIn
* @prop {undefined} initialZoomOut
* @prop {undefined} initialZoomInEnd
* @prop {undefined} initialZoomOutEnd
* @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
* @prop {{ itemData: SlideData; index: number }} itemData
* @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
*/
/**
* @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
*
* @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*
* @prop {(itemData: SlideData, index: number) => SlideData} itemData
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*
* @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*
* @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*
* @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*
* @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*
* @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*
* @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*
* @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*
*
* @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*
* @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*
* @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*
* @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*
* @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
*
* @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
*
*/
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent : PhotoSwipeEvent & PhotoSwipeEventsMap[T]} AugmentedEvent
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {(event: AugmentedEvent) => void} EventCallback
*/
/**
* Base PhotoSwipe event object
*
* @template {keyof PhotoSwipeEventsMap} T
*/
class PhotoSwipeEvent {
/**
* @param {T} type
* @param {PhotoSwipeEventsMap[T]} [details]
*/
constructor(type, details) {
this.type = type;
this.defaultPrevented = false;
if (details) {
Object.assign(this, details);
}
}
preventDefault() {
this.defaultPrevented = true;
}
}
/**
* PhotoSwipe base class that can listen and dispatch for events.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
*/
class Eventable {
constructor() {
/**
* @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent) => void)[] }}
*/
this._listeners = {};
/**
* @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter[] }}
*/
this._filters = {};
/** @type {PhotoSwipe | undefined} */
this.pswp = undefined;
/** @type {PhotoSwipeOptions | undefined} */
this.options = undefined;
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
* @param {number} priority
*/
addFilter(name, fn, priority = 100) {
var _this$_filters$name, _this$_filters$name2, _this$pswp;
if (!this._filters[name]) {
this._filters[name] = [];
}
(_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
fn,
priority
});
(_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
(_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
*/
removeFilter(name, fn) {
if (this._filters[name]) {
// @ts-expect-error
this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
}
if (this.pswp) {
this.pswp.removeFilter(name, fn);
}
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {Parameters} args
* @returns {Parameters[0]}
*/
applyFilters(name, ...args) {
var _this$_filters$name3;
(_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
// @ts-expect-error
args[0] = filter.fn.apply(this, args);
});
return args[0];
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
on(name, fn) {
var _this$_listeners$name, _this$pswp2;
if (!this._listeners[name]) {
this._listeners[name] = [];
}
(_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
// also bind events to PhotoSwipe Core,
// if it's open.
(_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
off(name, fn) {
var _this$pswp3;
if (this._listeners[name]) {
// @ts-expect-error
this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
}
(_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {PhotoSwipeEventsMap[T]} [details]
* @returns {AugmentedEvent}
*/
dispatch(name, details) {
var _this$_listeners$name2;
if (this.pswp) {
return this.pswp.dispatch(name, details);
}
const event =
/** @type {AugmentedEvent} */
new PhotoSwipeEvent(name, details);
(_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
listener.call(this, event);
});
return event;
}
}
class Placeholder {
/**
* @param {string | false} imageSrc
* @param {HTMLElement} container
*/
constructor(imageSrc, container) {
// Create placeholder
// (stretched thumbnail or simple div behind the main image)
/** @type {HTMLImageElement | HTMLDivElement | null} */
this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
if (imageSrc) {
const imgEl =
/** @type {HTMLImageElement} */
this.element;
imgEl.decoding = 'async';
imgEl.alt = '';
imgEl.src = imageSrc;
imgEl.setAttribute('role', 'presentation');
}
this.element.setAttribute('aria-hidden', 'true');
}
/**
* @param {number} width
* @param {number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.element.tagName === 'IMG') {
// Use transform scale() to modify img placeholder size
// (instead of changing width/height directly).
// This helps with performance, specifically in iOS15 Safari.
setWidthHeight(this.element, 250, 'auto');
this.element.style.transformOrigin = '0 0';
this.element.style.transform = toTransformString(0, 0, width / 250);
} else {
setWidthHeight(this.element, width, height);
}
}
destroy() {
var _this$element;
if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
this.element.remove();
}
this.element = null;
}
}
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../util/util.js').LoadState} LoadState */
class Content {
/**
* @param {SlideData} itemData Slide data
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
*/
constructor(itemData, instance, index) {
this.instance = instance;
this.data = itemData;
this.index = index;
/** @type {HTMLImageElement | HTMLDivElement | undefined} */
this.element = undefined;
/** @type {Placeholder | undefined} */
this.placeholder = undefined;
/** @type {Slide | undefined} */
this.slide = undefined;
this.displayedImageWidth = 0;
this.displayedImageHeight = 0;
this.width = Number(this.data.w) || Number(this.data.width) || 0;
this.height = Number(this.data.h) || Number(this.data.height) || 0;
this.isAttached = false;
this.hasSlide = false;
this.isDecoding = false;
/** @type {LoadState} */
this.state = LOAD_STATE.IDLE;
if (this.data.type) {
this.type = this.data.type;
} else if (this.data.src) {
this.type = 'image';
} else {
this.type = 'html';
}
this.instance.dispatch('contentInit', {
content: this
});
}
removePlaceholder() {
if (this.placeholder && !this.keepPlaceholder()) {
// With delay, as image might be loaded, but not rendered
setTimeout(() => {
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
}, 1000);
}
}
/**
* Preload content
*
* @param {boolean} isLazy
* @param {boolean} [reload]
*/
load(isLazy, reload) {
if (this.slide && this.usePlaceholder()) {
if (!this.placeholder) {
const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide,
// as rendering (even small stretched thumbnail) is an expensive operation
this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
} else {
const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
if (placeholderEl && !placeholderEl.parentElement) {
this.slide.container.prepend(placeholderEl);
}
}
}
if (this.element && !reload) {
return;
}
if (this.instance.dispatch('contentLoad', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
if (this.isImageContent()) {
this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
// Due to Safari feature, we must define sizes before srcset.
if (this.displayedImageWidth) {
this.loadImage(isLazy);
}
} else {
this.element = createElement('pswp__content', 'div');
this.element.innerHTML = this.data.html || '';
}
if (reload && this.slide) {
this.slide.updateContentSize(true);
}
}
/**
* Preload image
*
* @param {boolean} isLazy
*/
loadImage(isLazy) {
var _this$data$src, _this$data$alt;
if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
const imageElement =
/** @type HTMLImageElement */
this.element;
this.updateSrcsetSizes();
if (this.data.srcset) {
imageElement.srcset = this.data.srcset;
}
imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
this.state = LOAD_STATE.LOADING;
if (imageElement.complete) {
this.onLoaded();
} else {
imageElement.onload = () => {
this.onLoaded();
};
imageElement.onerror = () => {
this.onError();
};
}
}
/**
* Assign slide to content
*
* @param {Slide} slide
*/
setSlide(slide) {
this.slide = slide;
this.hasSlide = true;
this.instance = slide.pswp; // todo: do we need to unset slide?
}
/**
* Content load success handler
*/
onLoaded() {
this.state = LOAD_STATE.LOADED;
if (this.slide && this.element) {
this.instance.dispatch('loadComplete', {
slide: this.slide,
content: this
}); // if content is reloaded
if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
this.append();
this.slide.updateContentSize(true);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/**
* Content load error handler
*/
onError() {
this.state = LOAD_STATE.ERROR;
if (this.slide) {
this.displayError();
this.instance.dispatch('loadComplete', {
slide: this.slide,
isError: true,
content: this
});
this.instance.dispatch('loadError', {
slide: this.slide,
content: this
});
}
}
/**
* @returns {Boolean} If the content is currently loading
*/
isLoading() {
return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
}
/**
* @returns {Boolean} If the content is in error state
*/
isError() {
return this.state === LOAD_STATE.ERROR;
}
/**
* @returns {boolean} If the content is image
*/
isImageContent() {
return this.type === 'image';
}
/**
* Update content size
*
* @param {Number} width
* @param {Number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.placeholder) {
this.placeholder.setDisplayedSize(width, height);
}
if (this.instance.dispatch('contentResize', {
content: this,
width,
height
}).defaultPrevented) {
return;
}
setWidthHeight(this.element, width, height);
if (this.isImageContent() && !this.isError()) {
const isInitialSizeUpdate = !this.displayedImageWidth && width;
this.displayedImageWidth = width;
this.displayedImageHeight = height;
if (isInitialSizeUpdate) {
this.loadImage(false);
} else {
this.updateSrcsetSizes();
}
if (this.slide) {
this.instance.dispatch('imageSizeChange', {
slide: this.slide,
width,
height,
content: this
});
}
}
}
/**
* @returns {boolean} If the content can be zoomed
*/
isZoomable() {
return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
}
/**
* Update image srcset sizes attribute based on width and height
*/
updateSrcsetSizes() {
// Handle srcset sizes attribute.
//
// Never lower quality, if it was increased previously.
// Chrome does this automatically, Firefox and Safari do not,
// so we store largest used size in dataset.
if (!this.isImageContent() || !this.element || !this.data.srcset) {
return;
}
const image =
/** @type HTMLImageElement */
this.element;
const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
image.sizes = sizesWidth + 'px';
image.dataset.largestUsedSize = String(sizesWidth);
}
}
/**
* @returns {boolean} If content should use a placeholder (from msrc by default)
*/
usePlaceholder() {
return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
}
/**
* Preload content with lazy-loading param
*/
lazyLoad() {
if (this.instance.dispatch('contentLazyLoad', {
content: this
}).defaultPrevented) {
return;
}
this.load(true);
}
/**
* @returns {boolean} If placeholder should be kept after content is loaded
*/
keepPlaceholder() {
return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
}
/**
* Destroy the content
*/
destroy() {
this.hasSlide = false;
this.slide = undefined;
if (this.instance.dispatch('contentDestroy', {
content: this
}).defaultPrevented) {
return;
}
this.remove();
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
if (this.isImageContent() && this.element) {
this.element.onload = null;
this.element.onerror = null;
this.element = undefined;
}
}
/**
* Display error message
*/
displayError() {
if (this.slide) {
var _this$instance$option, _this$instance$option2;
let errorMsgEl = createElement('pswp__error-msg', 'div');
errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
errorMsgEl =
/** @type {HTMLDivElement} */
this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
this.element = createElement('pswp__content pswp__error-msg-container', 'div');
this.element.appendChild(errorMsgEl);
this.slide.container.innerText = '';
this.slide.container.appendChild(this.element);
this.slide.updateContentSize(true);
this.removePlaceholder();
}
}
/**
* Append the content
*/
append() {
if (this.isAttached || !this.element) {
return;
}
this.isAttached = true;
if (this.state === LOAD_STATE.ERROR) {
this.displayError();
return;
}
if (this.instance.dispatch('contentAppend', {
content: this
}).defaultPrevented) {
return;
}
const supportsDecode = ('decode' in this.element);
if (this.isImageContent()) {
// Use decode() on nearby slides
//
// Nearby slide images are in DOM and not hidden via display:none.
// However, they are placed offscreen (to the left and right side).
//
// Some browsers do not composite the image until it's actually visible,
// using decode() helps.
//
// You might ask "why dont you just decode() and then append all images",
// that's because I want to show image before it's fully loaded,
// as browser can render parts of image while it is loading.
// We do not do this in Safari due to partial loading bug.
if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
this.isDecoding = true; // purposefully using finally instead of then,
// as if srcset sizes changes dynamically - it may cause decode error
/** @type {HTMLImageElement} */
this.element.decode().catch(() => {}).finally(() => {
this.isDecoding = false;
this.appendImage();
});
} else {
this.appendImage();
}
} else if (this.slide && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
}
/**
* Activate the slide,
* active slide is generally the current one,
* meaning the user can see it.
*/
activate() {
if (this.instance.dispatch('contentActivate', {
content: this
}).defaultPrevented || !this.slide) {
return;
}
if (this.isImageContent() && this.isDecoding && !isSafari()) {
// add image to slide when it becomes active,
// even if it's not finished decoding
this.appendImage();
} else if (this.isError()) {
this.load(false, true); // try to reload
}
if (this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'false');
}
}
/**
* Deactivate the content
*/
deactivate() {
this.instance.dispatch('contentDeactivate', {
content: this
});
if (this.slide && this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'true');
}
}
/**
* Remove the content from DOM
*/
remove() {
this.isAttached = false;
if (this.instance.dispatch('contentRemove', {
content: this
}).defaultPrevented) {
return;
}
if (this.element && this.element.parentNode) {
this.element.remove();
}
if (this.placeholder && this.placeholder.element) {
this.placeholder.element.remove();
}
}
/**
* Append the image content to slide container
*/
appendImage() {
if (!this.isAttached) {
return;
}
if (this.instance.dispatch('contentAppendImage', {
content: this
}).defaultPrevented) {
return;
} // ensure that element exists and is not already appended
if (this.slide && this.element && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/**
* @param {PhotoSwipeOptions} options
* @param {PhotoSwipeBase} pswp
* @returns {Point}
*/
function getViewportSize(options, pswp) {
if (options.getViewportSizeFn) {
const newViewportSize = options.getViewportSizeFn(options, pswp);
if (newViewportSize) {
return newViewportSize;
}
}
return {
x: document.documentElement.clientWidth,
// TODO: height on mobile is very incosistent due to toolbar
// find a way to improve this
//
// document.documentElement.clientHeight - doesn't seem to work well
y: window.innerHeight
};
}
/**
* Parses padding option.
* Supported formats:
*
* // Object
* padding: {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* }
*
* // A function that returns the object
* paddingFn: (viewportSize, itemData, index) => {
* return {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* };
* }
*
* // Legacy variant
* paddingLeft: 0,
* paddingRight: 0,
* paddingTop: 0,
* paddingBottom: 0,
*
* @param {'left' | 'top' | 'bottom' | 'right'} prop
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
* @param {SlideData} itemData Data about the slide
* @param {number} index Slide index
* @returns {number}
*/
function parsePaddingOption(prop, options, viewportSize, itemData, index) {
let paddingValue = 0;
if (options.paddingFn) {
paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
} else if (options.padding) {
paddingValue = options.padding[prop];
} else {
const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
if (options[legacyPropName]) {
// @ts-expect-error
paddingValue = options[legacyPropName];
}
}
return Number(paddingValue) || 0;
}
/**
* @param {PhotoSwipeOptions} options
* @param {Point} viewportSize
* @param {SlideData} itemData
* @param {number} index
* @returns {Point}
*/
function getPanAreaSize(options, viewportSize, itemData, index) {
return {
x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
};
}
const MAX_IMAGE_WIDTH = 4000;
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
/**
* Calculates zoom levels for specific slide.
* Depends on viewport size and image size.
*/
class ZoomLevel {
/**
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {SlideData} itemData Slide data
* @param {number} index Slide index
* @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
*/
constructor(options, itemData, index, pswp) {
this.pswp = pswp;
this.options = options;
this.itemData = itemData;
this.index = index;
/** @type { Point | null } */
this.panAreaSize = null;
/** @type { Point | null } */
this.elementSize = null;
this.fit = 1;
this.fill = 1;
this.vFill = 1;
this.initial = 1;
this.secondary = 1;
this.max = 1;
this.min = 1;
}
/**
* Calculate initial, secondary and maximum zoom level for the specified slide.
*
* It should be called when either image or viewport size changes.
*
* @param {number} maxWidth
* @param {number} maxHeight
* @param {Point} panAreaSize
*/
update(maxWidth, maxHeight, panAreaSize) {
/** @type {Point} */
const elementSize = {
x: maxWidth,
y: maxHeight
};
this.elementSize = elementSize;
this.panAreaSize = panAreaSize;
const hRatio = panAreaSize.x / elementSize.x;
const vRatio = panAreaSize.y / elementSize.y;
this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
// when it has 100% of viewport vertical space (height)
this.vFill = Math.min(1, vRatio);
this.initial = this._getInitial();
this.secondary = this._getSecondary();
this.max = Math.max(this.initial, this.secondary, this._getMax());
this.min = Math.min(this.fit, this.initial, this.secondary);
if (this.pswp) {
this.pswp.dispatch('zoomLevelsUpdate', {
zoomLevels: this,
slideData: this.itemData
});
}
}
/**
* Parses user-defined zoom option.
*
* @private
* @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
* @returns { number | undefined }
*/
_parseZoomLevelOption(optionPrefix) {
const optionName =
/** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
optionPrefix + 'ZoomLevel';
const optionValue = this.options[optionName];
if (!optionValue) {
return;
}
if (typeof optionValue === 'function') {
return optionValue(this);
}
if (optionValue === 'fill') {
return this.fill;
}
if (optionValue === 'fit') {
return this.fit;
}
return Number(optionValue);
}
/**
* Get zoom level to which image will be zoomed after double-tap gesture,
* or when user clicks on zoom icon,
* or mouse-click on image itself.
* If you return 1 image will be zoomed to its original size.
*
* @private
* @return {number}
*/
_getSecondary() {
let currZoomLevel = this._parseZoomLevelOption('secondary');
if (currZoomLevel) {
return currZoomLevel;
} // 3x of "fit" state, but not larger than original
currZoomLevel = Math.min(1, this.fit * 3);
if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
}
return currZoomLevel;
}
/**
* Get initial image zoom level.
*
* @private
* @return {number}
*/
_getInitial() {
return this._parseZoomLevelOption('initial') || this.fit;
}
/**
* Maximum zoom level when user zooms
* via zoom/pinch gesture,
* via cmd/ctrl-wheel or via trackpad.
*
* @private
* @return {number}
*/
_getMax() {
// max zoom level is x4 from "fit state",
// used for zoom gesture and ctrl/trackpad zoom
return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
}
}
/**
* Lazy-load an image
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* @param {SlideData} itemData Data about the slide
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
function lazyLoadData(itemData, instance, index) {
const content = instance.createContentFromData(itemData, index);
/** @type {ZoomLevel | undefined} */
let zoomLevel;
const {
options
} = instance; // We need to know dimensions of the image to preload it,
// as it might use srcset, and we need to define sizes
if (options) {
zoomLevel = new ZoomLevel(options, itemData, -1);
let viewportSize;
if (instance.pswp) {
viewportSize = instance.pswp.viewportSize;
} else {
viewportSize = getViewportSize(options, instance);
}
const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
zoomLevel.update(content.width, content.height, panAreaSize);
}
content.lazyLoad();
if (zoomLevel) {
content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
}
return content;
}
/**
* Lazy-loads specific slide.
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* By default, it loads image based on viewport size and initial zoom level.
*
* @param {number} index Slide index
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
* @returns {Content | undefined}
*/
function lazyLoadSlide(index, instance) {
const itemData = instance.getItemData(index);
if (instance.dispatch('lazyLoadSlide', {
index,
itemData
}).defaultPrevented) {
return;
}
return lazyLoadData(itemData, instance, index);
}
/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
/** @typedef {import("../slide/slide.js").SlideData} SlideData */
/**
* PhotoSwipe base class that can retrieve data about every slide.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
*/
class PhotoSwipeBase extends Eventable {
/**
* Get total number of slides
*
* @returns {number}
*/
getNumItems() {
var _this$options;
let numItems = 0;
const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
if (dataSource && 'length' in dataSource) {
// may be an array or just object with length property
numItems = dataSource.length;
} else if (dataSource && 'gallery' in dataSource) {
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
if (dataSource.items) {
numItems = dataSource.items.length;
}
} // legacy event, before filters were introduced
const event = this.dispatch('numItems', {
dataSource,
numItems
});
return this.applyFilters('numItems', event.numItems, dataSource);
}
/**
* @param {SlideData} slideData
* @param {number} index
* @returns {Content}
*/
createContentFromData(slideData, index) {
return new Content(slideData, this, index);
}
/**
* Get item data by index.
*
* "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
* For example, it may contain properties like
* `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
*
* @param {number} index
* @returns {SlideData}
*/
getItemData(index) {
var _this$options2;
const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
/** @type {SlideData | HTMLElement} */
let dataSourceItem = {};
if (Array.isArray(dataSource)) {
// Datasource is an array of elements
dataSourceItem = dataSource[index];
} else if (dataSource && 'gallery' in dataSource) {
// dataSource has gallery property,
// thus it was created by Lightbox, based on
// gallery and children options
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
dataSourceItem = dataSource.items[index];
}
let itemData = dataSourceItem;
if (itemData instanceof Element) {
itemData = this._domElementToItemData(itemData);
} // Dispatching the itemData event,
// it's a legacy verion before filters were introduced
const event = this.dispatch('itemData', {
itemData: itemData || {},
index
});
return this.applyFilters('itemData', event.itemData, index);
}
/**
* Get array of gallery DOM elements,
* based on childSelector and gallery element.
*
* @param {HTMLElement} galleryElement
* @returns {HTMLElement[]}
*/
_getGalleryDOMElements(galleryElement) {
var _this$options3, _this$options4;
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
}
return [galleryElement];
}
/**
* Converts DOM element to item data object.
*
* @param {HTMLElement} element DOM element
* @returns {SlideData}
*/
_domElementToItemData(element) {
/** @type {SlideData} */
const itemData = {
element
};
const linkEl =
/** @type {HTMLAnchorElement} */
element.tagName === 'A' ? element : element.querySelector('a');
if (linkEl) {
// src comes from data-pswp-src attribute,
// if it's empty link href is used
itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
if (linkEl.dataset.pswpSrcset) {
itemData.srcset = linkEl.dataset.pswpSrcset;
}
itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
itemData.w = itemData.width;
itemData.h = itemData.height;
if (linkEl.dataset.pswpType) {
itemData.type = linkEl.dataset.pswpType;
}
const thumbnailEl = element.querySelector('img');
if (thumbnailEl) {
var _thumbnailEl$getAttri;
// msrc is URL to placeholder image that's displayed before large image is loaded
// by default it's displayed only for the first slide
itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
}
if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
itemData.thumbCropped = true;
}
}
return this.applyFilters('domItemData', itemData, element, linkEl);
}
/**
* Lazy-load by slide data
*
* @param {SlideData} itemData Data about the slide
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
lazyLoadData(itemData, index) {
return lazyLoadData(itemData, this, index);
}
}
/**
* @template T
* @typedef {import('../types.js').Type} Type
*/
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/content.js').default} Content */
/** @typedef {import('../core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
/** @typedef {import('../core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('../core/eventable.js').EventCallback} EventCallback
*/
/**
* PhotoSwipe Lightbox
*
* - If user has unsupported browser it falls back to default browser action (just opens URL)
* - Binds click event to links that should open PhotoSwipe
* - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes)
* - Initializes PhotoSwipe
*
*
* Loader options use the same object as PhotoSwipe, and supports such options:
*
* gallery - Element | Element[] | NodeList | string selector for the gallery element
* children - Element | Element[] | NodeList | string selector for the gallery children
*
*/
class PhotoSwipeLightbox extends PhotoSwipeBase {
/**
* @param {PhotoSwipeOptions} [options]
*/
constructor(options) {
super();
/** @type {PhotoSwipeOptions} */
this.options = options || {};
this._uid = 0;
this.shouldOpen = false;
/**
* @private
* @type {Content | undefined}
*/
this._preloadedContent = undefined;
this.onThumbnailsClick = this.onThumbnailsClick.bind(this);
}
/**
* Initialize lightbox, should be called only once.
* It's not included in the main constructor, so you may bind events before it.
*/
init() {
// Bind click events to each gallery
getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => {
galleryElement.addEventListener('click', this.onThumbnailsClick, false);
});
}
/**
* @param {MouseEvent} e
*/
onThumbnailsClick(e) {
// Exit and allow default browser action if:
if (specialKeyUsed(e) // ... if clicked with a special key (ctrl/cmd...)
|| window.pswp) {
// ... if PhotoSwipe is already open
return;
} // If both clientX and clientY are 0 or not defined,
// the event is likely triggered by keyboard,
// so we do not pass the initialPoint
//
// Note that some screen readers emulate the mouse position,
// so it's not the ideal way to detect them.
//
/** @type {Point | null} */
let initialPoint = {
x: e.clientX,
y: e.clientY
};
if (!initialPoint.x && !initialPoint.y) {
initialPoint = null;
}
let clickedIndex = this.getClickedIndex(e);
clickedIndex = this.applyFilters('clickedIndex', clickedIndex, e, this);
/** @type {DataSource} */
const dataSource = {
gallery:
/** @type {HTMLElement} */
e.currentTarget
};
if (clickedIndex >= 0) {
e.preventDefault();
this.loadAndOpen(clickedIndex, dataSource, initialPoint);
}
}
/**
* Get index of gallery item that was clicked.
*
* @param {MouseEvent} e click event
* @returns {number}
*/
getClickedIndex(e) {
// legacy option
if (this.options.getClickedIndexFn) {
return this.options.getClickedIndexFn.call(this, e);
}
const clickedTarget =
/** @type {HTMLElement} */
e.target;
const childElements = getElementsFromOption(this.options.children, this.options.childSelector,
/** @type {HTMLElement} */
e.currentTarget);
const clickedChildIndex = childElements.findIndex(child => child === clickedTarget || child.contains(clickedTarget));
if (clickedChildIndex !== -1) {
return clickedChildIndex;
} else if (this.options.children || this.options.childSelector) {
// click wasn't on a child element
return -1;
} // There is only one item (which is the gallery)
return 0;
}
/**
* Load and open PhotoSwipe
*
* @param {number} index
* @param {DataSource} [dataSource]
* @param {Point | null} [initialPoint]
* @returns {boolean}
*/
loadAndOpen(index, dataSource, initialPoint) {
// Check if the gallery is already open
if (window.pswp || !this.options) {
return false;
} // Use the first gallery element if dataSource is not provided
if (!dataSource && this.options.gallery && this.options.children) {
const galleryElements = getElementsFromOption(this.options.gallery);
if (galleryElements[0]) {
dataSource = {
gallery: galleryElements[0]
};
}
} // set initial index
this.options.index = index; // define options for PhotoSwipe constructor
this.options.initialPointerPos = initialPoint;
this.shouldOpen = true;
this.preload(index, dataSource);
return true;
}
/**
* Load the main module and the slide content by index
*
* @param {number} index
* @param {DataSource} [dataSource]
*/
preload(index, dataSource) {
const {
options
} = this;
if (dataSource) {
options.dataSource = dataSource;
} // Add the main module
/** @type {Promise>[]} */
const promiseArray = [];
const pswpModuleType = typeof options.pswpModule;
if (isPswpClass(options.pswpModule)) {
promiseArray.push(Promise.resolve(
/** @type {Type} */
options.pswpModule));
} else if (pswpModuleType === 'string') {
throw new Error('pswpModule as string is no longer supported');
} else if (pswpModuleType === 'function') {
promiseArray.push(
/** @type {() => Promise>} */
options.pswpModule());
} else {
throw new Error('pswpModule is not valid');
} // Add custom-defined promise, if any
if (typeof options.openPromise === 'function') {
// allow developers to perform some task before opening
promiseArray.push(options.openPromise());
}
if (options.preloadFirstSlide !== false && index >= 0) {
this._preloadedContent = lazyLoadSlide(index, this);
} // Wait till all promises resolve and open PhotoSwipe
const uid = ++this._uid;
Promise.all(promiseArray).then(iterableModules => {
if (this.shouldOpen) {
const mainModule = iterableModules[0];
this._openPhotoswipe(mainModule, uid);
}
});
}
/**
* @private
* @param {Type | { default: Type }} module
* @param {number} uid
*/
_openPhotoswipe(module, uid) {
// Cancel opening if UID doesn't match the current one
// (if user clicked on another gallery item before current was loaded).
//
// Or if shouldOpen flag is set to false
// (developer may modify it via public API)
if (uid !== this._uid && this.shouldOpen) {
return;
}
this.shouldOpen = false; // PhotoSwipe is already open
if (window.pswp) {
return;
}
/**
* Pass data to PhotoSwipe and open init
*
* @type {PhotoSwipe}
*/
const pswp = typeof module === 'object' ? new module.default(this.options) // eslint-disable-line
: new module(this.options); // eslint-disable-line
this.pswp = pswp;
window.pswp = pswp; // map listeners from Lightbox to PhotoSwipe Core
/** @type {(keyof PhotoSwipeEventsMap)[]} */
Object.keys(this._listeners).forEach(name => {
var _this$_listeners$name;
(_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.forEach(fn => {
pswp.on(name,
/** @type {EventCallback} */
fn);
});
}); // same with filters
/** @type {(keyof PhotoSwipeFiltersMap)[]} */
Object.keys(this._filters).forEach(name => {
var _this$_filters$name;
(_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.forEach(filter => {
pswp.addFilter(name, filter.fn, filter.priority);
});
});
if (this._preloadedContent) {
pswp.contentLoader.addToCache(this._preloadedContent);
this._preloadedContent = undefined;
}
pswp.on('destroy', () => {
// clean up public variables
this.pswp = undefined;
delete window.pswp;
});
pswp.init();
}
/**
* Unbinds all events, closes PhotoSwipe if it's open.
*/
destroy() {
var _this$pswp;
(_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.destroy();
this.shouldOpen = false;
this._listeners = {};
getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => {
galleryElement.removeEventListener('click', this.onThumbnailsClick, false);
});
}
}
export { PhotoSwipeLightbox as default };
//# sourceMappingURL=photoswipe-lightbox.esm.js.map
================================================
FILE: dist/photoswipe.css
================================================
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
.pswp {
--pswp-bg: #000;
--pswp-placeholder-bg: #222;
--pswp-root-z-index: 100000;
--pswp-preloader-color: rgba(79, 79, 79, 0.4);
--pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
/* defined via js:
--pswp-transition-duration: 333ms; */
--pswp-icon-color: #fff;
--pswp-icon-color-secondary: #4f4f4f;
--pswp-icon-stroke-color: #4f4f4f;
--pswp-icon-stroke-width: 2px;
--pswp-error-text-color: var(--pswp-icon-color);
}
/*
Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
*/
.pswp {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: var(--pswp-root-z-index);
display: none;
touch-action: none;
outline: 0;
opacity: 0.003;
contain: layout style size;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Prevents focus outline on the root element,
(it may be focused initially) */
.pswp:focus {
outline: 0;
}
.pswp * {
box-sizing: border-box;
}
.pswp img {
max-width: none;
}
.pswp--open {
display: block;
}
.pswp,
.pswp__bg {
transform: translateZ(0);
will-change: opacity;
}
.pswp__bg {
opacity: 0.005;
background: var(--pswp-bg);
}
.pswp,
.pswp__scroll-wrap {
overflow: hidden;
}
.pswp__scroll-wrap,
.pswp__bg,
.pswp__container,
.pswp__item,
.pswp__content,
.pswp__img,
.pswp__zoom-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pswp__img,
.pswp__zoom-wrap {
width: auto;
height: auto;
}
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
cursor: -webkit-zoom-in;
cursor: -moz-zoom-in;
cursor: zoom-in;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* :active to override grabbing cursor */
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
.pswp__img {
cursor: -webkit-zoom-out;
cursor: -moz-zoom-out;
cursor: zoom-out;
}
/* Prevent selection and tap highlights */
.pswp__container,
.pswp__img,
.pswp__button,
.pswp__counter {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pswp__item {
/* z-index for fade transition */
z-index: 1;
overflow: hidden;
}
.pswp__hidden {
display: none !important;
}
/* Allow to click through pswp__content element, but not its children */
.pswp__content {
pointer-events: none;
}
.pswp__content > * {
pointer-events: auto;
}
/*
PhotoSwipe UI
*/
/*
Error message appears when image is not loaded
(JS option errorMsg controls markup)
*/
.pswp__error-msg-container {
display: grid;
}
.pswp__error-msg {
margin: auto;
font-size: 1em;
line-height: 1;
color: var(--pswp-error-text-color);
}
/*
class pswp__hide-on-close is applied to elements that
should hide (for example fade out) when PhotoSwipe is closed
and show (for example fade in) when PhotoSwipe is opened
*/
.pswp .pswp__hide-on-close {
opacity: 0.005;
will-change: opacity;
transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
z-index: 10; /* always overlap slide content */
pointer-events: none; /* hidden elements should not be clickable */
}
/* class pswp--ui-visible is added when opening or closing transition starts */
.pswp--ui-visible .pswp__hide-on-close {
opacity: 1;
pointer-events: auto;
}
/* styles, including css reset */
.pswp__button {
position: relative;
display: block;
width: 50px;
height: 60px;
padding: 0;
margin: 0;
overflow: hidden;
cursor: pointer;
background: none;
border: 0;
box-shadow: none;
opacity: 0.85;
-webkit-appearance: none;
-webkit-touch-callout: none;
}
.pswp__button:hover,
.pswp__button:active,
.pswp__button:focus {
transition: none;
padding: 0;
background: none;
border: 0;
box-shadow: none;
opacity: 1;
}
.pswp__button:disabled {
opacity: 0.3;
cursor: auto;
}
.pswp__icn {
fill: var(--pswp-icon-color);
color: var(--pswp-icon-color-secondary);
}
.pswp__icn {
position: absolute;
top: 14px;
left: 9px;
width: 32px;
height: 32px;
overflow: hidden;
pointer-events: none;
}
.pswp__icn-shadow {
stroke: var(--pswp-icon-stroke-color);
stroke-width: var(--pswp-icon-stroke-width);
fill: none;
}
.pswp__icn:focus {
outline: 0;
}
/*
div element that matches size of large image,
large image loads on top of it,
used when msrc is not provided
*/
div.pswp__img--placeholder,
.pswp__img--with-bg {
background: var(--pswp-placeholder-bg);
}
.pswp__top-bar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 60px;
display: flex;
flex-direction: row;
justify-content: flex-end;
z-index: 10;
/* allow events to pass through top bar itself */
pointer-events: none !important;
}
.pswp__top-bar > * {
pointer-events: auto;
/* this makes transition significantly more smooth,
even though inner elements are not animated */
will-change: opacity;
}
/*
Close button
*/
.pswp__button--close {
margin-right: 6px;
}
/*
Arrow buttons
*/
.pswp__button--arrow {
position: absolute;
top: 0;
width: 75px;
height: 100px;
top: 50%;
margin-top: -50px;
}
.pswp__button--arrow:disabled {
display: none;
cursor: default;
}
.pswp__button--arrow .pswp__icn {
top: 50%;
margin-top: -30px;
width: 60px;
height: 60px;
background: none;
border-radius: 0;
}
.pswp--one-slide .pswp__button--arrow {
display: none;
}
/* hide arrows on touch screens */
.pswp--touch .pswp__button--arrow {
visibility: hidden;
}
/* show arrows only after mouse was used */
.pswp--has_mouse .pswp__button--arrow {
visibility: visible;
}
.pswp__button--arrow--prev {
right: auto;
left: 0px;
}
.pswp__button--arrow--next {
right: 0px;
}
.pswp__button--arrow--next .pswp__icn {
left: auto;
right: 14px;
/* flip horizontally */
transform: scale(-1, 1);
}
/*
Zoom button
*/
.pswp__button--zoom {
display: none;
}
.pswp--zoom-allowed .pswp__button--zoom {
display: block;
}
/* "+" => "-" */
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
display: none;
}
/*
Loading indicator
*/
.pswp__preloader {
position: relative;
overflow: hidden;
width: 50px;
height: 60px;
margin-right: auto;
}
.pswp__preloader .pswp__icn {
opacity: 0;
transition: opacity 0.2s linear;
animation: pswp-clockwise 600ms linear infinite;
}
.pswp__preloader--active .pswp__icn {
opacity: 0.85;
}
@keyframes pswp-clockwise {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/*
"1 of 10" counter
*/
.pswp__counter {
height: 30px;
margin-top: 15px;
margin-inline-start: 20px;
font-size: 14px;
line-height: 30px;
color: var(--pswp-icon-color);
text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
opacity: 0.85;
}
.pswp--one-slide .pswp__counter {
display: none;
}
================================================
FILE: dist/photoswipe.esm.js
================================================
/*!
* PhotoSwipe 5.4.4 - https://photoswipe.com
* (c) 2024 Dmytro Semenov
*/
/** @typedef {import('../photoswipe.js').Point} Point */
/**
* @template {keyof HTMLElementTagNameMap} T
* @param {string} className
* @param {T} tagName
* @param {Node} [appendToEl]
* @returns {HTMLElementTagNameMap[T]}
*/
function createElement(className, tagName, appendToEl) {
const el = document.createElement(tagName);
if (className) {
el.className = className;
}
if (appendToEl) {
appendToEl.appendChild(el);
}
return el;
}
/**
* @param {Point} p1
* @param {Point} p2
* @returns {Point}
*/
function equalizePoints(p1, p2) {
p1.x = p2.x;
p1.y = p2.y;
if (p2.id !== undefined) {
p1.id = p2.id;
}
return p1;
}
/**
* @param {Point} p
*/
function roundPoint(p) {
p.x = Math.round(p.x);
p.y = Math.round(p.y);
}
/**
* Returns distance between two points.
*
* @param {Point} p1
* @param {Point} p2
* @returns {number}
*/
function getDistanceBetween(p1, p2) {
const x = Math.abs(p1.x - p2.x);
const y = Math.abs(p1.y - p2.y);
return Math.sqrt(x * x + y * y);
}
/**
* Whether X and Y positions of points are equal
*
* @param {Point} p1
* @param {Point} p2
* @returns {boolean}
*/
function pointsEqual(p1, p2) {
return p1.x === p2.x && p1.y === p2.y;
}
/**
* The float result between the min and max values.
*
* @param {number} val
* @param {number} min
* @param {number} max
* @returns {number}
*/
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
/**
* Get transform string
*
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
* @returns {string}
*/
function toTransformString(x, y, scale) {
let propValue = `translate3d(${x}px,${y || 0}px,0)`;
if (scale !== undefined) {
propValue += ` scale3d(${scale},${scale},1)`;
}
return propValue;
}
/**
* Apply transform:translate(x, y) scale(scale) to element
*
* @param {HTMLElement} el
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
*/
function setTransform(el, x, y, scale) {
el.style.transform = toTransformString(x, y, scale);
}
const defaultCSSEasing = 'cubic-bezier(.4,0,.22,1)';
/**
* Apply CSS transition to element
*
* @param {HTMLElement} el
* @param {string} [prop] CSS property to animate
* @param {number} [duration] in ms
* @param {string} [ease] CSS easing function
*/
function setTransitionStyle(el, prop, duration, ease) {
// inOut: 'cubic-bezier(.4, 0, .22, 1)', // for "toggle state" transitions
// out: 'cubic-bezier(0, 0, .22, 1)', // for "show" transitions
// in: 'cubic-bezier(.4, 0, 1, 1)'// for "hide" transitions
el.style.transition = prop ? `${prop} ${duration}ms ${ease || defaultCSSEasing}` : 'none';
}
/**
* Apply width and height CSS properties to element
*
* @param {HTMLElement} el
* @param {string | number} w
* @param {string | number} h
*/
function setWidthHeight(el, w, h) {
el.style.width = typeof w === 'number' ? `${w}px` : w;
el.style.height = typeof h === 'number' ? `${h}px` : h;
}
/**
* @param {HTMLElement} el
*/
function removeTransitionStyle(el) {
setTransitionStyle(el);
}
/**
* @param {HTMLImageElement} img
* @returns {Promise}
*/
function decodeImage(img) {
if ('decode' in img) {
return img.decode().catch(() => {});
}
if (img.complete) {
return Promise.resolve(img);
}
return new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});
}
/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
const LOAD_STATE = {
IDLE: 'idle',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error'
};
/**
* Check if click or keydown event was dispatched
* with a special key or via mouse wheel.
*
* @param {MouseEvent | KeyboardEvent} e
* @returns {boolean}
*/
function specialKeyUsed(e) {
return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
}
/**
* Parse `gallery` or `children` options.
*
* @param {import('../photoswipe.js').ElementProvider} [option]
* @param {string} [legacySelector]
* @param {HTMLElement | Document} [parent]
* @returns HTMLElement[]
*/
function getElementsFromOption(option, legacySelector, parent = document) {
/** @type {HTMLElement[]} */
let elements = [];
if (option instanceof Element) {
elements = [option];
} else if (option instanceof NodeList || Array.isArray(option)) {
elements = Array.from(option);
} else {
const selector = typeof option === 'string' ? option : legacySelector;
if (selector) {
elements = Array.from(parent.querySelectorAll(selector));
}
}
return elements;
}
/**
* Check if browser is Safari
*
* @returns {boolean}
*/
function isSafari() {
return !!(navigator.vendor && navigator.vendor.match(/apple/i));
}
// Detect passive event listener support
let supportsPassive = false;
/* eslint-disable */
try {
/* @ts-ignore */
window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
get: () => {
supportsPassive = true;
}
}));
} catch (e) {}
/* eslint-enable */
/**
* @typedef {Object} PoolItem
* @prop {HTMLElement | Window | Document | undefined | null} target
* @prop {string} type
* @prop {EventListenerOrEventListenerObject} listener
* @prop {boolean} [passive]
*/
class DOMEvents {
constructor() {
/**
* @type {PoolItem[]}
* @private
*/
this._pool = [];
}
/**
* Adds event listeners
*
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type Can be multiple, separated by space.
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
*/
add(target, type, listener, passive) {
this._toggleListener(target, type, listener, passive);
}
/**
* Removes event listeners
*
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
*/
remove(target, type, listener, passive) {
this._toggleListener(target, type, listener, passive, true);
}
/**
* Removes all bound events
*/
removeAll() {
this._pool.forEach(poolItem => {
this._toggleListener(poolItem.target, poolItem.type, poolItem.listener, poolItem.passive, true, true);
});
this._pool = [];
}
/**
* Adds or removes event
*
* @private
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
* @param {boolean} [unbind] Whether the event should be added or removed
* @param {boolean} [skipPool] Whether events pool should be skipped
*/
_toggleListener(target, type, listener, passive, unbind, skipPool) {
if (!target) {
return;
}
const methodName = unbind ? 'removeEventListener' : 'addEventListener';
const types = type.split(' ');
types.forEach(eType => {
if (eType) {
// Events pool is used to easily unbind all events when PhotoSwipe is closed,
// so developer doesn't need to do this manually
if (!skipPool) {
if (unbind) {
// Remove from the events pool
this._pool = this._pool.filter(poolItem => {
return poolItem.type !== eType || poolItem.listener !== listener || poolItem.target !== target;
});
} else {
// Add to the events pool
this._pool.push({
target,
type: eType,
listener,
passive
});
}
} // most PhotoSwipe events call preventDefault,
// and we do not need browser to scroll the page
const eventOptions = supportsPassive ? {
passive: passive || false
} : false;
target[methodName](eType, listener, eventOptions);
}
});
}
}
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/**
* @param {PhotoSwipeOptions} options
* @param {PhotoSwipeBase} pswp
* @returns {Point}
*/
function getViewportSize(options, pswp) {
if (options.getViewportSizeFn) {
const newViewportSize = options.getViewportSizeFn(options, pswp);
if (newViewportSize) {
return newViewportSize;
}
}
return {
x: document.documentElement.clientWidth,
// TODO: height on mobile is very incosistent due to toolbar
// find a way to improve this
//
// document.documentElement.clientHeight - doesn't seem to work well
y: window.innerHeight
};
}
/**
* Parses padding option.
* Supported formats:
*
* // Object
* padding: {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* }
*
* // A function that returns the object
* paddingFn: (viewportSize, itemData, index) => {
* return {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* };
* }
*
* // Legacy variant
* paddingLeft: 0,
* paddingRight: 0,
* paddingTop: 0,
* paddingBottom: 0,
*
* @param {'left' | 'top' | 'bottom' | 'right'} prop
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
* @param {SlideData} itemData Data about the slide
* @param {number} index Slide index
* @returns {number}
*/
function parsePaddingOption(prop, options, viewportSize, itemData, index) {
let paddingValue = 0;
if (options.paddingFn) {
paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
} else if (options.padding) {
paddingValue = options.padding[prop];
} else {
const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
if (options[legacyPropName]) {
// @ts-expect-error
paddingValue = options[legacyPropName];
}
}
return Number(paddingValue) || 0;
}
/**
* @param {PhotoSwipeOptions} options
* @param {Point} viewportSize
* @param {SlideData} itemData
* @param {number} index
* @returns {Point}
*/
function getPanAreaSize(options, viewportSize, itemData, index) {
return {
x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
};
}
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {Record} Point */
/** @typedef {'x' | 'y'} Axis */
/**
* Calculates minimum, maximum and initial (center) bounds of a slide
*/
class PanBounds {
/**
* @param {Slide} slide
*/
constructor(slide) {
this.slide = slide;
this.currZoomLevel = 1;
this.center =
/** @type {Point} */
{
x: 0,
y: 0
};
this.max =
/** @type {Point} */
{
x: 0,
y: 0
};
this.min =
/** @type {Point} */
{
x: 0,
y: 0
};
}
/**
* _getItemBounds
*
* @param {number} currZoomLevel
*/
update(currZoomLevel) {
this.currZoomLevel = currZoomLevel;
if (!this.slide.width) {
this.reset();
} else {
this._updateAxis('x');
this._updateAxis('y');
this.slide.pswp.dispatch('calcBounds', {
slide: this.slide
});
}
}
/**
* _calculateItemBoundsForAxis
*
* @param {Axis} axis
*/
_updateAxis(axis) {
const {
pswp
} = this.slide;
const elSize = this.slide[axis === 'x' ? 'width' : 'height'] * this.currZoomLevel;
const paddingProp = axis === 'x' ? 'left' : 'top';
const padding = parsePaddingOption(paddingProp, pswp.options, pswp.viewportSize, this.slide.data, this.slide.index);
const panAreaSize = this.slide.panAreaSize[axis]; // Default position of element.
// By default, it is center of viewport:
this.center[axis] = Math.round((panAreaSize - elSize) / 2) + padding; // maximum pan position
this.max[axis] = elSize > panAreaSize ? Math.round(panAreaSize - elSize) + padding : this.center[axis]; // minimum pan position
this.min[axis] = elSize > panAreaSize ? padding : this.center[axis];
} // _getZeroBounds
reset() {
this.center.x = 0;
this.center.y = 0;
this.max.x = 0;
this.max.y = 0;
this.min.x = 0;
this.min.y = 0;
}
/**
* Correct pan position if it's beyond the bounds
*
* @param {Axis} axis x or y
* @param {number} panOffset
* @returns {number}
*/
correctPan(axis, panOffset) {
// checkPanBounds
return clamp(panOffset, this.max[axis], this.min[axis]);
}
}
const MAX_IMAGE_WIDTH = 4000;
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
/**
* Calculates zoom levels for specific slide.
* Depends on viewport size and image size.
*/
class ZoomLevel {
/**
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {SlideData} itemData Slide data
* @param {number} index Slide index
* @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
*/
constructor(options, itemData, index, pswp) {
this.pswp = pswp;
this.options = options;
this.itemData = itemData;
this.index = index;
/** @type { Point | null } */
this.panAreaSize = null;
/** @type { Point | null } */
this.elementSize = null;
this.fit = 1;
this.fill = 1;
this.vFill = 1;
this.initial = 1;
this.secondary = 1;
this.max = 1;
this.min = 1;
}
/**
* Calculate initial, secondary and maximum zoom level for the specified slide.
*
* It should be called when either image or viewport size changes.
*
* @param {number} maxWidth
* @param {number} maxHeight
* @param {Point} panAreaSize
*/
update(maxWidth, maxHeight, panAreaSize) {
/** @type {Point} */
const elementSize = {
x: maxWidth,
y: maxHeight
};
this.elementSize = elementSize;
this.panAreaSize = panAreaSize;
const hRatio = panAreaSize.x / elementSize.x;
const vRatio = panAreaSize.y / elementSize.y;
this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
// when it has 100% of viewport vertical space (height)
this.vFill = Math.min(1, vRatio);
this.initial = this._getInitial();
this.secondary = this._getSecondary();
this.max = Math.max(this.initial, this.secondary, this._getMax());
this.min = Math.min(this.fit, this.initial, this.secondary);
if (this.pswp) {
this.pswp.dispatch('zoomLevelsUpdate', {
zoomLevels: this,
slideData: this.itemData
});
}
}
/**
* Parses user-defined zoom option.
*
* @private
* @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
* @returns { number | undefined }
*/
_parseZoomLevelOption(optionPrefix) {
const optionName =
/** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
optionPrefix + 'ZoomLevel';
const optionValue = this.options[optionName];
if (!optionValue) {
return;
}
if (typeof optionValue === 'function') {
return optionValue(this);
}
if (optionValue === 'fill') {
return this.fill;
}
if (optionValue === 'fit') {
return this.fit;
}
return Number(optionValue);
}
/**
* Get zoom level to which image will be zoomed after double-tap gesture,
* or when user clicks on zoom icon,
* or mouse-click on image itself.
* If you return 1 image will be zoomed to its original size.
*
* @private
* @return {number}
*/
_getSecondary() {
let currZoomLevel = this._parseZoomLevelOption('secondary');
if (currZoomLevel) {
return currZoomLevel;
} // 3x of "fit" state, but not larger than original
currZoomLevel = Math.min(1, this.fit * 3);
if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
}
return currZoomLevel;
}
/**
* Get initial image zoom level.
*
* @private
* @return {number}
*/
_getInitial() {
return this._parseZoomLevelOption('initial') || this.fit;
}
/**
* Maximum zoom level when user zooms
* via zoom/pinch gesture,
* via cmd/ctrl-wheel or via trackpad.
*
* @private
* @return {number}
*/
_getMax() {
// max zoom level is x4 from "fit state",
// used for zoom gesture and ctrl/trackpad zoom
return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
}
}
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/**
* Renders and allows to control a single slide
*/
class Slide {
/**
* @param {SlideData} data
* @param {number} index
* @param {PhotoSwipe} pswp
*/
constructor(data, index, pswp) {
this.data = data;
this.index = index;
this.pswp = pswp;
this.isActive = index === pswp.currIndex;
this.currentResolution = 0;
/** @type {Point} */
this.panAreaSize = {
x: 0,
y: 0
};
/** @type {Point} */
this.pan = {
x: 0,
y: 0
};
this.isFirstSlide = this.isActive && !pswp.opener.isOpen;
this.zoomLevels = new ZoomLevel(pswp.options, data, index, pswp);
this.pswp.dispatch('gettingData', {
slide: this,
data: this.data,
index
});
this.content = this.pswp.contentLoader.getContentBySlide(this);
this.container = createElement('pswp__zoom-wrap', 'div');
/** @type {HTMLElement | null} */
this.holderElement = null;
this.currZoomLevel = 1;
/** @type {number} */
this.width = this.content.width;
/** @type {number} */
this.height = this.content.height;
this.heavyAppended = false;
this.bounds = new PanBounds(this);
this.prevDisplayedWidth = -1;
this.prevDisplayedHeight = -1;
this.pswp.dispatch('slideInit', {
slide: this
});
}
/**
* If this slide is active/current/visible
*
* @param {boolean} isActive
*/
setIsActive(isActive) {
if (isActive && !this.isActive) {
// slide just became active
this.activate();
} else if (!isActive && this.isActive) {
// slide just became non-active
this.deactivate();
}
}
/**
* Appends slide content to DOM
*
* @param {HTMLElement} holderElement
*/
append(holderElement) {
this.holderElement = holderElement;
this.container.style.transformOrigin = '0 0'; // Slide appended to DOM
if (!this.data) {
return;
}
this.calculateSize();
this.load();
this.updateContentSize();
this.appendHeavy();
this.holderElement.appendChild(this.container);
this.zoomAndPanToInitial();
this.pswp.dispatch('firstZoomPan', {
slide: this
});
this.applyCurrentZoomPan();
this.pswp.dispatch('afterSetContent', {
slide: this
});
if (this.isActive) {
this.activate();
}
}
load() {
this.content.load(false);
this.pswp.dispatch('slideLoad', {
slide: this
});
}
/**
* Append "heavy" DOM elements
*
* This may depend on a type of slide,
* but generally these are large images.
*/
appendHeavy() {
const {
pswp
} = this;
const appendHeavyNearby = true; // todo
// Avoid appending heavy elements during animations
if (this.heavyAppended || !pswp.opener.isOpen || pswp.mainScroll.isShifted() || !this.isActive && !appendHeavyNearby) {
return;
}
if (this.pswp.dispatch('appendHeavy', {
slide: this
}).defaultPrevented) {
return;
}
this.heavyAppended = true;
this.content.append();
this.pswp.dispatch('appendHeavyContent', {
slide: this
});
}
/**
* Triggered when this slide is active (selected).
*
* If it's part of opening/closing transition -
* activate() will trigger after the transition is ended.
*/
activate() {
this.isActive = true;
this.appendHeavy();
this.content.activate();
this.pswp.dispatch('slideActivate', {
slide: this
});
}
/**
* Triggered when this slide becomes inactive.
*
* Slide can become inactive only after it was active.
*/
deactivate() {
this.isActive = false;
this.content.deactivate();
if (this.currZoomLevel !== this.zoomLevels.initial) {
// allow filtering
this.calculateSize();
} // reset zoom level
this.currentResolution = 0;
this.zoomAndPanToInitial();
this.applyCurrentZoomPan();
this.updateContentSize();
this.pswp.dispatch('slideDeactivate', {
slide: this
});
}
/**
* The slide should destroy itself, it will never be used again.
* (unbind all events and destroy internal components)
*/
destroy() {
this.content.hasSlide = false;
this.content.remove();
this.container.remove();
this.pswp.dispatch('slideDestroy', {
slide: this
});
}
resize() {
if (this.currZoomLevel === this.zoomLevels.initial || !this.isActive) {
// Keep initial zoom level if it was before the resize,
// as well as when this slide is not active
// Reset position and scale to original state
this.calculateSize();
this.currentResolution = 0;
this.zoomAndPanToInitial();
this.applyCurrentZoomPan();
this.updateContentSize();
} else {
// readjust pan position if it's beyond the bounds
this.calculateSize();
this.bounds.update(this.currZoomLevel);
this.panTo(this.pan.x, this.pan.y);
}
}
/**
* Apply size to current slide content,
* based on the current resolution and scale.
*
* @param {boolean} [force] if size should be updated even if dimensions weren't changed
*/
updateContentSize(force) {
// Use initial zoom level
// if resolution is not defined (user didn't zoom yet)
const scaleMultiplier = this.currentResolution || this.zoomLevels.initial;
if (!scaleMultiplier) {
return;
}
const width = Math.round(this.width * scaleMultiplier) || this.pswp.viewportSize.x;
const height = Math.round(this.height * scaleMultiplier) || this.pswp.viewportSize.y;
if (!this.sizeChanged(width, height) && !force) {
return;
}
this.content.setDisplayedSize(width, height);
}
/**
* @param {number} width
* @param {number} height
*/
sizeChanged(width, height) {
if (width !== this.prevDisplayedWidth || height !== this.prevDisplayedHeight) {
this.prevDisplayedWidth = width;
this.prevDisplayedHeight = height;
return true;
}
return false;
}
/** @returns {HTMLImageElement | HTMLDivElement | null | undefined} */
getPlaceholderElement() {
var _this$content$placeho;
return (_this$content$placeho = this.content.placeholder) === null || _this$content$placeho === void 0 ? void 0 : _this$content$placeho.element;
}
/**
* Zoom current slide image to...
*
* @param {number} destZoomLevel Destination zoom level.
* @param {Point} [centerPoint]
* Transform origin center point, or false if viewport center should be used.
* @param {number | false} [transitionDuration] Transition duration, may be set to 0.
* @param {boolean} [ignoreBounds] Minimum and maximum zoom levels will be ignored.
*/
zoomTo(destZoomLevel, centerPoint, transitionDuration, ignoreBounds) {
const {
pswp
} = this;
if (!this.isZoomable() || pswp.mainScroll.isShifted()) {
return;
}
pswp.dispatch('beforeZoomTo', {
destZoomLevel,
centerPoint,
transitionDuration
}); // stop all pan and zoom transitions
pswp.animations.stopAllPan(); // if (!centerPoint) {
// centerPoint = pswp.getViewportCenterPoint();
// }
const prevZoomLevel = this.currZoomLevel;
if (!ignoreBounds) {
destZoomLevel = clamp(destZoomLevel, this.zoomLevels.min, this.zoomLevels.max);
} // if (transitionDuration === undefined) {
// transitionDuration = this.pswp.options.zoomAnimationDuration;
// }
this.setZoomLevel(destZoomLevel);
this.pan.x = this.calculateZoomToPanOffset('x', centerPoint, prevZoomLevel);
this.pan.y = this.calculateZoomToPanOffset('y', centerPoint, prevZoomLevel);
roundPoint(this.pan);
const finishTransition = () => {
this._setResolution(destZoomLevel);
this.applyCurrentZoomPan();
};
if (!transitionDuration) {
finishTransition();
} else {
pswp.animations.startTransition({
isPan: true,
name: 'zoomTo',
target: this.container,
transform: this.getCurrentTransform(),
onComplete: finishTransition,
duration: transitionDuration,
easing: pswp.options.easing
});
}
}
/**
* @param {Point} [centerPoint]
*/
toggleZoom(centerPoint) {
this.zoomTo(this.currZoomLevel === this.zoomLevels.initial ? this.zoomLevels.secondary : this.zoomLevels.initial, centerPoint, this.pswp.options.zoomAnimationDuration);
}
/**
* Updates zoom level property and recalculates new pan bounds,
* unlike zoomTo it does not apply transform (use applyCurrentZoomPan)
*
* @param {number} currZoomLevel
*/
setZoomLevel(currZoomLevel) {
this.currZoomLevel = currZoomLevel;
this.bounds.update(this.currZoomLevel);
}
/**
* Get pan position after zoom at a given `point`.
*
* Always call setZoomLevel(newZoomLevel) beforehand to recalculate
* pan bounds according to the new zoom level.
*
* @param {'x' | 'y'} axis
* @param {Point} [point]
* point based on which zoom is performed, usually refers to the current mouse position,
* if false - viewport center will be used.
* @param {number} [prevZoomLevel] Zoom level before new zoom was applied.
* @returns {number}
*/
calculateZoomToPanOffset(axis, point, prevZoomLevel) {
const totalPanDistance = this.bounds.max[axis] - this.bounds.min[axis];
if (totalPanDistance === 0) {
return this.bounds.center[axis];
}
if (!point) {
point = this.pswp.getViewportCenterPoint();
}
if (!prevZoomLevel) {
prevZoomLevel = this.zoomLevels.initial;
}
const zoomFactor = this.currZoomLevel / prevZoomLevel;
return this.bounds.correctPan(axis, (this.pan[axis] - point[axis]) * zoomFactor + point[axis]);
}
/**
* Apply pan and keep it within bounds.
*
* @param {number} panX
* @param {number} panY
*/
panTo(panX, panY) {
this.pan.x = this.bounds.correctPan('x', panX);
this.pan.y = this.bounds.correctPan('y', panY);
this.applyCurrentZoomPan();
}
/**
* If the slide in the current state can be panned by the user
* @returns {boolean}
*/
isPannable() {
return Boolean(this.width) && this.currZoomLevel > this.zoomLevels.fit;
}
/**
* If the slide can be zoomed
* @returns {boolean}
*/
isZoomable() {
return Boolean(this.width) && this.content.isZoomable();
}
/**
* Apply transform and scale based on
* the current pan position (this.pan) and zoom level (this.currZoomLevel)
*/
applyCurrentZoomPan() {
this._applyZoomTransform(this.pan.x, this.pan.y, this.currZoomLevel);
if (this === this.pswp.currSlide) {
this.pswp.dispatch('zoomPanUpdate', {
slide: this
});
}
}
zoomAndPanToInitial() {
this.currZoomLevel = this.zoomLevels.initial; // pan according to the zoom level
this.bounds.update(this.currZoomLevel);
equalizePoints(this.pan, this.bounds.center);
this.pswp.dispatch('initialZoomPan', {
slide: this
});
}
/**
* Set translate and scale based on current resolution
*
* @param {number} x
* @param {number} y
* @param {number} zoom
* @private
*/
_applyZoomTransform(x, y, zoom) {
zoom /= this.currentResolution || this.zoomLevels.initial;
setTransform(this.container, x, y, zoom);
}
calculateSize() {
const {
pswp
} = this;
equalizePoints(this.panAreaSize, getPanAreaSize(pswp.options, pswp.viewportSize, this.data, this.index));
this.zoomLevels.update(this.width, this.height, this.panAreaSize);
pswp.dispatch('calcSlideSize', {
slide: this
});
}
/** @returns {string} */
getCurrentTransform() {
const scale = this.currZoomLevel / (this.currentResolution || this.zoomLevels.initial);
return toTransformString(this.pan.x, this.pan.y, scale);
}
/**
* Set resolution and re-render the image.
*
* For example, if the real image size is 2000x1500,
* and resolution is 0.5 - it will be rendered as 1000x750.
*
* Image with zoom level 2 and resolution 0.5 is
* the same as image with zoom level 1 and resolution 1.
*
* Used to optimize animations and make
* sure that browser renders image in the highest quality.
* Also used by responsive images to load the correct one.
*
* @param {number} newResolution
*/
_setResolution(newResolution) {
if (newResolution === this.currentResolution) {
return;
}
this.currentResolution = newResolution;
this.updateContentSize();
this.pswp.dispatch('resolutionChanged');
}
}
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('./gestures.js').default} Gestures */
const PAN_END_FRICTION = 0.35;
const VERTICAL_DRAG_FRICTION = 0.6; // 1 corresponds to the third of viewport height
const MIN_RATIO_TO_CLOSE = 0.4; // Minimum speed required to navigate
// to next or previous slide
const MIN_NEXT_SLIDE_SPEED = 0.5;
/**
* @param {number} initialVelocity
* @param {number} decelerationRate
* @returns {number}
*/
function project(initialVelocity, decelerationRate) {
return initialVelocity * decelerationRate / (1 - decelerationRate);
}
/**
* Handles single pointer dragging
*/
class DragHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures) {
this.gestures = gestures;
this.pswp = gestures.pswp;
/** @type {Point} */
this.startPan = {
x: 0,
y: 0
};
}
start() {
if (this.pswp.currSlide) {
equalizePoints(this.startPan, this.pswp.currSlide.pan);
}
this.pswp.animations.stopAll();
}
change() {
const {
p1,
prevP1,
dragAxis
} = this.gestures;
const {
currSlide
} = this.pswp;
if (dragAxis === 'y' && this.pswp.options.closeOnVerticalDrag && currSlide && currSlide.currZoomLevel <= currSlide.zoomLevels.fit && !this.gestures.isMultitouch) {
// Handle vertical drag to close
const panY = currSlide.pan.y + (p1.y - prevP1.y);
if (!this.pswp.dispatch('verticalDrag', {
panY
}).defaultPrevented) {
this._setPanWithFriction('y', panY, VERTICAL_DRAG_FRICTION);
const bgOpacity = 1 - Math.abs(this._getVerticalDragRatio(currSlide.pan.y));
this.pswp.applyBgOpacity(bgOpacity);
currSlide.applyCurrentZoomPan();
}
} else {
const mainScrollChanged = this._panOrMoveMainScroll('x');
if (!mainScrollChanged) {
this._panOrMoveMainScroll('y');
if (currSlide) {
roundPoint(currSlide.pan);
currSlide.applyCurrentZoomPan();
}
}
}
}
end() {
const {
velocity
} = this.gestures;
const {
mainScroll,
currSlide
} = this.pswp;
let indexDiff = 0;
this.pswp.animations.stopAll(); // Handle main scroll if it's shifted
if (mainScroll.isShifted()) {
// Position of the main scroll relative to the viewport
const mainScrollShiftDiff = mainScroll.x - mainScroll.getCurrSlideX(); // Ratio between 0 and 1:
// 0 - slide is not visible at all,
// 0.5 - half of the slide is visible
// 1 - slide is fully visible
const currentSlideVisibilityRatio = mainScrollShiftDiff / this.pswp.viewportSize.x; // Go next slide.
//
// - if velocity and its direction is matched,
// and we see at least tiny part of the next slide
//
// - or if we see less than 50% of the current slide
// and velocity is close to 0
//
if (velocity.x < -MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio < 0 || velocity.x < 0.1 && currentSlideVisibilityRatio < -0.5) {
// Go to next slide
indexDiff = 1;
velocity.x = Math.min(velocity.x, 0);
} else if (velocity.x > MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio > 0 || velocity.x > -0.1 && currentSlideVisibilityRatio > 0.5) {
// Go to prev slide
indexDiff = -1;
velocity.x = Math.max(velocity.x, 0);
}
mainScroll.moveIndexBy(indexDiff, true, velocity.x);
} // Restore zoom level
if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.max || this.gestures.isMultitouch) {
this.gestures.zoomLevels.correctZoomPan(true);
} else {
// we run two animations instead of one,
// as each axis has own pan boundaries and thus different spring function
// (correctZoomPan does not have this functionality,
// it animates all properties with single timing function)
this._finishPanGestureForAxis('x');
this._finishPanGestureForAxis('y');
}
}
/**
* @private
* @param {'x' | 'y'} axis
*/
_finishPanGestureForAxis(axis) {
const {
velocity
} = this.gestures;
const {
currSlide
} = this.pswp;
if (!currSlide) {
return;
}
const {
pan,
bounds
} = currSlide;
const panPos = pan[axis];
const restoreBgOpacity = this.pswp.bgOpacity < 1 && axis === 'y'; // 0.995 means - scroll view loses 0.5% of its velocity per millisecond
// Increasing this number will reduce travel distance
const decelerationRate = 0.995; // 0.99
// Pan position if there is no bounds
const projectedPosition = panPos + project(velocity[axis], decelerationRate);
if (restoreBgOpacity) {
const vDragRatio = this._getVerticalDragRatio(panPos);
const projectedVDragRatio = this._getVerticalDragRatio(projectedPosition); // If we are above and moving upwards,
// or if we are below and moving downwards
if (vDragRatio < 0 && projectedVDragRatio < -MIN_RATIO_TO_CLOSE || vDragRatio > 0 && projectedVDragRatio > MIN_RATIO_TO_CLOSE) {
this.pswp.close();
return;
}
} // Pan position with corrected bounds
const correctedPanPosition = bounds.correctPan(axis, projectedPosition); // Exit if pan position should not be changed
// or if speed it too low
if (panPos === correctedPanPosition) {
return;
} // Overshoot if the final position is out of pan bounds
const dampingRatio = correctedPanPosition === projectedPosition ? 1 : 0.82;
const initialBgOpacity = this.pswp.bgOpacity;
const totalPanDist = correctedPanPosition - panPos;
this.pswp.animations.startSpring({
name: 'panGesture' + axis,
isPan: true,
start: panPos,
end: correctedPanPosition,
velocity: velocity[axis],
dampingRatio,
onUpdate: pos => {
// Animate opacity of background relative to Y pan position of an image
if (restoreBgOpacity && this.pswp.bgOpacity < 1) {
// 0 - start of animation, 1 - end of animation
const animationProgressRatio = 1 - (correctedPanPosition - pos) / totalPanDist; // We clamp opacity to keep it between 0 and 1.
// As progress ratio can be larger than 1 due to overshoot,
// and we do not want to bounce opacity.
this.pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * animationProgressRatio, 0, 1));
}
pan[axis] = Math.floor(pos);
currSlide.applyCurrentZoomPan();
}
});
}
/**
* Update position of the main scroll,
* or/and update pan position of the current slide.
*
* Should return true if it changes (or can change) main scroll.
*
* @private
* @param {'x' | 'y'} axis
* @returns {boolean}
*/
_panOrMoveMainScroll(axis) {
const {
p1,
dragAxis,
prevP1,
isMultitouch
} = this.gestures;
const {
currSlide,
mainScroll
} = this.pswp;
const delta = p1[axis] - prevP1[axis];
const newMainScrollX = mainScroll.x + delta;
if (!delta || !currSlide) {
return false;
} // Always move main scroll if image can not be panned
if (axis === 'x' && !currSlide.isPannable() && !isMultitouch) {
mainScroll.moveTo(newMainScrollX, true);
return true; // changed main scroll
}
const {
bounds
} = currSlide;
const newPan = currSlide.pan[axis] + delta;
if (this.pswp.options.allowPanToNext && dragAxis === 'x' && axis === 'x' && !isMultitouch) {
const currSlideMainScrollX = mainScroll.getCurrSlideX(); // Position of the main scroll relative to the viewport
const mainScrollShiftDiff = mainScroll.x - currSlideMainScrollX;
const isLeftToRight = delta > 0;
const isRightToLeft = !isLeftToRight;
if (newPan > bounds.min[axis] && isLeftToRight) {
// Panning from left to right, beyond the left edge
// Wether the image was at minimum pan position (or less)
// when this drag gesture started.
// Minimum pan position refers to the left edge of the image.
const wasAtMinPanPosition = bounds.min[axis] <= this.startPan[axis];
if (wasAtMinPanPosition) {
mainScroll.moveTo(newMainScrollX, true);
return true;
} else {
this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
}
} else if (newPan < bounds.max[axis] && isRightToLeft) {
// Paning from right to left, beyond the right edge
// Maximum pan position refers to the right edge of the image.
const wasAtMaxPanPosition = this.startPan[axis] <= bounds.max[axis];
if (wasAtMaxPanPosition) {
mainScroll.moveTo(newMainScrollX, true);
return true;
} else {
this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
}
} else {
// If main scroll is shifted
if (mainScrollShiftDiff !== 0) {
// If main scroll is shifted right
if (mainScrollShiftDiff > 0
/*&& isRightToLeft*/
) {
mainScroll.moveTo(Math.max(newMainScrollX, currSlideMainScrollX), true);
return true;
} else if (mainScrollShiftDiff < 0
/*&& isLeftToRight*/
) {
// Main scroll is shifted left (Position is less than 0 comparing to the viewport 0)
mainScroll.moveTo(Math.min(newMainScrollX, currSlideMainScrollX), true);
return true;
}
} else {
// We are within pan bounds, so just pan
this._setPanWithFriction(axis, newPan);
}
}
} else {
if (axis === 'y') {
// Do not pan vertically if main scroll is shifted o
if (!mainScroll.isShifted() && bounds.min.y !== bounds.max.y) {
this._setPanWithFriction(axis, newPan);
}
} else {
this._setPanWithFriction(axis, newPan);
}
}
return false;
} // If we move above - the ratio is negative
// If we move below the ratio is positive
/**
* Relation between pan Y position and third of viewport height.
*
* When we are at initial position (center bounds) - the ratio is 0,
* if position is shifted upwards - the ratio is negative,
* if position is shifted downwards - the ratio is positive.
*
* @private
* @param {number} panY The current pan Y position.
* @returns {number}
*/
_getVerticalDragRatio(panY) {
var _this$pswp$currSlide$, _this$pswp$currSlide;
return (panY - ((_this$pswp$currSlide$ = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.bounds.center.y) !== null && _this$pswp$currSlide$ !== void 0 ? _this$pswp$currSlide$ : 0)) / (this.pswp.viewportSize.y / 3);
}
/**
* Set pan position of the current slide.
* Apply friction if the position is beyond the pan bounds,
* or if custom friction is defined.
*
* @private
* @param {'x' | 'y'} axis
* @param {number} potentialPan
* @param {number} [customFriction] (0.1 - 1)
*/
_setPanWithFriction(axis, potentialPan, customFriction) {
const {
currSlide
} = this.pswp;
if (!currSlide) {
return;
}
const {
pan,
bounds
} = currSlide;
const correctedPan = bounds.correctPan(axis, potentialPan); // If we are out of pan bounds
if (correctedPan !== potentialPan || customFriction) {
const delta = Math.round(potentialPan - pan[axis]);
pan[axis] += delta * (customFriction || PAN_END_FRICTION);
} else {
pan[axis] = potentialPan;
}
}
}
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('./gestures.js').default} Gestures */
const UPPER_ZOOM_FRICTION = 0.05;
const LOWER_ZOOM_FRICTION = 0.15;
/**
* Get center point between two points
*
* @param {Point} p
* @param {Point} p1
* @param {Point} p2
* @returns {Point}
*/
function getZoomPointsCenter(p, p1, p2) {
p.x = (p1.x + p2.x) / 2;
p.y = (p1.y + p2.y) / 2;
return p;
}
class ZoomHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures) {
this.gestures = gestures;
/**
* @private
* @type {Point}
*/
this._startPan = {
x: 0,
y: 0
};
/**
* @private
* @type {Point}
*/
this._startZoomPoint = {
x: 0,
y: 0
};
/**
* @private
* @type {Point}
*/
this._zoomPoint = {
x: 0,
y: 0
};
/** @private */
this._wasOverFitZoomLevel = false;
/** @private */
this._startZoomLevel = 1;
}
start() {
const {
currSlide
} = this.gestures.pswp;
if (currSlide) {
this._startZoomLevel = currSlide.currZoomLevel;
equalizePoints(this._startPan, currSlide.pan);
}
this.gestures.pswp.animations.stopAllPan();
this._wasOverFitZoomLevel = false;
}
change() {
const {
p1,
startP1,
p2,
startP2,
pswp
} = this.gestures;
const {
currSlide
} = pswp;
if (!currSlide) {
return;
}
const minZoomLevel = currSlide.zoomLevels.min;
const maxZoomLevel = currSlide.zoomLevels.max;
if (!currSlide.isZoomable() || pswp.mainScroll.isShifted()) {
return;
}
getZoomPointsCenter(this._startZoomPoint, startP1, startP2);
getZoomPointsCenter(this._zoomPoint, p1, p2);
let currZoomLevel = 1 / getDistanceBetween(startP1, startP2) * getDistanceBetween(p1, p2) * this._startZoomLevel; // slightly over the zoom.fit
if (currZoomLevel > currSlide.zoomLevels.initial + currSlide.zoomLevels.initial / 15) {
this._wasOverFitZoomLevel = true;
}
if (currZoomLevel < minZoomLevel) {
if (pswp.options.pinchToClose && !this._wasOverFitZoomLevel && this._startZoomLevel <= currSlide.zoomLevels.initial) {
// fade out background if zooming out
const bgOpacity = 1 - (minZoomLevel - currZoomLevel) / (minZoomLevel / 1.2);
if (!pswp.dispatch('pinchClose', {
bgOpacity
}).defaultPrevented) {
pswp.applyBgOpacity(bgOpacity);
}
} else {
// Apply the friction if zoom level is below the min
currZoomLevel = minZoomLevel - (minZoomLevel - currZoomLevel) * LOWER_ZOOM_FRICTION;
}
} else if (currZoomLevel > maxZoomLevel) {
// Apply the friction if zoom level is above the max
currZoomLevel = maxZoomLevel + (currZoomLevel - maxZoomLevel) * UPPER_ZOOM_FRICTION;
}
currSlide.pan.x = this._calculatePanForZoomLevel('x', currZoomLevel);
currSlide.pan.y = this._calculatePanForZoomLevel('y', currZoomLevel);
currSlide.setZoomLevel(currZoomLevel);
currSlide.applyCurrentZoomPan();
}
end() {
const {
pswp
} = this.gestures;
const {
currSlide
} = pswp;
if ((!currSlide || currSlide.currZoomLevel < currSlide.zoomLevels.initial) && !this._wasOverFitZoomLevel && pswp.options.pinchToClose) {
pswp.close();
} else {
this.correctZoomPan();
}
}
/**
* @private
* @param {'x' | 'y'} axis
* @param {number} currZoomLevel
* @returns {number}
*/
_calculatePanForZoomLevel(axis, currZoomLevel) {
const zoomFactor = currZoomLevel / this._startZoomLevel;
return this._zoomPoint[axis] - (this._startZoomPoint[axis] - this._startPan[axis]) * zoomFactor;
}
/**
* Correct currZoomLevel and pan if they are
* beyond minimum or maximum values.
* With animation.
*
* @param {boolean} [ignoreGesture]
* Wether gesture coordinates should be ignored when calculating destination pan position.
*/
correctZoomPan(ignoreGesture) {
const {
pswp
} = this.gestures;
const {
currSlide
} = pswp;
if (!(currSlide !== null && currSlide !== void 0 && currSlide.isZoomable())) {
return;
}
if (this._zoomPoint.x === 0) {
ignoreGesture = true;
}
const prevZoomLevel = currSlide.currZoomLevel;
/** @type {number} */
let destinationZoomLevel;
let currZoomLevelNeedsChange = true;
if (prevZoomLevel < currSlide.zoomLevels.initial) {
destinationZoomLevel = currSlide.zoomLevels.initial; // zoom to min
} else if (prevZoomLevel > currSlide.zoomLevels.max) {
destinationZoomLevel = currSlide.zoomLevels.max; // zoom to max
} else {
currZoomLevelNeedsChange = false;
destinationZoomLevel = prevZoomLevel;
}
const initialBgOpacity = pswp.bgOpacity;
const restoreBgOpacity = pswp.bgOpacity < 1;
const initialPan = equalizePoints({
x: 0,
y: 0
}, currSlide.pan);
let destinationPan = equalizePoints({
x: 0,
y: 0
}, initialPan);
if (ignoreGesture) {
this._zoomPoint.x = 0;
this._zoomPoint.y = 0;
this._startZoomPoint.x = 0;
this._startZoomPoint.y = 0;
this._startZoomLevel = prevZoomLevel;
equalizePoints(this._startPan, initialPan);
}
if (currZoomLevelNeedsChange) {
destinationPan = {
x: this._calculatePanForZoomLevel('x', destinationZoomLevel),
y: this._calculatePanForZoomLevel('y', destinationZoomLevel)
};
} // set zoom level, so pan bounds are updated according to it
currSlide.setZoomLevel(destinationZoomLevel);
destinationPan = {
x: currSlide.bounds.correctPan('x', destinationPan.x),
y: currSlide.bounds.correctPan('y', destinationPan.y)
}; // return zoom level and its bounds to initial
currSlide.setZoomLevel(prevZoomLevel);
const panNeedsChange = !pointsEqual(destinationPan, initialPan);
if (!panNeedsChange && !currZoomLevelNeedsChange && !restoreBgOpacity) {
// update resolution after gesture
currSlide._setResolution(destinationZoomLevel);
currSlide.applyCurrentZoomPan(); // nothing to animate
return;
}
pswp.animations.stopAllPan();
pswp.animations.startSpring({
isPan: true,
start: 0,
end: 1000,
velocity: 0,
dampingRatio: 1,
naturalFrequency: 40,
onUpdate: now => {
now /= 1000; // 0 - start, 1 - end
if (panNeedsChange || currZoomLevelNeedsChange) {
if (panNeedsChange) {
currSlide.pan.x = initialPan.x + (destinationPan.x - initialPan.x) * now;
currSlide.pan.y = initialPan.y + (destinationPan.y - initialPan.y) * now;
}
if (currZoomLevelNeedsChange) {
const newZoomLevel = prevZoomLevel + (destinationZoomLevel - prevZoomLevel) * now;
currSlide.setZoomLevel(newZoomLevel);
}
currSlide.applyCurrentZoomPan();
} // Restore background opacity
if (restoreBgOpacity && pswp.bgOpacity < 1) {
// We clamp opacity to keep it between 0 and 1.
// As progress ratio can be larger than 1 due to overshoot,
// and we do not want to bounce opacity.
pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * now, 0, 1));
}
},
onComplete: () => {
// update resolution after transition ends
currSlide._setResolution(destinationZoomLevel);
currSlide.applyCurrentZoomPan();
}
});
}
}
/**
* @template {string} T
* @template {string} P
* @typedef {import('../types.js').AddPostfix} AddPostfix
*/
/** @typedef {import('./gestures.js').default} Gestures */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {'imageClick' | 'bgClick' | 'tap' | 'doubleTap'} Actions */
/**
* Whether the tap was performed on the main slide
* (rather than controls or caption).
*
* @param {PointerEvent} event
* @returns {boolean}
*/
function didTapOnMainContent(event) {
return !!
/** @type {HTMLElement} */
event.target.closest('.pswp__container');
}
/**
* Tap, double-tap handler.
*/
class TapHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures) {
this.gestures = gestures;
}
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
click(point, originalEvent) {
const targetClassList =
/** @type {HTMLElement} */
originalEvent.target.classList;
const isImageClick = targetClassList.contains('pswp__img');
const isBackgroundClick = targetClassList.contains('pswp__item') || targetClassList.contains('pswp__zoom-wrap');
if (isImageClick) {
this._doClickOrTapAction('imageClick', point, originalEvent);
} else if (isBackgroundClick) {
this._doClickOrTapAction('bgClick', point, originalEvent);
}
}
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
tap(point, originalEvent) {
if (didTapOnMainContent(originalEvent)) {
this._doClickOrTapAction('tap', point, originalEvent);
}
}
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
doubleTap(point, originalEvent) {
if (didTapOnMainContent(originalEvent)) {
this._doClickOrTapAction('doubleTap', point, originalEvent);
}
}
/**
* @private
* @param {Actions} actionName
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
_doClickOrTapAction(actionName, point, originalEvent) {
var _this$gestures$pswp$e;
const {
pswp
} = this.gestures;
const {
currSlide
} = pswp;
const actionFullName =
/** @type {AddPostfix} */
actionName + 'Action';
const optionValue = pswp.options[actionFullName];
if (pswp.dispatch(actionFullName, {
point,
originalEvent
}).defaultPrevented) {
return;
}
if (typeof optionValue === 'function') {
optionValue.call(pswp, point, originalEvent);
return;
}
switch (optionValue) {
case 'close':
case 'next':
pswp[optionValue]();
break;
case 'zoom':
currSlide === null || currSlide === void 0 || currSlide.toggleZoom(point);
break;
case 'zoom-or-close':
// by default click zooms current image,
// if it can not be zoomed - gallery will be closed
if (currSlide !== null && currSlide !== void 0 && currSlide.isZoomable() && currSlide.zoomLevels.secondary !== currSlide.zoomLevels.initial) {
currSlide.toggleZoom(point);
} else if (pswp.options.clickToCloseNonZoomable) {
pswp.close();
}
break;
case 'toggle-controls':
(_this$gestures$pswp$e = this.gestures.pswp.element) === null || _this$gestures$pswp$e === void 0 || _this$gestures$pswp$e.classList.toggle('pswp--ui-visible'); // if (_controlsVisible) {
// _ui.hideControls();
// } else {
// _ui.showControls();
// }
break;
}
}
}
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').Point} Point */
// How far should user should drag
// until we can determine that the gesture is swipe and its direction
const AXIS_SWIPE_HYSTERISIS = 10; //const PAN_END_FRICTION = 0.35;
const DOUBLE_TAP_DELAY = 300; // ms
const MIN_TAP_DISTANCE = 25; // px
/**
* Gestures class bind touch, pointer or mouse events
* and emits drag to drag-handler and zoom events zoom-handler.
*
* Drag and zoom events are emited in requestAnimationFrame,
* and only when one of pointers was actually changed.
*/
class Gestures {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
/** @type {'x' | 'y' | null} */
this.dragAxis = null; // point objects are defined once and reused
// PhotoSwipe keeps track only of two pointers, others are ignored
/** @type {Point} */
this.p1 = {
x: 0,
y: 0
}; // the first pressed pointer
/** @type {Point} */
this.p2 = {
x: 0,
y: 0
}; // the second pressed pointer
/** @type {Point} */
this.prevP1 = {
x: 0,
y: 0
};
/** @type {Point} */
this.prevP2 = {
x: 0,
y: 0
};
/** @type {Point} */
this.startP1 = {
x: 0,
y: 0
};
/** @type {Point} */
this.startP2 = {
x: 0,
y: 0
};
/** @type {Point} */
this.velocity = {
x: 0,
y: 0
};
/** @type {Point}
* @private
*/
this._lastStartP1 = {
x: 0,
y: 0
};
/** @type {Point}
* @private
*/
this._intervalP1 = {
x: 0,
y: 0
};
/** @private */
this._numActivePoints = 0;
/** @type {Point[]}
* @private
*/
this._ongoingPointers = [];
/** @private */
this._touchEventEnabled = 'ontouchstart' in window;
/** @private */
this._pointerEventEnabled = !!window.PointerEvent;
this.supportsTouch = this._touchEventEnabled || this._pointerEventEnabled && navigator.maxTouchPoints > 1;
/** @private */
this._numActivePoints = 0;
/** @private */
this._intervalTime = 0;
/** @private */
this._velocityCalculated = false;
this.isMultitouch = false;
this.isDragging = false;
this.isZooming = false;
/** @type {number | null} */
this.raf = null;
/** @type {NodeJS.Timeout | null}
* @private
*/
this._tapTimer = null;
if (!this.supportsTouch) {
// disable pan to next slide for non-touch devices
pswp.options.allowPanToNext = false;
}
this.drag = new DragHandler(this);
this.zoomLevels = new ZoomHandler(this);
this.tapHandler = new TapHandler(this);
pswp.on('bindEvents', () => {
pswp.events.add(pswp.scrollWrap, 'click',
/** @type EventListener */
this._onClick.bind(this));
if (this._pointerEventEnabled) {
this._bindEvents('pointer', 'down', 'up', 'cancel');
} else if (this._touchEventEnabled) {
this._bindEvents('touch', 'start', 'end', 'cancel'); // In previous versions we also bound mouse event here,
// in case device supports both touch and mouse events,
// but newer versions of browsers now support PointerEvent.
// on iOS10 if you bind touchmove/end after touchstart,
// and you don't preventDefault touchstart (which PhotoSwipe does),
// preventDefault will have no effect on touchmove and touchend.
// Unless you bind it previously.
if (pswp.scrollWrap) {
pswp.scrollWrap.ontouchmove = () => {};
pswp.scrollWrap.ontouchend = () => {};
}
} else {
this._bindEvents('mouse', 'down', 'up');
}
});
}
/**
* @private
* @param {'mouse' | 'touch' | 'pointer'} pref
* @param {'down' | 'start'} down
* @param {'up' | 'end'} up
* @param {'cancel'} [cancel]
*/
_bindEvents(pref, down, up, cancel) {
const {
pswp
} = this;
const {
events
} = pswp;
const cancelEvent = cancel ? pref + cancel : '';
events.add(pswp.scrollWrap, pref + down,
/** @type EventListener */
this.onPointerDown.bind(this));
events.add(window, pref + 'move',
/** @type EventListener */
this.onPointerMove.bind(this));
events.add(window, pref + up,
/** @type EventListener */
this.onPointerUp.bind(this));
if (cancelEvent) {
events.add(pswp.scrollWrap, cancelEvent,
/** @type EventListener */
this.onPointerUp.bind(this));
}
}
/**
* @param {PointerEvent} e
*/
onPointerDown(e) {
// We do not call preventDefault for touch events
// to allow browser to show native dialog on longpress
// (the one that allows to save image or open it in new tab).
//
// Desktop Safari allows to drag images when preventDefault isn't called on mousedown,
// even though preventDefault IS called on mousemove. That's why we preventDefault mousedown.
const isMousePointer = e.type === 'mousedown' || e.pointerType === 'mouse'; // Allow dragging only via left mouse button.
// http://www.quirksmode.org/js/events_properties.html
// https://developer.mozilla.org/en-US/docs/Web/API/event.button
if (isMousePointer && e.button > 0) {
return;
}
const {
pswp
} = this; // if PhotoSwipe is opening or closing
if (!pswp.opener.isOpen) {
e.preventDefault();
return;
}
if (pswp.dispatch('pointerDown', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (isMousePointer) {
pswp.mouseDetected(); // preventDefault mouse event to prevent
// browser image drag feature
this._preventPointerEventBehaviour(e, 'down');
}
pswp.animations.stopAll();
this._updatePoints(e, 'down');
if (this._numActivePoints === 1) {
this.dragAxis = null; // we need to store initial point to determine the main axis,
// drag is activated only after the axis is determined
equalizePoints(this.startP1, this.p1);
}
if (this._numActivePoints > 1) {
// Tap or double tap should not trigger if more than one pointer
this._clearTapTimer();
this.isMultitouch = true;
} else {
this.isMultitouch = false;
}
}
/**
* @param {PointerEvent} e
*/
onPointerMove(e) {
this._preventPointerEventBehaviour(e, 'move');
if (!this._numActivePoints) {
return;
}
this._updatePoints(e, 'move');
if (this.pswp.dispatch('pointerMove', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (this._numActivePoints === 1 && !this.isDragging) {
if (!this.dragAxis) {
this._calculateDragDirection();
} // Drag axis was detected, emit drag.start
if (this.dragAxis && !this.isDragging) {
if (this.isZooming) {
this.isZooming = false;
this.zoomLevels.end();
}
this.isDragging = true;
this._clearTapTimer(); // Tap can not trigger after drag
// Adjust starting point
this._updateStartPoints();
this._intervalTime = Date.now(); //this._startTime = this._intervalTime;
this._velocityCalculated = false;
equalizePoints(this._intervalP1, this.p1);
this.velocity.x = 0;
this.velocity.y = 0;
this.drag.start();
this._rafStopLoop();
this._rafRenderLoop();
}
} else if (this._numActivePoints > 1 && !this.isZooming) {
this._finishDrag();
this.isZooming = true; // Adjust starting points
this._updateStartPoints();
this.zoomLevels.start();
this._rafStopLoop();
this._rafRenderLoop();
}
}
/**
* @private
*/
_finishDrag() {
if (this.isDragging) {
this.isDragging = false; // Try to calculate velocity,
// if it wasn't calculated yet in drag.change
if (!this._velocityCalculated) {
this._updateVelocity(true);
}
this.drag.end();
this.dragAxis = null;
}
}
/**
* @param {PointerEvent} e
*/
onPointerUp(e) {
if (!this._numActivePoints) {
return;
}
this._updatePoints(e, 'up');
if (this.pswp.dispatch('pointerUp', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (this._numActivePoints === 0) {
this._rafStopLoop();
if (this.isDragging) {
this._finishDrag();
} else if (!this.isZooming && !this.isMultitouch) {
//this.zoomLevels.correctZoomPan();
this._finishTap(e);
}
}
if (this._numActivePoints < 2 && this.isZooming) {
this.isZooming = false;
this.zoomLevels.end();
if (this._numActivePoints === 1) {
// Since we have 1 point left, we need to reinitiate drag
this.dragAxis = null;
this._updateStartPoints();
}
}
}
/**
* @private
*/
_rafRenderLoop() {
if (this.isDragging || this.isZooming) {
this._updateVelocity();
if (this.isDragging) {
// make sure that pointer moved since the last update
if (!pointsEqual(this.p1, this.prevP1)) {
this.drag.change();
}
} else
/* if (this.isZooming) */
{
if (!pointsEqual(this.p1, this.prevP1) || !pointsEqual(this.p2, this.prevP2)) {
this.zoomLevels.change();
}
}
this._updatePrevPoints();
this.raf = requestAnimationFrame(this._rafRenderLoop.bind(this));
}
}
/**
* Update velocity at 50ms interval
*
* @private
* @param {boolean} [force]
*/
_updateVelocity(force) {
const time = Date.now();
const duration = time - this._intervalTime;
if (duration < 50 && !force) {
return;
}
this.velocity.x = this._getVelocity('x', duration);
this.velocity.y = this._getVelocity('y', duration);
this._intervalTime = time;
equalizePoints(this._intervalP1, this.p1);
this._velocityCalculated = true;
}
/**
* @private
* @param {PointerEvent} e
*/
_finishTap(e) {
const {
mainScroll
} = this.pswp; // Do not trigger tap events if main scroll is shifted
if (mainScroll.isShifted()) {
// restore main scroll position
// (usually happens if stopped in the middle of animation)
mainScroll.moveIndexBy(0, true);
return;
} // Do not trigger tap for touchcancel or pointercancel
if (e.type.indexOf('cancel') > 0) {
return;
} // Trigger click instead of tap for mouse events
if (e.type === 'mouseup' || e.pointerType === 'mouse') {
this.tapHandler.click(this.startP1, e);
return;
} // Disable delay if there is no doubleTapAction
const tapDelay = this.pswp.options.doubleTapAction ? DOUBLE_TAP_DELAY : 0; // If tapTimer is defined - we tapped recently,
// check if the current tap is close to the previous one,
// if yes - trigger double tap
if (this._tapTimer) {
this._clearTapTimer(); // Check if two taps were more or less on the same place
if (getDistanceBetween(this._lastStartP1, this.startP1) < MIN_TAP_DISTANCE) {
this.tapHandler.doubleTap(this.startP1, e);
}
} else {
equalizePoints(this._lastStartP1, this.startP1);
this._tapTimer = setTimeout(() => {
this.tapHandler.tap(this.startP1, e);
this._clearTapTimer();
}, tapDelay);
}
}
/**
* @private
*/
_clearTapTimer() {
if (this._tapTimer) {
clearTimeout(this._tapTimer);
this._tapTimer = null;
}
}
/**
* Get velocity for axis
*
* @private
* @param {'x' | 'y'} axis
* @param {number} duration
* @returns {number}
*/
_getVelocity(axis, duration) {
// displacement is like distance, but can be negative.
const displacement = this.p1[axis] - this._intervalP1[axis];
if (Math.abs(displacement) > 1 && duration > 5) {
return displacement / duration;
}
return 0;
}
/**
* @private
*/
_rafStopLoop() {
if (this.raf) {
cancelAnimationFrame(this.raf);
this.raf = null;
}
}
/**
* @private
* @param {PointerEvent} e
* @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
*/
_preventPointerEventBehaviour(e, pointerType) {
const preventPointerEvent = this.pswp.applyFilters('preventPointerEvent', true, e, pointerType);
if (preventPointerEvent) {
e.preventDefault();
}
}
/**
* Parses and normalizes points from the touch, mouse or pointer event.
* Updates p1 and p2.
*
* @private
* @param {PointerEvent | TouchEvent} e
* @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
*/
_updatePoints(e, pointerType) {
if (this._pointerEventEnabled) {
const pointerEvent =
/** @type {PointerEvent} */
e; // Try to find the current pointer in ongoing pointers by its ID
const pointerIndex = this._ongoingPointers.findIndex(ongoingPointer => {
return ongoingPointer.id === pointerEvent.pointerId;
});
if (pointerType === 'up' && pointerIndex > -1) {
// release the pointer - remove it from ongoing
this._ongoingPointers.splice(pointerIndex, 1);
} else if (pointerType === 'down' && pointerIndex === -1) {
// add new pointer
this._ongoingPointers.push(this._convertEventPosToPoint(pointerEvent, {
x: 0,
y: 0
}));
} else if (pointerIndex > -1) {
// update existing pointer
this._convertEventPosToPoint(pointerEvent, this._ongoingPointers[pointerIndex]);
}
this._numActivePoints = this._ongoingPointers.length; // update points that PhotoSwipe uses
// to calculate position and scale
if (this._numActivePoints > 0) {
equalizePoints(this.p1, this._ongoingPointers[0]);
}
if (this._numActivePoints > 1) {
equalizePoints(this.p2, this._ongoingPointers[1]);
}
} else {
const touchEvent =
/** @type {TouchEvent} */
e;
this._numActivePoints = 0;
if (touchEvent.type.indexOf('touch') > -1) {
// Touch Event
// https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
if (touchEvent.touches && touchEvent.touches.length > 0) {
this._convertEventPosToPoint(touchEvent.touches[0], this.p1);
this._numActivePoints++;
if (touchEvent.touches.length > 1) {
this._convertEventPosToPoint(touchEvent.touches[1], this.p2);
this._numActivePoints++;
}
}
} else {
// Mouse Event
this._convertEventPosToPoint(
/** @type {PointerEvent} */
e, this.p1);
if (pointerType === 'up') {
// clear all points on mouseup
this._numActivePoints = 0;
} else {
this._numActivePoints++;
}
}
}
}
/** update points that were used during previous rAF tick
* @private
*/
_updatePrevPoints() {
equalizePoints(this.prevP1, this.p1);
equalizePoints(this.prevP2, this.p2);
}
/** update points at the start of gesture
* @private
*/
_updateStartPoints() {
equalizePoints(this.startP1, this.p1);
equalizePoints(this.startP2, this.p2);
this._updatePrevPoints();
}
/** @private */
_calculateDragDirection() {
if (this.pswp.mainScroll.isShifted()) {
// if main scroll position is shifted – direction is always horizontal
this.dragAxis = 'x';
} else {
// calculate delta of the last touchmove tick
const diff = Math.abs(this.p1.x - this.startP1.x) - Math.abs(this.p1.y - this.startP1.y);
if (diff !== 0) {
// check if pointer was shifted horizontally or vertically
const axisToCheck = diff > 0 ? 'x' : 'y';
if (Math.abs(this.p1[axisToCheck] - this.startP1[axisToCheck]) >= AXIS_SWIPE_HYSTERISIS) {
this.dragAxis = axisToCheck;
}
}
}
}
/**
* Converts touch, pointer or mouse event
* to PhotoSwipe point.
*
* @private
* @param {Touch | PointerEvent} e
* @param {Point} p
* @returns {Point}
*/
_convertEventPosToPoint(e, p) {
p.x = e.pageX - this.pswp.offset.x;
p.y = e.pageY - this.pswp.offset.y;
if ('pointerId' in e) {
p.id = e.pointerId;
} else if (e.identifier !== undefined) {
p.id = e.identifier;
}
return p;
}
/**
* @private
* @param {PointerEvent} e
*/
_onClick(e) {
// Do not allow click event to pass through after drag
if (this.pswp.mainScroll.isShifted()) {
e.preventDefault();
e.stopPropagation();
}
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/** @typedef {import('./slide/slide.js').default} Slide */
/** @typedef {{ el: HTMLDivElement; slide?: Slide }} ItemHolder */
const MAIN_SCROLL_END_FRICTION = 0.35; // const MIN_SWIPE_TRANSITION_DURATION = 250;
// const MAX_SWIPE_TRABSITION_DURATION = 500;
// const DEFAULT_SWIPE_TRANSITION_DURATION = 333;
/**
* Handles movement of the main scrolling container
* (for example, it repositions when user swipes left or right).
*
* Also stores its state.
*/
class MainScroll {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
this.x = 0;
this.slideWidth = 0;
/** @private */
this._currPositionIndex = 0;
/** @private */
this._prevPositionIndex = 0;
/** @private */
this._containerShiftIndex = -1;
/** @type {ItemHolder[]} */
this.itemHolders = [];
}
/**
* Position the scroller and slide containers
* according to viewport size.
*
* @param {boolean} [resizeSlides] Whether slides content should resized
*/
resize(resizeSlides) {
const {
pswp
} = this;
const newSlideWidth = Math.round(pswp.viewportSize.x + pswp.viewportSize.x * pswp.options.spacing); // Mobile browsers might trigger a resize event during a gesture.
// (due to toolbar appearing or hiding).
// Avoid re-adjusting main scroll position if width wasn't changed
const slideWidthChanged = newSlideWidth !== this.slideWidth;
if (slideWidthChanged) {
this.slideWidth = newSlideWidth;
this.moveTo(this.getCurrSlideX());
}
this.itemHolders.forEach((itemHolder, index) => {
if (slideWidthChanged) {
setTransform(itemHolder.el, (index + this._containerShiftIndex) * this.slideWidth);
}
if (resizeSlides && itemHolder.slide) {
itemHolder.slide.resize();
}
});
}
/**
* Reset X position of the main scroller to zero
*/
resetPosition() {
// Position on the main scroller (offset)
// it is independent from slide index
this._currPositionIndex = 0;
this._prevPositionIndex = 0; // This will force recalculation of size on next resize()
this.slideWidth = 0; // _containerShiftIndex*viewportSize will give you amount of transform of the current slide
this._containerShiftIndex = -1;
}
/**
* Create and append array of three items
* that hold data about slides in DOM
*/
appendHolders() {
this.itemHolders = []; // append our three slide holders -
// previous, current, and next
for (let i = 0; i < 3; i++) {
const el = createElement('pswp__item', 'div', this.pswp.container);
el.setAttribute('role', 'group');
el.setAttribute('aria-roledescription', 'slide');
el.setAttribute('aria-hidden', 'true'); // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
el.style.display = i === 1 ? 'block' : 'none';
this.itemHolders.push({
el //index: -1
});
}
}
/**
* Whether the main scroll can be horizontally swiped to the next or previous slide.
* @returns {boolean}
*/
canBeSwiped() {
return this.pswp.getNumItems() > 1;
}
/**
* Move main scroll by X amount of slides.
* For example:
* `-1` will move to the previous slide,
* `0` will reset the scroll position of the current slide,
* `3` will move three slides forward
*
* If loop option is enabled - index will be automatically looped too,
* (for example `-1` will move to the last slide of the gallery).
*
* @param {number} diff
* @param {boolean} [animate]
* @param {number} [velocityX]
* @returns {boolean} whether index was changed or not
*/
moveIndexBy(diff, animate, velocityX) {
const {
pswp
} = this;
let newIndex = pswp.potentialIndex + diff;
const numSlides = pswp.getNumItems();
if (pswp.canLoop()) {
newIndex = pswp.getLoopedIndex(newIndex);
const distance = (diff + numSlides) % numSlides;
if (distance <= numSlides / 2) {
// go forward
diff = distance;
} else {
// go backwards
diff = distance - numSlides;
}
} else {
if (newIndex < 0) {
newIndex = 0;
} else if (newIndex >= numSlides) {
newIndex = numSlides - 1;
}
diff = newIndex - pswp.potentialIndex;
}
pswp.potentialIndex = newIndex;
this._currPositionIndex -= diff;
pswp.animations.stopMainScroll();
const destinationX = this.getCurrSlideX();
if (!animate) {
this.moveTo(destinationX);
this.updateCurrItem();
} else {
pswp.animations.startSpring({
isMainScroll: true,
start: this.x,
end: destinationX,
velocity: velocityX || 0,
naturalFrequency: 30,
dampingRatio: 1,
//0.7,
onUpdate: x => {
this.moveTo(x);
},
onComplete: () => {
this.updateCurrItem();
pswp.appendHeavy();
}
});
let currDiff = pswp.potentialIndex - pswp.currIndex;
if (pswp.canLoop()) {
const currDistance = (currDiff + numSlides) % numSlides;
if (currDistance <= numSlides / 2) {
// go forward
currDiff = currDistance;
} else {
// go backwards
currDiff = currDistance - numSlides;
}
} // Force-append new slides during transition
// if difference between slides is more than 1
if (Math.abs(currDiff) > 1) {
this.updateCurrItem();
}
}
return Boolean(diff);
}
/**
* X position of the main scroll for the current slide
* (ignores position during dragging)
* @returns {number}
*/
getCurrSlideX() {
return this.slideWidth * this._currPositionIndex;
}
/**
* Whether scroll position is shifted.
* For example, it will return true if the scroll is being dragged or animated.
* @returns {boolean}
*/
isShifted() {
return this.x !== this.getCurrSlideX();
}
/**
* Update slides X positions and set their content
*/
updateCurrItem() {
var _this$itemHolders$;
const {
pswp
} = this;
const positionDifference = this._prevPositionIndex - this._currPositionIndex;
if (!positionDifference) {
return;
}
this._prevPositionIndex = this._currPositionIndex;
pswp.currIndex = pswp.potentialIndex;
let diffAbs = Math.abs(positionDifference);
/** @type {ItemHolder | undefined} */
let tempHolder;
if (diffAbs >= 3) {
this._containerShiftIndex += positionDifference + (positionDifference > 0 ? -3 : 3);
diffAbs = 3; // If slides are changed by 3 screens or more - clean up previous slides
this.itemHolders.forEach(itemHolder => {
var _itemHolder$slide;
(_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.destroy();
itemHolder.slide = undefined;
});
}
for (let i = 0; i < diffAbs; i++) {
if (positionDifference > 0) {
tempHolder = this.itemHolders.shift();
if (tempHolder) {
this.itemHolders[2] = tempHolder; // move first to last
this._containerShiftIndex++;
setTransform(tempHolder.el, (this._containerShiftIndex + 2) * this.slideWidth);
pswp.setContent(tempHolder, pswp.currIndex - diffAbs + i + 2);
}
} else {
tempHolder = this.itemHolders.pop();
if (tempHolder) {
this.itemHolders.unshift(tempHolder); // move last to first
this._containerShiftIndex--;
setTransform(tempHolder.el, this._containerShiftIndex * this.slideWidth);
pswp.setContent(tempHolder, pswp.currIndex + diffAbs - i - 2);
}
}
} // Reset transfrom every 50ish navigations in one direction.
//
// Otherwise transform will keep growing indefinitely,
// which might cause issues as browsers have a maximum transform limit.
// I wasn't able to reach it, but just to be safe.
// This should not cause noticable lag.
if (Math.abs(this._containerShiftIndex) > 50 && !this.isShifted()) {
this.resetPosition();
this.resize();
} // Pan transition might be running (and consntantly updating pan position)
pswp.animations.stopAllPan();
this.itemHolders.forEach((itemHolder, i) => {
if (itemHolder.slide) {
// Slide in the 2nd holder is always active
itemHolder.slide.setIsActive(i === 1);
}
});
pswp.currSlide = (_this$itemHolders$ = this.itemHolders[1]) === null || _this$itemHolders$ === void 0 ? void 0 : _this$itemHolders$.slide;
pswp.contentLoader.updateLazy(positionDifference);
if (pswp.currSlide) {
pswp.currSlide.applyCurrentZoomPan();
}
pswp.dispatch('change');
}
/**
* Move the X position of the main scroll container
*
* @param {number} x
* @param {boolean} [dragging]
*/
moveTo(x, dragging) {
if (!this.pswp.canLoop() && dragging) {
// Apply friction
let newSlideIndexOffset = (this.slideWidth * this._currPositionIndex - x) / this.slideWidth;
newSlideIndexOffset += this.pswp.currIndex;
const delta = Math.round(x - this.x);
if (newSlideIndexOffset < 0 && delta > 0 || newSlideIndexOffset >= this.pswp.getNumItems() - 1 && delta < 0) {
x = this.x + delta * MAIN_SCROLL_END_FRICTION;
}
}
this.x = x;
if (this.pswp.container) {
setTransform(this.pswp.container, x);
}
this.pswp.dispatch('moveMainScroll', {
x,
dragging: dragging !== null && dragging !== void 0 ? dragging : false
});
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/**
* @template T
* @typedef {import('./types.js').Methods} Methods
*/
const KeyboardKeyCodesMap = {
Escape: 27,
z: 90,
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40,
Tab: 9
};
/**
* @template {keyof KeyboardKeyCodesMap} T
* @param {T} key
* @param {boolean} isKeySupported
* @returns {T | number | undefined}
*/
const getKeyboardEventKey = (key, isKeySupported) => {
return isKeySupported ? key : KeyboardKeyCodesMap[key];
};
/**
* - Manages keyboard shortcuts.
* - Helps trap focus within photoswipe.
*/
class Keyboard {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
/** @private */
this._wasFocused = false;
pswp.on('bindEvents', () => {
if (pswp.options.trapFocus) {
// Dialog was likely opened by keyboard if initial point is not defined
if (!pswp.options.initialPointerPos) {
// focus causes layout,
// which causes lag during the animation,
// that's why we delay it until the opener transition ends
this._focusRoot();
}
pswp.events.add(document, 'focusin',
/** @type EventListener */
this._onFocusIn.bind(this));
}
pswp.events.add(document, 'keydown',
/** @type EventListener */
this._onKeyDown.bind(this));
});
const lastActiveElement =
/** @type {HTMLElement} */
document.activeElement;
pswp.on('destroy', () => {
if (pswp.options.returnFocus && lastActiveElement && this._wasFocused) {
lastActiveElement.focus();
}
});
}
/** @private */
_focusRoot() {
if (!this._wasFocused && this.pswp.element) {
this.pswp.element.focus();
this._wasFocused = true;
}
}
/**
* @private
* @param {KeyboardEvent} e
*/
_onKeyDown(e) {
const {
pswp
} = this;
if (pswp.dispatch('keydown', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (specialKeyUsed(e)) {
// don't do anything if special key pressed
// to prevent from overriding default browser actions
// for example, in Chrome on Mac cmd+arrow-left returns to previous page
return;
}
/** @type {Methods | undefined} */
let keydownAction;
/** @type {'x' | 'y' | undefined} */
let axis;
let isForward = false;
const isKeySupported = ('key' in e);
switch (isKeySupported ? e.key : e.keyCode) {
case getKeyboardEventKey('Escape', isKeySupported):
if (pswp.options.escKey) {
keydownAction = 'close';
}
break;
case getKeyboardEventKey('z', isKeySupported):
keydownAction = 'toggleZoom';
break;
case getKeyboardEventKey('ArrowLeft', isKeySupported):
axis = 'x';
break;
case getKeyboardEventKey('ArrowUp', isKeySupported):
axis = 'y';
break;
case getKeyboardEventKey('ArrowRight', isKeySupported):
axis = 'x';
isForward = true;
break;
case getKeyboardEventKey('ArrowDown', isKeySupported):
isForward = true;
axis = 'y';
break;
case getKeyboardEventKey('Tab', isKeySupported):
this._focusRoot();
break;
} // if left/right/top/bottom key
if (axis) {
// prevent page scroll
e.preventDefault();
const {
currSlide
} = pswp;
if (pswp.options.arrowKeys && axis === 'x' && pswp.getNumItems() > 1) {
keydownAction = isForward ? 'next' : 'prev';
} else if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.fit) {
// up/down arrow keys pan the image vertically
// left/right arrow keys pan horizontally.
// Unless there is only one image,
// or arrowKeys option is disabled
currSlide.pan[axis] += isForward ? -80 : 80;
currSlide.panTo(currSlide.pan.x, currSlide.pan.y);
}
}
if (keydownAction) {
e.preventDefault(); // @ts-ignore
pswp[keydownAction]();
}
}
/**
* Trap focus inside photoswipe
*
* @private
* @param {FocusEvent} e
*/
_onFocusIn(e) {
const {
template
} = this.pswp;
if (template && document !== e.target && template !== e.target && !template.contains(
/** @type {Node} */
e.target)) {
// focus root element
template.focus();
}
}
}
const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)';
/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
/** @typedef {Object} DefaultCssAnimationProps
*
* @prop {HTMLElement} target
* @prop {number} [duration]
* @prop {string} [easing]
* @prop {string} [transform]
* @prop {string} [opacity]
* */
/** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */
/**
* Runs CSS transition.
*/
class CSSAnimation {
/**
* onComplete can be unpredictable, be careful about current state
*
* @param {CssAnimationProps} props
*/
constructor(props) {
var _props$prop;
this.props = props;
const {
target,
onComplete,
transform,
onFinish = () => {},
duration = 333,
easing = DEFAULT_EASING
} = props;
this.onFinish = onFinish; // support only transform and opacity
const prop = transform ? 'transform' : 'opacity';
const propValue = (_props$prop = props[prop]) !== null && _props$prop !== void 0 ? _props$prop : '';
/** @private */
this._target = target;
/** @private */
this._onComplete = onComplete;
/** @private */
this._finished = false;
/** @private */
this._onTransitionEnd = this._onTransitionEnd.bind(this); // Using timeout hack to make sure that animation
// starts even if the animated property was changed recently,
// otherwise transitionend might not fire or transition won't start.
// https://drafts.csswg.org/css-transitions/#starting
//
// ¯\_(ツ)_/¯
/** @private */
this._helperTimeout = setTimeout(() => {
setTransitionStyle(target, prop, duration, easing);
this._helperTimeout = setTimeout(() => {
target.addEventListener('transitionend', this._onTransitionEnd, false);
target.addEventListener('transitioncancel', this._onTransitionEnd, false); // Safari occasionally does not emit transitionend event
// if element property was modified during the transition,
// which may be caused by resize or third party component,
// using timeout as a safety fallback
this._helperTimeout = setTimeout(() => {
this._finalizeAnimation();
}, duration + 500);
target.style[prop] = propValue;
}, 30); // Do not reduce this number
}, 0);
}
/**
* @private
* @param {TransitionEvent} e
*/
_onTransitionEnd(e) {
if (e.target === this._target) {
this._finalizeAnimation();
}
}
/**
* @private
*/
_finalizeAnimation() {
if (!this._finished) {
this._finished = true;
this.onFinish();
if (this._onComplete) {
this._onComplete();
}
}
} // Destroy is called automatically onFinish
destroy() {
if (this._helperTimeout) {
clearTimeout(this._helperTimeout);
}
removeTransitionStyle(this._target);
this._target.removeEventListener('transitionend', this._onTransitionEnd, false);
this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false);
if (!this._finished) {
this._finalizeAnimation();
}
}
}
const DEFAULT_NATURAL_FREQUENCY = 12;
const DEFAULT_DAMPING_RATIO = 0.75;
/**
* Spring easing helper
*/
class SpringEaser {
/**
* @param {number} initialVelocity Initial velocity, px per ms.
*
* @param {number} [dampingRatio]
* Determines how bouncy animation will be.
* From 0 to 1, 0 - always overshoot, 1 - do not overshoot.
* "overshoot" refers to part of animation that
* goes beyond the final value.
*
* @param {number} [naturalFrequency]
* Determines how fast animation will slow down.
* The higher value - the stiffer the transition will be,
* and the faster it will slow down.
* Recommended value from 10 to 50
*/
constructor(initialVelocity, dampingRatio, naturalFrequency) {
this.velocity = initialVelocity * 1000; // convert to "pixels per second"
// https://en.wikipedia.org/wiki/Damping_ratio
this._dampingRatio = dampingRatio || DEFAULT_DAMPING_RATIO; // https://en.wikipedia.org/wiki/Natural_frequency
this._naturalFrequency = naturalFrequency || DEFAULT_NATURAL_FREQUENCY;
this._dampedFrequency = this._naturalFrequency;
if (this._dampingRatio < 1) {
this._dampedFrequency *= Math.sqrt(1 - this._dampingRatio * this._dampingRatio);
}
}
/**
* @param {number} deltaPosition Difference between current and end position of the animation
* @param {number} deltaTime Frame duration in milliseconds
*
* @returns {number} Displacement, relative to the end position.
*/
easeFrame(deltaPosition, deltaTime) {
// Inspired by Apple Webkit and Android spring function implementation
// https://en.wikipedia.org/wiki/Oscillation
// https://en.wikipedia.org/wiki/Damping_ratio
// we ignore mass (assume that it's 1kg)
let displacement = 0;
let coeff;
deltaTime /= 1000;
const naturalDumpingPow = Math.E ** (-this._dampingRatio * this._naturalFrequency * deltaTime);
if (this._dampingRatio === 1) {
coeff = this.velocity + this._naturalFrequency * deltaPosition;
displacement = (deltaPosition + coeff * deltaTime) * naturalDumpingPow;
this.velocity = displacement * -this._naturalFrequency + coeff * naturalDumpingPow;
} else if (this._dampingRatio < 1) {
coeff = 1 / this._dampedFrequency * (this._dampingRatio * this._naturalFrequency * deltaPosition + this.velocity);
const dumpedFCos = Math.cos(this._dampedFrequency * deltaTime);
const dumpedFSin = Math.sin(this._dampedFrequency * deltaTime);
displacement = naturalDumpingPow * (deltaPosition * dumpedFCos + coeff * dumpedFSin);
this.velocity = displacement * -this._naturalFrequency * this._dampingRatio + naturalDumpingPow * (-this._dampedFrequency * deltaPosition * dumpedFSin + this._dampedFrequency * coeff * dumpedFCos);
} // Overdamped (>1) damping ratio is not supported
return displacement;
}
}
/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
/**
* @typedef {Object} DefaultSpringAnimationProps
*
* @prop {number} start
* @prop {number} end
* @prop {number} velocity
* @prop {number} [dampingRatio]
* @prop {number} [naturalFrequency]
* @prop {(end: number) => void} onUpdate
*/
/** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */
class SpringAnimation {
/**
* @param {SpringAnimationProps} props
*/
constructor(props) {
this.props = props;
this._raf = 0;
const {
start,
end,
velocity,
onUpdate,
onComplete,
onFinish = () => {},
dampingRatio,
naturalFrequency
} = props;
this.onFinish = onFinish;
const easer = new SpringEaser(velocity, dampingRatio, naturalFrequency);
let prevTime = Date.now();
let deltaPosition = start - end;
const animationLoop = () => {
if (this._raf) {
deltaPosition = easer.easeFrame(deltaPosition, Date.now() - prevTime); // Stop the animation if velocity is low and position is close to end
if (Math.abs(deltaPosition) < 1 && Math.abs(easer.velocity) < 50) {
// Finalize the animation
onUpdate(end);
if (onComplete) {
onComplete();
}
this.onFinish();
} else {
prevTime = Date.now();
onUpdate(deltaPosition + end);
this._raf = requestAnimationFrame(animationLoop);
}
}
};
this._raf = requestAnimationFrame(animationLoop);
} // Destroy is called automatically onFinish
destroy() {
if (this._raf >= 0) {
cancelAnimationFrame(this._raf);
}
this._raf = 0;
}
}
/** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */
/** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */
/** @typedef {Object} SharedAnimationProps
* @prop {string} [name]
* @prop {boolean} [isPan]
* @prop {boolean} [isMainScroll]
* @prop {VoidFunction} [onComplete]
* @prop {VoidFunction} [onFinish]
*/
/** @typedef {SpringAnimation | CSSAnimation} Animation */
/** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */
/**
* Manages animations
*/
class Animations {
constructor() {
/** @type {Animation[]} */
this.activeAnimations = [];
}
/**
* @param {SpringAnimationProps} props
*/
startSpring(props) {
this._start(props, true);
}
/**
* @param {CssAnimationProps} props
*/
startTransition(props) {
this._start(props);
}
/**
* @private
* @param {AnimationProps} props
* @param {boolean} [isSpring]
* @returns {Animation}
*/
_start(props, isSpring) {
const animation = isSpring ? new SpringAnimation(
/** @type SpringAnimationProps */
props) : new CSSAnimation(
/** @type CssAnimationProps */
props);
this.activeAnimations.push(animation);
animation.onFinish = () => this.stop(animation);
return animation;
}
/**
* @param {Animation} animation
*/
stop(animation) {
animation.destroy();
const index = this.activeAnimations.indexOf(animation);
if (index > -1) {
this.activeAnimations.splice(index, 1);
}
}
stopAll() {
// _stopAllAnimations
this.activeAnimations.forEach(animation => {
animation.destroy();
});
this.activeAnimations = [];
}
/**
* Stop all pan or zoom transitions
*/
stopAllPan() {
this.activeAnimations = this.activeAnimations.filter(animation => {
if (animation.props.isPan) {
animation.destroy();
return false;
}
return true;
});
}
stopMainScroll() {
this.activeAnimations = this.activeAnimations.filter(animation => {
if (animation.props.isMainScroll) {
animation.destroy();
return false;
}
return true;
});
}
/**
* Returns true if main scroll transition is running
*/
// isMainScrollRunning() {
// return this.activeAnimations.some((animation) => {
// return animation.props.isMainScroll;
// });
// }
/**
* Returns true if any pan or zoom transition is running
*/
isPanRunning() {
return this.activeAnimations.some(animation => {
return animation.props.isPan;
});
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/**
* Handles scroll wheel.
* Can pan and zoom current slide image.
*/
class ScrollWheel {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
pswp.events.add(pswp.element, 'wheel',
/** @type EventListener */
this._onWheel.bind(this));
}
/**
* @private
* @param {WheelEvent} e
*/
_onWheel(e) {
e.preventDefault();
const {
currSlide
} = this.pswp;
let {
deltaX,
deltaY
} = e;
if (!currSlide) {
return;
}
if (this.pswp.dispatch('wheel', {
originalEvent: e
}).defaultPrevented) {
return;
}
if (e.ctrlKey || this.pswp.options.wheelToZoom) {
// zoom
if (currSlide.isZoomable()) {
let zoomFactor = -deltaY;
if (e.deltaMode === 1
/* DOM_DELTA_LINE */
) {
zoomFactor *= 0.05;
} else {
zoomFactor *= e.deltaMode ? 1 : 0.002;
}
zoomFactor = 2 ** zoomFactor;
const destZoomLevel = currSlide.currZoomLevel * zoomFactor;
currSlide.zoomTo(destZoomLevel, {
x: e.clientX,
y: e.clientY
});
}
} else {
// pan
if (currSlide.isPannable()) {
if (e.deltaMode === 1
/* DOM_DELTA_LINE */
) {
// 18 - average line height
deltaX *= 18;
deltaY *= 18;
}
currSlide.panTo(currSlide.pan.x - deltaX, currSlide.pan.y - deltaY);
}
}
}
}
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/**
* @template T
* @typedef {import('../types.js').Methods} Methods
*/
/**
* @typedef {Object} UIElementMarkupProps
* @prop {boolean} [isCustomSVG]
* @prop {string} inner
* @prop {string} [outlineID]
* @prop {number | string} [size]
*/
/**
* @typedef {Object} UIElementData
* @prop {DefaultUIElements | string} [name]
* @prop {string} [className]
* @prop {UIElementMarkup} [html]
* @prop {boolean} [isButton]
* @prop {keyof HTMLElementTagNameMap} [tagName]
* @prop {string} [title]
* @prop {string} [ariaLabel]
* @prop {(element: HTMLElement, pswp: PhotoSwipe) => void} [onInit]
* @prop {Methods | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void)} [onClick]
* @prop {'bar' | 'wrapper' | 'root'} [appendTo]
* @prop {number} [order]
*/
/** @typedef {'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter'} DefaultUIElements */
/** @typedef {string | UIElementMarkupProps} UIElementMarkup */
/**
* @param {UIElementMarkup} [htmlData]
* @returns {string}
*/
function addElementHTML(htmlData) {
if (typeof htmlData === 'string') {
// Allow developers to provide full svg,
// For example:
//
//
//
//
// Can also be any HTML string.
return htmlData;
}
if (!htmlData || !htmlData.isCustomSVG) {
return '';
}
const svgData = htmlData;
let out = ''; // replace all %d with size
out = out.split('%d').join(
/** @type {string} */
svgData.size || 32); // Icons may contain outline/shadow,
// to make it we "clone" base icon shape and add border to it.
// Icon itself and border are styled via CSS.
//
// Property shadowID defines ID of element that should be cloned.
if (svgData.outlineID) {
out += ' ';
}
out += svgData.inner;
out += ' ';
return out;
}
class UIElement {
/**
* @param {PhotoSwipe} pswp
* @param {UIElementData} data
*/
constructor(pswp, data) {
var _container;
const name = data.name || data.className;
let elementHTML = data.html; // @ts-expect-error lookup only by `data.name` maybe?
if (pswp.options[name] === false) {
// exit if element is disabled from options
return;
} // Allow to override SVG icons from options
// @ts-expect-error lookup only by `data.name` maybe?
if (typeof pswp.options[name + 'SVG'] === 'string') {
// arrowPrevSVG
// arrowNextSVG
// closeSVG
// zoomSVG
// @ts-expect-error lookup only by `data.name` maybe?
elementHTML = pswp.options[name + 'SVG'];
}
pswp.dispatch('uiElementCreate', {
data
});
let className = '';
if (data.isButton) {
className += 'pswp__button ';
className += data.className || `pswp__button--${data.name}`;
} else {
className += data.className || `pswp__${data.name}`;
}
let tagName = data.isButton ? data.tagName || 'button' : data.tagName || 'div';
tagName =
/** @type {keyof HTMLElementTagNameMap} */
tagName.toLowerCase();
/** @type {HTMLElement} */
const element = createElement(className, tagName);
if (data.isButton) {
if (tagName === 'button') {
/** @type {HTMLButtonElement} */
element.type = 'button';
}
let {
title
} = data;
const {
ariaLabel
} = data; // @ts-expect-error lookup only by `data.name` maybe?
if (typeof pswp.options[name + 'Title'] === 'string') {
// @ts-expect-error lookup only by `data.name` maybe?
title = pswp.options[name + 'Title'];
}
if (title) {
element.title = title;
}
const ariaText = ariaLabel || title;
if (ariaText) {
element.setAttribute('aria-label', ariaText);
}
}
element.innerHTML = addElementHTML(elementHTML);
if (data.onInit) {
data.onInit(element, pswp);
}
if (data.onClick) {
element.onclick = e => {
if (typeof data.onClick === 'string') {
// @ts-ignore
pswp[data.onClick]();
} else if (typeof data.onClick === 'function') {
data.onClick(e, element, pswp);
}
};
} // Top bar is default position
const appendTo = data.appendTo || 'bar';
/** @type {HTMLElement | undefined} root element by default */
let container = pswp.element;
if (appendTo === 'bar') {
if (!pswp.topBar) {
pswp.topBar = createElement('pswp__top-bar pswp__hide-on-close', 'div', pswp.scrollWrap);
}
container = pswp.topBar;
} else {
// element outside of top bar gets a secondary class
// that makes element fade out on close
element.classList.add('pswp__hide-on-close');
if (appendTo === 'wrapper') {
container = pswp.scrollWrap;
}
}
(_container = container) === null || _container === void 0 || _container.appendChild(pswp.applyFilters('uiElement', element, data));
}
}
/*
Backward and forward arrow buttons
*/
/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/**
*
* @param {HTMLElement} element
* @param {PhotoSwipe} pswp
* @param {boolean} [isNextButton]
*/
function initArrowButton(element, pswp, isNextButton) {
element.classList.add('pswp__button--arrow'); // TODO: this should point to a unique id for this instance
element.setAttribute('aria-controls', 'pswp__items');
pswp.on('change', () => {
if (!pswp.options.loop) {
if (isNextButton) {
/** @type {HTMLButtonElement} */
element.disabled = !(pswp.currIndex < pswp.getNumItems() - 1);
} else {
/** @type {HTMLButtonElement} */
element.disabled = !(pswp.currIndex > 0);
}
}
});
}
/** @type {UIElementData} */
const arrowPrev = {
name: 'arrowPrev',
className: 'pswp__button--arrow--prev',
title: 'Previous',
order: 10,
isButton: true,
appendTo: 'wrapper',
html: {
isCustomSVG: true,
size: 60,
inner: ' ',
outlineID: 'pswp__icn-arrow'
},
onClick: 'prev',
onInit: initArrowButton
};
/** @type {UIElementData} */
const arrowNext = {
name: 'arrowNext',
className: 'pswp__button--arrow--next',
title: 'Next',
order: 11,
isButton: true,
appendTo: 'wrapper',
html: {
isCustomSVG: true,
size: 60,
inner: ' ',
outlineID: 'pswp__icn-arrow'
},
onClick: 'next',
onInit: (el, pswp) => {
initArrowButton(el, pswp, true);
}
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const closeButton = {
name: 'close',
title: 'Close',
order: 20,
isButton: true,
html: {
isCustomSVG: true,
inner: ' ',
outlineID: 'pswp__icn-close'
},
onClick: 'close'
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const zoomButton = {
name: 'zoom',
title: 'Zoom',
order: 10,
isButton: true,
html: {
isCustomSVG: true,
// eslint-disable-next-line max-len
inner: ' ' + ' ' + ' ',
outlineID: 'pswp__icn-zoom'
},
onClick: 'toggleZoom'
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const loadingIndicator = {
name: 'preloader',
appendTo: 'bar',
order: 7,
html: {
isCustomSVG: true,
// eslint-disable-next-line max-len
inner: ' ',
outlineID: 'pswp__icn-loading'
},
onInit: (indicatorElement, pswp) => {
/** @type {boolean | undefined} */
let isVisible;
/** @type {NodeJS.Timeout | null} */
let delayTimeout = null;
/**
* @param {string} className
* @param {boolean} add
*/
const toggleIndicatorClass = (className, add) => {
indicatorElement.classList.toggle('pswp__preloader--' + className, add);
};
/**
* @param {boolean} visible
*/
const setIndicatorVisibility = visible => {
if (isVisible !== visible) {
isVisible = visible;
toggleIndicatorClass('active', visible);
}
};
const updatePreloaderVisibility = () => {
var _pswp$currSlide;
if (!((_pswp$currSlide = pswp.currSlide) !== null && _pswp$currSlide !== void 0 && _pswp$currSlide.content.isLoading())) {
setIndicatorVisibility(false);
if (delayTimeout) {
clearTimeout(delayTimeout);
delayTimeout = null;
}
return;
}
if (!delayTimeout) {
// display loading indicator with delay
delayTimeout = setTimeout(() => {
var _pswp$currSlide2;
setIndicatorVisibility(Boolean((_pswp$currSlide2 = pswp.currSlide) === null || _pswp$currSlide2 === void 0 ? void 0 : _pswp$currSlide2.content.isLoading()));
delayTimeout = null;
}, pswp.options.preloaderDelay);
}
};
pswp.on('change', updatePreloaderVisibility);
pswp.on('loadComplete', e => {
if (pswp.currSlide === e.slide) {
updatePreloaderVisibility();
}
}); // expose the method
if (pswp.ui) {
pswp.ui.updatePreloaderVisibility = updatePreloaderVisibility;
}
}
};
/** @type {import('./ui-element.js').UIElementData} UIElementData */
const counterIndicator = {
name: 'counter',
order: 5,
onInit: (counterElement, pswp) => {
pswp.on('change', () => {
counterElement.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems();
});
}
};
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
/**
* Set special class on element when image is zoomed.
*
* By default, it is used to adjust
* zoom icon and zoom cursor via CSS.
*
* @param {HTMLElement} el
* @param {boolean} isZoomedIn
*/
function setZoomedIn(el, isZoomedIn) {
el.classList.toggle('pswp--zoomed-in', isZoomedIn);
}
class UI {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
this.isRegistered = false;
/** @type {UIElementData[]} */
this.uiElementsData = [];
/** @type {(UIElement | UIElementData)[]} */
this.items = [];
/** @type {() => void} */
this.updatePreloaderVisibility = () => {};
/**
* @private
* @type {number | undefined}
*/
this._lastUpdatedZoomLevel = undefined;
}
init() {
const {
pswp
} = this;
this.isRegistered = false;
this.uiElementsData = [closeButton, arrowPrev, arrowNext, zoomButton, loadingIndicator, counterIndicator];
pswp.dispatch('uiRegister'); // sort by order
this.uiElementsData.sort((a, b) => {
// default order is 0
return (a.order || 0) - (b.order || 0);
});
this.items = [];
this.isRegistered = true;
this.uiElementsData.forEach(uiElementData => {
this.registerElement(uiElementData);
});
pswp.on('change', () => {
var _pswp$element;
(_pswp$element = pswp.element) === null || _pswp$element === void 0 || _pswp$element.classList.toggle('pswp--one-slide', pswp.getNumItems() === 1);
});
pswp.on('zoomPanUpdate', () => this._onZoomPanUpdate());
}
/**
* @param {UIElementData} elementData
*/
registerElement(elementData) {
if (this.isRegistered) {
this.items.push(new UIElement(this.pswp, elementData));
} else {
this.uiElementsData.push(elementData);
}
}
/**
* Fired each time zoom or pan position is changed.
* Update classes that control visibility of zoom button and cursor icon.
*
* @private
*/
_onZoomPanUpdate() {
const {
template,
currSlide,
options
} = this.pswp;
if (this.pswp.opener.isClosing || !template || !currSlide) {
return;
}
let {
currZoomLevel
} = currSlide; // if not open yet - check against initial zoom level
if (!this.pswp.opener.isOpen) {
currZoomLevel = currSlide.zoomLevels.initial;
}
if (currZoomLevel === this._lastUpdatedZoomLevel) {
return;
}
this._lastUpdatedZoomLevel = currZoomLevel;
const currZoomLevelDiff = currSlide.zoomLevels.initial - currSlide.zoomLevels.secondary; // Initial and secondary zoom levels are almost equal
if (Math.abs(currZoomLevelDiff) < 0.01 || !currSlide.isZoomable()) {
// disable zoom
setZoomedIn(template, false);
template.classList.remove('pswp--zoom-allowed');
return;
}
template.classList.add('pswp--zoom-allowed');
const potentialZoomLevel = currZoomLevel === currSlide.zoomLevels.initial ? currSlide.zoomLevels.secondary : currSlide.zoomLevels.initial;
setZoomedIn(template, potentialZoomLevel <= currZoomLevel);
if (options.imageClickAction === 'zoom' || options.imageClickAction === 'zoom-or-close') {
template.classList.add('pswp--click-to-zoom');
}
}
}
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {{ x: number; y: number; w: number; innerRect?: { w: number; h: number; x: number; y: number } }} Bounds */
/**
* @param {HTMLElement} el
* @returns Bounds
*/
function getBoundsByElement(el) {
const thumbAreaRect = el.getBoundingClientRect();
return {
x: thumbAreaRect.left,
y: thumbAreaRect.top,
w: thumbAreaRect.width
};
}
/**
* @param {HTMLElement} el
* @param {number} imageWidth
* @param {number} imageHeight
* @returns Bounds
*/
function getCroppedBoundsByElement(el, imageWidth, imageHeight) {
const thumbAreaRect = el.getBoundingClientRect(); // fill image into the area
// (do they same as object-fit:cover does to retrieve coordinates)
const hRatio = thumbAreaRect.width / imageWidth;
const vRatio = thumbAreaRect.height / imageHeight;
const fillZoomLevel = hRatio > vRatio ? hRatio : vRatio;
const offsetX = (thumbAreaRect.width - imageWidth * fillZoomLevel) / 2;
const offsetY = (thumbAreaRect.height - imageHeight * fillZoomLevel) / 2;
/**
* Coordinates of the image,
* as if it was not cropped,
* height is calculated automatically
*
* @type {Bounds}
*/
const bounds = {
x: thumbAreaRect.left + offsetX,
y: thumbAreaRect.top + offsetY,
w: imageWidth * fillZoomLevel
}; // Coordinates of inner crop area
// relative to the image
bounds.innerRect = {
w: thumbAreaRect.width,
h: thumbAreaRect.height,
x: offsetX,
y: offsetY
};
return bounds;
}
/**
* Get dimensions of thumbnail image
* (click on which opens photoswipe or closes photoswipe to)
*
* @param {number} index
* @param {SlideData} itemData
* @param {PhotoSwipe} instance PhotoSwipe instance
* @returns {Bounds | undefined}
*/
function getThumbBounds(index, itemData, instance) {
// legacy event, before filters were introduced
const event = instance.dispatch('thumbBounds', {
index,
itemData,
instance
}); // @ts-expect-error
if (event.thumbBounds) {
// @ts-expect-error
return event.thumbBounds;
}
const {
element
} = itemData;
/** @type {Bounds | undefined} */
let thumbBounds;
/** @type {HTMLElement | null | undefined} */
let thumbnail;
if (element && instance.options.thumbSelector !== false) {
const thumbSelector = instance.options.thumbSelector || 'img';
thumbnail = element.matches(thumbSelector) ? element :
/** @type {HTMLElement | null} */
element.querySelector(thumbSelector);
}
thumbnail = instance.applyFilters('thumbEl', thumbnail, itemData, index);
if (thumbnail) {
if (!itemData.thumbCropped) {
thumbBounds = getBoundsByElement(thumbnail);
} else {
thumbBounds = getCroppedBoundsByElement(thumbnail, itemData.width || itemData.w || 0, itemData.height || itemData.h || 0);
}
}
return instance.applyFilters('thumbBounds', thumbBounds, itemData, index);
}
/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../slide/content.js').default} ContentDefault */
/** @typedef {import('../slide/slide.js').default} Slide */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
/**
* Allow adding an arbitrary props to the Content
* https://photoswipe.com/custom-content/#using-webp-image-format
* @typedef {ContentDefault & Record} Content
*/
/** @typedef {{ x?: number; y?: number }} Point */
/**
* @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
*
*
* https://photoswipe.com/adding-ui-elements/
*
* @prop {undefined} uiRegister
* @prop {{ data: UIElementData }} uiElementCreate
*
*
* https://photoswipe.com/events/#initialization-events
*
* @prop {undefined} beforeOpen
* @prop {undefined} firstUpdate
* @prop {undefined} initialLayout
* @prop {undefined} change
* @prop {undefined} afterInit
* @prop {undefined} bindEvents
*
*
* https://photoswipe.com/events/#opening-or-closing-transition-events
*
* @prop {undefined} openingAnimationStart
* @prop {undefined} openingAnimationEnd
* @prop {undefined} closingAnimationStart
* @prop {undefined} closingAnimationEnd
*
*
* https://photoswipe.com/events/#closing-events
*
* @prop {undefined} close
* @prop {undefined} destroy
*
*
* https://photoswipe.com/events/#pointer-and-gesture-events
*
* @prop {{ originalEvent: PointerEvent }} pointerDown
* @prop {{ originalEvent: PointerEvent }} pointerMove
* @prop {{ originalEvent: PointerEvent }} pointerUp
* @prop {{ bgOpacity: number }} pinchClose can be default prevented
* @prop {{ panY: number }} verticalDrag can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*
* @prop {{ content: Content }} contentInit
* @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
* @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
* @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
* @prop {{ content: Content; slide: Slide }} loadError
* @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
* @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
* @prop {{ content: Content }} contentLazyLoad can be default prevented
* @prop {{ content: Content }} contentAppend can be default prevented
* @prop {{ content: Content }} contentActivate can be default prevented
* @prop {{ content: Content }} contentDeactivate can be default prevented
* @prop {{ content: Content }} contentRemove can be default prevented
* @prop {{ content: Content }} contentDestroy can be default prevented
*
*
* undocumented
*
* @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
*
* @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
* @prop {{ x: number; dragging: boolean }} moveMainScroll
* @prop {{ slide: Slide }} firstZoomPan
* @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
* @prop {undefined} beforeResize
* @prop {undefined} resize
* @prop {undefined} viewportSize
* @prop {undefined} updateScrollOffset
* @prop {{ slide: Slide }} slideInit
* @prop {{ slide: Slide }} afterSetContent
* @prop {{ slide: Slide }} slideLoad
* @prop {{ slide: Slide }} appendHeavy can be default prevented
* @prop {{ slide: Slide }} appendHeavyContent
* @prop {{ slide: Slide }} slideActivate
* @prop {{ slide: Slide }} slideDeactivate
* @prop {{ slide: Slide }} slideDestroy
* @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
* @prop {{ slide: Slide }} zoomPanUpdate
* @prop {{ slide: Slide }} initialZoomPan
* @prop {{ slide: Slide }} calcSlideSize
* @prop {undefined} resolutionChanged
* @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
* @prop {{ content: Content }} contentAppendImage can be default prevented
* @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
* @prop {undefined} lazyLoad
* @prop {{ slide: Slide }} calcBounds
* @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
*
*
* legacy
*
* @prop {undefined} init
* @prop {undefined} initialZoomIn
* @prop {undefined} initialZoomOut
* @prop {undefined} initialZoomInEnd
* @prop {undefined} initialZoomOutEnd
* @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
* @prop {{ itemData: SlideData; index: number }} itemData
* @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
*/
/**
* @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
*
* @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*
* @prop {(itemData: SlideData, index: number) => SlideData} itemData
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*
* @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*
* @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*
* @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*
* @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*
* @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*
* @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*
* @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*
*
* @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*
* @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*
* @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*
* @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*
* @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
*
* @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
*
*/
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent : PhotoSwipeEvent & PhotoSwipeEventsMap[T]} AugmentedEvent
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {(event: AugmentedEvent) => void} EventCallback
*/
/**
* Base PhotoSwipe event object
*
* @template {keyof PhotoSwipeEventsMap} T
*/
class PhotoSwipeEvent {
/**
* @param {T} type
* @param {PhotoSwipeEventsMap[T]} [details]
*/
constructor(type, details) {
this.type = type;
this.defaultPrevented = false;
if (details) {
Object.assign(this, details);
}
}
preventDefault() {
this.defaultPrevented = true;
}
}
/**
* PhotoSwipe base class that can listen and dispatch for events.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
*/
class Eventable {
constructor() {
/**
* @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent) => void)[] }}
*/
this._listeners = {};
/**
* @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter[] }}
*/
this._filters = {};
/** @type {PhotoSwipe | undefined} */
this.pswp = undefined;
/** @type {PhotoSwipeOptions | undefined} */
this.options = undefined;
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
* @param {number} priority
*/
addFilter(name, fn, priority = 100) {
var _this$_filters$name, _this$_filters$name2, _this$pswp;
if (!this._filters[name]) {
this._filters[name] = [];
}
(_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
fn,
priority
});
(_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
(_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
*/
removeFilter(name, fn) {
if (this._filters[name]) {
// @ts-expect-error
this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
}
if (this.pswp) {
this.pswp.removeFilter(name, fn);
}
}
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {Parameters} args
* @returns {Parameters[0]}
*/
applyFilters(name, ...args) {
var _this$_filters$name3;
(_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
// @ts-expect-error
args[0] = filter.fn.apply(this, args);
});
return args[0];
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
on(name, fn) {
var _this$_listeners$name, _this$pswp2;
if (!this._listeners[name]) {
this._listeners[name] = [];
}
(_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
// also bind events to PhotoSwipe Core,
// if it's open.
(_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
off(name, fn) {
var _this$pswp3;
if (this._listeners[name]) {
// @ts-expect-error
this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
}
(_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
}
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {PhotoSwipeEventsMap[T]} [details]
* @returns {AugmentedEvent}
*/
dispatch(name, details) {
var _this$_listeners$name2;
if (this.pswp) {
return this.pswp.dispatch(name, details);
}
const event =
/** @type {AugmentedEvent} */
new PhotoSwipeEvent(name, details);
(_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
listener.call(this, event);
});
return event;
}
}
class Placeholder {
/**
* @param {string | false} imageSrc
* @param {HTMLElement} container
*/
constructor(imageSrc, container) {
// Create placeholder
// (stretched thumbnail or simple div behind the main image)
/** @type {HTMLImageElement | HTMLDivElement | null} */
this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
if (imageSrc) {
const imgEl =
/** @type {HTMLImageElement} */
this.element;
imgEl.decoding = 'async';
imgEl.alt = '';
imgEl.src = imageSrc;
imgEl.setAttribute('role', 'presentation');
}
this.element.setAttribute('aria-hidden', 'true');
}
/**
* @param {number} width
* @param {number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.element.tagName === 'IMG') {
// Use transform scale() to modify img placeholder size
// (instead of changing width/height directly).
// This helps with performance, specifically in iOS15 Safari.
setWidthHeight(this.element, 250, 'auto');
this.element.style.transformOrigin = '0 0';
this.element.style.transform = toTransformString(0, 0, width / 250);
} else {
setWidthHeight(this.element, width, height);
}
}
destroy() {
var _this$element;
if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
this.element.remove();
}
this.element = null;
}
}
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../util/util.js').LoadState} LoadState */
class Content {
/**
* @param {SlideData} itemData Slide data
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
*/
constructor(itemData, instance, index) {
this.instance = instance;
this.data = itemData;
this.index = index;
/** @type {HTMLImageElement | HTMLDivElement | undefined} */
this.element = undefined;
/** @type {Placeholder | undefined} */
this.placeholder = undefined;
/** @type {Slide | undefined} */
this.slide = undefined;
this.displayedImageWidth = 0;
this.displayedImageHeight = 0;
this.width = Number(this.data.w) || Number(this.data.width) || 0;
this.height = Number(this.data.h) || Number(this.data.height) || 0;
this.isAttached = false;
this.hasSlide = false;
this.isDecoding = false;
/** @type {LoadState} */
this.state = LOAD_STATE.IDLE;
if (this.data.type) {
this.type = this.data.type;
} else if (this.data.src) {
this.type = 'image';
} else {
this.type = 'html';
}
this.instance.dispatch('contentInit', {
content: this
});
}
removePlaceholder() {
if (this.placeholder && !this.keepPlaceholder()) {
// With delay, as image might be loaded, but not rendered
setTimeout(() => {
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
}, 1000);
}
}
/**
* Preload content
*
* @param {boolean} isLazy
* @param {boolean} [reload]
*/
load(isLazy, reload) {
if (this.slide && this.usePlaceholder()) {
if (!this.placeholder) {
const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide,
// as rendering (even small stretched thumbnail) is an expensive operation
this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
} else {
const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
if (placeholderEl && !placeholderEl.parentElement) {
this.slide.container.prepend(placeholderEl);
}
}
}
if (this.element && !reload) {
return;
}
if (this.instance.dispatch('contentLoad', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
if (this.isImageContent()) {
this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
// Due to Safari feature, we must define sizes before srcset.
if (this.displayedImageWidth) {
this.loadImage(isLazy);
}
} else {
this.element = createElement('pswp__content', 'div');
this.element.innerHTML = this.data.html || '';
}
if (reload && this.slide) {
this.slide.updateContentSize(true);
}
}
/**
* Preload image
*
* @param {boolean} isLazy
*/
loadImage(isLazy) {
var _this$data$src, _this$data$alt;
if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
content: this,
isLazy
}).defaultPrevented) {
return;
}
const imageElement =
/** @type HTMLImageElement */
this.element;
this.updateSrcsetSizes();
if (this.data.srcset) {
imageElement.srcset = this.data.srcset;
}
imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
this.state = LOAD_STATE.LOADING;
if (imageElement.complete) {
this.onLoaded();
} else {
imageElement.onload = () => {
this.onLoaded();
};
imageElement.onerror = () => {
this.onError();
};
}
}
/**
* Assign slide to content
*
* @param {Slide} slide
*/
setSlide(slide) {
this.slide = slide;
this.hasSlide = true;
this.instance = slide.pswp; // todo: do we need to unset slide?
}
/**
* Content load success handler
*/
onLoaded() {
this.state = LOAD_STATE.LOADED;
if (this.slide && this.element) {
this.instance.dispatch('loadComplete', {
slide: this.slide,
content: this
}); // if content is reloaded
if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
this.append();
this.slide.updateContentSize(true);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/**
* Content load error handler
*/
onError() {
this.state = LOAD_STATE.ERROR;
if (this.slide) {
this.displayError();
this.instance.dispatch('loadComplete', {
slide: this.slide,
isError: true,
content: this
});
this.instance.dispatch('loadError', {
slide: this.slide,
content: this
});
}
}
/**
* @returns {Boolean} If the content is currently loading
*/
isLoading() {
return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
}
/**
* @returns {Boolean} If the content is in error state
*/
isError() {
return this.state === LOAD_STATE.ERROR;
}
/**
* @returns {boolean} If the content is image
*/
isImageContent() {
return this.type === 'image';
}
/**
* Update content size
*
* @param {Number} width
* @param {Number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.placeholder) {
this.placeholder.setDisplayedSize(width, height);
}
if (this.instance.dispatch('contentResize', {
content: this,
width,
height
}).defaultPrevented) {
return;
}
setWidthHeight(this.element, width, height);
if (this.isImageContent() && !this.isError()) {
const isInitialSizeUpdate = !this.displayedImageWidth && width;
this.displayedImageWidth = width;
this.displayedImageHeight = height;
if (isInitialSizeUpdate) {
this.loadImage(false);
} else {
this.updateSrcsetSizes();
}
if (this.slide) {
this.instance.dispatch('imageSizeChange', {
slide: this.slide,
width,
height,
content: this
});
}
}
}
/**
* @returns {boolean} If the content can be zoomed
*/
isZoomable() {
return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
}
/**
* Update image srcset sizes attribute based on width and height
*/
updateSrcsetSizes() {
// Handle srcset sizes attribute.
//
// Never lower quality, if it was increased previously.
// Chrome does this automatically, Firefox and Safari do not,
// so we store largest used size in dataset.
if (!this.isImageContent() || !this.element || !this.data.srcset) {
return;
}
const image =
/** @type HTMLImageElement */
this.element;
const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
image.sizes = sizesWidth + 'px';
image.dataset.largestUsedSize = String(sizesWidth);
}
}
/**
* @returns {boolean} If content should use a placeholder (from msrc by default)
*/
usePlaceholder() {
return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
}
/**
* Preload content with lazy-loading param
*/
lazyLoad() {
if (this.instance.dispatch('contentLazyLoad', {
content: this
}).defaultPrevented) {
return;
}
this.load(true);
}
/**
* @returns {boolean} If placeholder should be kept after content is loaded
*/
keepPlaceholder() {
return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
}
/**
* Destroy the content
*/
destroy() {
this.hasSlide = false;
this.slide = undefined;
if (this.instance.dispatch('contentDestroy', {
content: this
}).defaultPrevented) {
return;
}
this.remove();
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = undefined;
}
if (this.isImageContent() && this.element) {
this.element.onload = null;
this.element.onerror = null;
this.element = undefined;
}
}
/**
* Display error message
*/
displayError() {
if (this.slide) {
var _this$instance$option, _this$instance$option2;
let errorMsgEl = createElement('pswp__error-msg', 'div');
errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
errorMsgEl =
/** @type {HTMLDivElement} */
this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
this.element = createElement('pswp__content pswp__error-msg-container', 'div');
this.element.appendChild(errorMsgEl);
this.slide.container.innerText = '';
this.slide.container.appendChild(this.element);
this.slide.updateContentSize(true);
this.removePlaceholder();
}
}
/**
* Append the content
*/
append() {
if (this.isAttached || !this.element) {
return;
}
this.isAttached = true;
if (this.state === LOAD_STATE.ERROR) {
this.displayError();
return;
}
if (this.instance.dispatch('contentAppend', {
content: this
}).defaultPrevented) {
return;
}
const supportsDecode = ('decode' in this.element);
if (this.isImageContent()) {
// Use decode() on nearby slides
//
// Nearby slide images are in DOM and not hidden via display:none.
// However, they are placed offscreen (to the left and right side).
//
// Some browsers do not composite the image until it's actually visible,
// using decode() helps.
//
// You might ask "why dont you just decode() and then append all images",
// that's because I want to show image before it's fully loaded,
// as browser can render parts of image while it is loading.
// We do not do this in Safari due to partial loading bug.
if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
this.isDecoding = true; // purposefully using finally instead of then,
// as if srcset sizes changes dynamically - it may cause decode error
/** @type {HTMLImageElement} */
this.element.decode().catch(() => {}).finally(() => {
this.isDecoding = false;
this.appendImage();
});
} else {
this.appendImage();
}
} else if (this.slide && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
}
/**
* Activate the slide,
* active slide is generally the current one,
* meaning the user can see it.
*/
activate() {
if (this.instance.dispatch('contentActivate', {
content: this
}).defaultPrevented || !this.slide) {
return;
}
if (this.isImageContent() && this.isDecoding && !isSafari()) {
// add image to slide when it becomes active,
// even if it's not finished decoding
this.appendImage();
} else if (this.isError()) {
this.load(false, true); // try to reload
}
if (this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'false');
}
}
/**
* Deactivate the content
*/
deactivate() {
this.instance.dispatch('contentDeactivate', {
content: this
});
if (this.slide && this.slide.holderElement) {
this.slide.holderElement.setAttribute('aria-hidden', 'true');
}
}
/**
* Remove the content from DOM
*/
remove() {
this.isAttached = false;
if (this.instance.dispatch('contentRemove', {
content: this
}).defaultPrevented) {
return;
}
if (this.element && this.element.parentNode) {
this.element.remove();
}
if (this.placeholder && this.placeholder.element) {
this.placeholder.element.remove();
}
}
/**
* Append the image content to slide container
*/
appendImage() {
if (!this.isAttached) {
return;
}
if (this.instance.dispatch('contentAppendImage', {
content: this
}).defaultPrevented) {
return;
} // ensure that element exists and is not already appended
if (this.slide && this.element && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
this.removePlaceholder();
}
}
}
/** @typedef {import('./content.js').default} Content */
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
const MIN_SLIDES_TO_CACHE = 5;
/**
* Lazy-load an image
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* @param {SlideData} itemData Data about the slide
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
function lazyLoadData(itemData, instance, index) {
const content = instance.createContentFromData(itemData, index);
/** @type {ZoomLevel | undefined} */
let zoomLevel;
const {
options
} = instance; // We need to know dimensions of the image to preload it,
// as it might use srcset, and we need to define sizes
if (options) {
zoomLevel = new ZoomLevel(options, itemData, -1);
let viewportSize;
if (instance.pswp) {
viewportSize = instance.pswp.viewportSize;
} else {
viewportSize = getViewportSize(options, instance);
}
const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
zoomLevel.update(content.width, content.height, panAreaSize);
}
content.lazyLoad();
if (zoomLevel) {
content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
}
return content;
}
/**
* Lazy-loads specific slide.
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* By default, it loads image based on viewport size and initial zoom level.
*
* @param {number} index Slide index
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
* @returns {Content | undefined}
*/
function lazyLoadSlide(index, instance) {
const itemData = instance.getItemData(index);
if (instance.dispatch('lazyLoadSlide', {
index,
itemData
}).defaultPrevented) {
return;
}
return lazyLoadData(itemData, instance, index);
}
class ContentLoader {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp; // Total amount of cached images
this.limit = Math.max(pswp.options.preload[0] + pswp.options.preload[1] + 1, MIN_SLIDES_TO_CACHE);
/** @type {Content[]} */
this._cachedItems = [];
}
/**
* Lazy load nearby slides based on `preload` option.
*
* @param {number} [diff] Difference between slide indexes that was changed recently, or 0.
*/
updateLazy(diff) {
const {
pswp
} = this;
if (pswp.dispatch('lazyLoad').defaultPrevented) {
return;
}
const {
preload
} = pswp.options;
const isForward = diff === undefined ? true : diff >= 0;
let i; // preload[1] - num items to preload in forward direction
for (i = 0; i <= preload[1]; i++) {
this.loadSlideByIndex(pswp.currIndex + (isForward ? i : -i));
} // preload[0] - num items to preload in backward direction
for (i = 1; i <= preload[0]; i++) {
this.loadSlideByIndex(pswp.currIndex + (isForward ? -i : i));
}
}
/**
* @param {number} initialIndex
*/
loadSlideByIndex(initialIndex) {
const index = this.pswp.getLoopedIndex(initialIndex); // try to get cached content
let content = this.getContentByIndex(index);
if (!content) {
// no cached content, so try to load from scratch:
content = lazyLoadSlide(index, this.pswp); // if content can be loaded, add it to cache:
if (content) {
this.addToCache(content);
}
}
}
/**
* @param {Slide} slide
* @returns {Content}
*/
getContentBySlide(slide) {
let content = this.getContentByIndex(slide.index);
if (!content) {
// create content if not found in cache
content = this.pswp.createContentFromData(slide.data, slide.index);
this.addToCache(content);
} // assign slide to content
content.setSlide(slide);
return content;
}
/**
* @param {Content} content
*/
addToCache(content) {
// move to the end of array
this.removeByIndex(content.index);
this._cachedItems.push(content);
if (this._cachedItems.length > this.limit) {
// Destroy the first content that's not attached
const indexToRemove = this._cachedItems.findIndex(item => {
return !item.isAttached && !item.hasSlide;
});
if (indexToRemove !== -1) {
const removedItem = this._cachedItems.splice(indexToRemove, 1)[0];
removedItem.destroy();
}
}
}
/**
* Removes an image from cache, does not destroy() it, just removes.
*
* @param {number} index
*/
removeByIndex(index) {
const indexToRemove = this._cachedItems.findIndex(item => item.index === index);
if (indexToRemove !== -1) {
this._cachedItems.splice(indexToRemove, 1);
}
}
/**
* @param {number} index
* @returns {Content | undefined}
*/
getContentByIndex(index) {
return this._cachedItems.find(content => content.index === index);
}
destroy() {
this._cachedItems.forEach(content => content.destroy());
this._cachedItems = [];
}
}
/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
/** @typedef {import("../slide/slide.js").SlideData} SlideData */
/**
* PhotoSwipe base class that can retrieve data about every slide.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
*/
class PhotoSwipeBase extends Eventable {
/**
* Get total number of slides
*
* @returns {number}
*/
getNumItems() {
var _this$options;
let numItems = 0;
const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
if (dataSource && 'length' in dataSource) {
// may be an array or just object with length property
numItems = dataSource.length;
} else if (dataSource && 'gallery' in dataSource) {
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
if (dataSource.items) {
numItems = dataSource.items.length;
}
} // legacy event, before filters were introduced
const event = this.dispatch('numItems', {
dataSource,
numItems
});
return this.applyFilters('numItems', event.numItems, dataSource);
}
/**
* @param {SlideData} slideData
* @param {number} index
* @returns {Content}
*/
createContentFromData(slideData, index) {
return new Content(slideData, this, index);
}
/**
* Get item data by index.
*
* "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
* For example, it may contain properties like
* `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
*
* @param {number} index
* @returns {SlideData}
*/
getItemData(index) {
var _this$options2;
const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
/** @type {SlideData | HTMLElement} */
let dataSourceItem = {};
if (Array.isArray(dataSource)) {
// Datasource is an array of elements
dataSourceItem = dataSource[index];
} else if (dataSource && 'gallery' in dataSource) {
// dataSource has gallery property,
// thus it was created by Lightbox, based on
// gallery and children options
// query DOM elements
if (!dataSource.items) {
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
}
dataSourceItem = dataSource.items[index];
}
let itemData = dataSourceItem;
if (itemData instanceof Element) {
itemData = this._domElementToItemData(itemData);
} // Dispatching the itemData event,
// it's a legacy verion before filters were introduced
const event = this.dispatch('itemData', {
itemData: itemData || {},
index
});
return this.applyFilters('itemData', event.itemData, index);
}
/**
* Get array of gallery DOM elements,
* based on childSelector and gallery element.
*
* @param {HTMLElement} galleryElement
* @returns {HTMLElement[]}
*/
_getGalleryDOMElements(galleryElement) {
var _this$options3, _this$options4;
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
}
return [galleryElement];
}
/**
* Converts DOM element to item data object.
*
* @param {HTMLElement} element DOM element
* @returns {SlideData}
*/
_domElementToItemData(element) {
/** @type {SlideData} */
const itemData = {
element
};
const linkEl =
/** @type {HTMLAnchorElement} */
element.tagName === 'A' ? element : element.querySelector('a');
if (linkEl) {
// src comes from data-pswp-src attribute,
// if it's empty link href is used
itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
if (linkEl.dataset.pswpSrcset) {
itemData.srcset = linkEl.dataset.pswpSrcset;
}
itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
itemData.w = itemData.width;
itemData.h = itemData.height;
if (linkEl.dataset.pswpType) {
itemData.type = linkEl.dataset.pswpType;
}
const thumbnailEl = element.querySelector('img');
if (thumbnailEl) {
var _thumbnailEl$getAttri;
// msrc is URL to placeholder image that's displayed before large image is loaded
// by default it's displayed only for the first slide
itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
}
if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
itemData.thumbCropped = true;
}
}
return this.applyFilters('domItemData', itemData, element, linkEl);
}
/**
* Lazy-load by slide data
*
* @param {SlideData} itemData Data about the slide
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
lazyLoadData(itemData, index) {
return lazyLoadData(itemData, this, index);
}
}
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/** @typedef {import('./slide/get-thumb-bounds.js').Bounds} Bounds */
/** @typedef {import('./util/animations.js').AnimationProps} AnimationProps */
// some browsers do not paint
// elements which opacity is set to 0,
// since we need to pre-render elements for the animation -
// we set it to the minimum amount
const MIN_OPACITY = 0.003;
/**
* Manages opening and closing transitions of the PhotoSwipe.
*
* It can perform zoom, fade or no transition.
*/
class Opener {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp) {
this.pswp = pswp;
this.isClosed = true;
this.isOpen = false;
this.isClosing = false;
this.isOpening = false;
/**
* @private
* @type {number | false | undefined}
*/
this._duration = undefined;
/** @private */
this._useAnimation = false;
/** @private */
this._croppedZoom = false;
/** @private */
this._animateRootOpacity = false;
/** @private */
this._animateBgOpacity = false;
/**
* @private
* @type { HTMLDivElement | HTMLImageElement | null | undefined }
*/
this._placeholder = undefined;
/**
* @private
* @type { HTMLDivElement | undefined }
*/
this._opacityElement = undefined;
/**
* @private
* @type { HTMLDivElement | undefined }
*/
this._cropContainer1 = undefined;
/**
* @private
* @type { HTMLElement | null | undefined }
*/
this._cropContainer2 = undefined;
/**
* @private
* @type {Bounds | undefined}
*/
this._thumbBounds = undefined;
this._prepareOpen = this._prepareOpen.bind(this); // Override initial zoom and pan position
pswp.on('firstZoomPan', this._prepareOpen);
}
open() {
this._prepareOpen();
this._start();
}
close() {
if (this.isClosed || this.isClosing || this.isOpening) {
// if we close during opening animation
// for now do nothing,
// browsers aren't good at changing the direction of the CSS transition
return;
}
const slide = this.pswp.currSlide;
this.isOpen = false;
this.isOpening = false;
this.isClosing = true;
this._duration = this.pswp.options.hideAnimationDuration;
if (slide && slide.currZoomLevel * slide.width >= this.pswp.options.maxWidthToAnimate) {
this._duration = 0;
}
this._applyStartProps();
setTimeout(() => {
this._start();
}, this._croppedZoom ? 30 : 0);
}
/** @private */
_prepareOpen() {
this.pswp.off('firstZoomPan', this._prepareOpen);
if (!this.isOpening) {
const slide = this.pswp.currSlide;
this.isOpening = true;
this.isClosing = false;
this._duration = this.pswp.options.showAnimationDuration;
if (slide && slide.zoomLevels.initial * slide.width >= this.pswp.options.maxWidthToAnimate) {
this._duration = 0;
}
this._applyStartProps();
}
}
/** @private */
_applyStartProps() {
const {
pswp
} = this;
const slide = this.pswp.currSlide;
const {
options
} = pswp;
if (options.showHideAnimationType === 'fade') {
options.showHideOpacity = true;
this._thumbBounds = undefined;
} else if (options.showHideAnimationType === 'none') {
options.showHideOpacity = false;
this._duration = 0;
this._thumbBounds = undefined;
} else if (this.isOpening && pswp._initialThumbBounds) {
// Use initial bounds if defined
this._thumbBounds = pswp._initialThumbBounds;
} else {
this._thumbBounds = this.pswp.getThumbBounds();
}
this._placeholder = slide === null || slide === void 0 ? void 0 : slide.getPlaceholderElement();
pswp.animations.stopAll(); // Discard animations when duration is less than 50ms
this._useAnimation = Boolean(this._duration && this._duration > 50);
this._animateZoom = Boolean(this._thumbBounds) && (slide === null || slide === void 0 ? void 0 : slide.content.usePlaceholder()) && (!this.isClosing || !pswp.mainScroll.isShifted());
if (!this._animateZoom) {
this._animateRootOpacity = true;
if (this.isOpening && slide) {
slide.zoomAndPanToInitial();
slide.applyCurrentZoomPan();
}
} else {
var _options$showHideOpac;
this._animateRootOpacity = (_options$showHideOpac = options.showHideOpacity) !== null && _options$showHideOpac !== void 0 ? _options$showHideOpac : false;
}
this._animateBgOpacity = !this._animateRootOpacity && this.pswp.options.bgOpacity > MIN_OPACITY;
this._opacityElement = this._animateRootOpacity ? pswp.element : pswp.bg;
if (!this._useAnimation) {
this._duration = 0;
this._animateZoom = false;
this._animateBgOpacity = false;
this._animateRootOpacity = true;
if (this.isOpening) {
if (pswp.element) {
pswp.element.style.opacity = String(MIN_OPACITY);
}
pswp.applyBgOpacity(1);
}
return;
}
if (this._animateZoom && this._thumbBounds && this._thumbBounds.innerRect) {
var _this$pswp$currSlide;
// Properties are used when animation from cropped thumbnail
this._croppedZoom = true;
this._cropContainer1 = this.pswp.container;
this._cropContainer2 = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.holderElement;
if (pswp.container) {
pswp.container.style.overflow = 'hidden';
pswp.container.style.width = pswp.viewportSize.x + 'px';
}
} else {
this._croppedZoom = false;
}
if (this.isOpening) {
// Apply styles before opening transition
if (this._animateRootOpacity) {
if (pswp.element) {
pswp.element.style.opacity = String(MIN_OPACITY);
}
pswp.applyBgOpacity(1);
} else {
if (this._animateBgOpacity && pswp.bg) {
pswp.bg.style.opacity = String(MIN_OPACITY);
}
if (pswp.element) {
pswp.element.style.opacity = '1';
}
}
if (this._animateZoom) {
this._setClosedStateZoomPan();
if (this._placeholder) {
// tell browser that we plan to animate the placeholder
this._placeholder.style.willChange = 'transform'; // hide placeholder to allow hiding of
// elements that overlap it (such as icons over the thumbnail)
this._placeholder.style.opacity = String(MIN_OPACITY);
}
}
} else if (this.isClosing) {
// hide nearby slides to make sure that
// they are not painted during the transition
if (pswp.mainScroll.itemHolders[0]) {
pswp.mainScroll.itemHolders[0].el.style.display = 'none';
}
if (pswp.mainScroll.itemHolders[2]) {
pswp.mainScroll.itemHolders[2].el.style.display = 'none';
}
if (this._croppedZoom) {
if (pswp.mainScroll.x !== 0) {
// shift the main scroller to zero position
pswp.mainScroll.resetPosition();
pswp.mainScroll.resize();
}
}
}
}
/** @private */
_start() {
if (this.isOpening && this._useAnimation && this._placeholder && this._placeholder.tagName === 'IMG') {
// To ensure smooth animation
// we wait till the current slide image placeholder is decoded,
// but no longer than 250ms,
// and no shorter than 50ms
// (just using requestanimationframe is not enough in Firefox,
// for some reason)
new Promise(resolve => {
let decoded = false;
let isDelaying = true;
decodeImage(
/** @type {HTMLImageElement} */
this._placeholder).finally(() => {
decoded = true;
if (!isDelaying) {
resolve(true);
}
});
setTimeout(() => {
isDelaying = false;
if (decoded) {
resolve(true);
}
}, 50);
setTimeout(resolve, 250);
}).finally(() => this._initiate());
} else {
this._initiate();
}
}
/** @private */
_initiate() {
var _this$pswp$element, _this$pswp$element2;
(_this$pswp$element = this.pswp.element) === null || _this$pswp$element === void 0 || _this$pswp$element.style.setProperty('--pswp-transition-duration', this._duration + 'ms');
this.pswp.dispatch(this.isOpening ? 'openingAnimationStart' : 'closingAnimationStart'); // legacy event
this.pswp.dispatch(
/** @type {'initialZoomIn' | 'initialZoomOut'} */
'initialZoom' + (this.isOpening ? 'In' : 'Out'));
(_this$pswp$element2 = this.pswp.element) === null || _this$pswp$element2 === void 0 || _this$pswp$element2.classList.toggle('pswp--ui-visible', this.isOpening);
if (this.isOpening) {
if (this._placeholder) {
// unhide the placeholder
this._placeholder.style.opacity = '1';
}
this._animateToOpenState();
} else if (this.isClosing) {
this._animateToClosedState();
}
if (!this._useAnimation) {
this._onAnimationComplete();
}
}
/** @private */
_onAnimationComplete() {
const {
pswp
} = this;
this.isOpen = this.isOpening;
this.isClosed = this.isClosing;
this.isOpening = false;
this.isClosing = false;
pswp.dispatch(this.isOpen ? 'openingAnimationEnd' : 'closingAnimationEnd'); // legacy event
pswp.dispatch(
/** @type {'initialZoomInEnd' | 'initialZoomOutEnd'} */
'initialZoom' + (this.isOpen ? 'InEnd' : 'OutEnd'));
if (this.isClosed) {
pswp.destroy();
} else if (this.isOpen) {
var _pswp$currSlide;
if (this._animateZoom && pswp.container) {
pswp.container.style.overflow = 'visible';
pswp.container.style.width = '100%';
}
(_pswp$currSlide = pswp.currSlide) === null || _pswp$currSlide === void 0 || _pswp$currSlide.applyCurrentZoomPan();
}
}
/** @private */
_animateToOpenState() {
const {
pswp
} = this;
if (this._animateZoom) {
if (this._croppedZoom && this._cropContainer1 && this._cropContainer2) {
this._animateTo(this._cropContainer1, 'transform', 'translate3d(0,0,0)');
this._animateTo(this._cropContainer2, 'transform', 'none');
}
if (pswp.currSlide) {
pswp.currSlide.zoomAndPanToInitial();
this._animateTo(pswp.currSlide.container, 'transform', pswp.currSlide.getCurrentTransform());
}
}
if (this._animateBgOpacity && pswp.bg) {
this._animateTo(pswp.bg, 'opacity', String(pswp.options.bgOpacity));
}
if (this._animateRootOpacity && pswp.element) {
this._animateTo(pswp.element, 'opacity', '1');
}
}
/** @private */
_animateToClosedState() {
const {
pswp
} = this;
if (this._animateZoom) {
this._setClosedStateZoomPan(true);
} // do not animate opacity if it's already at 0
if (this._animateBgOpacity && pswp.bgOpacity > 0.01 && pswp.bg) {
this._animateTo(pswp.bg, 'opacity', '0');
}
if (this._animateRootOpacity && pswp.element) {
this._animateTo(pswp.element, 'opacity', '0');
}
}
/**
* @private
* @param {boolean} [animate]
*/
_setClosedStateZoomPan(animate) {
if (!this._thumbBounds) return;
const {
pswp
} = this;
const {
innerRect
} = this._thumbBounds;
const {
currSlide,
viewportSize
} = pswp;
if (this._croppedZoom && innerRect && this._cropContainer1 && this._cropContainer2) {
const containerOnePanX = -viewportSize.x + (this._thumbBounds.x - innerRect.x) + innerRect.w;
const containerOnePanY = -viewportSize.y + (this._thumbBounds.y - innerRect.y) + innerRect.h;
const containerTwoPanX = viewportSize.x - innerRect.w;
const containerTwoPanY = viewportSize.y - innerRect.h;
if (animate) {
this._animateTo(this._cropContainer1, 'transform', toTransformString(containerOnePanX, containerOnePanY));
this._animateTo(this._cropContainer2, 'transform', toTransformString(containerTwoPanX, containerTwoPanY));
} else {
setTransform(this._cropContainer1, containerOnePanX, containerOnePanY);
setTransform(this._cropContainer2, containerTwoPanX, containerTwoPanY);
}
}
if (currSlide) {
equalizePoints(currSlide.pan, innerRect || this._thumbBounds);
currSlide.currZoomLevel = this._thumbBounds.w / currSlide.width;
if (animate) {
this._animateTo(currSlide.container, 'transform', currSlide.getCurrentTransform());
} else {
currSlide.applyCurrentZoomPan();
}
}
}
/**
* @private
* @param {HTMLElement} target
* @param {'transform' | 'opacity'} prop
* @param {string} propValue
*/
_animateTo(target, prop, propValue) {
if (!this._duration) {
target.style[prop] = propValue;
return;
}
const {
animations
} = this.pswp;
/** @type {AnimationProps} */
const animProps = {
duration: this._duration,
easing: this.pswp.options.easing,
onComplete: () => {
if (!animations.activeAnimations.length) {
this._onAnimationComplete();
}
},
target
};
animProps[prop] = propValue;
animations.startTransition(animProps);
}
}
/**
* @template T
* @typedef {import('./types.js').Type} Type
*/
/** @typedef {import('./slide/slide.js').SlideData} SlideData */
/** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */
/** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */
/** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
/** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
/** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('./core/eventable.js').EventCallback} EventCallback
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('./core/eventable.js').AugmentedEvent} AugmentedEvent
*/
/** @typedef {{ x: number; y: number; id?: string | number }} Point */
/** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */
/** @typedef {SlideData[]} DataSourceArray */
/** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */
/** @typedef {DataSourceArray | DataSourceObject} DataSource */
/** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */
/** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */
/** @typedef {Type | { default: Type }} PhotoSwipeModule */
/** @typedef {PhotoSwipeModule | Promise | (() => Promise)} PhotoSwipeModuleOption */
/**
* @typedef {string | NodeListOf | HTMLElement[] | HTMLElement} ElementProvider
*/
/** @typedef {Partial} PhotoSwipeOptions https://photoswipe.com/options/ */
/**
* @typedef {Object} PreparedPhotoSwipeOptions
*
* @prop {DataSource} [dataSource]
* Pass an array of any items via dataSource option. Its length will determine amount of slides
* (which may be modified further from numItems event).
*
* Each item should contain data that you need to generate slide
* (for image slide it would be src (image URL), width (image width), height, srcset, alt).
*
* If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter.
*
* @prop {number} bgOpacity
* Background backdrop opacity, always define it via this option and not via CSS rgba color.
*
* @prop {number} spacing
* Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport).
*
* @prop {boolean} allowPanToNext
* Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events.
*
* @prop {boolean} loop
* If set to true you'll be able to swipe from the last to the first image.
* Option is always false when there are less than 3 slides.
*
* @prop {boolean} [wheelToZoom]
* By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel.
*
* @prop {boolean} pinchToClose
* Pinch touch gesture to close the gallery.
*
* @prop {boolean} closeOnVerticalDrag
* Vertical drag gesture to close the PhotoSwipe.
*
* @prop {Padding} [padding]
* Slide area padding (in pixels).
*
* @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn]
* The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example:
*
* @prop {number | false} hideAnimationDuration
* Transition duration in milliseconds, can be 0.
*
* @prop {number | false} showAnimationDuration
* Transition duration in milliseconds, can be 0.
*
* @prop {number | false} zoomAnimationDuration
* Transition duration in milliseconds, can be 0.
*
* @prop {string} easing
* String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions.
*
* @prop {boolean} escKey
* Esc key to close.
*
* @prop {boolean} arrowKeys
* Left/right arrow keys for navigation.
*
* @prop {boolean} trapFocus
* Trap focus within PhotoSwipe element while it's open.
*
* @prop {boolean} returnFocus
* Restore focus the last active element after PhotoSwipe is closed.
*
* @prop {boolean} clickToCloseNonZoomable
* If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it.
*
* @prop {ActionType | ActionFn | false} imageClickAction
* Refer to click and tap actions page.
*
* @prop {ActionType | ActionFn | false} bgClickAction
* Refer to click and tap actions page.
*
* @prop {ActionType | ActionFn | false} tapAction
* Refer to click and tap actions page.
*
* @prop {ActionType | ActionFn | false} doubleTapAction
* Refer to click and tap actions page.
*
* @prop {number} preloaderDelay
* Delay before the loading indicator will be displayed,
* if image is loaded during it - the indicator will not be displayed at all. Can be zero.
*
* @prop {string} indexIndicatorSep
* Used for slide count indicator ("1 of 10 ").
*
* @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn]
* A function that should return slide viewport width and height, in format {x: 100, y: 100}.
*
* @prop {string} errorMsg
* Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter.
*
* @prop {[number, number]} preload
* Lazy loading of nearby slides based on direction of movement. Should be an array with two integers,
* first one - number of items to preload before the current image, second one - after the current image.
* Two nearby images are always loaded.
*
* @prop {string} [mainClass]
* Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space.
* Example on Styling page.
*
* @prop {HTMLElement} [appendToEl]
* Element to which PhotoSwipe dialog will be appended when it opens.
*
* @prop {number} maxWidthToAnimate
* Maximum width of image to animate, if initial rendered image width
* is larger than this value - the opening/closing transition will be automatically disabled.
*
* @prop {string} [closeTitle]
* Translating
*
* @prop {string} [zoomTitle]
* Translating
*
* @prop {string} [arrowPrevTitle]
* Translating
*
* @prop {string} [arrowNextTitle]
* Translating
*
* @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType]
* To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`).
* It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`.
*
* Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`.
*
* @prop {number} index
* Defines start slide index.
*
* @prop {(e: MouseEvent) => number} [getClickedIndexFn]
*
* @prop {boolean} [arrowPrev]
* @prop {boolean} [arrowNext]
* @prop {boolean} [zoom]
* @prop {boolean} [close]
* @prop {boolean} [counter]
*
* @prop {string} [arrowPrevSVG]
* @prop {string} [arrowNextSVG]
* @prop {string} [zoomSVG]
* @prop {string} [closeSVG]
* @prop {string} [counterSVG]
*
* @prop {string} [arrowPrevTitle]
* @prop {string} [arrowNextTitle]
* @prop {string} [zoomTitle]
* @prop {string} [closeTitle]
* @prop {string} [counterTitle]
*
* @prop {ZoomLevelOption} [initialZoomLevel]
* @prop {ZoomLevelOption} [secondaryZoomLevel]
* @prop {ZoomLevelOption} [maxZoomLevel]
*
* @prop {boolean} [mouseMovePan]
* @prop {Point | null} [initialPointerPos]
* @prop {boolean} [showHideOpacity]
*
* @prop {PhotoSwipeModuleOption} [pswpModule]
* @prop {() => Promise} [openPromise]
* @prop {boolean} [preloadFirstSlide]
* @prop {ElementProvider} [gallery]
* @prop {string} [gallerySelector]
* @prop {ElementProvider} [children]
* @prop {string} [childSelector]
* @prop {string | false} [thumbSelector]
*/
/** @type {PreparedPhotoSwipeOptions} */
const defaultOptions = {
allowPanToNext: true,
spacing: 0.1,
loop: true,
pinchToClose: true,
closeOnVerticalDrag: true,
hideAnimationDuration: 333,
showAnimationDuration: 333,
zoomAnimationDuration: 333,
escKey: true,
arrowKeys: true,
trapFocus: true,
returnFocus: true,
maxWidthToAnimate: 4000,
clickToCloseNonZoomable: true,
imageClickAction: 'zoom-or-close',
bgClickAction: 'close',
tapAction: 'toggle-controls',
doubleTapAction: 'zoom',
indexIndicatorSep: ' / ',
preloaderDelay: 2000,
bgOpacity: 0.8,
index: 0,
errorMsg: 'The image cannot be loaded',
preload: [1, 2],
easing: 'cubic-bezier(.4,0,.22,1)'
};
/**
* PhotoSwipe Core
*/
class PhotoSwipe extends PhotoSwipeBase {
/**
* @param {PhotoSwipeOptions} [options]
*/
constructor(options) {
super();
this.options = this._prepareOptions(options || {});
/**
* offset of viewport relative to document
*
* @type {Point}
*/
this.offset = {
x: 0,
y: 0
};
/**
* @type {Point}
* @private
*/
this._prevViewportSize = {
x: 0,
y: 0
};
/**
* Size of scrollable PhotoSwipe viewport
*
* @type {Point}
*/
this.viewportSize = {
x: 0,
y: 0
};
/**
* background (backdrop) opacity
*/
this.bgOpacity = 1;
this.currIndex = 0;
this.potentialIndex = 0;
this.isOpen = false;
this.isDestroying = false;
this.hasMouse = false;
/**
* @private
* @type {SlideData}
*/
this._initialItemData = {};
/** @type {Bounds | undefined} */
this._initialThumbBounds = undefined;
/** @type {HTMLDivElement | undefined} */
this.topBar = undefined;
/** @type {HTMLDivElement | undefined} */
this.element = undefined;
/** @type {HTMLDivElement | undefined} */
this.template = undefined;
/** @type {HTMLDivElement | undefined} */
this.container = undefined;
/** @type {HTMLElement | undefined} */
this.scrollWrap = undefined;
/** @type {Slide | undefined} */
this.currSlide = undefined;
this.events = new DOMEvents();
this.animations = new Animations();
this.mainScroll = new MainScroll(this);
this.gestures = new Gestures(this);
this.opener = new Opener(this);
this.keyboard = new Keyboard(this);
this.contentLoader = new ContentLoader(this);
}
/** @returns {boolean} */
init() {
if (this.isOpen || this.isDestroying) {
return false;
}
this.isOpen = true;
this.dispatch('init'); // legacy
this.dispatch('beforeOpen');
this._createMainStructure(); // add classes to the root element of PhotoSwipe
let rootClasses = 'pswp--open';
if (this.gestures.supportsTouch) {
rootClasses += ' pswp--touch';
}
if (this.options.mainClass) {
rootClasses += ' ' + this.options.mainClass;
}
if (this.element) {
this.element.className += ' ' + rootClasses;
}
this.currIndex = this.options.index || 0;
this.potentialIndex = this.currIndex;
this.dispatch('firstUpdate'); // starting index can be modified here
// initialize scroll wheel handler to block the scroll
this.scrollWheel = new ScrollWheel(this); // sanitize index
if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) {
this.currIndex = 0;
}
if (!this.gestures.supportsTouch) {
// enable mouse features if no touch support detected
this.mouseDetected();
} // causes forced synchronous layout
this.updateSize();
this.offset.y = window.pageYOffset;
this._initialItemData = this.getItemData(this.currIndex);
this.dispatch('gettingData', {
index: this.currIndex,
data: this._initialItemData,
slide: undefined
}); // *Layout* - calculate size and position of elements here
this._initialThumbBounds = this.getThumbBounds();
this.dispatch('initialLayout');
this.on('openingAnimationEnd', () => {
const {
itemHolders
} = this.mainScroll; // Add content to the previous and next slide
if (itemHolders[0]) {
itemHolders[0].el.style.display = 'block';
this.setContent(itemHolders[0], this.currIndex - 1);
}
if (itemHolders[2]) {
itemHolders[2].el.style.display = 'block';
this.setContent(itemHolders[2], this.currIndex + 1);
}
this.appendHeavy();
this.contentLoader.updateLazy();
this.events.add(window, 'resize', this._handlePageResize.bind(this));
this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this));
this.dispatch('bindEvents');
}); // set content for center slide (first time)
if (this.mainScroll.itemHolders[1]) {
this.setContent(this.mainScroll.itemHolders[1], this.currIndex);
}
this.dispatch('change');
this.opener.open();
this.dispatch('afterInit');
return true;
}
/**
* Get looped slide index
* (for example, -1 will return the last slide)
*
* @param {number} index
* @returns {number}
*/
getLoopedIndex(index) {
const numSlides = this.getNumItems();
if (this.options.loop) {
if (index > numSlides - 1) {
index -= numSlides;
}
if (index < 0) {
index += numSlides;
}
}
return clamp(index, 0, numSlides - 1);
}
appendHeavy() {
this.mainScroll.itemHolders.forEach(itemHolder => {
var _itemHolder$slide;
(_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.appendHeavy();
});
}
/**
* Change the slide
* @param {number} index New index
*/
goTo(index) {
this.mainScroll.moveIndexBy(this.getLoopedIndex(index) - this.potentialIndex);
}
/**
* Go to the next slide.
*/
next() {
this.goTo(this.potentialIndex + 1);
}
/**
* Go to the previous slide.
*/
prev() {
this.goTo(this.potentialIndex - 1);
}
/**
* @see slide/slide.js zoomTo
*
* @param {Parameters} args
*/
zoomTo(...args) {
var _this$currSlide;
(_this$currSlide = this.currSlide) === null || _this$currSlide === void 0 || _this$currSlide.zoomTo(...args);
}
/**
* @see slide/slide.js toggleZoom
*/
toggleZoom() {
var _this$currSlide2;
(_this$currSlide2 = this.currSlide) === null || _this$currSlide2 === void 0 || _this$currSlide2.toggleZoom();
}
/**
* Close the gallery.
* After closing transition ends - destroy it
*/
close() {
if (!this.opener.isOpen || this.isDestroying) {
return;
}
this.isDestroying = true;
this.dispatch('close');
this.events.removeAll();
this.opener.close();
}
/**
* Destroys the gallery:
* - instantly closes the gallery
* - unbinds events,
* - cleans intervals and timeouts
* - removes elements from DOM
*/
destroy() {
var _this$element;
if (!this.isDestroying) {
this.options.showHideAnimationType = 'none';
this.close();
return;
}
this.dispatch('destroy');
this._listeners = {};
if (this.scrollWrap) {
this.scrollWrap.ontouchmove = null;
this.scrollWrap.ontouchend = null;
}
(_this$element = this.element) === null || _this$element === void 0 || _this$element.remove();
this.mainScroll.itemHolders.forEach(itemHolder => {
var _itemHolder$slide2;
(_itemHolder$slide2 = itemHolder.slide) === null || _itemHolder$slide2 === void 0 || _itemHolder$slide2.destroy();
});
this.contentLoader.destroy();
this.events.removeAll();
}
/**
* Refresh/reload content of a slide by its index
*
* @param {number} slideIndex
*/
refreshSlideContent(slideIndex) {
this.contentLoader.removeByIndex(slideIndex);
this.mainScroll.itemHolders.forEach((itemHolder, i) => {
var _this$currSlide$index, _this$currSlide3;
let potentialHolderIndex = ((_this$currSlide$index = (_this$currSlide3 = this.currSlide) === null || _this$currSlide3 === void 0 ? void 0 : _this$currSlide3.index) !== null && _this$currSlide$index !== void 0 ? _this$currSlide$index : 0) - 1 + i;
if (this.canLoop()) {
potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex);
}
if (potentialHolderIndex === slideIndex) {
// set the new slide content
this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current
if (i === 1) {
var _itemHolder$slide3;
this.currSlide = itemHolder.slide;
(_itemHolder$slide3 = itemHolder.slide) === null || _itemHolder$slide3 === void 0 || _itemHolder$slide3.setIsActive(true);
}
}
});
this.dispatch('change');
}
/**
* Set slide content
*
* @param {ItemHolder} holder mainScroll.itemHolders array item
* @param {number} index Slide index
* @param {boolean} [force] If content should be set even if index wasn't changed
*/
setContent(holder, index, force) {
if (this.canLoop()) {
index = this.getLoopedIndex(index);
}
if (holder.slide) {
if (holder.slide.index === index && !force) {
// exit if holder already contains this slide
// this could be common when just three slides are used
return;
} // destroy previous slide
holder.slide.destroy();
holder.slide = undefined;
} // exit if no loop and index is out of bounds
if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) {
return;
}
const itemData = this.getItemData(index);
holder.slide = new Slide(itemData, index, this); // set current slide
if (index === this.currIndex) {
this.currSlide = holder.slide;
}
holder.slide.append(holder.el);
}
/** @returns {Point} */
getViewportCenterPoint() {
return {
x: this.viewportSize.x / 2,
y: this.viewportSize.y / 2
};
}
/**
* Update size of all elements.
* Executed on init and on page resize.
*
* @param {boolean} [force] Update size even if size of viewport was not changed.
*/
updateSize(force) {
// let item;
// let itemIndex;
if (this.isDestroying) {
// exit if PhotoSwipe is closed or closing
// (to avoid errors, as resize event might be delayed)
return;
} //const newWidth = this.scrollWrap.clientWidth;
//const newHeight = this.scrollWrap.clientHeight;
const newViewportSize = getViewportSize(this.options, this);
if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) {
// Exit if dimensions were not changed
return;
} //this._prevViewportSize.x = newWidth;
//this._prevViewportSize.y = newHeight;
equalizePoints(this._prevViewportSize, newViewportSize);
this.dispatch('beforeResize');
equalizePoints(this.viewportSize, this._prevViewportSize);
this._updatePageScrollOffset();
this.dispatch('viewportSize'); // Resize slides only after opener animation is finished
// and don't re-calculate size on inital size update
this.mainScroll.resize(this.opener.isOpen);
if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) {
this.mouseDetected();
}
this.dispatch('resize');
}
/**
* @param {number} opacity
*/
applyBgOpacity(opacity) {
this.bgOpacity = Math.max(opacity, 0);
if (this.bg) {
this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity);
}
}
/**
* Whether mouse is detected
*/
mouseDetected() {
if (!this.hasMouse) {
var _this$element2;
this.hasMouse = true;
(_this$element2 = this.element) === null || _this$element2 === void 0 || _this$element2.classList.add('pswp--has_mouse');
}
}
/**
* Page resize event handler
*
* @private
*/
_handlePageResize() {
this.updateSize(); // In iOS webview, if element size depends on document size,
// it'll be measured incorrectly in resize event
//
// https://bugs.webkit.org/show_bug.cgi?id=170595
// https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d
if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) {
setTimeout(() => {
this.updateSize();
}, 500);
}
}
/**
* Page scroll offset is used
* to get correct coordinates
* relative to PhotoSwipe viewport.
*
* @private
*/
_updatePageScrollOffset() {
this.setScrollOffset(0, window.pageYOffset);
}
/**
* @param {number} x
* @param {number} y
*/
setScrollOffset(x, y) {
this.offset.x = x;
this.offset.y = y;
this.dispatch('updateScrollOffset');
}
/**
* Create main HTML structure of PhotoSwipe,
* and add it to DOM
*
* @private
*/
_createMainStructure() {
// root DOM element of PhotoSwipe (.pswp)
this.element = createElement('pswp', 'div');
this.element.setAttribute('tabindex', '-1');
this.element.setAttribute('role', 'dialog'); // template is legacy prop
this.template = this.element; // Background is added as a separate element,
// as animating opacity is faster than animating rgba()
this.bg = createElement('pswp__bg', 'div', this.element);
this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element);
this.container = createElement('pswp__container', 'div', this.scrollWrap); // aria pattern: carousel
this.scrollWrap.setAttribute('aria-roledescription', 'carousel');
this.container.setAttribute('aria-live', 'off');
this.container.setAttribute('id', 'pswp__items');
this.mainScroll.appendHolders();
this.ui = new UI(this);
this.ui.init(); // append to DOM
(this.options.appendToEl || document.body).appendChild(this.element);
}
/**
* Get position and dimensions of small thumbnail
* {x:,y:,w:}
*
* Height is optional (calculated based on the large image)
*
* @returns {Bounds | undefined}
*/
getThumbBounds() {
return getThumbBounds(this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this);
}
/**
* If the PhotoSwipe can have continuous loop
* @returns Boolean
*/
canLoop() {
return this.options.loop && this.getNumItems() > 2;
}
/**
* @private
* @param {PhotoSwipeOptions} options
* @returns {PreparedPhotoSwipeOptions}
*/
_prepareOptions(options) {
if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) {
options.showHideAnimationType = 'none';
options.zoomAnimationDuration = 0;
}
/** @type {PreparedPhotoSwipeOptions} */
return { ...defaultOptions,
...options
};
}
}
export { PhotoSwipe as default };
//# sourceMappingURL=photoswipe.esm.js.map
================================================
FILE: dist/types/core/base.d.ts
================================================
export default PhotoSwipeBase;
export type PhotoSwipe = import("../photoswipe.js").default;
export type SlideData = import("../slide/slide.js").SlideData;
/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
/** @typedef {import("../slide/slide.js").SlideData} SlideData */
/**
* PhotoSwipe base class that can retrieve data about every slide.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
*/
declare class PhotoSwipeBase extends Eventable {
/**
* Get total number of slides
*
* @returns {number}
*/
getNumItems(): number;
/**
* @param {SlideData} slideData
* @param {number} index
* @returns {Content}
*/
createContentFromData(slideData: SlideData, index: number): Content;
/**
* Get item data by index.
*
* "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
* For example, it may contain properties like
* `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
*
* @param {number} index
* @returns {SlideData}
*/
getItemData(index: number): SlideData;
/**
* Get array of gallery DOM elements,
* based on childSelector and gallery element.
*
* @param {HTMLElement} galleryElement
* @returns {HTMLElement[]}
*/
_getGalleryDOMElements(galleryElement: HTMLElement): HTMLElement[];
/**
* Converts DOM element to item data object.
*
* @param {HTMLElement} element DOM element
* @returns {SlideData}
*/
_domElementToItemData(element: HTMLElement): SlideData;
/**
* Lazy-load by slide data
*
* @param {SlideData} itemData Data about the slide
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
lazyLoadData(itemData: SlideData, index: number): Content;
}
import Eventable from "./eventable.js";
import Content from "../slide/content.js";
================================================
FILE: dist/types/core/eventable.d.ts
================================================
export default Eventable;
export type PhotoSwipeLightbox = import('../lightbox/lightbox.js').default;
export type PhotoSwipe = import('../photoswipe.js').default;
export type PhotoSwipeOptions = import('../photoswipe.js').PhotoSwipeOptions;
export type DataSource = import('../photoswipe.js').DataSource;
export type UIElementData = import('../ui/ui-element.js').UIElementData;
export type ContentDefault = import('../slide/content.js').default;
export type Slide = import('../slide/slide.js').default;
export type SlideData = import('../slide/slide.js').SlideData;
export type ZoomLevel = import('../slide/zoom-level.js').default;
export type Bounds = import('../slide/get-thumb-bounds.js').Bounds;
/**
* Allow adding an arbitrary props to the Content
* https://photoswipe.com/custom-content/#using-webp-image-format
*/
export type Content = ContentDefault & Record;
export type Point = {
x?: number;
y?: number;
};
/**
* https://photoswipe.com/events/
*
*
* https://photoswipe.com/adding-ui-elements/
*/
export type PhotoSwipeEventsMap = {
uiRegister: undefined;
/**
* https://photoswipe.com/events/#initialization-events
*/
uiElementCreate: {
data: UIElementData;
};
beforeOpen: undefined;
firstUpdate: undefined;
initialLayout: undefined;
change: undefined;
afterInit: undefined;
/**
* https://photoswipe.com/events/#opening-or-closing-transition-events
*/
bindEvents: undefined;
openingAnimationStart: undefined;
openingAnimationEnd: undefined;
closingAnimationStart: undefined;
/**
* https://photoswipe.com/events/#closing-events
*/
closingAnimationEnd: undefined;
close: undefined;
/**
* https://photoswipe.com/events/#pointer-and-gesture-events
*/
destroy: undefined;
pointerDown: {
originalEvent: PointerEvent;
};
pointerMove: {
originalEvent: PointerEvent;
};
pointerUp: {
originalEvent: PointerEvent;
};
/**
* can be default prevented
*/
pinchClose: {
bgOpacity: number;
};
/**
* can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*/
verticalDrag: {
panY: number;
};
contentInit: {
content: Content;
};
/**
* can be default prevented
*/
contentLoad: {
content: Content;
isLazy: boolean;
};
/**
* can be default prevented
*/
contentLoadImage: {
content: Content;
isLazy: boolean;
};
loadComplete: {
content: Content;
slide: Slide;
isError?: boolean;
};
loadError: {
content: Content;
slide: Slide;
};
/**
* can be default prevented
*/
contentResize: {
content: Content;
width: number;
height: number;
};
imageSizeChange: {
content: Content;
width: number;
height: number;
slide: Slide;
};
/**
* can be default prevented
*/
contentLazyLoad: {
content: Content;
};
/**
* can be default prevented
*/
contentAppend: {
content: Content;
};
/**
* can be default prevented
*/
contentActivate: {
content: Content;
};
/**
* can be default prevented
*/
contentDeactivate: {
content: Content;
};
/**
* can be default prevented
*/
contentRemove: {
content: Content;
};
/**
* can be default prevented
*
*
* undocumented
*/
contentDestroy: {
content: Content;
};
/**
* can be default prevented
*/
imageClickAction: {
point: Point;
originalEvent: PointerEvent;
};
/**
* can be default prevented
*/
bgClickAction: {
point: Point;
originalEvent: PointerEvent;
};
/**
* can be default prevented
*/
tapAction: {
point: Point;
originalEvent: PointerEvent;
};
/**
* can be default prevented
*/
doubleTapAction: {
point: Point;
originalEvent: PointerEvent;
};
/**
* can be default prevented
*/
keydown: {
originalEvent: KeyboardEvent;
};
moveMainScroll: {
x: number;
dragging: boolean;
};
firstZoomPan: {
slide: Slide;
};
gettingData: {
slide: Slide | undefined;
data: SlideData;
index: number;
};
beforeResize: undefined;
resize: undefined;
viewportSize: undefined;
updateScrollOffset: undefined;
slideInit: {
slide: Slide;
};
afterSetContent: {
slide: Slide;
};
slideLoad: {
slide: Slide;
};
/**
* can be default prevented
*/
appendHeavy: {
slide: Slide;
};
appendHeavyContent: {
slide: Slide;
};
slideActivate: {
slide: Slide;
};
slideDeactivate: {
slide: Slide;
};
slideDestroy: {
slide: Slide;
};
beforeZoomTo: {
destZoomLevel: number;
centerPoint: Point | undefined;
transitionDuration: number | false | undefined;
};
zoomPanUpdate: {
slide: Slide;
};
initialZoomPan: {
slide: Slide;
};
calcSlideSize: {
slide: Slide;
};
resolutionChanged: undefined;
/**
* can be default prevented
*/
wheel: {
originalEvent: WheelEvent;
};
/**
* can be default prevented
*/
contentAppendImage: {
content: Content;
};
/**
* can be default prevented
*/
lazyLoadSlide: {
index: number;
itemData: SlideData;
};
lazyLoad: undefined;
calcBounds: {
slide: Slide;
};
/**
* legacy
*/
zoomLevelsUpdate: {
zoomLevels: ZoomLevel;
slideData: SlideData;
};
init: undefined;
initialZoomIn: undefined;
initialZoomOut: undefined;
initialZoomInEnd: undefined;
initialZoomOutEnd: undefined;
numItems: {
dataSource: DataSource | undefined;
numItems: number;
};
itemData: {
itemData: SlideData;
index: number;
};
thumbBounds: {
index: number;
itemData: SlideData;
instance: PhotoSwipe;
};
};
/**
* https://photoswipe.com/filters/
*/
export type PhotoSwipeFiltersMap = {
/**
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*/
numItems: (numItems: number, dataSource: DataSource | undefined) => number;
/**
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*/
itemData: (itemData: SlideData, index: number) => SlideData;
/**
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*/
domItemData: (itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData;
/**
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*/
clickedIndex: (clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number;
/**
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*/
placeholderSrc: (placeholderSrc: string | false, content: Content) => string | false;
/**
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*/
isContentLoading: (isContentLoading: boolean, content: Content) => boolean;
/**
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*/
isContentZoomable: (isContentZoomable: boolean, content: Content) => boolean;
/**
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*/
useContentPlaceholder: (useContentPlaceholder: boolean, content: Content) => boolean;
/**
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*/
isKeepingPlaceholder: (isKeepingPlaceholder: boolean, content: Content) => boolean;
/**
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*/
contentErrorElement: (contentErrorElement: HTMLElement, content: Content) => HTMLElement;
/**
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*/
uiElement: (element: HTMLElement, data: UIElementData) => HTMLElement;
/**
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*/
thumbEl: (thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement;
/**
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*/
thumbBounds: (thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds;
srcsetSizesWidth: (srcsetSizesWidth: number, content: Content) => number;
preventPointerEvent: (preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean;
};
export type Filter = {
fn: PhotoSwipeFiltersMap[T];
priority: number;
};
export type AugmentedEvent = PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent : PhotoSwipeEvent & PhotoSwipeEventsMap[T];
export type EventCallback = (event: AugmentedEvent) => void;
/**
* PhotoSwipe base class that can listen and dispatch for events.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
*/
declare class Eventable {
/**
* @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent) => void)[] }}
*/
_listeners: {
uiRegister?: ((event: PhotoSwipeEvent<"uiRegister">) => void)[] | undefined;
/**
* https://photoswipe.com/events/#initialization-events
*/
uiElementCreate?: ((event: PhotoSwipeEvent<"uiElementCreate"> & {
data: import("../ui/ui-element.js").UIElementData;
}) => void)[] | undefined;
beforeOpen?: ((event: PhotoSwipeEvent<"beforeOpen">) => void)[] | undefined;
firstUpdate?: ((event: PhotoSwipeEvent<"firstUpdate">) => void)[] | undefined;
initialLayout?: ((event: PhotoSwipeEvent<"initialLayout">) => void)[] | undefined;
change?: ((event: PhotoSwipeEvent<"change">) => void)[] | undefined;
afterInit?: ((event: PhotoSwipeEvent<"afterInit">) => void)[] | undefined;
/**
* https://photoswipe.com/events/#opening-or-closing-transition-events
*/
bindEvents?: ((event: PhotoSwipeEvent<"bindEvents">) => void)[] | undefined;
openingAnimationStart?: ((event: PhotoSwipeEvent<"openingAnimationStart">) => void)[] | undefined;
openingAnimationEnd?: ((event: PhotoSwipeEvent<"openingAnimationEnd">) => void)[] | undefined;
closingAnimationStart?: ((event: PhotoSwipeEvent<"closingAnimationStart">) => void)[] | undefined;
/**
* https://photoswipe.com/events/#closing-events
*/
closingAnimationEnd?: ((event: PhotoSwipeEvent<"closingAnimationEnd">) => void)[] | undefined;
close?: ((event: PhotoSwipeEvent<"close">) => void)[] | undefined;
/**
* https://photoswipe.com/events/#pointer-and-gesture-events
*/
destroy?: ((event: PhotoSwipeEvent<"destroy">) => void)[] | undefined;
pointerDown?: ((event: PhotoSwipeEvent<"pointerDown"> & {
originalEvent: PointerEvent;
}) => void)[] | undefined;
pointerMove?: ((event: PhotoSwipeEvent<"pointerMove"> & {
originalEvent: PointerEvent;
}) => void)[] | undefined;
pointerUp?: ((event: PhotoSwipeEvent<"pointerUp"> & {
originalEvent: PointerEvent;
}) => void)[] | undefined;
/**
* can be default prevented
*/
pinchClose?: ((event: PhotoSwipeEvent<"pinchClose"> & {
bgOpacity: number;
}) => void)[] | undefined;
/**
* can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*/
verticalDrag?: ((event: PhotoSwipeEvent<"verticalDrag"> & {
panY: number;
}) => void)[] | undefined;
contentInit?: ((event: PhotoSwipeEvent<"contentInit"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentLoad?: ((event: PhotoSwipeEvent<"contentLoad"> & {
content: Content;
isLazy: boolean;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentLoadImage?: ((event: PhotoSwipeEvent<"contentLoadImage"> & {
content: Content;
isLazy: boolean;
}) => void)[] | undefined;
loadComplete?: ((event: PhotoSwipeEvent<"loadComplete"> & {
content: Content;
slide: import("../slide/slide.js").default;
isError?: boolean | undefined;
}) => void)[] | undefined;
loadError?: ((event: PhotoSwipeEvent<"loadError"> & {
content: Content;
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentResize?: ((event: PhotoSwipeEvent<"contentResize"> & {
content: Content;
width: number;
height: number;
}) => void)[] | undefined;
imageSizeChange?: ((event: PhotoSwipeEvent<"imageSizeChange"> & {
content: Content;
width: number;
height: number;
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentLazyLoad?: ((event: PhotoSwipeEvent<"contentLazyLoad"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentAppend?: ((event: PhotoSwipeEvent<"contentAppend"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentActivate?: ((event: PhotoSwipeEvent<"contentActivate"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentDeactivate?: ((event: PhotoSwipeEvent<"contentDeactivate"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentRemove?: ((event: PhotoSwipeEvent<"contentRemove"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*
*
* undocumented
*/
contentDestroy?: ((event: PhotoSwipeEvent<"contentDestroy"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
imageClickAction?: ((event: PhotoSwipeEvent<"imageClickAction"> & {
point: Point;
originalEvent: PointerEvent;
}) => void)[] | undefined;
/**
* can be default prevented
*/
bgClickAction?: ((event: PhotoSwipeEvent<"bgClickAction"> & {
point: Point;
originalEvent: PointerEvent;
}) => void)[] | undefined;
/**
* can be default prevented
*/
tapAction?: ((event: PhotoSwipeEvent<"tapAction"> & {
point: Point;
originalEvent: PointerEvent;
}) => void)[] | undefined;
/**
* can be default prevented
*/
doubleTapAction?: ((event: PhotoSwipeEvent<"doubleTapAction"> & {
point: Point;
originalEvent: PointerEvent;
}) => void)[] | undefined;
/**
* can be default prevented
*/
keydown?: ((event: PhotoSwipeEvent<"keydown"> & {
originalEvent: KeyboardEvent;
}) => void)[] | undefined;
moveMainScroll?: ((event: PhotoSwipeEvent<"moveMainScroll"> & {
x: number;
dragging: boolean;
}) => void)[] | undefined;
firstZoomPan?: ((event: PhotoSwipeEvent<"firstZoomPan"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
gettingData?: ((event: PhotoSwipeEvent<"gettingData"> & {
slide: import("../slide/slide.js").default | undefined;
data: import("../slide/slide.js").SlideData;
index: number;
}) => void)[] | undefined;
beforeResize?: ((event: PhotoSwipeEvent<"beforeResize">) => void)[] | undefined;
resize?: ((event: PhotoSwipeEvent<"resize">) => void)[] | undefined;
viewportSize?: ((event: PhotoSwipeEvent<"viewportSize">) => void)[] | undefined;
updateScrollOffset?: ((event: PhotoSwipeEvent<"updateScrollOffset">) => void)[] | undefined;
slideInit?: ((event: PhotoSwipeEvent<"slideInit"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
afterSetContent?: ((event: PhotoSwipeEvent<"afterSetContent"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
slideLoad?: ((event: PhotoSwipeEvent<"slideLoad"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
/**
* can be default prevented
*/
appendHeavy?: ((event: PhotoSwipeEvent<"appendHeavy"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
appendHeavyContent?: ((event: PhotoSwipeEvent<"appendHeavyContent"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
slideActivate?: ((event: PhotoSwipeEvent<"slideActivate"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
slideDeactivate?: ((event: PhotoSwipeEvent<"slideDeactivate"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
slideDestroy?: ((event: PhotoSwipeEvent<"slideDestroy"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
beforeZoomTo?: ((event: PhotoSwipeEvent<"beforeZoomTo"> & {
destZoomLevel: number;
centerPoint: Point | undefined;
transitionDuration: number | false | undefined;
}) => void)[] | undefined;
zoomPanUpdate?: ((event: PhotoSwipeEvent<"zoomPanUpdate"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
initialZoomPan?: ((event: PhotoSwipeEvent<"initialZoomPan"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
calcSlideSize?: ((event: PhotoSwipeEvent<"calcSlideSize"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
resolutionChanged?: ((event: PhotoSwipeEvent<"resolutionChanged">) => void)[] | undefined;
/**
* can be default prevented
*/
wheel?: ((event: PhotoSwipeEvent<"wheel"> & {
originalEvent: WheelEvent;
}) => void)[] | undefined;
/**
* can be default prevented
*/
contentAppendImage?: ((event: PhotoSwipeEvent<"contentAppendImage"> & {
content: Content;
}) => void)[] | undefined;
/**
* can be default prevented
*/
lazyLoadSlide?: ((event: PhotoSwipeEvent<"lazyLoadSlide"> & {
index: number;
itemData: import("../slide/slide.js").SlideData;
}) => void)[] | undefined;
lazyLoad?: ((event: PhotoSwipeEvent<"lazyLoad">) => void)[] | undefined;
calcBounds?: ((event: PhotoSwipeEvent<"calcBounds"> & {
slide: import("../slide/slide.js").default;
}) => void)[] | undefined;
/**
* legacy
*/
zoomLevelsUpdate?: ((event: PhotoSwipeEvent<"zoomLevelsUpdate"> & {
zoomLevels: import("../slide/zoom-level.js").default;
slideData: import("../slide/slide.js").SlideData;
}) => void)[] | undefined;
init?: ((event: PhotoSwipeEvent<"init">) => void)[] | undefined;
initialZoomIn?: ((event: PhotoSwipeEvent<"initialZoomIn">) => void)[] | undefined;
initialZoomOut?: ((event: PhotoSwipeEvent<"initialZoomOut">) => void)[] | undefined;
initialZoomInEnd?: ((event: PhotoSwipeEvent<"initialZoomInEnd">) => void)[] | undefined;
initialZoomOutEnd?: ((event: PhotoSwipeEvent<"initialZoomOutEnd">) => void)[] | undefined;
numItems?: ((event: PhotoSwipeEvent<"numItems"> & {
dataSource: import("../photoswipe.js").DataSource | undefined;
numItems: number;
}) => void)[] | undefined;
itemData?: ((event: PhotoSwipeEvent<"itemData"> & {
itemData: import("../slide/slide.js").SlideData;
index: number;
}) => void)[] | undefined;
thumbBounds?: ((event: PhotoSwipeEvent<"thumbBounds"> & {
index: number;
itemData: import("../slide/slide.js").SlideData;
instance: import("../photoswipe.js").default;
}) => void)[] | undefined;
};
/**
* @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter[] }}
*/
_filters: {
/**
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*/
numItems?: Filter<"numItems">[] | undefined;
/**
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*/
itemData?: Filter<"itemData">[] | undefined;
/**
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*/
domItemData?: Filter<"domItemData">[] | undefined;
/**
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*/
clickedIndex?: Filter<"clickedIndex">[] | undefined;
/**
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*/
placeholderSrc?: Filter<"placeholderSrc">[] | undefined;
/**
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*/
isContentLoading?: Filter<"isContentLoading">[] | undefined;
/**
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*/
isContentZoomable?: Filter<"isContentZoomable">[] | undefined;
/**
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*/
useContentPlaceholder?: Filter<"useContentPlaceholder">[] | undefined;
/**
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*/
isKeepingPlaceholder?: Filter<"isKeepingPlaceholder">[] | undefined;
/**
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*/
contentErrorElement?: Filter<"contentErrorElement">[] | undefined;
/**
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*/
uiElement?: Filter<"uiElement">[] | undefined;
/**
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*/
thumbEl?: Filter<"thumbEl">[] | undefined;
/**
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*/
thumbBounds?: Filter<"thumbBounds">[] | undefined;
srcsetSizesWidth?: Filter<"srcsetSizesWidth">[] | undefined;
preventPointerEvent?: Filter<"preventPointerEvent">[] | undefined;
};
/** @type {PhotoSwipe | undefined} */
pswp: PhotoSwipe | undefined;
/** @type {PhotoSwipeOptions | undefined} */
options: Partial | undefined;
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
* @param {number} priority
*/
addFilter(name: T, fn: PhotoSwipeFiltersMap[T], priority?: number): void;
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {PhotoSwipeFiltersMap[T]} fn
*/
removeFilter(name: T_1, fn: PhotoSwipeFiltersMap[T_1]): void;
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @param {T} name
* @param {Parameters} args
* @returns {Parameters[0]}
*/
applyFilters(name: T_2, ...args: Parameters): Parameters[0];
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
on(name: T_3, fn: EventCallback): void;
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {EventCallback} fn
*/
off(name: T_4, fn: EventCallback): void;
/**
* @template {keyof PhotoSwipeEventsMap} T
* @param {T} name
* @param {PhotoSwipeEventsMap[T]} [details]
* @returns {AugmentedEvent}
*/
dispatch(name: T_5, details?: PhotoSwipeEventsMap[T_5] | undefined): AugmentedEvent;
}
/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
/** @typedef {import('../slide/content.js').default} ContentDefault */
/** @typedef {import('../slide/slide.js').default} Slide */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
/**
* Allow adding an arbitrary props to the Content
* https://photoswipe.com/custom-content/#using-webp-image-format
* @typedef {ContentDefault & Record} Content
*/
/** @typedef {{ x?: number; y?: number }} Point */
/**
* @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
*
*
* https://photoswipe.com/adding-ui-elements/
*
* @prop {undefined} uiRegister
* @prop {{ data: UIElementData }} uiElementCreate
*
*
* https://photoswipe.com/events/#initialization-events
*
* @prop {undefined} beforeOpen
* @prop {undefined} firstUpdate
* @prop {undefined} initialLayout
* @prop {undefined} change
* @prop {undefined} afterInit
* @prop {undefined} bindEvents
*
*
* https://photoswipe.com/events/#opening-or-closing-transition-events
*
* @prop {undefined} openingAnimationStart
* @prop {undefined} openingAnimationEnd
* @prop {undefined} closingAnimationStart
* @prop {undefined} closingAnimationEnd
*
*
* https://photoswipe.com/events/#closing-events
*
* @prop {undefined} close
* @prop {undefined} destroy
*
*
* https://photoswipe.com/events/#pointer-and-gesture-events
*
* @prop {{ originalEvent: PointerEvent }} pointerDown
* @prop {{ originalEvent: PointerEvent }} pointerMove
* @prop {{ originalEvent: PointerEvent }} pointerUp
* @prop {{ bgOpacity: number }} pinchClose can be default prevented
* @prop {{ panY: number }} verticalDrag can be default prevented
*
*
* https://photoswipe.com/events/#slide-content-events
*
* @prop {{ content: Content }} contentInit
* @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
* @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
* @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
* @prop {{ content: Content; slide: Slide }} loadError
* @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
* @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
* @prop {{ content: Content }} contentLazyLoad can be default prevented
* @prop {{ content: Content }} contentAppend can be default prevented
* @prop {{ content: Content }} contentActivate can be default prevented
* @prop {{ content: Content }} contentDeactivate can be default prevented
* @prop {{ content: Content }} contentRemove can be default prevented
* @prop {{ content: Content }} contentDestroy can be default prevented
*
*
* undocumented
*
* @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
* @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
*
* @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
* @prop {{ x: number; dragging: boolean }} moveMainScroll
* @prop {{ slide: Slide }} firstZoomPan
* @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
* @prop {undefined} beforeResize
* @prop {undefined} resize
* @prop {undefined} viewportSize
* @prop {undefined} updateScrollOffset
* @prop {{ slide: Slide }} slideInit
* @prop {{ slide: Slide }} afterSetContent
* @prop {{ slide: Slide }} slideLoad
* @prop {{ slide: Slide }} appendHeavy can be default prevented
* @prop {{ slide: Slide }} appendHeavyContent
* @prop {{ slide: Slide }} slideActivate
* @prop {{ slide: Slide }} slideDeactivate
* @prop {{ slide: Slide }} slideDestroy
* @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
* @prop {{ slide: Slide }} zoomPanUpdate
* @prop {{ slide: Slide }} initialZoomPan
* @prop {{ slide: Slide }} calcSlideSize
* @prop {undefined} resolutionChanged
* @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
* @prop {{ content: Content }} contentAppendImage can be default prevented
* @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
* @prop {undefined} lazyLoad
* @prop {{ slide: Slide }} calcBounds
* @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
*
*
* legacy
*
* @prop {undefined} init
* @prop {undefined} initialZoomIn
* @prop {undefined} initialZoomOut
* @prop {undefined} initialZoomInEnd
* @prop {undefined} initialZoomOutEnd
* @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
* @prop {{ itemData: SlideData; index: number }} itemData
* @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
*/
/**
* @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
*
* @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
* Modify the total amount of slides. Example on Data sources page.
* https://photoswipe.com/filters/#numitems
*
* @prop {(itemData: SlideData, index: number) => SlideData} itemData
* Modify slide item data. Example on Data sources page.
* https://photoswipe.com/filters/#itemdata
*
* @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
* Modify item data when it's parsed from DOM element. Example on Data sources page.
* https://photoswipe.com/filters/#domitemdata
*
* @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
* Modify clicked gallery item index.
* https://photoswipe.com/filters/#clickedindex
*
* @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
* Modify placeholder image source.
* https://photoswipe.com/filters/#placeholdersrc
*
* @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
* Modify if the content is currently loading.
* https://photoswipe.com/filters/#iscontentloading
*
* @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
* Modify if the content can be zoomed.
* https://photoswipe.com/filters/#iscontentzoomable
*
* @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
* Modify if the placeholder should be used for the content.
* https://photoswipe.com/filters/#usecontentplaceholder
*
* @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
* Modify if the placeholder should be kept after the content is loaded.
* https://photoswipe.com/filters/#iskeepingplaceholder
*
*
* @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
* Modify an element when the content has error state (for example, if image cannot be loaded).
* https://photoswipe.com/filters/#contenterrorelement
*
* @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
* Modify a UI element that's being created.
* https://photoswipe.com/filters/#uielement
*
* @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
* Modify the thumbnail element from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbel
*
* @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
* https://photoswipe.com/filters/#thumbbounds
*
* @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
*
* @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
*
*/
/**
* @template {keyof PhotoSwipeFiltersMap} T
* @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent : PhotoSwipeEvent & PhotoSwipeEventsMap[T]} AugmentedEvent
*/
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {(event: AugmentedEvent) => void} EventCallback
*/
/**
* Base PhotoSwipe event object
*
* @template {keyof PhotoSwipeEventsMap} T
*/
declare class PhotoSwipeEvent {
/**
* @param {T} type
* @param {PhotoSwipeEventsMap[T]} [details]
*/
constructor(type: T, details?: PhotoSwipeEventsMap[T] | undefined);
type: T;
defaultPrevented: boolean;
preventDefault(): void;
}
================================================
FILE: dist/types/gestures/drag-handler.d.ts
================================================
export default DragHandler;
export type Point = import('../photoswipe.js').Point;
export type Gestures = import('./gestures.js').default;
/**
* Handles single pointer dragging
*/
declare class DragHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures: Gestures);
gestures: import("./gestures.js").default;
pswp: import("../photoswipe.js").default;
/** @type {Point} */
startPan: Point;
start(): void;
change(): void;
end(): void;
/**
* @private
* @param {'x' | 'y'} axis
*/
private _finishPanGestureForAxis;
/**
* Update position of the main scroll,
* or/and update pan position of the current slide.
*
* Should return true if it changes (or can change) main scroll.
*
* @private
* @param {'x' | 'y'} axis
* @returns {boolean}
*/
private _panOrMoveMainScroll;
/**
* Relation between pan Y position and third of viewport height.
*
* When we are at initial position (center bounds) - the ratio is 0,
* if position is shifted upwards - the ratio is negative,
* if position is shifted downwards - the ratio is positive.
*
* @private
* @param {number} panY The current pan Y position.
* @returns {number}
*/
private _getVerticalDragRatio;
/**
* Set pan position of the current slide.
* Apply friction if the position is beyond the pan bounds,
* or if custom friction is defined.
*
* @private
* @param {'x' | 'y'} axis
* @param {number} potentialPan
* @param {number} [customFriction] (0.1 - 1)
*/
private _setPanWithFriction;
}
================================================
FILE: dist/types/gestures/gestures.d.ts
================================================
export default Gestures;
export type PhotoSwipe = import('../photoswipe.js').default;
export type Point = import('../photoswipe.js').Point;
/**
* Gestures class bind touch, pointer or mouse events
* and emits drag to drag-handler and zoom events zoom-handler.
*
* Drag and zoom events are emited in requestAnimationFrame,
* and only when one of pointers was actually changed.
*/
declare class Gestures {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("../photoswipe.js").default;
/** @type {'x' | 'y' | null} */
dragAxis: 'x' | 'y' | null;
/** @type {Point} */
p1: Point;
/** @type {Point} */
p2: Point;
/** @type {Point} */
prevP1: Point;
/** @type {Point} */
prevP2: Point;
/** @type {Point} */
startP1: Point;
/** @type {Point} */
startP2: Point;
/** @type {Point} */
velocity: Point;
/** @type {Point}
* @private
*/
private _lastStartP1;
/** @type {Point}
* @private
*/
private _intervalP1;
/** @private */
private _numActivePoints;
/** @type {Point[]}
* @private
*/
private _ongoingPointers;
/** @private */
private _touchEventEnabled;
/** @private */
private _pointerEventEnabled;
supportsTouch: boolean;
/** @private */
private _intervalTime;
/** @private */
private _velocityCalculated;
isMultitouch: boolean;
isDragging: boolean;
isZooming: boolean;
/** @type {number | null} */
raf: number | null;
/** @type {NodeJS.Timeout | null}
* @private
*/
private _tapTimer;
drag: DragHandler;
zoomLevels: ZoomHandler;
tapHandler: TapHandler;
/**
* @private
* @param {'mouse' | 'touch' | 'pointer'} pref
* @param {'down' | 'start'} down
* @param {'up' | 'end'} up
* @param {'cancel'} [cancel]
*/
private _bindEvents;
/**
* @param {PointerEvent} e
*/
onPointerDown(e: PointerEvent): void;
/**
* @param {PointerEvent} e
*/
onPointerMove(e: PointerEvent): void;
/**
* @private
*/
private _finishDrag;
/**
* @param {PointerEvent} e
*/
onPointerUp(e: PointerEvent): void;
/**
* @private
*/
private _rafRenderLoop;
/**
* Update velocity at 50ms interval
*
* @private
* @param {boolean} [force]
*/
private _updateVelocity;
/**
* @private
* @param {PointerEvent} e
*/
private _finishTap;
/**
* @private
*/
private _clearTapTimer;
/**
* Get velocity for axis
*
* @private
* @param {'x' | 'y'} axis
* @param {number} duration
* @returns {number}
*/
private _getVelocity;
/**
* @private
*/
private _rafStopLoop;
/**
* @private
* @param {PointerEvent} e
* @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
*/
private _preventPointerEventBehaviour;
/**
* Parses and normalizes points from the touch, mouse or pointer event.
* Updates p1 and p2.
*
* @private
* @param {PointerEvent | TouchEvent} e
* @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
*/
private _updatePoints;
/** update points that were used during previous rAF tick
* @private
*/
private _updatePrevPoints;
/** update points at the start of gesture
* @private
*/
private _updateStartPoints;
/** @private */
private _calculateDragDirection;
/**
* Converts touch, pointer or mouse event
* to PhotoSwipe point.
*
* @private
* @param {Touch | PointerEvent} e
* @param {Point} p
* @returns {Point}
*/
private _convertEventPosToPoint;
/**
* @private
* @param {PointerEvent} e
*/
private _onClick;
}
import DragHandler from "./drag-handler.js";
import ZoomHandler from "./zoom-handler.js";
import TapHandler from "./tap-handler.js";
================================================
FILE: dist/types/gestures/tap-handler.d.ts
================================================
export default TapHandler;
/**
*
*/
export type AddPostfix = import('../types.js').AddPostfix;
export type Gestures = import('./gestures.js').default;
export type Point = import('../photoswipe.js').Point;
export type Actions = 'imageClick' | 'bgClick' | 'tap' | 'doubleTap';
/**
* Tap, double-tap handler.
*/
declare class TapHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures: Gestures);
gestures: import("./gestures.js").default;
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
click(point: Point, originalEvent: PointerEvent): void;
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
tap(point: Point, originalEvent: PointerEvent): void;
/**
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
doubleTap(point: Point, originalEvent: PointerEvent): void;
/**
* @private
* @param {Actions} actionName
* @param {Point} point
* @param {PointerEvent} originalEvent
*/
private _doClickOrTapAction;
}
================================================
FILE: dist/types/gestures/zoom-handler.d.ts
================================================
export default ZoomHandler;
export type Point = import('../photoswipe.js').Point;
export type Gestures = import('./gestures.js').default;
declare class ZoomHandler {
/**
* @param {Gestures} gestures
*/
constructor(gestures: Gestures);
gestures: import("./gestures.js").default;
/**
* @private
* @type {Point}
*/
private _startPan;
/**
* @private
* @type {Point}
*/
private _startZoomPoint;
/**
* @private
* @type {Point}
*/
private _zoomPoint;
/** @private */
private _wasOverFitZoomLevel;
/** @private */
private _startZoomLevel;
start(): void;
change(): void;
end(): void;
/**
* @private
* @param {'x' | 'y'} axis
* @param {number} currZoomLevel
* @returns {number}
*/
private _calculatePanForZoomLevel;
/**
* Correct currZoomLevel and pan if they are
* beyond minimum or maximum values.
* With animation.
*
* @param {boolean} [ignoreGesture]
* Wether gesture coordinates should be ignored when calculating destination pan position.
*/
correctZoomPan(ignoreGesture?: boolean | undefined): void;
}
================================================
FILE: dist/types/keyboard.d.ts
================================================
export default Keyboard;
export type PhotoSwipe = import('./photoswipe.js').default;
/**
*
*/
export type Methods = import('./types.js').Methods;
/**
* - Manages keyboard shortcuts.
* - Helps trap focus within photoswipe.
*/
declare class Keyboard {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("./photoswipe.js").default;
/** @private */
private _wasFocused;
/** @private */
private _focusRoot;
/**
* @private
* @param {KeyboardEvent} e
*/
private _onKeyDown;
/**
* Trap focus inside photoswipe
*
* @private
* @param {FocusEvent} e
*/
private _onFocusIn;
}
================================================
FILE: dist/types/lightbox/lightbox.d.ts
================================================
export default PhotoSwipeLightbox;
/**
*
*/
export type Type = import('../types.js').Type;
export type PhotoSwipe = import('../photoswipe.js').default;
export type PhotoSwipeOptions = import('../photoswipe.js').PhotoSwipeOptions;
export type DataSource = import('../photoswipe.js').DataSource;
export type Point = import('../photoswipe.js').Point;
export type Content = import('../slide/content.js').default;
export type PhotoSwipeEventsMap = import('../core/eventable.js').PhotoSwipeEventsMap;
export type PhotoSwipeFiltersMap = import('../core/eventable.js').PhotoSwipeFiltersMap;
/**
*
*/
export type EventCallback = import('../core/eventable.js').EventCallback;
/**
* @template T
* @typedef {import('../types.js').Type} Type
*/
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/content.js').default} Content */
/** @typedef {import('../core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
/** @typedef {import('../core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
/**
* @template {keyof PhotoSwipeEventsMap} T
* @typedef {import('../core/eventable.js').EventCallback} EventCallback
*/
/**
* PhotoSwipe Lightbox
*
* - If user has unsupported browser it falls back to default browser action (just opens URL)
* - Binds click event to links that should open PhotoSwipe
* - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes)
* - Initializes PhotoSwipe
*
*
* Loader options use the same object as PhotoSwipe, and supports such options:
*
* gallery - Element | Element[] | NodeList | string selector for the gallery element
* children - Element | Element[] | NodeList | string selector for the gallery children
*
*/
declare class PhotoSwipeLightbox extends PhotoSwipeBase {
/**
* @param {PhotoSwipeOptions} [options]
*/
constructor(options?: Partial | undefined);
/** @type {PhotoSwipeOptions} */
options: Partial;
_uid: number;
shouldOpen: boolean;
/**
* @private
* @type {Content | undefined}
*/
private _preloadedContent;
/**
* @param {MouseEvent} e
*/
onThumbnailsClick(e: MouseEvent): void;
/**
* Initialize lightbox, should be called only once.
* It's not included in the main constructor, so you may bind events before it.
*/
init(): void;
/**
* Get index of gallery item that was clicked.
*
* @param {MouseEvent} e click event
* @returns {number}
*/
getClickedIndex(e: MouseEvent): number;
/**
* Load and open PhotoSwipe
*
* @param {number} index
* @param {DataSource} [dataSource]
* @param {Point | null} [initialPoint]
* @returns {boolean}
*/
loadAndOpen(index: number, dataSource?: import("../photoswipe.js").DataSource | undefined, initialPoint?: import("../photoswipe.js").Point | null | undefined): boolean;
/**
* Load the main module and the slide content by index
*
* @param {number} index
* @param {DataSource} [dataSource]
*/
preload(index: number, dataSource?: import("../photoswipe.js").DataSource | undefined): void;
/**
* @private
* @param {Type | { default: Type }} module
* @param {number} uid
*/
private _openPhotoswipe;
/**
* Unbinds all events, closes PhotoSwipe if it's open.
*/
destroy(): void;
}
import PhotoSwipeBase from "../core/base.js";
================================================
FILE: dist/types/main-scroll.d.ts
================================================
export default MainScroll;
export type PhotoSwipe = import('./photoswipe.js').default;
export type Slide = import('./slide/slide.js').default;
export type ItemHolder = {
el: HTMLDivElement;
slide?: Slide;
};
/**
* Handles movement of the main scrolling container
* (for example, it repositions when user swipes left or right).
*
* Also stores its state.
*/
declare class MainScroll {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("./photoswipe.js").default;
x: number;
slideWidth: number;
/** @private */
private _currPositionIndex;
/** @private */
private _prevPositionIndex;
/** @private */
private _containerShiftIndex;
/** @type {ItemHolder[]} */
itemHolders: ItemHolder[];
/**
* Position the scroller and slide containers
* according to viewport size.
*
* @param {boolean} [resizeSlides] Whether slides content should resized
*/
resize(resizeSlides?: boolean | undefined): void;
/**
* Reset X position of the main scroller to zero
*/
resetPosition(): void;
/**
* Create and append array of three items
* that hold data about slides in DOM
*/
appendHolders(): void;
/**
* Whether the main scroll can be horizontally swiped to the next or previous slide.
* @returns {boolean}
*/
canBeSwiped(): boolean;
/**
* Move main scroll by X amount of slides.
* For example:
* `-1` will move to the previous slide,
* `0` will reset the scroll position of the current slide,
* `3` will move three slides forward
*
* If loop option is enabled - index will be automatically looped too,
* (for example `-1` will move to the last slide of the gallery).
*
* @param {number} diff
* @param {boolean} [animate]
* @param {number} [velocityX]
* @returns {boolean} whether index was changed or not
*/
moveIndexBy(diff: number, animate?: boolean | undefined, velocityX?: number | undefined): boolean;
/**
* X position of the main scroll for the current slide
* (ignores position during dragging)
* @returns {number}
*/
getCurrSlideX(): number;
/**
* Whether scroll position is shifted.
* For example, it will return true if the scroll is being dragged or animated.
* @returns {boolean}
*/
isShifted(): boolean;
/**
* Update slides X positions and set their content
*/
updateCurrItem(): void;
/**
* Move the X position of the main scroll container
*
* @param {number} x
* @param {boolean} [dragging]
*/
moveTo(x: number, dragging?: boolean | undefined): void;
}
================================================
FILE: dist/types/opener.d.ts
================================================
export default Opener;
export type PhotoSwipe = import('./photoswipe.js').default;
export type Bounds = import('./slide/get-thumb-bounds.js').Bounds;
export type AnimationProps = import('./util/animations.js').AnimationProps;
/**
* Manages opening and closing transitions of the PhotoSwipe.
*
* It can perform zoom, fade or no transition.
*/
declare class Opener {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("./photoswipe.js").default;
isClosed: boolean;
isOpen: boolean;
isClosing: boolean;
isOpening: boolean;
/**
* @private
* @type {number | false | undefined}
*/
private _duration;
/** @private */
private _useAnimation;
/** @private */
private _croppedZoom;
/** @private */
private _animateRootOpacity;
/** @private */
private _animateBgOpacity;
/**
* @private
* @type { HTMLDivElement | HTMLImageElement | null | undefined }
*/
private _placeholder;
/**
* @private
* @type { HTMLDivElement | undefined }
*/
private _opacityElement;
/**
* @private
* @type { HTMLDivElement | undefined }
*/
private _cropContainer1;
/**
* @private
* @type { HTMLElement | null | undefined }
*/
private _cropContainer2;
/**
* @private
* @type {Bounds | undefined}
*/
private _thumbBounds;
/** @private */
private _prepareOpen;
open(): void;
close(): void;
/** @private */
private _applyStartProps;
_animateZoom: boolean | undefined;
/** @private */
private _start;
/** @private */
private _initiate;
/** @private */
private _onAnimationComplete;
/** @private */
private _animateToOpenState;
/** @private */
private _animateToClosedState;
/**
* @private
* @param {boolean} [animate]
*/
private _setClosedStateZoomPan;
/**
* @private
* @param {HTMLElement} target
* @param {'transform' | 'opacity'} prop
* @param {string} propValue
*/
private _animateTo;
}
================================================
FILE: dist/types/photoswipe.d.ts
================================================
export default PhotoSwipe;
/**
*
*/
export type Type = import('./types.js').Type;
export type SlideData = import('./slide/slide.js').SlideData;
export type ZoomLevelOption = import('./slide/zoom-level.js').ZoomLevelOption;
export type UIElementData = import('./ui/ui-element.js').UIElementData;
export type ItemHolder = import('./main-scroll.js').ItemHolder;
export type PhotoSwipeEventsMap = import('./core/eventable.js').PhotoSwipeEventsMap;
export type PhotoSwipeFiltersMap = import('./core/eventable.js').PhotoSwipeFiltersMap;
export type Bounds = import('./slide/get-thumb-bounds').Bounds;
/**
*
*/
export type EventCallback = import('./core/eventable.js').EventCallback;
/**
*
*/
export type AugmentedEvent = import('./core/eventable.js').AugmentedEvent;
export type Point = {
x: number;
y: number;
id?: string | number;
};
export type Padding = {
top: number;
bottom: number;
left: number;
right: number;
};
export type DataSourceArray = SlideData[];
export type DataSourceObject = {
gallery: HTMLElement;
items?: HTMLElement[];
};
export type DataSource = import("./slide/slide.js").SlideData[] | DataSourceObject;
export type ActionFn = (point: Point, originalEvent: PointerEvent) => void;
export type ActionType = 'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls';
export type PhotoSwipeModule = Type | {
default: Type;
};
export type PhotoSwipeModuleOption = PhotoSwipeModule | Promise | (() => Promise);
export type ElementProvider = string | NodeListOf | HTMLElement[] | HTMLElement;
/**
* https://photoswipe.com/options/
*/
export type PhotoSwipeOptions = Partial;
export type PreparedPhotoSwipeOptions = {
/**
* Pass an array of any items via dataSource option. Its length will determine amount of slides
* (which may be modified further from numItems event).
*
* Each item should contain data that you need to generate slide
* (for image slide it would be src (image URL), width (image width), height, srcset, alt).
*
* If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter.
*/
dataSource?: DataSource | undefined;
/**
* Background backdrop opacity, always define it via this option and not via CSS rgba color.
*/
bgOpacity: number;
/**
* Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport).
*/
spacing: number;
/**
* Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events.
*/
allowPanToNext: boolean;
/**
* If set to true you'll be able to swipe from the last to the first image.
* Option is always false when there are less than 3 slides.
*/
loop: boolean;
/**
* By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel.
*/
wheelToZoom?: boolean | undefined;
/**
* Pinch touch gesture to close the gallery.
*/
pinchToClose: boolean;
/**
* Vertical drag gesture to close the PhotoSwipe.
*/
closeOnVerticalDrag: boolean;
/**
* Slide area padding (in pixels).
*/
padding?: Padding | undefined;
/**
* The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example:
*/
paddingFn?: ((viewportSize: Point, itemData: SlideData, index: number) => Padding) | undefined;
/**
* Transition duration in milliseconds, can be 0.
*/
hideAnimationDuration: number | false;
/**
* Transition duration in milliseconds, can be 0.
*/
showAnimationDuration: number | false;
/**
* Transition duration in milliseconds, can be 0.
*/
zoomAnimationDuration: number | false;
/**
* String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions.
*/
easing: string;
/**
* Esc key to close.
*/
escKey: boolean;
/**
* Left/right arrow keys for navigation.
*/
arrowKeys: boolean;
/**
* Trap focus within PhotoSwipe element while it's open.
*/
trapFocus: boolean;
/**
* Restore focus the last active element after PhotoSwipe is closed.
*/
returnFocus: boolean;
/**
* If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it.
*/
clickToCloseNonZoomable: boolean;
/**
* Refer to click and tap actions page.
*/
imageClickAction: ActionType | ActionFn | false;
/**
* Refer to click and tap actions page.
*/
bgClickAction: ActionType | ActionFn | false;
/**
* Refer to click and tap actions page.
*/
tapAction: ActionType | ActionFn | false;
/**
* Refer to click and tap actions page.
*/
doubleTapAction: ActionType | ActionFn | false;
/**
* Delay before the loading indicator will be displayed,
* if image is loaded during it - the indicator will not be displayed at all. Can be zero.
*/
preloaderDelay: number;
/**
* Used for slide count indicator ("1 of 10 ").
*/
indexIndicatorSep: string;
/**
* A function that should return slide viewport width and height, in format {x: 100, y: 100}.
*/
getViewportSizeFn?: ((options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point) | undefined;
/**
* Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter.
*/
errorMsg: string;
/**
* Lazy loading of nearby slides based on direction of movement. Should be an array with two integers,
* first one - number of items to preload before the current image, second one - after the current image.
* Two nearby images are always loaded.
*/
preload: [number, number];
/**
* Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space.
* Example on Styling page.
*/
mainClass?: string | undefined;
/**
* Element to which PhotoSwipe dialog will be appended when it opens.
*/
appendToEl?: HTMLElement | undefined;
/**
* Maximum width of image to animate, if initial rendered image width
* is larger than this value - the opening/closing transition will be automatically disabled.
*/
maxWidthToAnimate: number;
/**
* Translating
*/
closeTitle?: string | undefined;
/**
* Translating
*/
zoomTitle?: string | undefined;
/**
* Translating
*/
arrowPrevTitle?: string | undefined;
/**
* Translating
*/
arrowNextTitle?: string | undefined;
/**
* To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`).
* It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`.
*
* Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`.
*/
showHideAnimationType?: "none" | "zoom" | "fade" | undefined;
/**
* Defines start slide index.
*/
index: number;
getClickedIndexFn?: ((e: MouseEvent) => number) | undefined;
arrowPrev?: boolean | undefined;
arrowNext?: boolean | undefined;
zoom?: boolean | undefined;
close?: boolean | undefined;
counter?: boolean | undefined;
arrowPrevSVG?: string | undefined;
arrowNextSVG?: string | undefined;
zoomSVG?: string | undefined;
closeSVG?: string | undefined;
counterSVG?: string | undefined;
counterTitle?: string | undefined;
initialZoomLevel?: import("./slide/zoom-level.js").ZoomLevelOption | undefined;
secondaryZoomLevel?: import("./slide/zoom-level.js").ZoomLevelOption | undefined;
maxZoomLevel?: import("./slide/zoom-level.js").ZoomLevelOption | undefined;
mouseMovePan?: boolean | undefined;
initialPointerPos?: Point | null | undefined;
showHideOpacity?: boolean | undefined;
pswpModule?: PhotoSwipeModuleOption | undefined;
openPromise?: (() => Promise) | undefined;
preloadFirstSlide?: boolean | undefined;
gallery?: ElementProvider | undefined;
gallerySelector?: string | undefined;
children?: ElementProvider | undefined;
childSelector?: string | undefined;
thumbSelector?: string | false | undefined;
};
/**
* PhotoSwipe Core
*/
declare class PhotoSwipe extends PhotoSwipeBase {
/**
* @param {PhotoSwipeOptions} [options]
*/
constructor(options?: Partial | undefined);
options: PreparedPhotoSwipeOptions;
/**
* offset of viewport relative to document
*
* @type {Point}
*/
offset: Point;
/**
* @type {Point}
* @private
*/
private _prevViewportSize;
/**
* Size of scrollable PhotoSwipe viewport
*
* @type {Point}
*/
viewportSize: Point;
/**
* background (backdrop) opacity
*/
bgOpacity: number;
currIndex: number;
potentialIndex: number;
isOpen: boolean;
isDestroying: boolean;
hasMouse: boolean;
/**
* @private
* @type {SlideData}
*/
private _initialItemData;
/** @type {Bounds | undefined} */
_initialThumbBounds: Bounds | undefined;
/** @type {HTMLDivElement | undefined} */
topBar: HTMLDivElement | undefined;
/** @type {HTMLDivElement | undefined} */
element: HTMLDivElement | undefined;
/** @type {HTMLDivElement | undefined} */
template: HTMLDivElement | undefined;
/** @type {HTMLDivElement | undefined} */
container: HTMLDivElement | undefined;
/** @type {HTMLElement | undefined} */
scrollWrap: HTMLElement | undefined;
/** @type {Slide | undefined} */
currSlide: Slide | undefined;
events: DOMEvents;
animations: Animations;
mainScroll: MainScroll;
gestures: Gestures;
opener: Opener;
keyboard: Keyboard;
contentLoader: ContentLoader;
/** @returns {boolean} */
init(): boolean;
scrollWheel: ScrollWheel | undefined;
/**
* Get looped slide index
* (for example, -1 will return the last slide)
*
* @param {number} index
* @returns {number}
*/
getLoopedIndex(index: number): number;
appendHeavy(): void;
/**
* Change the slide
* @param {number} index New index
*/
goTo(index: number): void;
/**
* Go to the next slide.
*/
next(): void;
/**
* Go to the previous slide.
*/
prev(): void;
/**
* @see slide/slide.js zoomTo
*
* @param {Parameters} args
*/
zoomTo(destZoomLevel: number, centerPoint?: Point | undefined, transitionDuration?: number | false | undefined, ignoreBounds?: boolean | undefined): void;
/**
* @see slide/slide.js toggleZoom
*/
toggleZoom(): void;
/**
* Close the gallery.
* After closing transition ends - destroy it
*/
close(): void;
/**
* Destroys the gallery:
* - instantly closes the gallery
* - unbinds events,
* - cleans intervals and timeouts
* - removes elements from DOM
*/
destroy(): void;
/**
* Refresh/reload content of a slide by its index
*
* @param {number} slideIndex
*/
refreshSlideContent(slideIndex: number): void;
/**
* Set slide content
*
* @param {ItemHolder} holder mainScroll.itemHolders array item
* @param {number} index Slide index
* @param {boolean} [force] If content should be set even if index wasn't changed
*/
setContent(holder: ItemHolder, index: number, force?: boolean | undefined): void;
/** @returns {Point} */
getViewportCenterPoint(): Point;
/**
* Update size of all elements.
* Executed on init and on page resize.
*
* @param {boolean} [force] Update size even if size of viewport was not changed.
*/
updateSize(force?: boolean | undefined): void;
/**
* @param {number} opacity
*/
applyBgOpacity(opacity: number): void;
/**
* Whether mouse is detected
*/
mouseDetected(): void;
/**
* Page resize event handler
*
* @private
*/
private _handlePageResize;
/**
* Page scroll offset is used
* to get correct coordinates
* relative to PhotoSwipe viewport.
*
* @private
*/
private _updatePageScrollOffset;
/**
* @param {number} x
* @param {number} y
*/
setScrollOffset(x: number, y: number): void;
/**
* Create main HTML structure of PhotoSwipe,
* and add it to DOM
*
* @private
*/
private _createMainStructure;
bg: HTMLDivElement | undefined;
ui: UI | undefined;
/**
* Get position and dimensions of small thumbnail
* {x:,y:,w:}
*
* Height is optional (calculated based on the large image)
*
* @returns {Bounds | undefined}
*/
getThumbBounds(): Bounds | undefined;
/**
* If the PhotoSwipe can have continuous loop
* @returns Boolean
*/
canLoop(): boolean;
/**
* @private
* @param {PhotoSwipeOptions} options
* @returns {PreparedPhotoSwipeOptions}
*/
private _prepareOptions;
}
import PhotoSwipeBase from "./core/base.js";
import Slide from "./slide/slide.js";
import DOMEvents from "./util/dom-events.js";
import Animations from "./util/animations.js";
import MainScroll from "./main-scroll.js";
import Gestures from "./gestures/gestures.js";
import Opener from "./opener.js";
import Keyboard from "./keyboard.js";
import ContentLoader from "./slide/loader.js";
import ScrollWheel from "./scroll-wheel.js";
import UI from "./ui/ui.js";
================================================
FILE: dist/types/scroll-wheel.d.ts
================================================
export default ScrollWheel;
export type PhotoSwipe = import('./photoswipe.js').default;
/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
/**
* Handles scroll wheel.
* Can pan and zoom current slide image.
*/
declare class ScrollWheel {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("./photoswipe.js").default;
/**
* @private
* @param {WheelEvent} e
*/
private _onWheel;
}
================================================
FILE: dist/types/slide/content.d.ts
================================================
export default Content;
export type Slide = import('./slide.js').default;
export type SlideData = import('./slide.js').SlideData;
export type PhotoSwipeBase = import('../core/base.js').default;
export type LoadState = import('../util/util.js').LoadState;
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {import('./slide.js').SlideData} SlideData */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../util/util.js').LoadState} LoadState */
declare class Content {
/**
* @param {SlideData} itemData Slide data
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
*/
constructor(itemData: SlideData, instance: PhotoSwipeBase, index: number);
instance: import("../core/base.js").default;
data: import("./slide.js").SlideData;
index: number;
/** @type {HTMLImageElement | HTMLDivElement | undefined} */
element: HTMLImageElement | HTMLDivElement | undefined;
/** @type {Placeholder | undefined} */
placeholder: Placeholder | undefined;
/** @type {Slide | undefined} */
slide: Slide | undefined;
displayedImageWidth: number;
displayedImageHeight: number;
width: number;
height: number;
isAttached: boolean;
hasSlide: boolean;
isDecoding: boolean;
/** @type {LoadState} */
state: LoadState;
type: string;
removePlaceholder(): void;
/**
* Preload content
*
* @param {boolean} isLazy
* @param {boolean} [reload]
*/
load(isLazy: boolean, reload?: boolean | undefined): void;
/**
* Preload image
*
* @param {boolean} isLazy
*/
loadImage(isLazy: boolean): void;
/**
* Assign slide to content
*
* @param {Slide} slide
*/
setSlide(slide: Slide): void;
/**
* Content load success handler
*/
onLoaded(): void;
/**
* Content load error handler
*/
onError(): void;
/**
* @returns {Boolean} If the content is currently loading
*/
isLoading(): boolean;
/**
* @returns {Boolean} If the content is in error state
*/
isError(): boolean;
/**
* @returns {boolean} If the content is image
*/
isImageContent(): boolean;
/**
* Update content size
*
* @param {Number} width
* @param {Number} height
*/
setDisplayedSize(width: number, height: number): void;
/**
* @returns {boolean} If the content can be zoomed
*/
isZoomable(): boolean;
/**
* Update image srcset sizes attribute based on width and height
*/
updateSrcsetSizes(): void;
/**
* @returns {boolean} If content should use a placeholder (from msrc by default)
*/
usePlaceholder(): boolean;
/**
* Preload content with lazy-loading param
*/
lazyLoad(): void;
/**
* @returns {boolean} If placeholder should be kept after content is loaded
*/
keepPlaceholder(): boolean;
/**
* Destroy the content
*/
destroy(): void;
/**
* Display error message
*/
displayError(): void;
/**
* Append the content
*/
append(): void;
/**
* Activate the slide,
* active slide is generally the current one,
* meaning the user can see it.
*/
activate(): void;
/**
* Deactivate the content
*/
deactivate(): void;
/**
* Remove the content from DOM
*/
remove(): void;
/**
* Append the image content to slide container
*/
appendImage(): void;
}
import Placeholder from "./placeholder.js";
================================================
FILE: dist/types/slide/get-thumb-bounds.d.ts
================================================
/**
* Get dimensions of thumbnail image
* (click on which opens photoswipe or closes photoswipe to)
*
* @param {number} index
* @param {SlideData} itemData
* @param {PhotoSwipe} instance PhotoSwipe instance
* @returns {Bounds | undefined}
*/
export function getThumbBounds(index: number, itemData: SlideData, instance: PhotoSwipe): Bounds | undefined;
export type SlideData = import('./slide.js').SlideData;
export type PhotoSwipe = import('../photoswipe.js').default;
export type Bounds = {
x: number;
y: number;
w: number;
innerRect?: {
w: number;
h: number;
x: number;
y: number;
};
};
================================================
FILE: dist/types/slide/loader.d.ts
================================================
/**
* Lazy-load an image
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* @param {SlideData} itemData Data about the slide
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
* @param {number} index
* @returns {Content} Image that is being decoded or false.
*/
export function lazyLoadData(itemData: SlideData, instance: PhotoSwipeBase, index: number): Content;
/**
* Lazy-loads specific slide.
* This function is used both by Lightbox and PhotoSwipe core,
* thus it can be called before dialog is opened.
*
* By default, it loads image based on viewport size and initial zoom level.
*
* @param {number} index Slide index
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
* @returns {Content | undefined}
*/
export function lazyLoadSlide(index: number, instance: PhotoSwipeBase): Content | undefined;
export default ContentLoader;
export type Content = import('./content.js').default;
export type Slide = import('./slide.js').default;
export type SlideData = import('./slide.js').SlideData;
export type PhotoSwipeBase = import('../core/base.js').default;
export type PhotoSwipe = import('../photoswipe.js').default;
declare class ContentLoader {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("../photoswipe.js").default;
limit: number;
/** @type {Content[]} */
_cachedItems: Content[];
/**
* Lazy load nearby slides based on `preload` option.
*
* @param {number} [diff] Difference between slide indexes that was changed recently, or 0.
*/
updateLazy(diff?: number | undefined): void;
/**
* @param {number} initialIndex
*/
loadSlideByIndex(initialIndex: number): void;
/**
* @param {Slide} slide
* @returns {Content}
*/
getContentBySlide(slide: Slide): Content;
/**
* @param {Content} content
*/
addToCache(content: Content): void;
/**
* Removes an image from cache, does not destroy() it, just removes.
*
* @param {number} index
*/
removeByIndex(index: number): void;
/**
* @param {number} index
* @returns {Content | undefined}
*/
getContentByIndex(index: number): Content | undefined;
destroy(): void;
}
================================================
FILE: dist/types/slide/pan-bounds.d.ts
================================================
export default PanBounds;
export type Slide = import('./slide.js').default;
export type Point = Record;
export type Axis = 'x' | 'y';
/** @typedef {import('./slide.js').default} Slide */
/** @typedef {Record} Point */
/** @typedef {'x' | 'y'} Axis */
/**
* Calculates minimum, maximum and initial (center) bounds of a slide
*/
declare class PanBounds {
/**
* @param {Slide} slide
*/
constructor(slide: Slide);
slide: import("./slide.js").default;
currZoomLevel: number;
center: {
x: number;
y: number;
};
max: {
x: number;
y: number;
};
min: {
x: number;
y: number;
};
/**
* _getItemBounds
*
* @param {number} currZoomLevel
*/
update(currZoomLevel: number): void;
/**
* _calculateItemBoundsForAxis
*
* @param {Axis} axis
*/
_updateAxis(axis: Axis): void;
reset(): void;
/**
* Correct pan position if it's beyond the bounds
*
* @param {Axis} axis x or y
* @param {number} panOffset
* @returns {number}
*/
correctPan(axis: Axis, panOffset: number): number;
}
================================================
FILE: dist/types/slide/placeholder.d.ts
================================================
export default Placeholder;
declare class Placeholder {
/**
* @param {string | false} imageSrc
* @param {HTMLElement} container
*/
constructor(imageSrc: string | false, container: HTMLElement);
/** @type {HTMLImageElement | HTMLDivElement | null} */
element: HTMLImageElement | HTMLDivElement | null;
/**
* @param {number} width
* @param {number} height
*/
setDisplayedSize(width: number, height: number): void;
destroy(): void;
}
================================================
FILE: dist/types/slide/slide.d.ts
================================================
export default Slide;
export type PhotoSwipe = import('../photoswipe.js').default;
export type Point = import('../photoswipe.js').Point;
export type SlideData = _SlideData & Record;
export type _SlideData = {
/**
* thumbnail element
*/
element?: HTMLElement | undefined;
/**
* image URL
*/
src?: string | undefined;
/**
* image srcset
*/
srcset?: string | undefined;
/**
* image width (deprecated)
*/
w?: number | undefined;
/**
* image height (deprecated)
*/
h?: number | undefined;
/**
* image width
*/
width?: number | undefined;
/**
* image height
*/
height?: number | undefined;
/**
* placeholder image URL that's displayed before large image is loaded
*/
msrc?: string | undefined;
/**
* image alt text
*/
alt?: string | undefined;
/**
* whether thumbnail is cropped client-side or not
*/
thumbCropped?: boolean | undefined;
/**
* html content of a slide
*/
html?: string | undefined;
/**
* slide type
*/
type?: string | undefined;
};
/**
* Renders and allows to control a single slide
*/
declare class Slide {
/**
* @param {SlideData} data
* @param {number} index
* @param {PhotoSwipe} pswp
*/
constructor(data: SlideData, index: number, pswp: PhotoSwipe);
data: SlideData;
index: number;
pswp: import("../photoswipe.js").default;
isActive: boolean;
currentResolution: number;
/** @type {Point} */
panAreaSize: Point;
/** @type {Point} */
pan: Point;
isFirstSlide: boolean;
zoomLevels: ZoomLevel;
content: import("./content.js").default;
container: HTMLDivElement;
/** @type {HTMLElement | null} */
holderElement: HTMLElement | null;
currZoomLevel: number;
/** @type {number} */
width: number;
/** @type {number} */
height: number;
heavyAppended: boolean;
bounds: PanBounds;
prevDisplayedWidth: number;
prevDisplayedHeight: number;
/**
* If this slide is active/current/visible
*
* @param {boolean} isActive
*/
setIsActive(isActive: boolean): void;
/**
* Appends slide content to DOM
*
* @param {HTMLElement} holderElement
*/
append(holderElement: HTMLElement): void;
load(): void;
/**
* Append "heavy" DOM elements
*
* This may depend on a type of slide,
* but generally these are large images.
*/
appendHeavy(): void;
/**
* Triggered when this slide is active (selected).
*
* If it's part of opening/closing transition -
* activate() will trigger after the transition is ended.
*/
activate(): void;
/**
* Triggered when this slide becomes inactive.
*
* Slide can become inactive only after it was active.
*/
deactivate(): void;
/**
* The slide should destroy itself, it will never be used again.
* (unbind all events and destroy internal components)
*/
destroy(): void;
resize(): void;
/**
* Apply size to current slide content,
* based on the current resolution and scale.
*
* @param {boolean} [force] if size should be updated even if dimensions weren't changed
*/
updateContentSize(force?: boolean | undefined): void;
/**
* @param {number} width
* @param {number} height
*/
sizeChanged(width: number, height: number): boolean;
/** @returns {HTMLImageElement | HTMLDivElement | null | undefined} */
getPlaceholderElement(): HTMLImageElement | HTMLDivElement | null | undefined;
/**
* Zoom current slide image to...
*
* @param {number} destZoomLevel Destination zoom level.
* @param {Point} [centerPoint]
* Transform origin center point, or false if viewport center should be used.
* @param {number | false} [transitionDuration] Transition duration, may be set to 0.
* @param {boolean} [ignoreBounds] Minimum and maximum zoom levels will be ignored.
*/
zoomTo(destZoomLevel: number, centerPoint?: import("../photoswipe.js").Point | undefined, transitionDuration?: number | false | undefined, ignoreBounds?: boolean | undefined): void;
/**
* @param {Point} [centerPoint]
*/
toggleZoom(centerPoint?: import("../photoswipe.js").Point | undefined): void;
/**
* Updates zoom level property and recalculates new pan bounds,
* unlike zoomTo it does not apply transform (use applyCurrentZoomPan)
*
* @param {number} currZoomLevel
*/
setZoomLevel(currZoomLevel: number): void;
/**
* Get pan position after zoom at a given `point`.
*
* Always call setZoomLevel(newZoomLevel) beforehand to recalculate
* pan bounds according to the new zoom level.
*
* @param {'x' | 'y'} axis
* @param {Point} [point]
* point based on which zoom is performed, usually refers to the current mouse position,
* if false - viewport center will be used.
* @param {number} [prevZoomLevel] Zoom level before new zoom was applied.
* @returns {number}
*/
calculateZoomToPanOffset(axis: 'x' | 'y', point?: import("../photoswipe.js").Point | undefined, prevZoomLevel?: number | undefined): number;
/**
* Apply pan and keep it within bounds.
*
* @param {number} panX
* @param {number} panY
*/
panTo(panX: number, panY: number): void;
/**
* If the slide in the current state can be panned by the user
* @returns {boolean}
*/
isPannable(): boolean;
/**
* If the slide can be zoomed
* @returns {boolean}
*/
isZoomable(): boolean;
/**
* Apply transform and scale based on
* the current pan position (this.pan) and zoom level (this.currZoomLevel)
*/
applyCurrentZoomPan(): void;
zoomAndPanToInitial(): void;
/**
* Set translate and scale based on current resolution
*
* @param {number} x
* @param {number} y
* @param {number} zoom
* @private
*/
private _applyZoomTransform;
calculateSize(): void;
/** @returns {string} */
getCurrentTransform(): string;
/**
* Set resolution and re-render the image.
*
* For example, if the real image size is 2000x1500,
* and resolution is 0.5 - it will be rendered as 1000x750.
*
* Image with zoom level 2 and resolution 0.5 is
* the same as image with zoom level 1 and resolution 1.
*
* Used to optimize animations and make
* sure that browser renders image in the highest quality.
* Also used by responsive images to load the correct one.
*
* @param {number} newResolution
*/
_setResolution(newResolution: number): void;
}
import ZoomLevel from "./zoom-level.js";
import PanBounds from "./pan-bounds.js";
================================================
FILE: dist/types/slide/zoom-level.d.ts
================================================
export default ZoomLevel;
export type PhotoSwipe = import('../photoswipe.js').default;
export type PhotoSwipeOptions = import('../photoswipe.js').PhotoSwipeOptions;
export type Point = import('../photoswipe.js').Point;
export type SlideData = import('../slide/slide.js').SlideData;
export type ZoomLevelOption = number | "fit" | "fill" | ((zoomLevelObject: ZoomLevel) => number);
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
/**
* Calculates zoom levels for specific slide.
* Depends on viewport size and image size.
*/
declare class ZoomLevel {
/**
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {SlideData} itemData Slide data
* @param {number} index Slide index
* @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
*/
constructor(options: PhotoSwipeOptions, itemData: SlideData, index: number, pswp?: import("../photoswipe.js").default | undefined);
pswp: import("../photoswipe.js").default | undefined;
options: Partial;
itemData: import("../slide/slide.js").SlideData;
index: number;
/** @type { Point | null } */
panAreaSize: Point | null;
/** @type { Point | null } */
elementSize: Point | null;
fit: number;
fill: number;
vFill: number;
initial: number;
secondary: number;
max: number;
min: number;
/**
* Calculate initial, secondary and maximum zoom level for the specified slide.
*
* It should be called when either image or viewport size changes.
*
* @param {number} maxWidth
* @param {number} maxHeight
* @param {Point} panAreaSize
*/
update(maxWidth: number, maxHeight: number, panAreaSize: Point): void;
/**
* Parses user-defined zoom option.
*
* @private
* @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
* @returns { number | undefined }
*/
private _parseZoomLevelOption;
/**
* Get zoom level to which image will be zoomed after double-tap gesture,
* or when user clicks on zoom icon,
* or mouse-click on image itself.
* If you return 1 image will be zoomed to its original size.
*
* @private
* @return {number}
*/
private _getSecondary;
/**
* Get initial image zoom level.
*
* @private
* @return {number}
*/
private _getInitial;
/**
* Maximum zoom level when user zooms
* via zoom/pinch gesture,
* via cmd/ctrl-wheel or via trackpad.
*
* @private
* @return {number}
*/
private _getMax;
}
================================================
FILE: dist/types/types.d.ts
================================================
export declare type Methods = {
[M in keyof T]: T[M] extends (...a: any) => any ? M : never;
}[keyof T];
export declare type AddPostfix = `${T}${P}`;
export interface Type extends Function {
new (...args: any[]): T;
}
================================================
FILE: dist/types/ui/button-arrow.d.ts
================================================
/** @type {UIElementData} */
export const arrowPrev: UIElementData;
/** @type {UIElementData} */
export const arrowNext: UIElementData;
export type UIElementData = import('./ui-element.js').UIElementData;
export type PhotoSwipe = import('../photoswipe.js').default;
================================================
FILE: dist/types/ui/button-close.d.ts
================================================
export default closeButton;
/** @type {import('./ui-element.js').UIElementData} UIElementData */
declare const closeButton: import('./ui-element.js').UIElementData;
================================================
FILE: dist/types/ui/button-zoom.d.ts
================================================
export default zoomButton;
/** @type {import('./ui-element.js').UIElementData} UIElementData */
declare const zoomButton: import('./ui-element.js').UIElementData;
================================================
FILE: dist/types/ui/counter-indicator.d.ts
================================================
/** @type {import('./ui-element.js').UIElementData} UIElementData */
export const counterIndicator: import('./ui-element.js').UIElementData;
================================================
FILE: dist/types/ui/loading-indicator.d.ts
================================================
/** @type {import('./ui-element.js').UIElementData} UIElementData */
export const loadingIndicator: import('./ui-element.js').UIElementData;
================================================
FILE: dist/types/ui/ui-element.d.ts
================================================
export default UIElement;
export type PhotoSwipe = import('../photoswipe.js').default;
/**
*
*/
export type Methods = import('../types.js').Methods;
export type UIElementMarkupProps = {
isCustomSVG?: boolean | undefined;
inner: string;
outlineID?: string | undefined;
size?: string | number | undefined;
};
export type UIElementData = {
name?: string | undefined;
className?: string | undefined;
html?: UIElementMarkup | undefined;
isButton?: boolean | undefined;
tagName?: keyof HTMLElementTagNameMap | undefined;
title?: string | undefined;
ariaLabel?: string | undefined;
onInit?: ((element: HTMLElement, pswp: PhotoSwipe) => void) | undefined;
onClick?: import("../types.js").Methods | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void) | undefined;
appendTo?: "bar" | "wrapper" | "root" | undefined;
order?: number | undefined;
};
export type DefaultUIElements = 'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter';
export type UIElementMarkup = string | UIElementMarkupProps;
declare class UIElement {
/**
* @param {PhotoSwipe} pswp
* @param {UIElementData} data
*/
constructor(pswp: PhotoSwipe, data: UIElementData);
}
================================================
FILE: dist/types/ui/ui.d.ts
================================================
export default UI;
export type PhotoSwipe = import('../photoswipe.js').default;
export type UIElementData = import('./ui-element.js').UIElementData;
declare class UI {
/**
* @param {PhotoSwipe} pswp
*/
constructor(pswp: PhotoSwipe);
pswp: import("../photoswipe.js").default;
isRegistered: boolean;
/** @type {UIElementData[]} */
uiElementsData: UIElementData[];
/** @type {(UIElement | UIElementData)[]} */
items: (UIElement | UIElementData)[];
/** @type {() => void} */
updatePreloaderVisibility: () => void;
/**
* @private
* @type {number | undefined}
*/
private _lastUpdatedZoomLevel;
init(): void;
/**
* @param {UIElementData} elementData
*/
registerElement(elementData: UIElementData): void;
/**
* Fired each time zoom or pan position is changed.
* Update classes that control visibility of zoom button and cursor icon.
*
* @private
*/
private _onZoomPanUpdate;
}
import UIElement from "./ui-element.js";
================================================
FILE: dist/types/util/animations.d.ts
================================================
export default Animations;
export type CssAnimationProps = import('./css-animation.js').CssAnimationProps;
export type SpringAnimationProps = import('./spring-animation.js').SpringAnimationProps;
export type SharedAnimationProps = {
name?: string | undefined;
isPan?: boolean | undefined;
isMainScroll?: boolean | undefined;
onComplete?: VoidFunction | undefined;
onFinish?: VoidFunction | undefined;
};
export type Animation = SpringAnimation | CSSAnimation;
export type AnimationProps = SpringAnimationProps | CssAnimationProps;
/** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */
/** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */
/** @typedef {Object} SharedAnimationProps
* @prop {string} [name]
* @prop {boolean} [isPan]
* @prop {boolean} [isMainScroll]
* @prop {VoidFunction} [onComplete]
* @prop {VoidFunction} [onFinish]
*/
/** @typedef {SpringAnimation | CSSAnimation} Animation */
/** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */
/**
* Manages animations
*/
declare class Animations {
/** @type {Animation[]} */
activeAnimations: Animation[];
/**
* @param {SpringAnimationProps} props
*/
startSpring(props: SpringAnimationProps): void;
/**
* @param {CssAnimationProps} props
*/
startTransition(props: CssAnimationProps): void;
/**
* @private
* @param {AnimationProps} props
* @param {boolean} [isSpring]
* @returns {Animation}
*/
private _start;
/**
* @param {Animation} animation
*/
stop(animation: Animation): void;
stopAll(): void;
/**
* Stop all pan or zoom transitions
*/
stopAllPan(): void;
stopMainScroll(): void;
/**
* Returns true if main scroll transition is running
*/
/**
* Returns true if any pan or zoom transition is running
*/
isPanRunning(): boolean;
}
import SpringAnimation from "./spring-animation.js";
import CSSAnimation from "./css-animation.js";
================================================
FILE: dist/types/util/css-animation.d.ts
================================================
export default CSSAnimation;
export type SharedAnimationProps = import('./animations.js').SharedAnimationProps;
export type DefaultCssAnimationProps = {
target: HTMLElement;
duration?: number | undefined;
easing?: string | undefined;
transform?: string | undefined;
opacity?: string | undefined;
};
export type CssAnimationProps = SharedAnimationProps & DefaultCssAnimationProps;
/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
/** @typedef {Object} DefaultCssAnimationProps
*
* @prop {HTMLElement} target
* @prop {number} [duration]
* @prop {string} [easing]
* @prop {string} [transform]
* @prop {string} [opacity]
* */
/** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */
/**
* Runs CSS transition.
*/
declare class CSSAnimation {
/**
* onComplete can be unpredictable, be careful about current state
*
* @param {CssAnimationProps} props
*/
constructor(props: CssAnimationProps);
props: CssAnimationProps;
onFinish: VoidFunction;
/** @private */
private _target;
/** @private */
private _onComplete;
/** @private */
private _finished;
/**
* @private
* @param {TransitionEvent} e
*/
private _onTransitionEnd;
/** @private */
private _helperTimeout;
/**
* @private
*/
private _finalizeAnimation;
destroy(): void;
}
================================================
FILE: dist/types/util/dom-events.d.ts
================================================
export default DOMEvents;
export type PoolItem = {
target: HTMLElement | Window | Document | undefined | null;
type: string;
listener: EventListenerOrEventListenerObject;
passive?: boolean | undefined;
};
/**
* @typedef {Object} PoolItem
* @prop {HTMLElement | Window | Document | undefined | null} target
* @prop {string} type
* @prop {EventListenerOrEventListenerObject} listener
* @prop {boolean} [passive]
*/
declare class DOMEvents {
/**
* @type {PoolItem[]}
* @private
*/
private _pool;
/**
* Adds event listeners
*
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type Can be multiple, separated by space.
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
*/
add(target: PoolItem['target'], type: PoolItem['type'], listener: PoolItem['listener'], passive?: PoolItem['passive']): void;
/**
* Removes event listeners
*
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
*/
remove(target: PoolItem['target'], type: PoolItem['type'], listener: PoolItem['listener'], passive?: PoolItem['passive']): void;
/**
* Removes all bound events
*/
removeAll(): void;
/**
* Adds or removes event
*
* @private
* @param {PoolItem['target']} target
* @param {PoolItem['type']} type
* @param {PoolItem['listener']} listener
* @param {PoolItem['passive']} [passive]
* @param {boolean} [unbind] Whether the event should be added or removed
* @param {boolean} [skipPool] Whether events pool should be skipped
*/
private _toggleListener;
}
================================================
FILE: dist/types/util/spring-animation.d.ts
================================================
export default SpringAnimation;
export type SharedAnimationProps = import('./animations.js').SharedAnimationProps;
export type DefaultSpringAnimationProps = {
start: number;
end: number;
velocity: number;
dampingRatio?: number | undefined;
naturalFrequency?: number | undefined;
onUpdate: (end: number) => void;
};
export type SpringAnimationProps = SharedAnimationProps & DefaultSpringAnimationProps;
/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
/**
* @typedef {Object} DefaultSpringAnimationProps
*
* @prop {number} start
* @prop {number} end
* @prop {number} velocity
* @prop {number} [dampingRatio]
* @prop {number} [naturalFrequency]
* @prop {(end: number) => void} onUpdate
*/
/** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */
declare class SpringAnimation {
/**
* @param {SpringAnimationProps} props
*/
constructor(props: SpringAnimationProps);
props: SpringAnimationProps;
_raf: number;
onFinish: VoidFunction;
destroy(): void;
}
================================================
FILE: dist/types/util/spring-easer.d.ts
================================================
export default SpringEaser;
/**
* Spring easing helper
*/
declare class SpringEaser {
/**
* @param {number} initialVelocity Initial velocity, px per ms.
*
* @param {number} [dampingRatio]
* Determines how bouncy animation will be.
* From 0 to 1, 0 - always overshoot, 1 - do not overshoot.
* "overshoot" refers to part of animation that
* goes beyond the final value.
*
* @param {number} [naturalFrequency]
* Determines how fast animation will slow down.
* The higher value - the stiffer the transition will be,
* and the faster it will slow down.
* Recommended value from 10 to 50
*/
constructor(initialVelocity: number, dampingRatio?: number | undefined, naturalFrequency?: number | undefined);
velocity: number;
_dampingRatio: number;
_naturalFrequency: number;
_dampedFrequency: number;
/**
* @param {number} deltaPosition Difference between current and end position of the animation
* @param {number} deltaTime Frame duration in milliseconds
*
* @returns {number} Displacement, relative to the end position.
*/
easeFrame(deltaPosition: number, deltaTime: number): number;
}
================================================
FILE: dist/types/util/util.d.ts
================================================
/** @typedef {import('../photoswipe.js').Point} Point */
/**
* @template {keyof HTMLElementTagNameMap} T
* @param {string} className
* @param {T} tagName
* @param {Node} [appendToEl]
* @returns {HTMLElementTagNameMap[T]}
*/
export function createElement(className: string, tagName: T, appendToEl?: Node | undefined): HTMLElementTagNameMap[T];
/**
* @param {Point} p1
* @param {Point} p2
* @returns {Point}
*/
export function equalizePoints(p1: Point, p2: Point): Point;
/**
* @param {Point} p
*/
export function roundPoint(p: Point): void;
/**
* Returns distance between two points.
*
* @param {Point} p1
* @param {Point} p2
* @returns {number}
*/
export function getDistanceBetween(p1: Point, p2: Point): number;
/**
* Whether X and Y positions of points are equal
*
* @param {Point} p1
* @param {Point} p2
* @returns {boolean}
*/
export function pointsEqual(p1: Point, p2: Point): boolean;
/**
* The float result between the min and max values.
*
* @param {number} val
* @param {number} min
* @param {number} max
* @returns {number}
*/
export function clamp(val: number, min: number, max: number): number;
/**
* Get transform string
*
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
* @returns {string}
*/
export function toTransformString(x: number, y?: number | undefined, scale?: number | undefined): string;
/**
* Apply transform:translate(x, y) scale(scale) to element
*
* @param {HTMLElement} el
* @param {number} x
* @param {number} [y]
* @param {number} [scale]
*/
export function setTransform(el: HTMLElement, x: number, y?: number | undefined, scale?: number | undefined): void;
/**
* Apply CSS transition to element
*
* @param {HTMLElement} el
* @param {string} [prop] CSS property to animate
* @param {number} [duration] in ms
* @param {string} [ease] CSS easing function
*/
export function setTransitionStyle(el: HTMLElement, prop?: string | undefined, duration?: number | undefined, ease?: string | undefined): void;
/**
* Apply width and height CSS properties to element
*
* @param {HTMLElement} el
* @param {string | number} w
* @param {string | number} h
*/
export function setWidthHeight(el: HTMLElement, w: string | number, h: string | number): void;
/**
* @param {HTMLElement} el
*/
export function removeTransitionStyle(el: HTMLElement): void;
/**
* @param {HTMLImageElement} img
* @returns {Promise}
*/
export function decodeImage(img: HTMLImageElement): Promise;
/**
* Check if click or keydown event was dispatched
* with a special key or via mouse wheel.
*
* @param {MouseEvent | KeyboardEvent} e
* @returns {boolean}
*/
export function specialKeyUsed(e: MouseEvent | KeyboardEvent): boolean;
/**
* Parse `gallery` or `children` options.
*
* @param {import('../photoswipe.js').ElementProvider} [option]
* @param {string} [legacySelector]
* @param {HTMLElement | Document} [parent]
* @returns HTMLElement[]
*/
export function getElementsFromOption(option?: import("../photoswipe.js").ElementProvider | undefined, legacySelector?: string | undefined, parent?: Document | HTMLElement | undefined): HTMLElement[];
/**
* Check if variable is PhotoSwipe class
*
* @param {any} fn
* @returns {boolean}
*/
export function isPswpClass(fn: any): boolean;
/**
* Check if browser is Safari
*
* @returns {boolean}
*/
export function isSafari(): boolean;
/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
export const LOAD_STATE: {
IDLE: 'idle';
LOADING: 'loading';
LOADED: 'loaded';
ERROR: 'error';
};
export type Point = import('../photoswipe.js').Point;
export type LoadState = {
IDLE: "idle";
LOADING: "loading";
LOADED: "loaded";
ERROR: "error";
}[keyof {
IDLE: "idle";
LOADING: "loading";
LOADED: "loaded";
ERROR: "error";
}];
================================================
FILE: dist/types/util/viewport-size.d.ts
================================================
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
/** @typedef {import('../photoswipe.js').Point} Point */
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
/**
* @param {PhotoSwipeOptions} options
* @param {PhotoSwipeBase} pswp
* @returns {Point}
*/
export function getViewportSize(options: PhotoSwipeOptions, pswp: PhotoSwipeBase): Point;
/**
* Parses padding option.
* Supported formats:
*
* // Object
* padding: {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* }
*
* // A function that returns the object
* paddingFn: (viewportSize, itemData, index) => {
* return {
* top: 0,
* bottom: 0,
* left: 0,
* right: 0
* };
* }
*
* // Legacy variant
* paddingLeft: 0,
* paddingRight: 0,
* paddingTop: 0,
* paddingBottom: 0,
*
* @param {'left' | 'top' | 'bottom' | 'right'} prop
* @param {PhotoSwipeOptions} options PhotoSwipe options
* @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
* @param {SlideData} itemData Data about the slide
* @param {number} index Slide index
* @returns {number}
*/
export function parsePaddingOption(prop: 'left' | 'top' | 'bottom' | 'right', options: PhotoSwipeOptions, viewportSize: Point, itemData: SlideData, index: number): number;
/**
* @param {PhotoSwipeOptions} options
* @param {Point} viewportSize
* @param {SlideData} itemData
* @param {number} index
* @returns {Point}
*/
export function getPanAreaSize(options: PhotoSwipeOptions, viewportSize: Point, itemData: SlideData, index: number): Point;
export type PhotoSwipeOptions = import('../photoswipe.js').PhotoSwipeOptions;
export type PhotoSwipeBase = import('../core/base.js').default;
export type Point = import('../photoswipe.js').Point;
export type SlideData = import('../slide/slide.js').SlideData;
================================================
FILE: dist/umd/README.md
================================================
`umd/` folder contains transpiled version of PhotoSwipe in universal module definition format.
Use it only if you are unable to use ESM version.
Basic example:
```html
Test Gallery
```
================================================
FILE: docs/adding-ui-elements.md
================================================
---
id: adding-ui-elements
title: Adding UI elements
sidebar_label: Adding UI elements
---
Use method `pswp.ui.registerElement` to add any interactive element inside PhotoSwipe. It must be called within or after `uiRegister` event. For example:
## Adding a Button to the Toolbar
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--with-custom-button',
children:'a',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'test-button',
ariaLabel: 'Toggle zoom',
order: 9,
isButton: true,
html: 'Test',
onClick: (event, el) => {
if ( confirm('Do you want to toggle zoom?') ) {
lightbox.pswp.toggleZoom();
}
}
});
});
lightbox.init();
```
```css pswpcode
button.pswp__button--test-button {
background: #136912 !important;
font-size: 20px;
color: #fff;
}
```
## Adding HTML Indicator to the Toolbar
Display zoom level of the current image.
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--with-custom-toolbar-indicator',
children:'a',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'zoom-level-indicator',
order: 9,
onInit: (el, pswp) => {
pswp.on('zoomPanUpdate', (e) => {
if (e.slide === pswp.currSlide) {
el.innerText = 'Zoom level is ' + Math.round(pswp.currSlide.currZoomLevel * 100) + '%';
}
});
}
});
});
lightbox.init();
```
```css pswpcode
.pswp__zoom-level-indicator {
background: #136912;
font-size: 16px;
line-height: 1;
font-weight: bold;
color: #fff;
height: auto;
align-self: center;
padding: 4px 6px 5px;
margin-right: 4px;
}
```
## Adding Download Button
When you provide an SVG, make sure that it has `aria-hidden="true"` and `pswp__icn` class to preserve styling.
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--with-download-button',
children:'a',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'download-button',
order: 8,
isButton: true,
tagName: 'a',
// SVG with outline
html: {
isCustomSVG: true,
inner: ' ',
outlineID: 'pswp__icn-download'
},
// Or provide full svg:
// html: ' ',
// Or provide any other markup:
// html: ' '
onInit: (el, pswp) => {
el.setAttribute('download', '');
el.setAttribute('target', '_blank');
el.setAttribute('rel', 'noopener');
pswp.on('change', () => {
console.log('change');
el.href = pswp.currSlide.data.src;
});
}
});
});
lightbox.init();
```
```css pswpcode
button.pswp__button--test-button {
background: #136912 !important;
font-size: 20px;
color: #fff;
}
```
## Adding Navigation Indicator (bullets)
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--with-bullets',
children:'a',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'bulletsIndicator',
className: 'pswp__bullets-indicator',
appendTo: 'wrapper',
onInit: (el, pswp) => {
const bullets = [];
let bullet;
let prevIndex = -1;
for (let i = 0; i < pswp.getNumItems(); i++) {
bullet = document.createElement('div');
bullet.className = 'pswp__bullet';
bullet.onclick = (e) => {
pswp.goTo(bullets.indexOf(e.target));
};
el.appendChild(bullet);
bullets.push(bullet);
}
pswp.on('change', (a,) => {
if (prevIndex >= 0) {
bullets[prevIndex].classList.remove('pswp__bullet--active');
}
bullets[pswp.currIndex].classList.add('pswp__bullet--active');
prevIndex = pswp.currIndex;
});
}
});
});
lightbox.init();
```
```css pswpcode
.pswp__bullets-indicator {
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
bottom: 30px;
left: 50%;
transform: translate(-50%, 0);
}
.pswp__bullet {
width: 30px;
height: 30px;
border-radius: 50%;
background: #fff;
margin: 0 5px;
}
.pswp__bullet--active {
background: green;
}
```
## ui.registerElement() API
```js
// registerElement method must be called within or after uiRegister event
pswp.ui.registerElement({
// Unique name of the UI element,
name: 'test123',
// Classname of the element.
// Optional, if not defined - name will be used
// in format pswp__button--name, or pswp__name
className: undefined,
// Order of element, default order elements:
// counter - 5, zoom button - 10, info - 15, close - 20.
order: 9,
// If element should be
// rendered as button
isButton: true,
// Element tag name,
// Optional, if not defined - button or div will be used
tagName: 'a',
// Button title, optional
title: 'Button title',
// Button aria-label attribute,
// if not defined - title will be used
ariaLabel: undefined,
// html string, will be added inside button, optional
// can also be an object with svg data
html: 'Test',
// Element container, possible values:
// - 'bar' (top toolbar, .pswp__top-bar, default value),
// - 'wrapper' (scroll viewport wrapper, .pswp__scroll-wrap),
// - 'root' (root element of the dialog, .pswp)
// If you add a text inside 'wrapper' - it won't be selectable,
// as PhotoSwipe intersects all touch events there.
appendTo: 'bar',
// callback is triggered right before
// corresponding element is added to DOM
// (while dialog is opening/creating)
onInit: function(el, pswp) {
// el - reference to your DOM element
// pswp - PhotoSwipe object
// You may modify element here, for example:
el.classList.add('my-test-class');
},
// when user clicks or taps on element
onClick: function (event, el, pswp) {
console.log('clicked element:', el);
}
});
```
All default buttons and elements also use this syntax, so you can look up more examples in folder `/src/js/ui/` within the repository.
If you need to override or slightly adjust existing buttons - feel free to use [`uiElement` filter](/filters#uielement).
`registerElement` is not the only method to add various UI elements, it's just an optional shortcut. Feel free to append elements manually.
================================================
FILE: docs/adjusting-zoom-level.md
================================================
---
id: adjusting-zoom-level
title: Adjusting Zoom Level
sidebar_label: Adjusting zoom level
---
Photoswipe has three zoom level options:
- `initialZoomLevel` - zoom level when photoswipe is opened.
- `secondaryZoomLevel` - zoom level when user clicks "zoom" button, double-taps image, or clicks an image. If it equals to initial - secondary zoom functionality is disabled.
- `maxZoomLevel` - maximum zoom level when user zooms via zoom/pinch gesture, via ctrl-wheel or via trackpad. Always highest among three.
## Supported values
Each zoom level option can be:
- A positive `Number`, where `1` is original image size.
- A `String`:
- `'fit'` - image fits into viewport.
- `'fill'` - image fills the viewport (similar to `background-size:cover`).
- In both cases image will not be larger than original.
- A `Function` that should return number. Use it to define dynamic zoom level. Function is called separately for each image when it is rendered, or resized, or lazy-loaded. For example to set custom `secondaryZoomLevel`:
```js
secondaryZoomLevel: (zoomLevelObject) => {
// zoomLevelObject is instance of ZoomLevel class
console.log('Element size:', zoomLevelObject.elementSize);
console.log('Pan area size (viewport minus padding):', zoomLevelObject.panAreaSize);
console.log('Item index:', zoomLevelObject.index);
console.log('Item data:', zoomLevelObject.itemData);
// return desired zoom level
return 1;
}
```
## Default behaviour
- Initial zoom level is `fit`.
- Secondary zoom level is `2.5x` of `fit`, but not wider than `3000px`.
- Maximum zoom level is `4x` of `fit`.
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery:'#gallery--open-in-original-size',
children:'a',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
## Open images in `fill` state
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery:'#gallery--open-in-fill-state',
children:'a',
initialZoomLevel: 'fill',
secondaryZoomLevel: 1,
maxZoomLevel: 2,
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
## Secondary zoom level is higher than initial
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery:'#gallery--secondary-zoom-higher',
children: 'a',
mouseMovePan: true,
initialZoomLevel: 'fit',
secondaryZoomLevel: 1.5,
maxZoomLevel: 1,
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
## Initial zoom level is higher than secondary
Initial zoom level is set to `1` (original image size), you may want to disable opening closing transition (`showHideAnimationType:'none'`) - as the larger the image - the harder it is to animate it smoothly. In this example it's not disabled, just to show you how it behaves:
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery:'#gallery--initial-higher',
children: 'a',
mouseMovePan: true,
initialZoomLevel: 1,
secondaryZoomLevel: 'fit',
maxZoomLevel: 4,
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
## Initial and secondary zoom level are equal
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery:'#gallery--zoom-levels-equal',
children: 'a',
mouseMovePan: true,
initialZoomLevel: 'fill',
secondaryZoomLevel: 'fill',
maxZoomLevel: 3,
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
## Image is smaller than initial and secondary
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery:'#very-small-image',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
```html pswpcode
```
## Dynamic zoom level
Change zoom levels based on viewport size and device orientation:
- fill 100% height of viewport on phones with portrait orientation,
- otherwise fit image into viewport
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
function isPhonePortrait() {
return window.matchMedia('(max-width: 600px) and (orientation: portrait)').matches;
}
const lightbox = new PhotoSwipeLightbox({
gallery:'#gallery--dynamic-zoom-level',
children:'a',
initialZoomLevel: (zoomLevelObject) => {
if (isPhonePortrait()) {
return zoomLevelObject.vFill;
} else {
return zoomLevelObject.fit;
}
},
secondaryZoomLevel: (zoomLevelObject) => {
if (isPhonePortrait()) {
return zoomLevelObject.fit;
} else {
return 1;
}
},
maxZoomLevel: 1,
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
lightbox.init();
```
================================================
FILE: docs/caption.md
================================================
---
id: caption
title: Caption
sidebar_label: Caption
---
PhotoSwipe does not support caption out of box, but you may implement a basic caption via API, as you can see below. Or you may use a [dynamic caption plugin](https://github.com/dimsemenov/photoswipe-dynamic-caption-plugin).
**Important!** Please make sure that caption is always accessible without PhotoSwipe for screen reader users — the lightbox is disabled in unsupported browsers. If you are unable to show the caption text on the page - make sure that image has a proper `alt` attribute, `aria-labelledby`, or `` inside ``.
import { captionTemplate } from '@site/src/components/PswpCodePreview/gallery-templates/caption.js';
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--with-custom-caption',
children:'.pswp-gallery__item',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'custom-caption',
order: 9,
isButton: false,
appendTo: 'root',
html: 'Caption text',
onInit: (el, pswp) => {
lightbox.pswp.on('change', () => {
const currSlideElement = lightbox.pswp.currSlide.data.element;
let captionHTML = '';
if (currSlideElement) {
const hiddenCaption = currSlideElement.querySelector('.hidden-caption-content');
if (hiddenCaption) {
// get caption from element with class hidden-caption-content
captionHTML = hiddenCaption.innerHTML;
} else {
// get caption from alt attribute
captionHTML = currSlideElement.querySelector('img').getAttribute('alt');
}
}
el.innerHTML = captionHTML || '';
});
}
});
});
lightbox.init();
```
```css pswpcode
.pswp__custom-caption {
background: rgba(75, 150, 75, 0.75);
font-size: 16px;
color: #fff;
width: calc(100% - 32px);
max-width: 400px;
padding: 2px 8px;
border-radius: 4px;
position: absolute;
left: 50%;
bottom: 16px;
transform: translateX(-50%);
}
.pswp__custom-caption a {
color: #fff;
text-decoration: underline;
}
.hidden-caption-content {
display: none;
}
```
================================================
FILE: docs/click-and-tap-actions.md
================================================
---
id: click-and-tap-actions
title: Click Actions
sidebar_label: Click actions
---
List of options:
- `imageClickAction` - click on image with mouse.
- `tapAction` - tap on PhotoSwipe viewport content (excluding buttons).
- `doubleTapAction` - double tap on anything. Tap delay is removed if this option is set to `false`.
- `bgClickAction` - click on area around image (background), with mouse.
## Supported action values
- `'zoom'` - zooms current image ([depending on secondary zoom level](adjusting-zoom-level.md)) (default `doubleTapAction`).
- `'zoom-or-close'` - image will be closed if it can not be zoomed (default `imageClickAction`).
- `'toggle-controls'` - toggle visibility of controls (default `tapAction`).
- `'next'` - move to the next slide
- `'close'` - close the gallery
- A Function that may perform any action, for example:
```js
imageClickAction: (releasePoint, e) => {}
```
## Click on image moves to the next slide
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--click-to-next',
children:'a',
imageClickAction: 'next',
tapAction: 'next',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.init();
```
## Disable tap delay, click/tap to close
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const options = {
gallery:'#gallery--click-to-close',
children:'a',
initialZoomLevel: 'fill',
secondaryZoomLevel: 'fit',
imageClickAction: 'close',
tapAction: 'close',
// tap delay is removed if set to false
doubleTapAction: false,
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
};
const lightbox = new PhotoSwipeLightbox(options);
lightbox.init();
```
```css pswpcode
/* override zoom cursor */
.pswp__img {
cursor: pointer !important;
}
```
================================================
FILE: docs/custom-content.md
================================================
---
id: custom-content
title: Custom Content in Slides
sidebar_label: Custom content
---
By default PhotoSwipe can only show images or raw HTML content, but you may use content events and filters to support new types.
Please note that PhotoSwipe is mainly designed to display photos. There are issues with displaying other types of content - for example, you can't swipe over iframes. Always have a fallback, for example if you embed Google Map - make sure that there is an outbound link to it.
## Using WebP image format
The example below uses `` instead of ` ` for slides that support webp. The webp image source is retrieved from `data-pswp-webp-src` attribute.
import { contentTypesTemplate } from '@site/src/components/PswpCodePreview/gallery-templates/content-types.js';
```js pswpcode
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '#gallery--webp-demo',
children: 'a',
pswpModule: () => import('/photoswipe/photoswipe.esm.js')
});
// Parse data-pswp-webp-src attribute
lightbox.addFilter('itemData', (itemData, index) => {
const webpSrc = itemData.element.dataset.pswpWebpSrc;
if (webpSrc) {
itemData.webpSrc = webpSrc;
}
return itemData;
});
// use instead of
lightbox.on('contentLoad', (e) => {
const { content, isLazy } = e;
if (content.data.webpSrc) {
// prevent to stop the default behavior
e.preventDefault();
content.pictureElement = document.createElement('picture');
const sourceWebp = document.createElement('source');
sourceWebp.srcset = content.data.webpSrc;
sourceWebp.type = 'image/webp';
const sourceJpg = document.createElement('source');
sourceJpg.srcset = content.data.src;
sourceJpg.type = 'image/jpeg';
content.element = document.createElement('img');
content.element.src = content.data.src;
content.element.setAttribute('alt', '');
content.element.className = 'pswp__img';
content.pictureElement.appendChild(sourceWebp);
content.pictureElement.appendChild(sourceJpg);
content.pictureElement.appendChild(content.element);
content.state = 'loading';
if (content.element.complete) {
content.onLoaded();
} else {
content.element.onload = () => {
content.onLoaded();
};
content.element.onerror = () => {
content.onError();
};
}
}
});
// by default PhotoSwipe appends ,
// but we want to append
lightbox.on('contentAppend', (e) => {
const { content } = e;
if (content.pictureElement && !content.pictureElement.parentNode) {
e.preventDefault();
content.slide.container.appendChild(content.pictureElement);
}
});
// for next/prev navigation with
// by default PhotoSwipe removes ,
// but we want to remove
lightbox.on('contentRemove', (e) => {
const { content } = e;
if (content.pictureElement && content.pictureElement.parentNode) {
e.preventDefault();
content.pictureElement.remove();
}
});
lightbox.init();
```
## Google Maps demo
Another example that shows a map `