├── .eslintignore ├── babel.config.js ├── tests └── unit │ ├── .eslintrc.js │ ├── mock.d.ts │ ├── __snapshots__ │ ├── photoswipe.spec.ts.snap │ ├── global.spec.ts.snap │ └── pswpUI.spec.ts.snap │ ├── utils.spec.ts │ ├── pswpUI.spec.ts │ ├── util.ts │ ├── global.spec.ts │ └── photoswipe.spec.ts ├── .postcssrc.js ├── types ├── shims-vue.d.ts ├── vue.d.ts └── index.d.ts ├── .travis.yml ├── dist ├── demo.html └── Photoswipe.umd.min.js ├── .editorconfig ├── .prettierignore ├── .gitignore ├── example ├── index.ts ├── video.vue └── sample.vue ├── vue.config.js ├── .prettierrc.js ├── src ├── main.ts ├── config.ts ├── type.ts ├── components │ ├── pswpUI.vue │ └── photoswipe.vue └── utils.ts ├── tsconfig.json ├── jest.config.js ├── .eslintrc.js ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/env'], 3 | } 4 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | 4 | export default Vue 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'lts/*' 4 | script: 'npm run test:coverage' 5 | after_success: 6 | - 'npx codecov' 7 | -------------------------------------------------------------------------------- /tests/unit/mock.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'photoswipe' { 2 | const mocked: jest.Mock 3 | export default mocked 4 | } 5 | 6 | interface Window { 7 | Image: typeof Image 8 | } 9 | -------------------------------------------------------------------------------- /dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Photoswipe demo 3 | 4 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ## OS 2 | .DS_Store 3 | .idea 4 | .editorconfig 5 | package-lock.json 6 | yarn.lock 7 | 8 | # Ignored suffix 9 | *.log 10 | *.md 11 | *.svg 12 | *.png 13 | *ignore 14 | *.gif 15 | 16 | ## Local 17 | 18 | 19 | ## Built-files 20 | .docz 21 | build 22 | dist 23 | dll 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw* 21 | -------------------------------------------------------------------------------- /types/vue.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Augment the typings of Vue.js 3 | */ 4 | 5 | import Vue from 'vue' 6 | import { Pswp, ManualCreateArgs } from '../src/type' 7 | 8 | interface $Pswp { 9 | open: (args: ManualCreateArgs) => Pswp 10 | current: Pswp 11 | } 12 | 13 | declare module 'vue/types/vue' { 14 | interface Vue { 15 | $Pswp: $Pswp 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PluginFunction } from 'vue' 2 | import Photoswipe from '@/components/photoswipe.vue' 3 | import { PswpItem, PswpOptions, PswpDirectiveOptions } from '../src/type' 4 | import './vue.d' 5 | 6 | declare const VuePswipe: PluginFunction 7 | 8 | export { PswpItem, PswpOptions, PswpDirectiveOptions, Photoswipe } 9 | 10 | export default VuePswipe 11 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import Vue, { CreateElement } from 'vue' // eslint-disable-line 2 | 3 | import PhotoswipePlugin from '../src/main' 4 | import Sample from './sample.vue' 5 | 6 | Vue.use(PhotoswipePlugin, { 7 | // history: true, 8 | }) 9 | 10 | /* eslint-disable no-new */ 11 | new Vue({ 12 | el: '#app', 13 | components: { 14 | Sample, 15 | }, 16 | render: (h: CreateElement) => h(Sample), 17 | }) 18 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: (config) => { 3 | config.module 4 | .rule('svg') 5 | .use('file-loader') 6 | .clear() 7 | .loader('url-loader') 8 | .options({ 9 | limit: 5 * 1024, 10 | name: 'img/[name].[hash:8].[ext]', 11 | }) 12 | }, 13 | css: { 14 | extract: false, 15 | }, 16 | lintOnSave: false, 17 | } 18 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 4, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | trailingComma: 'es5', 9 | bracketSpacing: true, 10 | arrowParens: 'always', 11 | rangeStart: 0, 12 | rangeEnd: Infinity, 13 | requirePragma: false, 14 | insertPragma: false, 15 | proseWrap: 'preserve', 16 | htmlWhitespaceSensitivity: 'css', 17 | endOfLine: 'lf', 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/photoswipe.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`photoswipe.vue :props :options render default options 1`] = ` 4 | "{ 5 | \\"galleryUID\\": 0, 6 | \\"history\\": false, 7 | \\"zoomEl\\": true, 8 | \\"shareEl\\": true, 9 | \\"shareButtons\\": [ 10 | { 11 | \\"id\\": \\"download\\", 12 | \\"label\\": \\"Download image\\", 13 | \\"url\\": \\"{{raw_image_url}}\\", 14 | \\"download\\": true 15 | } 16 | ], 17 | \\"index\\": 0, 18 | \\"showHideOpacity\\": false 19 | }" 20 | `; 21 | 22 | exports[`photoswipe.vue render Photoswipe 1`] = ``; 23 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction } from 'vue' // eslint-disable-line 2 | import { PswpOptions, ManualCreateArgs } from '@/type' 3 | import { GlobalOption } from '@/config' 4 | import { registerDirective, manualCreate, UI } from '@/utils' 5 | import PhotoswipeComponent from '@/components/photoswipe.vue' 6 | 7 | const install: PluginFunction = (Vue, options?: PswpOptions) => { 8 | if (options) GlobalOption.extend(options) 9 | 10 | registerDirective() 11 | 12 | Vue.component('Photoswipe', PhotoswipeComponent) 13 | 14 | // eslint-disable-next-line no-param-reassign 15 | Vue.prototype.$Pswp = { 16 | open(args: ManualCreateArgs) { 17 | UI.append() 18 | return manualCreate(args) 19 | }, 20 | } 21 | } 22 | 23 | export const Photoswipe = PhotoswipeComponent 24 | export default install 25 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { PswpOptions } from '@/type' 2 | import { isMobile } from '@/utils' 3 | 4 | export const customEvents: string[] = ['beforeOpen', 'opened'] 5 | 6 | const _isMobile = isMobile() 7 | export const defualtGlobalOption: PswpOptions = { 8 | // in spa no need history mode 9 | history: false, 10 | zoomEl: !_isMobile, 11 | shareEl: !_isMobile, 12 | shareButtons: [ 13 | { 14 | id: 'download', 15 | label: 'Download image', 16 | url: '{{raw_image_url}}', 17 | download: true, 18 | }, 19 | ], 20 | } 21 | 22 | export namespace GlobalOption { 23 | const _options: PswpOptions = defualtGlobalOption 24 | export const get = () => _options 25 | export const extend = (...partials: Partial[]) => { 26 | Object.assign(_options, ...partials) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": [ 16 | "src/*" 17 | ] 18 | }, 19 | "lib": [ 20 | "esnext", 21 | "dom", 22 | "dom.iterable", 23 | "scripthost" 24 | ] 25 | }, 26 | "include": [ 27 | "types/**/*.ts", 28 | "example/**/*.ts", 29 | "example/**/*.vue", 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue', 'ts', 'tsx'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 6 | '^.+\\.tsx?$': 'ts-jest', 7 | }, 8 | transformIgnorePatterns: ['/node_modules/'], 9 | coveragePathIgnorePatterns: ['/tests/unit/'], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1', 12 | }, 13 | snapshotSerializers: ['jest-serializer-vue'], 14 | testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'], 15 | testURL: 'http://localhost/', 16 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], 17 | globals: { 18 | 'ts-jest': { 19 | babelConfig: true, 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/airbnb', '@vue/typescript'], 7 | rules: { 8 | indent: ['error', 4], 9 | semi: ['error', 'never'], 10 | 'no-tabs': 'off', 11 | 'function-paren-newline': 'off', 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'no-return-assign': 'off', 15 | 'no-nested-ternary': 'off', 16 | 'consistent-return': 'off', 17 | 'class-methods-use-this': 'off', 18 | camelcase: 'off', 19 | 'import/no-extraneous-dependencies': 'off', 20 | 'no-unused-expressions': 'off', 21 | 'no-underscore-dangle': 'off', 22 | 'arrow-parens': 'off', 23 | 'comma-dangle': 'off', 24 | }, 25 | parserOptions: { 26 | parser: 'typescript-eslint-parser', 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/global.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`global function v-pswp directive transform to custom dataset 1`] = ` 4 | 12 | `; 13 | 14 | exports[`global function v-pswp directive update directive value 1`] = ` 15 | 23 | `; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GuoQichen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/pswpUI.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`pswpUI.vue render PswpUI 1`] = ` 4 | 33 | `; 34 | -------------------------------------------------------------------------------- /example/video.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 59 | 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pswipe", 3 | "version": "0.15.3", 4 | "description": "a vue plugin for photoswipe", 5 | "author": "guoqichen", 6 | "scripts": { 7 | "build": "vue-cli-service build --target lib --name Photoswipe ./src/main.ts", 8 | "dev": "vue-cli-service serve ./example/index.ts", 9 | "report": "npm run build -- --report", 10 | "test": "vue-cli-service test:unit --silent", 11 | "test:coverage": "npm test -- --coverage --silent" 12 | }, 13 | "main": "dist/Photoswipe.umd.min.js", 14 | "dependencies": { 15 | "@types/photoswipe": "^4.1.1", 16 | "photoswipe": "^4.1.3" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^23.1.4", 20 | "@vue/cli-plugin-babel": "^3.9.2", 21 | "@vue/cli-plugin-eslint": "^3.9.2", 22 | "@vue/cli-plugin-typescript": "^3.9.0", 23 | "@vue/cli-plugin-unit-jest": "^3.9.0", 24 | "@vue/cli-service": "^3.9.3", 25 | "@vue/eslint-config-airbnb": "^3.0.5", 26 | "@vue/eslint-config-typescript": "^3.2.1", 27 | "@vue/test-utils": "1.0.0-beta.29", 28 | "babel-core": "7.0.0-bridge.0", 29 | "codecov": "^3.5.0", 30 | "sass": "^1.22.7", 31 | "sass-loader": "^7.1.0", 32 | "ts-jest": "^23.0.0", 33 | "typescript": "^3.5.3", 34 | "video.js": "^5.20.5", 35 | "vue": "^2.6.10", 36 | "vue-class-component": "^6.3.2", 37 | "vue-property-decorator": "^7.3.0", 38 | "vue-template-compiler": "^2.6.10" 39 | }, 40 | "browserslist": [ 41 | "> 1%", 42 | "last 2 versions", 43 | "not ie <= 8" 44 | ], 45 | "keywords": [ 46 | "vue", 47 | "photoswipe" 48 | ], 49 | "license": "MIT", 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/GuoQichen/vue-pswipe.git" 53 | }, 54 | "types": "types/index.d.ts" 55 | } 56 | -------------------------------------------------------------------------------- /tests/unit/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import '@/config' 2 | import { Event } from '@/utils' 3 | 4 | const MOCK_EVENT = 'mockEvent' 5 | 6 | describe('Event', () => { 7 | let mockFn: jest.Mock 8 | beforeEach(() => { 9 | mockFn = jest.fn() 10 | }) 11 | 12 | afterEach(() => { 13 | mockFn.mockReset() 14 | Event.off(MOCK_EVENT) 15 | }) 16 | 17 | describe('emit', () => { 18 | it('invoke register function (without args)', () => { 19 | Event.on(MOCK_EVENT, mockFn) 20 | Event.emit(MOCK_EVENT) 21 | expect(mockFn).toBeCalled() 22 | }) 23 | 24 | it('invoke register function (with args)', () => { 25 | const args = [1, 2, 3] 26 | Event.on(MOCK_EVENT, mockFn) 27 | Event.emit(MOCK_EVENT, ...args) 28 | expect(mockFn).toBeCalledWith(...args) 29 | }) 30 | 31 | it('no invoke unregister function (registered event)', () => { 32 | Event.emit(MOCK_EVENT, mockFn) 33 | expect(mockFn).not.toBeCalled() 34 | }) 35 | 36 | it('no invoke unregister function (unregister event)', () => { 37 | Event.emit('otherMockEvent', mockFn) 38 | expect(mockFn).not.toBeCalled() 39 | }) 40 | }) 41 | 42 | describe('once', () => { 43 | it('invoke register function once', () => { 44 | Event.once(MOCK_EVENT, mockFn) 45 | Event.emit(MOCK_EVENT) 46 | Event.emit(MOCK_EVENT) 47 | expect(mockFn).toBeCalledTimes(1) 48 | }) 49 | }) 50 | 51 | describe('off', () => { 52 | it('clear all register function by event name', () => { 53 | const otherMockFn = jest.fn() 54 | Event.on(MOCK_EVENT, mockFn) 55 | Event.on(MOCK_EVENT, otherMockFn) 56 | Event.off(MOCK_EVENT) 57 | expect(mockFn).not.toBeCalled() 58 | expect(otherMockFn).not.toBeCalled() 59 | }) 60 | 61 | it('clear register function by returned value by on', () => { 62 | Event.on(MOCK_EVENT, mockFn)() 63 | expect(mockFn).not.toBeCalled() 64 | }) 65 | 66 | it('clear register function by function ref (correct ref)', () => { 67 | Event.on(MOCK_EVENT, mockFn) 68 | Event.off(MOCK_EVENT, mockFn) 69 | expect(mockFn).not.toBeCalled() 70 | }) 71 | 72 | it('clear register function by function ref (incorrect ref)', () => { 73 | Event.on(MOCK_EVENT, mockFn) 74 | Event.off(MOCK_EVENT, jest.fn()) 75 | Event.emit(MOCK_EVENT) 76 | expect(mockFn).toBeCalled() 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /tests/unit/pswpUI.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import { PhotoSwipe, PhotoSwipeMock, createPswp, createPswpUI, mockImageOnload } from './util' 3 | 4 | import '@/config' 5 | import { transitionEndEventName } from '@/utils' 6 | import { CurrentPswpItem } from '@/type' 7 | 8 | describe('pswpUI.vue', () => { 9 | beforeEach(() => { 10 | expect(PhotoSwipe).not.toHaveBeenCalled() 11 | }) 12 | 13 | afterEach(() => { 14 | PhotoSwipe.mockClear() 15 | }) 16 | 17 | it('render PswpUI ', () => { 18 | expect(createPswpUI()).toMatchSnapshot() 19 | }) 20 | 21 | describe('rotate function', () => { 22 | const createRotatePswp = () => { 23 | const pswpUIWrapper = createPswpUI() 24 | 25 | const pswpWrapper = createPswp({ 26 | propsData: { 27 | rotate: true, 28 | }, 29 | }) 30 | 31 | const pswp = PhotoSwipeMock.getPswp() 32 | 33 | return { 34 | pswp, 35 | pswpWrapper, 36 | pswpUIWrapper, 37 | } 38 | } 39 | 40 | it('render rotate buttons', () => { 41 | const pswpUIWrapper = createPswpUI() 42 | createPswp() 43 | const getRotateButton = () => pswpUIWrapper.findAll('.pswp__button--rotation') 44 | const pswp = PhotoSwipeMock.getPswp() 45 | 46 | expect(getRotateButton().length).toBe(0) 47 | 48 | pswpUIWrapper.setData({ 49 | rotate: true, 50 | }) 51 | expect(getRotateButton().length).toBe(2) 52 | 53 | pswp.destroy() 54 | expect(getRotateButton().length).toBe(0) 55 | }) 56 | 57 | it('handleRotate', () => { 58 | mockImageOnload.enable() 59 | const { pswp, pswpUIWrapper } = createRotatePswp() 60 | const currentItem: CurrentPswpItem = pswp.currItem as any 61 | const img: HTMLImageElement = currentItem.container.lastChild as any 62 | 63 | const rotate = (direction: 'left' | 'right') => { 64 | pswpUIWrapper.find(`.pswp__button--rotation--${direction}`).trigger('pswpTap') 65 | img.dispatchEvent(new Event(transitionEndEventName)) 66 | } 67 | const expectRotateDeg = (deg: number) => { 68 | expect(img.style.transform).toContain(`rotate(${deg}deg)`) 69 | } 70 | 71 | expect(img.style.transform).toBeFalsy() 72 | 73 | rotate('right') 74 | expectRotateDeg(90) 75 | 76 | // vertical 77 | rotate('right') 78 | expectRotateDeg(180) 79 | 80 | rotate('left') 81 | expectRotateDeg(90) 82 | mockImageOnload.disable() 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Options, Item } from 'photoswipe/dist/photoswipe-ui-default' 3 | 4 | /** 5 | * v-pswp directive config options 6 | */ 7 | export interface PswpDirectiveOptions { 8 | /** 9 | * path to image 10 | */ 11 | src: string 12 | /** 13 | * image size, 'width x height', eg: '100x100' 14 | */ 15 | size?: string 16 | /** 17 | * small image placeholder, 18 | * main (large) image loads on top of it, 19 | * if you skip this parameter - grey rectangle will be displayed, 20 | * try to define this property only when small image was loaded before 21 | */ 22 | msrc?: string 23 | /** 24 | * used by Default PhotoSwipe UI 25 | * if you skip it, there won't be any caption 26 | */ 27 | title?: string 28 | /** 29 | * to make URLs to a single image look like this: http://example.com/#&gid=1&pid=custom-first-id 30 | * instead of: http://example.com/#&gid=1&pid=1 31 | * enable options history:true, galleryPIDs:true and add pid (unique picture identifier) 32 | */ 33 | pid?: string | number 34 | } 35 | 36 | export interface PswpItem extends Item { 37 | el: HTMLElement 38 | src: string 39 | msrc?: string 40 | pid?: number | string 41 | verticalRotated?: boolean 42 | } 43 | 44 | export interface ManualImgItem extends Partial { 45 | src: string 46 | } 47 | 48 | export type ManualHtmlItem = Partial 49 | 50 | export interface CurrentPswpItem extends PswpItem { 51 | container: HTMLElement 52 | loaded: boolean 53 | } 54 | 55 | export type PswpOptions = Options 56 | 57 | export interface OpenPhotoSwipeArgs { 58 | index: number | string 59 | fromURL?: boolean 60 | thumbEls?: HTMLElement[] 61 | } 62 | 63 | export interface BeforeOpenEvent { 64 | index: number 65 | target: HTMLElement 66 | items: PswpItem[] 67 | options: PswpOptions 68 | } 69 | 70 | export interface Size { 71 | w: number 72 | h: number 73 | } 74 | 75 | export interface ManualCreateArgs { 76 | items: ManualOpenItem[] 77 | options?: PswpOptions 78 | } 79 | 80 | interface CreatePhotoSwipeArgs extends ManualCreateArgs { 81 | context?: Vue 82 | } 83 | 84 | export interface PswpProps { 85 | options: PswpOptions 86 | auto: boolean 87 | bubble: boolean 88 | lazy: boolean 89 | filter: Function 90 | rotate: boolean 91 | } 92 | 93 | // types 94 | export type Fn = (...args: any[]) => void 95 | 96 | export type Pswp = PhotoSwipe 97 | 98 | export type BeforeOpen = (continued?: boolean) => void 99 | 100 | export type Filter = (img: HTMLImageElement) => boolean 101 | 102 | export type FindIndex = (array: T[], predicate: (item: T, idx: number) => boolean) => number 103 | 104 | export type Closest = ( 105 | el: Node | null, 106 | predicate: (el: HTMLElement) => boolean 107 | ) => HTMLElement | false 108 | 109 | export type Single = (fn: Function) => (...args: any[]) => T 110 | 111 | export type BindEvent = (pswp: Pswp, context?: Vue) => void 112 | 113 | export type CreatePhotoSwipe = (arg: CreatePhotoSwipeArgs) => Pswp 114 | 115 | export type HandleWithoutSize = (pswp: Pswp) => void 116 | 117 | export type GetContainSize = ( 118 | areaWidth: number, 119 | areaHeight: number, 120 | width: number, 121 | height: number 122 | ) => Size 123 | 124 | export type RotateDirection = 'left' | 'right' 125 | 126 | export type StyleKey = Exclude 127 | 128 | export type ManualOpenItem = ManualImgItem | ManualHtmlItem 129 | -------------------------------------------------------------------------------- /tests/unit/util.ts: -------------------------------------------------------------------------------- 1 | import PhotoSwipe from 'photoswipe' 2 | import defaultUI from 'photoswipe/dist/photoswipe-ui-default' 3 | import { 4 | mount, 5 | createLocalVue, 6 | shallowMount, 7 | ShallowMountOptions, 8 | MountOptions, 9 | } from '@vue/test-utils' 10 | import Photoswipe from '@/components/photoswipe.vue' 11 | import PswpUI from '@/components/pswpUI.vue' 12 | import VuePswipe from '@/main' 13 | import { UI } from '@/utils' 14 | import { Pswp } from '@/type' 15 | import { Component } from 'vue' 16 | 17 | /** 18 | * mock photoswipe 19 | */ 20 | jest.mock('photoswipe', () => { 21 | const OriginalPhotoSwipe = jest.requireActual('photoswipe') 22 | return jest.fn((...args: any[]) => new OriginalPhotoSwipe(...args)) 23 | }) 24 | 25 | export { PhotoSwipe } 26 | 27 | /** 28 | * handy methods 29 | */ 30 | export namespace PhotoSwipeMock { 31 | export const getReceiveItems = () => PhotoSwipe.mock.calls[0][2] 32 | export const getReceiveOptions = () => PhotoSwipe.mock.calls[0][3] 33 | export const getPswp = () => (PhotoSwipe.mock.results[0].value as any) as Pswp 34 | } 35 | 36 | /** 37 | * create PhotoSwipe instance 38 | */ 39 | export const fakeSrc = 'https://placeimg.com/640/480/any' 40 | const imgSlots = `` 41 | 42 | const commonLocalVue = createLocalVue() 43 | commonLocalVue.use(VuePswipe) 44 | 45 | export interface CreatePswpOptions extends MountOptions { 46 | defaultSlots?: string | Component | string | Component[] 47 | withClick?: boolean 48 | } 49 | 50 | export const createPswp = ({ 51 | defaultSlots = imgSlots, 52 | localVue = commonLocalVue, 53 | withClick = true, 54 | ...options 55 | }: CreatePswpOptions = {}) => { 56 | const wrapper = mount(Photoswipe, { 57 | localVue, 58 | slots: { 59 | default: defaultSlots, 60 | }, 61 | ...options, 62 | }) 63 | 64 | withClick && wrapper.find('img').trigger('click') 65 | return wrapper 66 | } 67 | 68 | export const createPswpUI = (options?: ShallowMountOptions) => 69 | shallowMount(PswpUI, { 70 | localVue: commonLocalVue, 71 | ...options, 72 | }) 73 | 74 | /** 75 | * mock load image resource 76 | */ 77 | export namespace mockImageOnload { 78 | const loadedImgs = new Set() 79 | 80 | const originalImgProtoDesc = Object.getOwnPropertyDescriptor( 81 | window.Image.prototype, 82 | 'src' 83 | ) as PropertyDescriptor 84 | 85 | const mockImgProtoDesc = { 86 | set(this: HTMLImageElement, value: string) { 87 | if (originalImgProtoDesc.set) { 88 | originalImgProtoDesc.set.call(this, value) 89 | } 90 | loadedImgs.add(value) 91 | this.dispatchEvent(new CustomEvent('load')) 92 | const [, width = 0, height = 0] = 93 | value.match(/https:\/\/placeimg.com\/(\d+)\/(\d+)\/any/) || [] 94 | this.width = +width 95 | this.height = +height 96 | }, 97 | } as PropertyDescriptor 98 | 99 | const setImgProtoSrc = (enableMock: boolean) => { 100 | const desc = enableMock ? mockImgProtoDesc : originalImgProtoDesc 101 | 102 | Object.defineProperty(window.Image.prototype, 'src', desc) 103 | } 104 | 105 | export const enable = () => setImgProtoSrc(true) 106 | export const disable = () => { 107 | setImgProtoSrc(false) 108 | loadedImgs.clear() 109 | } 110 | export const isLoaded = (srcs: string[]) => srcs.every((src) => loadedImgs.has(src)) 111 | } 112 | 113 | /** 114 | * get fake images 115 | */ 116 | const getRandomSize = () => Math.floor(Math.random() * 1e3 + 1e2) 117 | 118 | const getRandomImgSrc = () => { 119 | const width: number = getRandomSize() 120 | const height: number = getRandomSize() 121 | return `https://placeimg.com/${width}/${height}/any` 122 | } 123 | 124 | export const getFakeImages = (length: number = 1): string[] => 125 | [...Array(length).keys()].map(() => getRandomImgSrc()) 126 | 127 | export const createProtoPswp = () => { 128 | const buttonTemplate = '' 129 | const slideHtml = 130 | '

Hello world example.com

' 131 | 132 | const wrapper = mount( 133 | { 134 | template: buttonTemplate, 135 | methods: { 136 | handleClick() { 137 | this.$Pswp.open({ 138 | items: [{ html: slideHtml }], 139 | }) 140 | }, 141 | }, 142 | }, 143 | { 144 | localVue: commonLocalVue, 145 | } 146 | ) 147 | 148 | wrapper.find('button').trigger('click') 149 | 150 | return wrapper 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-pswipe ![npm](https://img.shields.io/npm/v/vue-pswipe) [![Build Status](https://travis-ci.com/GuoQichen/vue-pswipe.svg?branch=master)](https://travis-ci.com/GuoQichen/vue-pswipe) [![codecov](https://codecov.io/gh/GuoQichen/vue-pswipe/branch/master/graph/badge.svg)](https://codecov.io/gh/GuoQichen/vue-pswipe) 2 | a Vue plugin for PhotoSwipe without set image size 3 | 4 | ## online example 5 | [![Edit Vue Template](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/619x48656r) 6 | 7 | ## install 8 | ``` 9 | npm install vue-pswipe 10 | ``` 11 | 12 | ## usage 13 | 14 | ```js 15 | // main.js 16 | import Photoswipe from 'vue-pswipe' 17 | 18 | Vue.use(Photoswipe, options) 19 | ``` 20 | see [complete options](http://photoswipe.com/documentation/options.html) 21 | 22 | you can set `v-pswp` directive in element to mark as clickable 23 | ```vue 24 | 25 | 29 | 30 | ``` 31 | 32 | ## props 33 | 34 | | Property | Type | Description | Default | 35 | | --- | --- | --- | --- | 36 | | options | object | original PhotoSwipe options, see [complete options](http://photoswipe.com/documentation/options.html) | - | 37 | | auto | boolean | automatically collect all img tags without the need for the `v-pswp` directive | false | 38 | | bubble | boolean | allow click event bubbling | false | 39 | | lazy | boolean | lazy loading image, you can set to false to preload all image | true | 40 | | rotate | boolean | add a rotate action button to the top bar, allow user to rotate the current image | false | 41 | 42 | ## directive 43 | 44 | ### `v-pswp: object|string` 45 | use for mark current element as gallery item, accept **image src** or **options object** 46 | 47 | Directive Options: 48 | ```typescript 49 | interface PswpDirectiveOptions { 50 | /** 51 | * path to image 52 | */ 53 | src: string 54 | /** 55 | * image size, 'width x height', eg: '100x100' 56 | */ 57 | size?: string 58 | /** 59 | * small image placeholder, 60 | * main (large) image loads on top of it, 61 | * if you skip this parameter - grey rectangle will be displayed, 62 | * try to define this property only when small image was loaded before 63 | */ 64 | msrc?: string 65 | /** 66 | * used by Default PhotoSwipe UI 67 | * if you skip it, there won't be any caption 68 | */ 69 | title?: string 70 | /** 71 | * to make URLs to a single image look like this: http://example.com/#&gid=1&pid=custom-first-id 72 | * instead of: http://example.com/#&gid=1&pid=1 73 | * enable options history: true, galleryPIDs: true and add pid (unique picture identifier) 74 | */ 75 | pid?: string | number 76 | } 77 | ``` 78 | 79 | ## event 80 | 81 | ### `beforeOpen` 82 | emit after click thumbnail, if listen to this event, **`next` function must be called to resolve this hook** 83 | 84 | Parameters: 85 | - `event`: 86 | - `index`: current image index 87 | - `target`: the target that triggers effective click event 88 | - `next`: 89 | 90 | must be called to resolve the hook. `next(false)` will abort open PhotoSwipe 91 | 92 | ### `opened` 93 | emit after photoswipe init, you can get current active photoswipe instance by parameter 94 | 95 | Parameters: 96 | - `pswp`: 97 | 98 | current photoswipe instance 99 | 100 | ### original PhotoSwipe event 101 | **support all original PhotoSwipe events**, see [original event](https://github.com/dimsemenov/PhotoSwipe/blob/master/website/documentation/api.md#events), eg: 102 | ```vue 103 | 104 | 108 | 109 | ``` 110 | 111 | WARNING: If you using Photoswipe component in HTML, not in a SFC, use `v-on` instead, because HTML tag and attributes are case insensitive 112 | ```vue 113 | 114 | 118 | 119 | ``` 120 | 121 | ## custom html 122 | In addition to using the `` tag, you can also use `Vue.prototype.$Pswp.open(params)` to directly open a PhotoSwipe. This is especially useful in the case of [Custom HTML Content in Slides](https://photoswipe.com/documentation/custom-html-in-slides.html). 123 | ```vue 124 | 127 | 142 | ``` 143 | 144 | `Vue.prototyp.$Pswp.open`: 145 | ```typescript 146 | type Open = (params: { 147 | items: PswpItem[], 148 | options?: PswpOptions 149 | }) => pswp 150 | ``` 151 | 152 | ## dynamic import 153 | **But cannot use `vue.prototype.$Pswp.open()`** 154 | ```vue 155 | 163 | ``` 164 | 165 | ## example 166 | ``` 167 | npm run dev 168 | ``` 169 | 170 | ## License 171 | [MIT](http://opensource.org/licenses/MIT) 172 | -------------------------------------------------------------------------------- /example/sample.vue: -------------------------------------------------------------------------------- 1 | 73 | 177 | 190 | -------------------------------------------------------------------------------- /tests/unit/global.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import { 3 | PhotoSwipe, 4 | PhotoSwipeMock, 5 | createPswp, 6 | fakeSrc, 7 | CreatePswpOptions, 8 | createProtoPswp, 9 | } from './util' 10 | 11 | import { createLocalVue } from '@vue/test-utils' 12 | import VuePswipe from '@/main' 13 | import Vue, { ComponentOptions } from 'vue' 14 | import { PswpDirectiveOptions } from '@/type' 15 | 16 | describe('global function', () => { 17 | beforeEach(() => { 18 | expect(PhotoSwipe).not.toHaveBeenCalled() 19 | }) 20 | 21 | afterEach(() => { 22 | PhotoSwipe.mockClear() 23 | }) 24 | 25 | it('global options', () => { 26 | const bgOpacity = 0.5 27 | const optionsLocalVue = createLocalVue() 28 | optionsLocalVue.use(VuePswipe, { 29 | bgOpacity, 30 | }) 31 | 32 | createPswp({ 33 | localVue: optionsLocalVue, 34 | }) 35 | 36 | const receiveOptions = PhotoSwipeMock.getReceiveOptions() 37 | expect(receiveOptions.bgOpacity).toBe(bgOpacity) 38 | }) 39 | 40 | it('allow listening to the original PhotoSwipe event', () => { 41 | const mock = jest.fn() 42 | 43 | createPswp({ 44 | listeners: { 45 | gettingData: mock, 46 | }, 47 | }) 48 | 49 | expect(mock).toBeCalled() 50 | }) 51 | 52 | describe('history mode', () => { 53 | const createHashPswp = (hashPath: string, options?: CreatePswpOptions) => { 54 | window.history.pushState({}, '', hashPath) 55 | 56 | const wrapper = createPswp({ 57 | withClick: false, 58 | attachToDocument: true, 59 | propsData: { 60 | options: { 61 | history: true, 62 | }, 63 | }, 64 | ...options, 65 | }) 66 | 67 | document.body.appendChild(wrapper.vm.$el) 68 | 69 | return wrapper 70 | } 71 | 72 | it('with history hash', () => { 73 | const wrapper = createHashPswp('/#&gid=1&pid=1') 74 | 75 | expect(PhotoSwipe).toBeCalled() 76 | wrapper.destroy() 77 | }) 78 | 79 | it('with custom hash', () => { 80 | const customPid = 'custom-first-id' 81 | 82 | const wrapper = createHashPswp(`/#&gid=1&pid=${customPid}`, { 83 | defaultSlots: ` 84 | 89 | `, 90 | propsData: { 91 | options: { 92 | history: true, 93 | galleryPIDs: true, 94 | }, 95 | }, 96 | }) 97 | 98 | expect(PhotoSwipe).toBeCalled() 99 | wrapper.destroy() 100 | }) 101 | 102 | it('with custom hash and set options.galleryPIDs to false', () => { 103 | const customPid = 'custom-first-id' 104 | 105 | expect(() => { 106 | createHashPswp(`/#&gid=1&pid=${customPid}`, { 107 | propsData: { 108 | options: { 109 | galleryPIDs: false, 110 | }, 111 | }, 112 | }) 113 | }).toThrowError( 114 | '[vue-pswipe] PhotoSwipe cannot be opened because the index is invalid. If you use a custom pid, set options.galleryPIDs to true.' 115 | ) 116 | }) 117 | }) 118 | 119 | describe('v-pswp directive', () => { 120 | const defaultPswpItem = { 121 | src: 'https://farm4.staticflickr.com/3894/15008518202_c265dfa55f_h.jpg', 122 | size: '1600x1600', 123 | msrc: 'https://farm4.staticflickr.com/3894/15008518202_b016d7d289_m.jpg', 124 | title: 'this is dummy caption', 125 | pid: 'custom-pid', 126 | } 127 | 128 | const createDirectivePswp = ({ 129 | pswpItem = defaultPswpItem, 130 | ...slotOptions 131 | }: createDirectivePswpArgs = {}) => 132 | createPswp({ 133 | defaultSlots: { 134 | data: () => ({ 135 | pswpItem, 136 | }), 137 | template: ` 138 | 142 | `, 143 | ...slotOptions, 144 | }, 145 | }) 146 | 147 | it('transform to custom dataset', () => { 148 | const wrapper = createDirectivePswp() 149 | 150 | expect(wrapper.find('img').vm.$el).toMatchSnapshot() 151 | }) 152 | 153 | it('data-pswp-src shorthand', () => { 154 | const wrapper = createDirectivePswp({ 155 | pswpItem: fakeSrc, 156 | }) 157 | 158 | const el = wrapper.find('img').vm.$el as HTMLElement 159 | 160 | expect(el.dataset.pswpSrc).toBe(fakeSrc) 161 | }) 162 | 163 | it('update directive value', () => { 164 | const anotherPswpItem = { 165 | src: 'https://farm6.staticflickr.com/5591/15008867125_b61960af01_h.jpg', 166 | size: '1600x1068', 167 | msrc: 'https://farm6.staticflickr.com/5591/15008867125_68a8ed88cc_m.jpg', 168 | title: 'this is another dummy caption', 169 | pid: 'another-custom-pid', 170 | } 171 | 172 | const wrapper = createDirectivePswp({ 173 | methods: { 174 | changePswpItem() { 175 | this.pswpItem = anotherPswpItem 176 | }, 177 | }, 178 | }) 179 | 180 | const img = wrapper.find('img') 181 | const vm = img.vm as ImageComponent 182 | 183 | vm.changePswpItem() 184 | expect(vm.$el).toMatchSnapshot() 185 | }) 186 | }) 187 | 188 | describe('Vue.prototype.$Pswp', () => { 189 | it('open()', () => { 190 | createProtoPswp() 191 | expect(PhotoSwipe).toBeCalled() 192 | }) 193 | }) 194 | }) 195 | 196 | interface ImageComponent extends Vue { 197 | pswpItem: PswpDirectiveOptions 198 | changePswpItem: () => void 199 | } 200 | 201 | type ImageOptions = ComponentOptions 202 | 203 | interface createDirectivePswpArgs extends ImageOptions { 204 | pswpItem?: string | PswpDirectiveOptions 205 | } 206 | -------------------------------------------------------------------------------- /src/components/pswpUI.vue: -------------------------------------------------------------------------------- 1 | 83 | 160 | 179 | -------------------------------------------------------------------------------- /src/components/photoswipe.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 229 | 238 | -------------------------------------------------------------------------------- /tests/unit/photoswipe.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import { 3 | PhotoSwipe, 4 | PhotoSwipeMock, 5 | createPswp, 6 | createPswpUI, 7 | fakeSrc, 8 | mockImageOnload, 9 | getFakeImages, 10 | CreatePswpOptions, 11 | } from './util' 12 | 13 | import { BeforeOpenEvent, BeforeOpen } from '@/type' 14 | 15 | describe('photoswipe.vue', () => { 16 | beforeEach(() => { 17 | expect(PhotoSwipe).not.toHaveBeenCalled() 18 | }) 19 | 20 | afterEach(() => { 21 | PhotoSwipe.mockClear() 22 | }) 23 | 24 | it('render Photoswipe', () => { 25 | expect( 26 | createPswp({ 27 | withClick: false, 28 | }) 29 | ).toMatchSnapshot() 30 | }) 31 | 32 | describe(':props', () => { 33 | describe(':options', () => { 34 | const createOptionsPswp = (options: Record = {}) => { 35 | createPswp({ 36 | propsData: { 37 | options, 38 | }, 39 | }) 40 | 41 | return PhotoSwipeMock.getReceiveOptions() 42 | } 43 | 44 | it('render default options', () => { 45 | const options = createOptionsPswp() 46 | expect(JSON.stringify(options, null, 2)).toMatchSnapshot() 47 | }) 48 | 49 | it('set options', () => { 50 | const bgOpacity = 0.5 51 | const options = createOptionsPswp({ 52 | bgOpacity, 53 | }) 54 | 55 | expect(options.bgOpacity).toBe(bgOpacity) 56 | }) 57 | 58 | it('set options.showHideOpacity', () => { 59 | const options = createOptionsPswp({ 60 | showHideOpacity: true, 61 | }) 62 | 63 | expect(options.showHideOpacity).toBe(true) 64 | }) 65 | }) 66 | 67 | describe(':auto', () => { 68 | const createAutoPswp = (auto: boolean, options?: CreatePswpOptions) => 69 | createPswp({ 70 | defaultSlots: ``, 71 | propsData: { 72 | auto, 73 | }, 74 | ...options, 75 | }) 76 | 77 | it('without auto', () => { 78 | createAutoPswp(false) 79 | expect(PhotoSwipe).not.toBeCalled() 80 | }) 81 | 82 | it('with auto', () => { 83 | createAutoPswp(true) 84 | expect(PhotoSwipe).toBeCalled() 85 | }) 86 | 87 | it('with auto and empty img', () => { 88 | createAutoPswp(true, { 89 | defaultSlots: ``, 90 | }) 91 | 92 | expect(PhotoSwipeMock.getReceiveItems()).toEqual([ 93 | expect.objectContaining({ 94 | msrc: '', 95 | src: '', 96 | }), 97 | ]) 98 | }) 99 | }) 100 | 101 | describe(':bubble', () => { 102 | const createBubblePswp = (bubble: boolean) => { 103 | createPswp({ 104 | defaultSlots: ` 105 |
106 | 107 |
108 | `, 109 | propsData: { 110 | bubble, 111 | }, 112 | }) 113 | } 114 | 115 | it('without bubble', () => { 116 | createBubblePswp(false) 117 | expect(PhotoSwipe).not.toBeCalled() 118 | }) 119 | 120 | it('with bubble', () => { 121 | createBubblePswp(true) 122 | expect(PhotoSwipe).toBeCalled() 123 | }) 124 | }) 125 | 126 | describe(':rotate', () => { 127 | it('set pswpUI rotate', () => { 128 | const pswpUIWrapper = createPswpUI() 129 | 130 | createPswp({ 131 | propsData: { 132 | rotate: true, 133 | }, 134 | }) 135 | 136 | expect(pswpUIWrapper.vm.$data.rotate).toBe(true) 137 | }) 138 | }) 139 | 140 | describe(':lazy', () => { 141 | const createLazyPswp = (lazy: boolean, fakeImageLength: number) => { 142 | const fakeImages = getFakeImages(fakeImageLength) 143 | 144 | const wrapper = createPswp({ 145 | withClick: false, 146 | defaultSlots: fakeImages.map((src, index) => ({ 147 | data: () => ({ 148 | pswpItem: { 149 | src, 150 | }, 151 | }), 152 | template: ` 153 | 158 | `, 159 | })), 160 | propsData: { 161 | lazy, 162 | }, 163 | }) 164 | 165 | return { 166 | wrapper, 167 | fakeImages, 168 | } 169 | } 170 | 171 | beforeEach(() => { 172 | mockImageOnload.enable() 173 | }) 174 | 175 | afterEach(() => { 176 | mockImageOnload.disable() 177 | }) 178 | 179 | it('only load current, the previous and the next', () => { 180 | const len = 10 181 | const { wrapper, fakeImages } = createLazyPswp(true, len) 182 | 183 | wrapper.find('#img-5').trigger('click') 184 | const shouldLoadedIndex = [4, 5, 6] 185 | 186 | expect(mockImageOnload.isLoaded(shouldLoadedIndex.map((i) => fakeImages[i]))).toBe( 187 | true 188 | ) 189 | 190 | expect( 191 | mockImageOnload.isLoaded( 192 | [...Array(len).keys()] 193 | .filter((i) => !shouldLoadedIndex.includes(i)) 194 | .map((i) => fakeImages[i]) 195 | ) 196 | ).toBe(false) 197 | }) 198 | 199 | it('preload all image', () => { 200 | const len = 10 201 | const { wrapper, fakeImages } = createLazyPswp(false, len) 202 | 203 | wrapper.find('#img-5').trigger('click') 204 | 205 | expect( 206 | mockImageOnload.isLoaded([...Array(len).keys()].map((i) => fakeImages[i])) 207 | ).toBe(true) 208 | }) 209 | }) 210 | }) 211 | 212 | describe('@event', () => { 213 | describe('@beforeOpen', () => { 214 | const getMock = (continued: boolean) => 215 | jest.fn((target: BeforeOpenEvent, next: BeforeOpen) => { 216 | next(continued) 217 | 218 | continued 219 | ? expect(PhotoSwipe).toBeCalled() 220 | : expect(PhotoSwipe).not.toBeCalled() 221 | }) 222 | 223 | const createBeforeOpenPswp = (continued: boolean) => { 224 | const mock = getMock(continued) 225 | 226 | createPswp({ 227 | listeners: { 228 | beforeOpen: mock, 229 | }, 230 | }) 231 | 232 | return { 233 | mock, 234 | } 235 | } 236 | 237 | it('called with specified parameters', () => { 238 | const { mock } = createBeforeOpenPswp(true) 239 | expect(mock.mock.calls[0].length).toBe(2) 240 | }) 241 | 242 | it('abort open PhotoSwipe', () => { 243 | const { mock } = createBeforeOpenPswp(false) 244 | expect(mock).toBeCalled() 245 | }) 246 | }) 247 | 248 | describe('@opened', () => { 249 | it('get pswp from cb', () => { 250 | const mock = jest.fn() 251 | const wrapper = createPswp({ 252 | withClick: false, 253 | }) 254 | wrapper.vm.$on('opened', mock) 255 | 256 | wrapper.find('img').trigger('click') 257 | const pswp = PhotoSwipeMock.getPswp() 258 | 259 | expect(mock).toBeCalled() 260 | expect(mock).toBeCalledWith(pswp) 261 | }) 262 | }) 263 | }) 264 | }) 265 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import PhotoSwipe from 'photoswipe' 2 | import defaultUI from 'photoswipe/dist/photoswipe-ui-default' 3 | import { 4 | PswpItem, 5 | Size, 6 | FindIndex, 7 | Closest, 8 | Single, 9 | PswpDirectiveOptions, 10 | CreatePhotoSwipe, 11 | BindEvent, 12 | HandleWithoutSize, 13 | Pswp, 14 | GetContainSize, 15 | RotateDirection, 16 | CurrentPswpItem, 17 | StyleKey, 18 | ManualCreateArgs, 19 | ManualOpenItem, 20 | } from '@/type' 21 | import { customEvents, GlobalOption } from '@/config' 22 | import Vue from 'vue' 23 | import PswpUI from '@/components/pswpUI.vue' 24 | 25 | export const isMobile = (): boolean => 26 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) 27 | 28 | export const isNum = (value: any): value is number => typeof value === 'number' 29 | 30 | export const isStr = (value: any): value is string => typeof value === 'string' 31 | 32 | export const isObj = (value: any): value is object => 33 | Object.prototype.toString.call(value) === '[object Object]' 34 | 35 | export const isFunction = (value: any): value is Function => 36 | Object.prototype.toString.call(value) === '[object Function]' 37 | 38 | const isDef = (value: any): boolean => value !== undefined && value !== null 39 | 40 | export const isImg = (value: any): value is HTMLImageElement => value && value.tagName === 'IMG' 41 | 42 | const isEle = (node: Node): node is HTMLElement => node.nodeType === 1 43 | 44 | export const isBgImg = (el: HTMLElement): boolean => !isImg(el) && !!el.dataset.pswpSrc 45 | 46 | /** 47 | * default error handle method 48 | * @param hint error hint 49 | */ 50 | export const errorHandler = (hint: string): never => { 51 | throw new Error(`[vue-pswipe] ${hint}`) 52 | } 53 | 54 | /** 55 | * get current active PhotoSwipe, else null 56 | */ 57 | export namespace CurrentPswp { 58 | /* eslint-disable no-shadow */ 59 | let currentPswp: Pswp | null = null 60 | const setupClean = (pswp: Pswp) => { 61 | pswp.listen('destroy', () => { 62 | currentPswp = null 63 | }) 64 | } 65 | export const get = () => currentPswp 66 | 67 | export const set = (pswp: Pswp | null) => { 68 | currentPswp = pswp 69 | if (pswp) setupClean(pswp) 70 | } 71 | } 72 | 73 | /** 74 | * get image size by polling 75 | * @param path the image src to get size 76 | * @return return promise 77 | */ 78 | export const getImageSize = (path: string) => 79 | new Promise((resolve) => { 80 | const img = new Image() 81 | let timer: number 82 | img.src = path 83 | img.addEventListener('error', () => { 84 | clearTimeout(timer) 85 | }) 86 | const check = () => { 87 | if (img.width > 0 || img.height > 0) { 88 | return resolve({ 89 | w: img.width, 90 | h: img.height, 91 | }) 92 | } 93 | timer = window.setTimeout(check, 40) 94 | } 95 | check() 96 | }) 97 | 98 | /** 99 | * returns the index of the first element predicate returns truthy 100 | * @param array the array to search 101 | * @param predicate the function invoked per iteration. 102 | * @return return the index of the found element, else -1. 103 | */ 104 | export const findIndex: FindIndex = (array, predicate) => { 105 | let index = -1 106 | array.some((item, idx) => { 107 | const result = predicate(item, idx) 108 | if (result) index = idx 109 | return result 110 | }) 111 | return index 112 | } 113 | 114 | /** 115 | * parse picture index and gallery index from URL (#&pid=1&gid=2) 116 | * @return return parsed hash, eg: { pid: 1, gid: 2 } 117 | */ 118 | export const parseHash = () => { 119 | const hash = window.location.hash.substring(1) 120 | const params: Record = {} 121 | 122 | if (hash.length < 5) return params 123 | 124 | hash.split('&').reduce((acc, cur) => { 125 | if (!cur) return acc 126 | const pair = cur.split('=') 127 | if (pair.length < 2) return acc 128 | const [key, value] = pair 129 | acc[key] = value 130 | return acc 131 | }, params) 132 | 133 | return params 134 | } 135 | 136 | /** 137 | * invoke querySelectorAll with specified context 138 | * @param selector css selector 139 | * @param context the query context 140 | * @return return the list of queries 141 | */ 142 | export const querySelectorList = ( 143 | selector: string, 144 | context: HTMLElement | Document = document 145 | ) => [...context.querySelectorAll(selector)] as T[] 146 | 147 | /** 148 | * find nearest parent element 149 | * @param el begin element 150 | * @param predicate the function invoked from begin element to body 151 | * @returns return the found element or false 152 | */ 153 | export const closest: Closest = (el, predicate) => 154 | !!el && isEle(el) && (predicate(el) ? el : closest(el.parentNode, predicate)) 155 | 156 | /** 157 | * singleton pattern 158 | * @param fn the function should be invoked only once 159 | * @return wrapped function 160 | */ 161 | export const single: Single = (fn) => { 162 | let result: any 163 | // eslint-disable-next-line func-names 164 | return function(this: any, ...args: any[]) { 165 | return result || (result = fn.apply(this, args)) 166 | } 167 | } 168 | 169 | /** 170 | * append element to document.body 171 | * @param el the element to be append to body 172 | * @return return appended element 173 | */ 174 | const append = (el: HTMLElement) => document.body.appendChild(el) 175 | 176 | /** 177 | * append element to body only once 178 | */ 179 | export const appendOnce = single(append) 180 | 181 | /** 182 | * set data-pswp-size to element 183 | * @param el the element to set data-pswp-size 184 | * @param size the size object contains w and h property 185 | */ 186 | export const setSize = (el: HTMLElement, { w, h }: Size) => { 187 | if (el && el.dataset) { 188 | // eslint-disable-next-line no-param-reassign 189 | el.dataset.pswpSize = `${w}x${h}` 190 | } 191 | } 192 | 193 | /** 194 | * get the image src according to auto 195 | * @param target the element to get the src 196 | * @param auto is it in auto mode 197 | */ 198 | export const getSrc = (target: HTMLImageElement | HTMLElement, auto: boolean): string => 199 | auto && isImg(target) ? target.src : target.dataset.pswpSrc || '' 200 | 201 | /** 202 | * determine whether el is a valid element based on auto and filter 203 | */ 204 | export const relevant = ( 205 | el: HTMLElement, 206 | auto: boolean, 207 | filter: (el: HTMLImageElement) => boolean 208 | ): boolean => (auto ? isImg(el) && filter(el) : !!el.dataset.pswpSrc) 209 | 210 | /** 211 | * Convert the first letter to uppercase 212 | */ 213 | const upperFirst = (str: string) => str.replace(/^\S/, (match) => match.toUpperCase()) 214 | 215 | /** 216 | * convert property to pswp property, eg: src => pswpSrc 217 | */ 218 | const getPswpDataKey = (property: string) => `pswp${upperFirst(property)}` 219 | 220 | /** 221 | * Set pswp data to the data attribute of the specified element 222 | */ 223 | export const setPswpData = (options: PswpDirectiveOptions, el: HTMLElement) => { 224 | Object.keys(options).forEach((key) => { 225 | el.dataset[getPswpDataKey(key)] = `${options[key as keyof PswpDirectiveOptions]}` // eslint-disable-line 226 | }) 227 | } 228 | 229 | /** 230 | * Set the pswp data according to the type of the parameter 231 | */ 232 | export const setPswpDataByCond = (el: HTMLElement, value: string | PswpDirectiveOptions) => { 233 | if (isStr(value)) setPswpData({ src: value }, el) 234 | if (isObj(value)) setPswpData(value as PswpDirectiveOptions, el) 235 | } 236 | 237 | /** 238 | * preset loaded msrc size to PswpItem 239 | */ 240 | export const presetSize = (item: PswpItem): void => { 241 | /* eslint-disable no-param-reassign */ 242 | const { src, msrc, el } = item 243 | if (item.w || item.h || !msrc) return 244 | 245 | let img: HTMLImageElement | null = new Image() 246 | img.src = msrc 247 | const { width: w, height: h } = img 248 | if (w && h) { 249 | item.w = w 250 | item.h = h 251 | src === msrc && setSize(el, { w, h }) 252 | } 253 | img = null 254 | } 255 | 256 | /** 257 | * allow listen original PhotoSwipe event in Photoswipe component 258 | * @param pswp original PhotoSwipe 259 | * @param context Photoswipe component 260 | */ 261 | const bindEvent: BindEvent = (pswp, context) => { 262 | if (!context) return 263 | Object.keys(context.$listeners) 264 | .filter((event) => !customEvents.includes(event)) 265 | .forEach((event) => { 266 | const fn = context.$listeners[event] 267 | if (isFunction(fn)) { 268 | pswp.listen(event, (...args: any[]) => { 269 | context.$emit(event, ...args) 270 | }) 271 | } 272 | }) 273 | } 274 | 275 | /** 276 | * set the size of the image after src is loaded 277 | * @param item the item that will be proxy 278 | * @param pswp original Photoswipe 279 | */ 280 | const hackItemImg = (item: PswpItem, pswp: Pswp) => { 281 | let img: HTMLImageElement | null = null 282 | Object.defineProperty(item, 'img', { 283 | get() { 284 | return img 285 | }, 286 | set(value) { 287 | if (isImg(value)) { 288 | value.addEventListener('load', () => { 289 | const { naturalWidth: w, naturalHeight: h } = value 290 | item.w = w 291 | item.h = h 292 | setSize(item.el, { w, h }) 293 | if (CurrentPswp.get() === pswp) { 294 | pswp.updateSize(true) 295 | } 296 | }) 297 | } 298 | img = value 299 | }, 300 | }) 301 | } 302 | 303 | /** 304 | * handle item without set size, use msrc first 305 | * @param pswp original PhotoSwipe 306 | */ 307 | const handleWithoutSize: HandleWithoutSize = (pswp) => { 308 | pswp.listen('gettingData', (index, item: PswpItem) => { 309 | presetSize(item) 310 | 311 | if ((item.el && item.el.dataset.pswpSize) || Object.getOwnPropertyDescriptor(item, 'img')) 312 | return 313 | 314 | // stop unexpected zoom-in animation 315 | if (pswp.currItem === item) { 316 | pswp.options.showAnimationDuration = 0 317 | } 318 | 319 | hackItemImg(item, pswp) 320 | }) 321 | } 322 | 323 | const revertRotate = (pswp: Pswp) => { 324 | pswp.listen('gettingData', (index, item: PswpItem) => { 325 | if (!item.verticalRotated) return 326 | 327 | const { w } = item 328 | item.w = item.h 329 | item.h = w 330 | item.verticalRotated = false 331 | }) 332 | } 333 | 334 | /** 335 | * manipulate Photoswipe default UI element 336 | */ 337 | export namespace UI { 338 | // eslint-disable-next-line import/no-mutable-exports 339 | export let el: HTMLElement 340 | export const mount = () => { 341 | if (!el) { 342 | const PswpUIComponent = new Vue(PswpUI).$mount() 343 | el = PswpUIComponent.$el 344 | } 345 | } 346 | export const append = () => { 347 | mount() 348 | appendOnce(el) 349 | } 350 | } 351 | 352 | /** 353 | * define item.w/item.h if needed 354 | * @param items 355 | */ 356 | const defineSize = (items: ManualOpenItem[]) => 357 | items.map((item) => { 358 | if (!isDef(item.w)) item.w = 0 359 | if (!isDef(item.h)) item.h = 0 360 | return item 361 | }) 362 | 363 | /** 364 | * create PhotoSwipe instance, setup listener, init PhotoSwipe 365 | * @return return created original PhotoSwipe instance 366 | */ 367 | export const createPhotoSwipe: CreatePhotoSwipe = ({ items, options, context }) => { 368 | const pswp = new PhotoSwipe(UI.el, defaultUI, items, options) 369 | bindEvent(pswp, context) 370 | handleWithoutSize(pswp) 371 | revertRotate(pswp) 372 | CurrentPswp.set(pswp) 373 | pswp.init() 374 | return pswp 375 | } 376 | 377 | /** 378 | * used for this.$Pswp.open() 379 | */ 380 | export const manualCreate = ({ items, options }: ManualCreateArgs) => 381 | createPhotoSwipe({ 382 | items: defineSize(items), 383 | options: { 384 | ...GlobalOption.get(), 385 | // disable transition entirely 386 | hideAnimationDuration: 0, 387 | showAnimationDuration: 0, 388 | ...options, 389 | // avoid refresh cant find match gallery 390 | history: false, 391 | }, 392 | }) 393 | 394 | /** 395 | * emulate background-size: contain, get calculated image size 396 | * @param areaWidth container width 397 | * @param areaHeight container height 398 | * @param width image width 399 | * @param height image height 400 | * @return calculated image size 401 | */ 402 | export const getContainSize: GetContainSize = (areaWidth, areaHeight, width, height) => { 403 | if (width <= areaWidth && height <= areaHeight) return { w: width, h: height } 404 | const ratio = width / height 405 | const areaRatio = areaWidth / areaHeight 406 | return areaRatio < ratio 407 | ? { w: areaWidth, h: areaWidth / ratio } 408 | : { w: areaHeight * ratio, h: areaHeight } 409 | } 410 | 411 | /** 412 | * custom event 413 | */ 414 | export namespace Event { 415 | const event: Record = {} 416 | 417 | export const off = (name: string, fn?: Function) => { 418 | if (!fn) return (event[name].length = 0) 419 | const pools = event[name] 420 | const index = pools.indexOf(fn) 421 | if (index !== -1) pools.splice(index, 1) 422 | } 423 | 424 | export const on = (name: string, fn: Function) => { 425 | if (!event[name]) event[name] = [] 426 | event[name].push(fn) 427 | return () => off(name, fn) 428 | } 429 | 430 | export const once = (name: string, fn: Function) => { 431 | const teardown = on(name, (...args: any[]) => { 432 | teardown() 433 | fn(...args) 434 | }) 435 | } 436 | 437 | export const emit = (name: string, ...args: any[]) => { 438 | const pools = event[name] 439 | if (!Array.isArray(pools)) return 440 | pools.forEach((fn) => fn(...args)) 441 | } 442 | } 443 | 444 | /** 445 | * get next transform degree from current image element 446 | * @param img current image element 447 | * @param direction rotate direction 448 | * @return transform degree 449 | */ 450 | export const getTransformDeg = (img: HTMLImageElement, direction: RotateDirection) => { 451 | const deg = Number(img.dataset.rotateDeg) || 0 452 | const offsets = direction === 'left' ? -90 : 90 453 | const transformDeg = deg + offsets 454 | img.dataset.rotateDeg = `${transformDeg}` 455 | return [deg, transformDeg] 456 | } 457 | 458 | /** 459 | * get container viewport size 460 | * @param container current container element 461 | * @param currentItem current pswp item 462 | * @return container viewport size 463 | */ 464 | export const getContainerSize = (container: HTMLElement, currentItem: CurrentPswpItem) => { 465 | const containerWidth = container.clientWidth 466 | const containerHeight = currentItem.vGap 467 | ? container.clientHeight - currentItem.vGap.top - currentItem.vGap.bottom 468 | : container.clientHeight 469 | return { 470 | w: containerWidth, 471 | h: containerHeight, 472 | } 473 | } 474 | 475 | /** 476 | * get transform scale string 477 | * @param w scale width 478 | * @param h scale height 479 | */ 480 | export const getScale = (w: number, h = w) => `scale(${w}, ${h})` 481 | 482 | /** 483 | * get transform scale 484 | * @param containerSize container size 485 | * @param img current image element 486 | * @param isVertical next rotate is vertical 487 | */ 488 | export const getCalculatedScale = ( 489 | containerSize: Size, 490 | img: HTMLImageElement, 491 | isVertical: boolean 492 | ): string[] => { 493 | const { naturalWidth, naturalHeight } = img 494 | 495 | const { w: horizontalWidth, h: horizontalHeight } = getContainSize( 496 | containerSize.w, 497 | containerSize.h, 498 | naturalWidth, 499 | naturalHeight 500 | ) 501 | 502 | const { w: verticalWidth, h: verticalHeight } = getContainSize( 503 | containerSize.w, 504 | containerSize.h, 505 | naturalHeight, 506 | naturalWidth 507 | ) 508 | 509 | const animatedScale = isVertical 510 | ? getScale(verticalHeight / horizontalWidth) 511 | : getScale(horizontalWidth / verticalWidth, horizontalHeight / verticalHeight) 512 | 513 | const verticalSilencedScale = getScale( 514 | verticalHeight / verticalWidth, 515 | verticalWidth / verticalHeight 516 | ) 517 | return [animatedScale, verticalSilencedScale] 518 | } 519 | 520 | /** 521 | * add vendor prefix to css property 522 | */ 523 | export const modernize = (() => { 524 | const cache: Record = {} 525 | const detectElement = document.createElement('div') 526 | const { style } = detectElement 527 | 528 | return (styleKey: T): T => { 529 | const cached = cache[styleKey] 530 | if (cached) return cached as T 531 | 532 | let key = styleKey 533 | 534 | /* istanbul ignore if */ 535 | if (!isDef(style[styleKey])) { 536 | // eslint-disable-next-line array-callback-return 537 | ;['Moz', 'ms', 'O', 'Webkit'].some((prefix) => { 538 | const prefixedStyleKey = (prefix + upperFirst(styleKey)) 539 | if (isDef(style[prefixedStyleKey])) { 540 | return (key = prefixedStyleKey) 541 | } 542 | }) 543 | } 544 | cache[styleKey] = key 545 | return key 546 | } 547 | })() 548 | 549 | /** 550 | * get prefixed transition end event name 551 | */ 552 | export const transitionEndEventName = (() => { 553 | const transitions = { 554 | transition: 'transitionend', 555 | OTransition: 'oTransitionEnd', 556 | MozTransition: 'transitionend', 557 | WebkitTransition: 'webkitTransitionEnd', 558 | } 559 | const detected = modernize('transition') 560 | return transitions[detected] 561 | })() 562 | 563 | /** 564 | * register v-pswp directive if needed 565 | */ 566 | export const registerDirective = () => { 567 | const pswpDirective = Vue.directive('pswp') 568 | if (!pswpDirective) { 569 | Vue.directive('pswp', { 570 | bind(el: HTMLElement, { value }: any) { 571 | setPswpDataByCond(el, value) 572 | }, 573 | update(el: HTMLElement, { value, oldValue }: any) { 574 | if (value === oldValue) return 575 | setPswpDataByCond(el, value) 576 | }, 577 | }) 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /dist/Photoswipe.umd.min.js: -------------------------------------------------------------------------------- 1 | (function(t,e){"object"===typeof exports&&"object"===typeof module?module.exports=e(require("vue")):"function"===typeof define&&define.amd?define([],e):"object"===typeof exports?exports["Photoswipe"]=e(require("vue")):t["Photoswipe"]=e(t["Vue"])})("undefined"!==typeof self?self:this,function(t){return function(t){var e={};function n(o){if(e[o])return e[o].exports;var i=e[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(o,i,function(e){return t[e]}.bind(null,i));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="fb15")}({"14fd":function(t,e,n){var o,i; 2 | /*! PhotoSwipe Default UI - 4.1.3 - 2019-01-08 3 | * http://photoswipe.com 4 | * Copyright (c) 2019 Dmitry Semenov; */ 5 | /*! PhotoSwipe Default UI - 4.1.3 - 2019-01-08 6 | * http://photoswipe.com 7 | * Copyright (c) 2019 Dmitry Semenov; */ 8 | (function(r,a){o=a,i="function"===typeof o?o.call(e,n,e,t):o,void 0===i||(t.exports=i)})(0,function(){"use strict";var t=function(t,e){var n,o,i,r,a,s,l,u,c,p,d,f,m,h,w,b,g,v,y=this,_=!1,x=!0,A=!0,I={barsSize:{top:44,bottom:"auto"},closeElClasses:["item","caption","zoom-wrap","ui","top-bar"],timeToIdle:4e3,timeToIdleOutside:1e3,loadingIndicatorDelay:1e3,addCaptionHTMLFn:function(t,e){return t.title?(e.children[0].innerHTML=t.title,!0):(e.children[0].innerHTML="",!1)},closeEl:!0,captionEl:!0,fullscreenEl:!0,zoomEl:!0,shareEl:!0,counterEl:!0,arrowEl:!0,preloaderEl:!0,tapToClose:!1,tapToToggleControls:!0,clickToCloseNonZoomable:!0,shareButtons:[{id:"facebook",label:"Share on Facebook",url:"https://www.facebook.com/sharer/sharer.php?u={{url}}"},{id:"twitter",label:"Tweet",url:"https://twitter.com/intent/tweet?text={{text}}&url={{url}}"},{id:"pinterest",label:"Pin it",url:"http://www.pinterest.com/pin/create/button/?url={{url}}&media={{image_url}}&description={{text}}"},{id:"download",label:"Download image",url:"{{raw_image_url}}",download:!0}],getImageURLForShare:function(){return t.currItem.src||""},getPageURLForShare:function(){return window.location.href},getTextForShare:function(){return t.currItem.title||""},indexIndicatorSep:" / ",fitControlsWidth:1200},k=function(t){if(b)return!0;t=t||window.event,w.timeToIdle&&w.mouseUsed&&!c&&N();for(var n,o,i=t.target||t.srcElement,r=i.getAttribute("class")||"",a=0;a-1&&(n.onTap(),o=!0);if(o){t.stopPropagation&&t.stopPropagation(),b=!0;var s=e.features.isOldAndroid?600:30;setTimeout(function(){b=!1},s)}},M=function(){return!t.likelyTouchDevice||w.mouseUsed||screen.width>w.fitControlsWidth},T=function(t,n,o){e[(o?"add":"remove")+"Class"](t,"pswp__"+n)},C=function(){var t=1===w.getNumItemsFn();t!==h&&(T(o,"ui--one-slide",t),h=t)},S=function(){T(l,"share-modal--hidden",A)},E=function(){return A=!A,A?(e.removeClass(l,"pswp__share-modal--fade-in"),setTimeout(function(){A&&S()},300)):(S(),setTimeout(function(){A||e.addClass(l,"pswp__share-modal--fade-in")},30)),A||O(),!1},D=function(e){e=e||window.event;var n=e.target||e.srcElement;return t.shout("shareLinkClick",e,n),!!n.href&&(!!n.hasAttribute("download")||(window.open(n.href,"pswp_share","scrollbars=yes,resizable=yes,toolbar=no,location=yes,width=550,height=420,top=100,left="+(window.screen?Math.round(screen.width/2-275):100)),A||E(),!1))},O=function(){for(var t,e,n,o,i,r="",a=0;a