├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── demo ├── app.component.ts ├── app.css ├── app.module.ts ├── img │ ├── image1-thumb.jpg │ ├── image1.jpg │ ├── image2-thumb.jpg │ ├── image2.jpg │ ├── image3-thumb.jpg │ ├── image3.jpg │ ├── image4-thumb.jpg │ └── image4.jpg ├── main.ts └── polyfills.ts ├── index.html ├── karma-main.js ├── karma.conf.js ├── package.json ├── src ├── index.ts ├── lightbox-config.service.ts ├── lightbox-event.service.ts ├── lightbox-overlay.component.spec.ts ├── lightbox-overlay.component.ts ├── lightbox.component.spec.ts ├── lightbox.component.ts ├── lightbox.css ├── lightbox.module.ts └── lightbox.service.ts ├── tsconfig-demo.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/*.map 3 | src/*.js 4 | /compiled 5 | *.map 6 | /img 7 | *.metadata.json 8 | *.log 9 | /dist 10 | *.d.ts 11 | *.ngsummary.json 12 | 13 | .angular -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | demo 3 | node_modules 4 | src -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10.16.0' 4 | addons: 5 | apt: 6 | packages: 7 | - sshpass 8 | before_script: 9 | - npm install -g yarn 10 | - yarn 11 | script: 12 | - npm test 13 | - npm run build 14 | after_success: 15 | - npm run build 16 | - ssh-keyscan $DOMAIN > ~/.ssh/known_hosts 17 | - sshpass -p "$PASS" rsync -ar --stats --exclude=.git ./ $USER@$DOMAIN:$FOLDER_PATH 18 | branches: 19 | only: 20 | - master 21 | notifications: 22 | email: false 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/themyth92/ngx-lightbox.svg?branch=master)](https://travis-ci.org/themyth92/ngx-lightbox) 2 | 3 | # Ngx-Lightbox 4 | 5 | A [lightbox2](https://github.com/lokesh/lightbox2) implementation port to use with new Angular without the need for jQuery 6 | 7 | ## Version 8 | 9 | - For Angular 5, 6, 7, please use ngx-lightbox 1.x.x. `npm install ngx-lightbox@1.2.0` 10 | - For Angular >= 8, please use ngx-lightbox 2.x.x. `npm install ngx-lightbox@2.0.0` 11 | - For Angular 2, 4, please use [angular2-lightbox](https://github.com/themyth92/angular2-lightbox) 12 | 13 | ## [Demo](https://themyth92.com/project/ngx-lightbox) 14 | 15 | ## Installation 16 | 17 | `npm install --save ngx-lightbox` 18 | 19 | Update your `angular.json` 20 | 21 | ``` 22 | { 23 | "styles": [ 24 | "./node_modules/ngx-lightbox/lightbox.css", 25 | ... 26 | ], 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Module: 33 | 34 | Import `LightboxModule` from `ngx-lightbox` 35 | 36 | ```javascript 37 | import { LightboxModule } from 'ngx-lightbox'; 38 | 39 | @NgModule({ 40 | imports: [ LightboxModule ] 41 | }) 42 | ``` 43 | 44 | ### Component 45 | 46 | 1. Markup 47 | 48 | ```html 49 |
50 | 51 |
52 | ``` 53 | 54 | 2. Component method 55 | 56 | ```javascript 57 | import { Lightbox } from 'ngx-lightbox'; 58 | 59 | export class AppComponent { 60 | private _album: Array = []; 61 | constructor(private _lightbox: Lightbox) { 62 | for (let i = 1; i <= 4; i++) { 63 | const src = 'demo/img/image' + i + '.jpg'; 64 | const caption = 'Image ' + i + ' caption here'; 65 | const thumb = 'demo/img/image' + i + '-thumb.jpg'; 66 | const album = { 67 | src: src, 68 | caption: caption, 69 | thumb: thumb 70 | }; 71 | 72 | this._albums.push(album); 73 | } 74 | } 75 | 76 | open(index: number): void { 77 | // open lightbox 78 | this._lightbox.open(this._albums, index); 79 | } 80 | 81 | close(): void { 82 | // close lightbox programmatically 83 | this._lightbox.close(); 84 | } 85 | } 86 | 87 | ``` 88 | 89 | Each `object` of `album` array inside your component may contains 3 properties : 90 | 91 | | Properties | Requirement | Description | 92 | | ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | 93 | | src | Required | The source image to your thumbnail that you want to with use lightbox when user click on `thumbnail` image | 94 | | caption | Optional | Your caption corresponding with your image | 95 | | thumb | Optional | Source of your thumbnail. It is being used inside your component markup so this properties depends on your naming. | 96 | 97 | 3. Listen to lightbox event 98 | 99 | You can listen to 3 events, which are either **CHANGE_PAGE**, **CLOSE** or **OPEN**. 100 | 101 | ```javascript 102 | import { LightboxEvent, LIGHTBOX_EVENT } from 'ngx-lightbox'; 103 | import { Subscription } from 'rxjs'; 104 | 105 | export class AppComponent { 106 | private _subscription: Subscription; 107 | constructor(private _lightboxEvent: LightboxEvent) {} 108 | open(index: number): void { 109 | // register your subscription and callback whe open lightbox is fired 110 | this._subscription = this._lightboxEvent.lightboxEvent$ 111 | .subscribe(event => this._onReceivedEvent(event)); 112 | } 113 | 114 | private _onReceivedEvent(event: any): void { 115 | // remember to unsubscribe the event when lightbox is closed 116 | if (event.id === LIGHTBOX_EVENT.CLOSE) { 117 | // event CLOSED is fired 118 | this._subscription.unsubscribe(); 119 | } 120 | 121 | if (event.id === LIGHTBOX_EVENT.OPEN) { 122 | // event OPEN is fired 123 | } 124 | 125 | if (event.id === LIGHTBOX_EVENT.CHANGE_PAGE) { 126 | // event change page is fired 127 | console.log(event.data); // -> image index that lightbox is switched to 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | ## Lightbox options 134 | 135 | Available options based on lightbox2 options 136 | 137 | | Properties | Default | Description | 138 | | --------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 139 | | fadeDuration | **0.7** seconds | _duration_ starting when the **src** image is **loaded** to **fully appear** onto screen. | 140 | | resizeDuration | **0.5** seconds | _duration_ starting when Lightbox container **change** its dimension from a _default/previous image_ to the _current image_ when the _current image_ is **loaded**. | 141 | | fitImageInViewPort | **true** | Determine whether lightbox will use the natural image _width/height_ or change the image _width/height_ to fit the view of current window. Change this option to **true** to prevent problem when image too big compare to browser windows. | 142 | | positionFromTop | **20** px | The position of lightbox from the top of window browser | 143 | | showImageNumberLabel | **false** | Determine whether to show the image number to user. The default text shown is `Image IMAGE_NUMBER of ALBUM_LENGTH` | 144 | | alwaysShowNavOnTouchDevices | **false** | Determine whether to show `left/right` arrow to user on Touch devices. | 145 | | wrapAround | **false** | Determine whether to move to the start of the album when user reaches the end of album and vice versa. Set it to **true** to enable this feature. | 146 | | disableKeyboardNav | **false** | Determine whether to disable navigation using keyboard event. | 147 | | disableScrolling | **false** | If **true**, prevent the page from scrolling while Lightbox is open. This works by settings overflow hidden on the body. | 148 | | centerVertically | **false** | If **true**, images will be centered vertically to the screen. | 149 | | albumLabel | "Image %1 of %2" | The text displayed below the caption when viewing an image set. The default text shows the current image number and the total number of images in the set. | 150 | | enableTransition | **true** | Transition animation between images will be disabled if this flag set to **false** | 151 | | showZoom | **false** | Zoom Buttons will be shown if this flag set to **true** | 152 | | showRotate | **false** | Rotate Buttons will be shown if this flag set to **true** | 153 | | showDownloadButton | **false** | Download button will be shown if this flag set to **true** | 154 | | containerElementResolver | () => document.body | Resolves the element that will contain the lightbox | 155 | 156 | 157 | **NOTE**: You can either override default config or during a specific opening window 158 | 159 | 1. Override default config 160 | 161 | ```javascript 162 | import { LightboxConfig } from 'ngx-lightbox'; 163 | 164 | export class AppComponent { 165 | constructor(private _lightboxConfig: LightboxConfig) { 166 | // override default config 167 | _lightboxConfig.fadeDuration = 1; 168 | } 169 | } 170 | ``` 171 | 172 | 2. Set config in a specific opening window 173 | 174 | ```javascript 175 | import { LightboxConfig, Lightbox } from 'ngx-lightbox'; 176 | 177 | export class AppComponent { 178 | constructor(private _lightboxConfig: LightboxConfig, private _lightbox: Lightbox) {} 179 | open(index: number) { 180 | // override the default config on second parameter 181 | this._lightbox.open(this._albums, index, { wrapAround: true, showImageNumberLabel: true }); 182 | } 183 | } 184 | ``` 185 | 186 | ### Overriding lightbox parent elements 187 | 188 | If you want to use any other parent element than your ``, please override the `containerElementResolver` property of your `LightboxConfig`. 189 | This can be used, e.g. if you are opening the lightbox from within a Shadow DOM based web component. 190 | 191 | ```js 192 | export class MyLightBoxTrigger { 193 | constructor( 194 | private _lightbox: Lightbox, 195 | private _lighboxConfig: LightboxConfig, 196 | ) { 197 | _lighboxConfig.containerElementResolver = (doc: Document) => doc.getElementById('my-lightbox-host'); 198 | } 199 | 200 | open(index: number): void { 201 | this._lightbox.open(this.images, index); // will put the lightbox child into e.g.
202 | } 203 | ``` 204 | 205 | ## Angular Universal 206 | 207 | This project works with universal out of the box with no additional configuration. 208 | 209 | ## License 210 | 211 | MIT 212 | 213 | ## Donation 214 | 215 | Buy me a beer if you like 216 | 217 | BTC: 1MFx5waJ7Sitn961DaXe3mQXrb7pEoSJct 218 | 219 | ETH: 0x2211F3d683eB1C2d753aD21D9Bd9110729C80B72 220 | 221 | NEO: ARrUrnbq1ogfsoabvCgJ5SHgknhzyUmtuS 222 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-lightbox": { 7 | "root": ".", 8 | "sourceRoot": ".", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist", 17 | "index": "index.html", 18 | "main": "demo/main.ts", 19 | "polyfills": "demo/polyfills.ts", 20 | "tsConfig": "tsconfig-demo.json", 21 | "styles": [ 22 | "./src/lightbox.css", 23 | "demo/app.css" 24 | ], 25 | "assets": [ 26 | { "glob": "**/*", "input": "./demo/img/", "output": "./demo/img/" } 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "optimization": true, 33 | "outputHashing": "all", 34 | "sourceMap": true, 35 | "namedChunks": false, 36 | "aot": true, 37 | "extractLicenses": true, 38 | "vendorChunk": false, 39 | "buildOptimizer": true 40 | } 41 | } 42 | }, 43 | "serve": { 44 | "builder": "@angular-devkit/build-angular:dev-server", 45 | "options": { 46 | "browserTarget": "ngx-lightbox:build" 47 | }, 48 | "configurations": { 49 | "production": { 50 | "browserTarget": "ngx-lightbox:build:production" 51 | } 52 | } 53 | }, 54 | "test": { 55 | "builder": "@angular-devkit/build-angular:karma", 56 | "options": { 57 | "main": "src/index.ts", 58 | "tsConfig": "tsconfig.json", 59 | "karmaConfig": "karma.conf.js", 60 | "scripts": [] 61 | } 62 | }, 63 | "lint": { 64 | "builder": "@angular-devkit/build-angular:tslint", 65 | "options": { 66 | "tsConfig": [ 67 | "tsconfig.json" 68 | ], 69 | "exclude": [ 70 | "**/node_modules/**" 71 | ] 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "cli": { 78 | "analytics": false 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /demo/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | import { Component } from '@angular/core'; 4 | 5 | import { IAlbum, IEvent, Lightbox, LIGHTBOX_EVENT, LightboxConfig, LightboxEvent } from '../src'; 6 | 7 | @Component({ 8 | selector: 'demo', 9 | template: ` 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 | `, 21 | host: { 22 | class: 'columns' 23 | } 24 | }) 25 | export class AppComponent { 26 | public albums: Array; 27 | private _subscription: Subscription; 28 | constructor( 29 | private _lightbox: Lightbox, 30 | private _lightboxEvent: LightboxEvent, 31 | private _lighboxConfig: LightboxConfig 32 | ) { 33 | this.albums = []; 34 | for (let i = 1; i <= 4; i++) { 35 | const src = 'demo/img/image' + i + '.jpg'; 36 | const caption = 'Image ' + i + ' caption here'; 37 | const thumb = 'demo/img/image' + i + '-thumb.jpg'; 38 | const album = { 39 | src: src, 40 | caption: caption, 41 | thumb: thumb 42 | }; 43 | 44 | this.albums.push(album); 45 | } 46 | 47 | // set default config 48 | this._lighboxConfig.fadeDuration = 1; 49 | } 50 | 51 | open(index: number): void { 52 | this._subscription = this._lightboxEvent.lightboxEvent$.subscribe((event: IEvent) => this._onReceivedEvent(event)); 53 | 54 | // override the default config 55 | this._lightbox.open(this.albums, index, { 56 | wrapAround: true, 57 | showImageNumberLabel: true, 58 | disableScrolling: true, 59 | showZoom: true, 60 | showRotate: true, 61 | showDownloadButton: true 62 | }); 63 | } 64 | 65 | private _onReceivedEvent(event: IEvent): void { 66 | if (event.id === LIGHTBOX_EVENT.CLOSE) { 67 | this._subscription.unsubscribe(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /demo/app.css: -------------------------------------------------------------------------------- 1 | .huge-margin-top { 2 | margin-top: 100vh; 3 | } 4 | 5 | .img-row { 6 | display: inline-block; 7 | } 8 | 9 | .img-frame { 10 | margin: 10px; 11 | border: 5px solid #fff; 12 | cursor: pointer; 13 | -webkit-transition-duration: 0.3s; 14 | transition-duration: 0.3s; 15 | -webkit-transition-property: transform; 16 | transition-property: transform; 17 | } 18 | 19 | .img-frame:hover, .img-frame:focus, .img-frame:active { 20 | -webkit-transform: translateY(-5px); 21 | transform: translateY(-5px); 22 | } 23 | -------------------------------------------------------------------------------- /demo/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { LightboxModule } from '../src'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [ BrowserModule, LightboxModule ], 9 | declarations: [ AppComponent ], 10 | bootstrap: [ AppComponent ] 11 | }) 12 | export class AppModule { } 13 | -------------------------------------------------------------------------------- /demo/img/image1-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image1-thumb.jpg -------------------------------------------------------------------------------- /demo/img/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image1.jpg -------------------------------------------------------------------------------- /demo/img/image2-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image2-thumb.jpg -------------------------------------------------------------------------------- /demo/img/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image2.jpg -------------------------------------------------------------------------------- /demo/img/image3-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image3-thumb.jpg -------------------------------------------------------------------------------- /demo/img/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image3.jpg -------------------------------------------------------------------------------- /demo/img/image4-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image4-thumb.jpg -------------------------------------------------------------------------------- /demo/img/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themyth92/ngx-lightbox/07fdc0e6c52fe0e439a23b98c88c2cda7f55a37a/demo/img/image4.jpg -------------------------------------------------------------------------------- /demo/main.ts: -------------------------------------------------------------------------------- 1 | import { AppModule } from './app.module'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | platformBrowserDynamic().bootstrapModule(AppModule); 5 | -------------------------------------------------------------------------------- /demo/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | // import 'core-js/es6/reflect'; 45 | // import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | /** 70 | * Need to import at least one locale-data with intl. 71 | */ 72 | // import 'intl/locale-data/jsonp/en'; 73 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 25 |
26 |
27 |
28 |
29 |
Gallery
30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | Fork me on GitHub 39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /karma-main.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | require('core-js/es/reflect'); 3 | require('zone.js/dist/zone'); 4 | require('zone.js/dist/long-stack-trace-zone'); 5 | require('zone.js/dist/proxy'); 6 | require('zone.js/dist/sync-test'); 7 | require('zone.js/dist/jasmine-patch'); 8 | require('zone.js/dist/async-test'); 9 | require('zone.js/dist/fake-async-test'); 10 | require('rxjs'); 11 | 12 | const testing = require('@angular/core/testing'); 13 | const browser = require('@angular/platform-browser-dynamic/testing'); 14 | 15 | testing.TestBed.initTestEnvironment( 16 | browser.BrowserDynamicTestingModule, 17 | browser.platformBrowserDynamicTesting() 18 | ); 19 | 20 | const appContext = require.context('./src', true, /\.spec\.ts/); 21 | 22 | appContext.keys().forEach(appContext); 23 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = config => { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | files: [ 9 | { pattern: './src/img/*.png', watched: false, included: false, served: true, nocache: false }, 10 | { pattern: './karma-main.js', watched: false } 11 | ], 12 | reporters: ['dots'], 13 | port: 9876, 14 | colors: true, 15 | logLevel: config.LOG_INFO, 16 | autoWatch: false, 17 | browsers: ['PhantomJS'], 18 | singleRun: true, 19 | browserConsoleLogOptions: { 20 | terminal: true, 21 | level: 'log' 22 | }, 23 | plugins: [ 24 | require('karma-jasmine'), 25 | require('karma-phantomjs-launcher'), 26 | require('karma-webpack'), 27 | require('karma-sourcemap-loader'), 28 | require('@angular-devkit/build-angular/plugins/karma') 29 | ], 30 | preprocessors: { 31 | './karma-main.js': ['webpack', 'sourcemap'] 32 | }, 33 | webpack: { 34 | mode: 'development', 35 | stats: 'errors-only', 36 | resolve: { 37 | modules: [ 38 | 'node_modules' 39 | ], 40 | extensions: ['.ts', '.js'] 41 | }, 42 | devtool: 'inline-source-map', 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.ts$/, 47 | loader: 'awesome-typescript-loader', 48 | include: [ 49 | path.resolve(__dirname, 'src') 50 | ] 51 | } 52 | ] 53 | }, 54 | plugins: [ 55 | new webpack.ContextReplacementPlugin( 56 | /@angular(\\|\/)core(\\|\/)src/, 57 | path.resolve(__dirname, '../src') 58 | ) 59 | ] 60 | }, 61 | webpackServer: { 62 | noInfo: true 63 | }, 64 | proxies: { 65 | '/src/img/': '/base/src/img/' 66 | } 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-lightbox", 3 | "version": "3.0.0", 4 | "description": "A port >= angular5 for lightbox2", 5 | "main": "index.js", 6 | "dependencies": { 7 | "file-saver": "^2.0.5", 8 | "ngx-filesaver": "14.0.0" 9 | }, 10 | "devDependencies": { 11 | "@angular-devkit/build-angular": "^14.0.0", 12 | "@angular/cli": "^14.0.0", 13 | "@angular/common": "^14.0.0", 14 | "@angular/compiler": "^14.0.0", 15 | "@angular/compiler-cli": "^14.0.0", 16 | "@angular/core": "^14.0.0", 17 | "@angular/platform-browser": "^14.0.0", 18 | "@angular/platform-browser-dynamic": "^14.0.0", 19 | "@types/file-saver": "^2.0.5", 20 | "@types/jasmine": "^4.3.0", 21 | "copyfiles": "^2.4.1", 22 | "core-js": "^3.25.3", 23 | "del-cli": "^5.0.0", 24 | "jasmine-core": "^4.4.0", 25 | "karma": "^6.4.1", 26 | "karma-jasmine": "^5.1.0", 27 | "karma-phantomjs-launcher": "^1.0.4", 28 | "karma-sourcemap-loader": "^0.3.8", 29 | "karma-webpack": "^5.0.0", 30 | "ngx-lightbox": "^2.6.2", 31 | "phantomjs-prebuilt": "^2.1.16", 32 | "rxjs": "^7.5.7", 33 | "tslint": "^6.1.3", 34 | "typescript": "4.7.4", 35 | "webpack": "^5.74.0", 36 | "zone.js": "^0.11.8" 37 | }, 38 | "scripts": { 39 | "start": "./node_modules/.bin/ng serve", 40 | "build": "./node_modules/.bin/ng build", 41 | "test": " ./node_modules/.bin/ng lint && ./node_modules/.bin/ng test", 42 | "lint": "./node_modules/.bin/ng lint", 43 | "prepublishOnly": "./node_modules/.bin/ngc && ./node_modules/.bin/copyfiles -u 1 src/img/* src/*.js src/*.d.ts src/*.js.map src/*.metadata.json src/*.css .", 44 | "postpublish": "./node_modules/.bin/del '*.{service,component,module,spec,ngfactory}.js' '*.{metadata,ngsummary}.json' index.js webpack.config.js '*.{ts,css,map}' '{compiled,img}' 'src/*.{d.ts,ngfactory.ts,js,map,json}'" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/themyth92/ngx-lightbox.git" 49 | }, 50 | "keywords": [ 51 | "lightbox2", 52 | "angularjs", 53 | "directives", 54 | "lightbox2", 55 | "angular", 56 | "directives" 57 | ], 58 | "author": "themyth92", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/themyth92/ngx-lightbox/issues" 62 | }, 63 | "homepage": "https://github.com/themyth92/ngx-lightbox#readme" 64 | } 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Lightbox } from './lightbox.service'; 2 | export { LightboxConfig } from './lightbox-config.service'; 3 | export { LightboxEvent, LIGHTBOX_EVENT, IAlbum, IEvent } from './lightbox-event.service'; 4 | export { LightboxModule } from './lightbox.module'; 5 | -------------------------------------------------------------------------------- /src/lightbox-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class LightboxConfig { 5 | public fadeDuration: number; 6 | public resizeDuration: number; 7 | public fitImageInViewPort: boolean; 8 | public positionFromTop: number; 9 | public showImageNumberLabel: boolean; 10 | public alwaysShowNavOnTouchDevices: boolean; 11 | public wrapAround: boolean; 12 | public disableKeyboardNav: boolean; 13 | public disableScrolling: boolean; 14 | public centerVertically: boolean; 15 | public enableTransition: boolean; 16 | public albumLabel: string; 17 | public showZoom: boolean; 18 | public showRotate: boolean; 19 | public showDownloadButton: boolean; 20 | public containerElementResolver: (document: any) => HTMLElement; 21 | 22 | constructor() { 23 | this.fadeDuration = 0.7; 24 | this.resizeDuration = 0.5; 25 | this.fitImageInViewPort = true; 26 | this.positionFromTop = 20; 27 | this.showImageNumberLabel = false; 28 | this.alwaysShowNavOnTouchDevices = false; 29 | this.wrapAround = false; 30 | this.disableKeyboardNav = false; 31 | this.disableScrolling = false; 32 | this.centerVertically = false; 33 | this.enableTransition = true; 34 | this.albumLabel = 'Image %1 of %2'; 35 | this.showZoom = false; 36 | this.showRotate = false; 37 | this.containerElementResolver = (documentRef) => documentRef.querySelector('body'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lightbox-event.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Subject } from 'rxjs'; 2 | 3 | import { Injectable} from '@angular/core'; 4 | 5 | export interface IEvent { 6 | id: number; 7 | data?: any; 8 | } 9 | 10 | export interface IAlbum { 11 | src: string; 12 | caption?: string; 13 | thumb: string; 14 | downloadUrl?: string; 15 | } 16 | 17 | export const LIGHTBOX_EVENT = { 18 | CHANGE_PAGE: 1, 19 | CLOSE: 2, 20 | OPEN: 3, 21 | ZOOM_IN: 4, 22 | ZOOM_OUT: 5, 23 | ROTATE_LEFT: 6, 24 | ROTATE_RIGHT: 7 25 | }; 26 | 27 | @Injectable() 28 | export class LightboxEvent { 29 | private _lightboxEventSource: Subject; 30 | public lightboxEvent$: Observable; 31 | constructor() { 32 | this._lightboxEventSource = new Subject(); 33 | this.lightboxEvent$ = this._lightboxEventSource.asObservable(); 34 | } 35 | 36 | broadcastLightboxEvent(event: any): void { 37 | this._lightboxEventSource.next(event); 38 | } 39 | } 40 | 41 | function getWindow (): any { 42 | return window; 43 | } 44 | 45 | @Injectable() 46 | export class LightboxWindowRef { 47 | get nativeWindow (): any { 48 | return getWindow(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/lightbox-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; 2 | import { LightboxEvent, LIGHTBOX_EVENT } from './lightbox-event.service'; 3 | import { LightboxOverlayComponent } from './lightbox-overlay.component'; 4 | 5 | describe('[ Unit - LightboxOverlayComponent ]', () => { 6 | let fixture: ComponentFixture; 7 | let lightboxEvent: LightboxEvent; 8 | let mockData: any; 9 | 10 | beforeEach(() => { 11 | mockData = { 12 | options: { 13 | fadeDuration: 1 14 | } 15 | }; 16 | }); 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ LightboxOverlayComponent ], 21 | providers: [ LightboxEvent ] 22 | }); 23 | 24 | fixture = TestBed.createComponent(LightboxOverlayComponent); 25 | 26 | // mock options and ref 27 | fixture.componentInstance.options = mockData.options; 28 | fixture.componentInstance.cmpRef = { destroy: jasmine.createSpy('spy') }; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | beforeEach(inject([ LightboxEvent ], (lEvent: LightboxEvent) => { 33 | lightboxEvent = lEvent; 34 | })); 35 | 36 | it('should init the component with correct styling', () => { 37 | expect(fixture.nativeElement.getAttribute('class')).toContain('lightboxOverlay animation fadeInOverlay'); 38 | expect(fixture.nativeElement.getAttribute('style')) 39 | .toMatch(new RegExp(`animation.*${mockData.options.fadeDuration}s`)); 40 | }); 41 | 42 | describe('{ method: close }', () => { 43 | it('should self destroy and broadcast event when component is closed', fakeAsync(() => { 44 | spyOn(lightboxEvent, 'broadcastLightboxEvent').and.callThrough(); 45 | fixture.componentInstance.close(); 46 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledWith({ id: LIGHTBOX_EVENT.CLOSE, data: null }); 47 | tick(); 48 | fixture.detectChanges(); 49 | expect(fixture.nativeElement.getAttribute('class')).toContain('lightboxOverlay animation fadeOutOverlay'); 50 | tick(mockData.options.fadeDuration * 1000 + 1); 51 | expect(fixture.componentInstance.cmpRef.destroy).toHaveBeenCalledTimes(1); 52 | })); 53 | }); 54 | 55 | describe('{ method: ngOnDestroy }', () => { 56 | it('should unsubscribe event when destroy is called', () => { 57 | spyOn(fixture.componentInstance['_subscription'], 'unsubscribe').and.callFake(() => {}); 58 | fixture.componentInstance.ngOnDestroy(); 59 | expect(fixture.componentInstance['_subscription'].unsubscribe).toHaveBeenCalledTimes(1); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/lightbox-overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | import { DOCUMENT } from '@angular/common'; 4 | import { 5 | AfterViewInit, 6 | Component, 7 | ElementRef, 8 | HostListener, 9 | Inject, 10 | Input, 11 | OnDestroy, 12 | Renderer2, 13 | } from '@angular/core'; 14 | 15 | import { 16 | IEvent, 17 | LIGHTBOX_EVENT, 18 | LightboxEvent, 19 | } from './lightbox-event.service'; 20 | 21 | @Component({ 22 | selector: '[lb-overlay]', 23 | template: '', 24 | host: { 25 | '[class]': 'classList' 26 | } 27 | }) 28 | export class LightboxOverlayComponent implements AfterViewInit, OnDestroy { 29 | @Input() options: any; 30 | @Input() cmpRef: any; 31 | public classList; 32 | private _subscription: Subscription; 33 | constructor( 34 | private _elemRef: ElementRef, 35 | private _rendererRef: Renderer2, 36 | private _lightboxEvent: LightboxEvent, 37 | @Inject(DOCUMENT) private _documentRef, 38 | ) { 39 | this.classList = 'lightboxOverlay animation fadeInOverlay'; 40 | this._subscription = this._lightboxEvent.lightboxEvent$.subscribe((event: IEvent) => this._onReceivedEvent(event)); 41 | } 42 | 43 | @HostListener('click') 44 | public close(): void { 45 | // broadcast to itself and all others subscriber including the components 46 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.CLOSE, data: null }); 47 | } 48 | 49 | public ngAfterViewInit(): void { 50 | const fadeDuration = this.options.fadeDuration; 51 | 52 | this._rendererRef.setStyle(this._elemRef.nativeElement, 53 | '-webkit-animation-duration', `${fadeDuration}s`); 54 | this._rendererRef.setStyle(this._elemRef.nativeElement, 55 | 'animation-duration', `${fadeDuration}s`); 56 | this._sizeOverlay(); 57 | } 58 | 59 | @HostListener('window:resize') 60 | public onResize(): void { 61 | this._sizeOverlay(); 62 | } 63 | 64 | public ngOnDestroy(): void { 65 | this._subscription.unsubscribe(); 66 | } 67 | 68 | private _sizeOverlay(): void { 69 | const width = this._getOverlayWidth(); 70 | const height = this._getOverlayHeight(); 71 | 72 | this._rendererRef.setStyle(this._elemRef.nativeElement, 'width', `${width}px`); 73 | this._rendererRef.setStyle(this._elemRef.nativeElement, 'height', `${height}px`); 74 | } 75 | 76 | private _onReceivedEvent(event: IEvent): void { 77 | switch (event.id) { 78 | case LIGHTBOX_EVENT.CLOSE: 79 | this._end(); 80 | break; 81 | default: 82 | break; 83 | } 84 | } 85 | 86 | private _end(): void { 87 | this.classList = 'lightboxOverlay animation fadeOutOverlay'; 88 | 89 | // queue self destruction after the animation has finished 90 | // FIXME: not sure if there is any way better than this 91 | setTimeout(() => { 92 | this.cmpRef.destroy(); 93 | }, this.options.fadeDuration * 1000); 94 | } 95 | 96 | private _getOverlayWidth(): number { 97 | return Math.max( 98 | this._documentRef.body.scrollWidth, 99 | this._documentRef.body.offsetWidth, 100 | this._documentRef.documentElement.clientWidth, 101 | this._documentRef.documentElement.scrollWidth, 102 | this._documentRef.documentElement.offsetWidth 103 | ); 104 | } 105 | 106 | private _getOverlayHeight(): number { 107 | return Math.max( 108 | this._documentRef.body.scrollHeight, 109 | this._documentRef.body.offsetHeight, 110 | this._documentRef.documentElement.clientHeight, 111 | this._documentRef.documentElement.scrollHeight, 112 | this._documentRef.documentElement.offsetHeight 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/lightbox.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, inject } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { LightboxEvent, LightboxWindowRef, LIGHTBOX_EVENT } from './lightbox-event.service'; 4 | import { LightboxComponent } from './lightbox.component'; 5 | 6 | describe('[ Unit - LightboxComponent ]', () => { 7 | let fixture: ComponentFixture; 8 | let lightboxEvent: LightboxEvent; 9 | let mockData: any; 10 | 11 | beforeEach(() => { 12 | mockData = { 13 | options: { 14 | fadeDuration: 1, 15 | resizeDuration: 0.5, 16 | fitImageInViewPort: true, 17 | positionFromTop: 20, 18 | showImageNumberLabel: false, 19 | alwaysShowNavOnTouchDevices: false, 20 | wrapAround: false, 21 | disableKeyboardNav: false 22 | }, 23 | currentIndex: 1, 24 | album: [{ 25 | src: 'src/img/next.png', 26 | thumb: 'thumb1', 27 | caption: 'caption1' 28 | }, { 29 | src: 'src/img/prev.png', 30 | thumb: 'thumb2', 31 | caption: 'caption2' 32 | }] 33 | }; 34 | }); 35 | 36 | beforeEach(() => { 37 | TestBed.configureTestingModule({ 38 | declarations: [ LightboxComponent ], 39 | providers: [ LightboxEvent, LightboxWindowRef ], 40 | imports: [ HttpClientTestingModule ] 41 | }); 42 | createComponent(); 43 | }); 44 | 45 | beforeEach(inject([ LightboxEvent ], (lEvent: LightboxEvent) => { 46 | lightboxEvent = lEvent; 47 | })); 48 | 49 | it('should initialize component with correct styling and default value', () => { 50 | expect(fixture.componentInstance.ui).toEqual({ 51 | showReloader: true, 52 | showLeftArrow: false, 53 | showRightArrow: false, 54 | showArrowNav: false, 55 | showPageNumber: false, 56 | showCaption: false, 57 | showZoomButton: false, 58 | showRotateButton: false, 59 | showDownloadButton: false, 60 | classList: 'lightbox animation fadeIn' 61 | }); 62 | expect(fixture.componentInstance.content).toEqual({ pageNumber: '' }); 63 | expect(fixture.componentInstance.album).toEqual(mockData.album); 64 | expect(fixture.componentInstance.options).toEqual(mockData.options); 65 | expect(fixture.componentInstance.currentImageIndex).toEqual(mockData.currentIndex); 66 | }); 67 | 68 | describe('{ method: ngOnDestroy }', () => { 69 | beforeEach(() => { 70 | fixture.componentInstance['_event'].keyup = jasmine.createSpy('keyup'); 71 | fixture.componentInstance['_event'].load = jasmine.createSpy('load'); 72 | spyOn(fixture.componentInstance['_event'].subscription, 'unsubscribe'); 73 | }); 74 | 75 | it('should call correct method if enable keyboard event', () => { 76 | fixture.componentInstance.options.disableKeyboardNav = false; 77 | fixture.componentInstance.ngOnDestroy(); 78 | expect(fixture.componentInstance['_event'].keyup).toHaveBeenCalledTimes(1); 79 | expect(fixture.componentInstance['_event'].subscription.unsubscribe).toHaveBeenCalledTimes(1); 80 | }); 81 | 82 | it('should not call if keyboard event is disabled', () => { 83 | fixture.componentInstance.options.disableKeyboardNav = true; 84 | fixture.componentInstance.ngOnDestroy(); 85 | expect(fixture.componentInstance['_event'].keyup).not.toHaveBeenCalled(); 86 | }); 87 | }); 88 | 89 | describe('{ method: close }', () => { 90 | it('should call `broadcastLightboxEvent` if classlist does contains expected class value', () => { 91 | const eventMock = { 92 | stopPropagation: jasmine.createSpy('spy'), 93 | target: { classList: { contains: jasmine.createSpy('contains').and.callFake(() => true) } } 94 | }; 95 | 96 | spyOn(lightboxEvent, 'broadcastLightboxEvent'); 97 | fixture.componentInstance.close(eventMock); 98 | expect(eventMock.stopPropagation).toHaveBeenCalledTimes(1); 99 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledTimes(1); 100 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledWith({ id: LIGHTBOX_EVENT.CLOSE, data: null }); 101 | }); 102 | }); 103 | 104 | describe('{ method: nextImage }', () => { 105 | it('should change to correct state', () => { 106 | mockData.currentIndex = 0; 107 | createComponent(); 108 | fixture.componentInstance['_event'].load = jasmine.createSpy('load'); 109 | spyOn(lightboxEvent, 'broadcastLightboxEvent'); 110 | fixture.componentInstance.nextImage(); 111 | expect(fixture.componentInstance.ui).toEqual({ 112 | showReloader: true, 113 | showLeftArrow: false, 114 | showRightArrow: false, 115 | showArrowNav: false, 116 | showPageNumber: false, 117 | showZoomButton: false, 118 | showRotateButton: false, 119 | showCaption: false, 120 | showDownloadButton: false, 121 | classList: 'lightbox animation fadeIn' 122 | }); 123 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledTimes(1); 124 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledWith({ id: LIGHTBOX_EVENT.CHANGE_PAGE, data: 1 }); 125 | }); 126 | 127 | it('should change to correct state when index is the last image', () => { 128 | fixture.componentInstance['_event'].load = jasmine.createSpy('load'); 129 | spyOn(lightboxEvent, 'broadcastLightboxEvent'); 130 | fixture.componentInstance.nextImage(); 131 | expect(fixture.componentInstance.ui).toEqual({ 132 | showReloader: true, 133 | showLeftArrow: false, 134 | showZoomButton: false, 135 | showRotateButton: false, 136 | showRightArrow: false, 137 | showArrowNav: false, 138 | showPageNumber: false, 139 | showCaption: false, 140 | showDownloadButton: false, 141 | classList: 'lightbox animation fadeIn' 142 | }); 143 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledTimes(1); 144 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledWith({ id: LIGHTBOX_EVENT.CHANGE_PAGE, data: 0 }); 145 | }); 146 | }); 147 | 148 | describe('{ method: prevImage }', () => { 149 | it('should change to correct state', () => { 150 | fixture.componentInstance['_event'].load = jasmine.createSpy('load'); 151 | spyOn(lightboxEvent, 'broadcastLightboxEvent'); 152 | fixture.componentInstance.prevImage(); 153 | expect(fixture.componentInstance.ui).toEqual({ 154 | showReloader: true, 155 | showLeftArrow: false, 156 | showRightArrow: false, 157 | showArrowNav: false, 158 | showZoomButton: false, 159 | showRotateButton: false, 160 | showPageNumber: false, 161 | showCaption: false, 162 | showDownloadButton: false, 163 | classList: 'lightbox animation fadeIn' 164 | }); 165 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledTimes(1); 166 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledWith({ id: LIGHTBOX_EVENT.CHANGE_PAGE, data: 0 }); 167 | }); 168 | 169 | it('should change to correct state when index is the first image', () => { 170 | mockData.currentIndex = 0; 171 | createComponent(); 172 | fixture.componentInstance['_event'].load = jasmine.createSpy('load'); 173 | spyOn(lightboxEvent, 'broadcastLightboxEvent'); 174 | fixture.componentInstance.nextImage(); 175 | expect(fixture.componentInstance.ui).toEqual({ 176 | showReloader: true, 177 | showLeftArrow: false, 178 | showRightArrow: false, 179 | showZoomButton: false, 180 | showRotateButton: false, 181 | showArrowNav: false, 182 | showPageNumber: false, 183 | showCaption: false, 184 | showDownloadButton: false, 185 | classList: 'lightbox animation fadeIn' 186 | }); 187 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledTimes(1); 188 | expect(lightboxEvent.broadcastLightboxEvent).toHaveBeenCalledWith({ id: LIGHTBOX_EVENT.CHANGE_PAGE, data: 1 }); 189 | }); 190 | }); 191 | 192 | function createComponent() { 193 | fixture = TestBed.createComponent(LightboxComponent); 194 | 195 | // mock options and ref 196 | fixture.componentInstance.options = mockData.options; 197 | fixture.componentInstance.album = mockData.album; 198 | fixture.componentInstance.currentImageIndex = mockData.currentIndex; 199 | fixture.componentInstance.cmpRef = { destroy: jasmine.createSpy('spy') }; 200 | fixture.detectChanges(); 201 | } 202 | }); 203 | -------------------------------------------------------------------------------- /src/lightbox.component.ts: -------------------------------------------------------------------------------- 1 | import { FileSaverService } from 'ngx-filesaver'; 2 | 3 | import { DOCUMENT } from '@angular/common'; 4 | import { 5 | AfterViewInit, 6 | Component, 7 | ElementRef, 8 | Inject, 9 | Input, 10 | OnDestroy, 11 | OnInit, 12 | Renderer2, 13 | SecurityContext, 14 | ViewChild, 15 | } from '@angular/core'; 16 | import { DomSanitizer } from '@angular/platform-browser'; 17 | 18 | import { 19 | IAlbum, 20 | IEvent, 21 | LIGHTBOX_EVENT, 22 | LightboxEvent, 23 | LightboxWindowRef, 24 | } from './lightbox-event.service'; 25 | 26 | @Component({ 27 | template: ` 28 |
29 |
30 | 36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | {{ content.pageNumber }} 51 |
52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 |
69 |
`, 70 | selector: '[lb-content]', 71 | host: { 72 | '(click)': 'close($event)', 73 | '[class]': 'ui.classList' 74 | } 75 | }) 76 | export class LightboxComponent implements OnInit, AfterViewInit, OnDestroy, OnInit { 77 | @Input() album: Array; 78 | @Input() currentImageIndex: number; 79 | @Input() options: any; 80 | @Input() cmpRef: any; 81 | @ViewChild('outerContainer', { static: false }) _outerContainerElem: ElementRef; 82 | @ViewChild('container', { static: false }) _containerElem: ElementRef; 83 | @ViewChild('leftArrow', { static: false }) _leftArrowElem: ElementRef; 84 | @ViewChild('rightArrow', { static: false }) _rightArrowElem: ElementRef; 85 | @ViewChild('navArrow', { static: false }) _navArrowElem: ElementRef; 86 | @ViewChild('dataContainer', { static: false }) _dataContainerElem: ElementRef; 87 | @ViewChild('image', { static: false }) _imageElem: ElementRef; 88 | @ViewChild('caption', { static: false }) _captionElem: ElementRef; 89 | @ViewChild('number', { static: false }) _numberElem: ElementRef; 90 | public content: any; 91 | public ui: any; 92 | private _cssValue: any; 93 | private _event: any; 94 | private _windowRef: any; 95 | private rotate: number; 96 | constructor( 97 | private _elemRef: ElementRef, 98 | private _rendererRef: Renderer2, 99 | private _lightboxEvent: LightboxEvent, 100 | public _lightboxElem: ElementRef, 101 | private _lightboxWindowRef: LightboxWindowRef, 102 | private _fileSaverService: FileSaverService, 103 | private _sanitizer: DomSanitizer, 104 | @Inject(DOCUMENT) private _documentRef 105 | ) { 106 | // initialize data 107 | this.options = this.options || {}; 108 | this.album = this.album || []; 109 | this.currentImageIndex = this.currentImageIndex || 0; 110 | this._windowRef = this._lightboxWindowRef.nativeWindow; 111 | 112 | // control the interactive of the directive 113 | this.ui = { 114 | // control the appear of the reloader 115 | // false: image has loaded completely and ready to be shown 116 | // true: image is still loading 117 | showReloader: true, 118 | 119 | // control the appear of the nav arrow 120 | // the arrowNav is the parent of both left and right arrow 121 | // in some cases, the parent shows but the child does not show 122 | showLeftArrow: false, 123 | showRightArrow: false, 124 | showArrowNav: false, 125 | 126 | // control the appear of the zoom and rotate buttons 127 | showZoomButton: false, 128 | showRotateButton: false, 129 | 130 | // control whether to show the 131 | // page number or not 132 | showPageNumber: false, 133 | showCaption: false, 134 | 135 | // control whether to show the download button or not 136 | showDownloadButton: false, 137 | classList: 'lightbox animation fadeIn' 138 | }; 139 | 140 | this.content = { 141 | pageNumber: '' 142 | }; 143 | 144 | this._event = {}; 145 | this._lightboxElem = this._elemRef; 146 | this._event.subscription = this._lightboxEvent.lightboxEvent$ 147 | .subscribe((event: IEvent) => this._onReceivedEvent(event)); 148 | this.rotate = 0; 149 | } 150 | 151 | ngOnInit(): void { 152 | this.album.forEach(album => { 153 | if (album.caption) { 154 | album.caption = this._sanitizer.sanitize(SecurityContext.HTML, album.caption); 155 | } 156 | }); 157 | } 158 | 159 | public ngAfterViewInit(): void { 160 | // need to init css value here, after the view ready 161 | // actually these values are always 0 162 | this._cssValue = { 163 | containerTopPadding: Math.round(this._getCssStyleValue(this._containerElem, 'padding-top')), 164 | containerRightPadding: Math.round(this._getCssStyleValue(this._containerElem, 'padding-right')), 165 | containerBottomPadding: Math.round(this._getCssStyleValue(this._containerElem, 'padding-bottom')), 166 | containerLeftPadding: Math.round(this._getCssStyleValue(this._containerElem, 'padding-left')), 167 | imageBorderWidthTop: Math.round(this._getCssStyleValue(this._imageElem, 'border-top-width')), 168 | imageBorderWidthBottom: Math.round(this._getCssStyleValue(this._imageElem, 'border-bottom-width')), 169 | imageBorderWidthLeft: Math.round(this._getCssStyleValue(this._imageElem, 'border-left-width')), 170 | imageBorderWidthRight: Math.round(this._getCssStyleValue(this._imageElem, 'border-right-width')) 171 | }; 172 | 173 | if (this._validateInputData()) { 174 | this._prepareComponent(); 175 | this._registerImageLoadingEvent(); 176 | } 177 | } 178 | 179 | public ngOnDestroy(): void { 180 | if (!this.options.disableKeyboardNav) { 181 | // unbind keyboard event 182 | this._disableKeyboardNav(); 183 | } 184 | 185 | this._event.subscription.unsubscribe(); 186 | } 187 | 188 | public close($event: any): void { 189 | $event.stopPropagation(); 190 | if ($event.target.classList.contains('lightbox') || 191 | $event.target.classList.contains('lb-loader') || 192 | $event.target.classList.contains('lb-close')) { 193 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.CLOSE, data: null }); 194 | } 195 | } 196 | 197 | public download($event: any): void { 198 | $event.stopPropagation(); 199 | const url = this.album[this.currentImageIndex].src; 200 | const downloadUrl = this.album[this.currentImageIndex].downloadUrl; 201 | const parts = url.split('/'); 202 | const fileName = parts[parts.length - 1]; 203 | const canvas = document.createElement('canvas'); 204 | const ctx = canvas.getContext('2d'); 205 | const preloader = new Image(); 206 | const _this = this 207 | 208 | preloader.onload = function () { 209 | // @ts-ignore 210 | canvas.width = this.naturalWidth; 211 | // @ts-ignore 212 | canvas.height = this.naturalHeight; 213 | 214 | // @ts-ignore 215 | ctx.drawImage(this, 0, 0); 216 | canvas.toBlob(function (blob) { 217 | _this._fileSaverService.save(blob, fileName) 218 | }, 'image/jpeg', 0.75); 219 | }; 220 | preloader.crossOrigin = ''; 221 | if(downloadUrl && downloadUrl.length > 0) 222 | preloader.src = this._sanitizer.sanitize(SecurityContext.URL, downloadUrl); 223 | else 224 | preloader.src = this._sanitizer.sanitize(SecurityContext.URL, url); 225 | } 226 | 227 | public control($event: any): void { 228 | $event.stopPropagation(); 229 | let height: number; 230 | let width: number; 231 | if ($event.target.classList.contains('lb-turnLeft')) { 232 | this.rotate = this.rotate - 90; 233 | this._rotateContainer(); 234 | this._calcTransformPoint(); 235 | this._documentRef.getElementById('image').style.transform = `rotate(${this.rotate}deg)`; 236 | this._documentRef.getElementById('image').style.webkitTransform = `rotate(${this.rotate}deg)`; 237 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.ROTATE_LEFT, data: null }); 238 | } else if ($event.target.classList.contains('lb-turnRight')) { 239 | this.rotate = this.rotate + 90; 240 | this._rotateContainer(); 241 | this._calcTransformPoint(); 242 | this._documentRef.getElementById('image').style.transform = `rotate(${this.rotate}deg)`; 243 | this._documentRef.getElementById('image').style.webkitTransform = `rotate(${this.rotate}deg)`; 244 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.ROTATE_RIGHT, data: null }); 245 | } else if ($event.target.classList.contains('lb-zoomOut')) { 246 | height = parseInt(this._documentRef.getElementById('outerContainer').style.height, 10) / 1.5; 247 | width = parseInt(this._documentRef.getElementById('outerContainer').style.width, 10) / 1.5; 248 | this._documentRef.getElementById('outerContainer').style.height = height + 'px'; 249 | this._documentRef.getElementById('outerContainer').style.width = width + 'px'; 250 | height = parseInt(this._documentRef.getElementById('image').style.height, 10) / 1.5; 251 | width = parseInt(this._documentRef.getElementById('image').style.width, 10) / 1.5; 252 | this._documentRef.getElementById('image').style.height = height + 'px'; 253 | this._documentRef.getElementById('image').style.width = width + 'px'; 254 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.ZOOM_OUT, data: null }); 255 | } else if ($event.target.classList.contains('lb-zoomIn')) { 256 | height = parseInt(this._documentRef.getElementById('outerContainer').style.height, 10) * 1.5; 257 | width = parseInt(this._documentRef.getElementById('outerContainer').style.width, 10) * 1.5; 258 | this._documentRef.getElementById('outerContainer').style.height = height + 'px'; 259 | this._documentRef.getElementById('outerContainer').style.width = width + 'px'; 260 | height = parseInt(this._documentRef.getElementById('image').style.height, 10) * 1.5; 261 | width = parseInt(this._documentRef.getElementById('image').style.width, 10) * 1.5; 262 | this._documentRef.getElementById('image').style.height = height + 'px'; 263 | this._documentRef.getElementById('image').style.width = width + 'px'; 264 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.ZOOM_IN, data: null }); 265 | } 266 | } 267 | 268 | private _rotateContainer(): void { 269 | let temp = this.rotate; 270 | if (temp < 0) { 271 | temp *= -1; 272 | } 273 | if (temp / 90 % 4 === 1 || temp / 90 % 4 === 3) { 274 | this._documentRef.getElementById('outerContainer').style.height = this._documentRef.getElementById('image').style.width; 275 | this._documentRef.getElementById('outerContainer').style.width = this._documentRef.getElementById('image').style.height; 276 | this._documentRef.getElementById('container').style.height = this._documentRef.getElementById('image').style.width; 277 | this._documentRef.getElementById('container').style.width = this._documentRef.getElementById('image').style.height; 278 | } else { 279 | this._documentRef.getElementById('outerContainer').style.height = this._documentRef.getElementById('image').style.height; 280 | this._documentRef.getElementById('outerContainer').style.width = this._documentRef.getElementById('image').style.width; 281 | this._documentRef.getElementById('container').style.height = this._documentRef.getElementById('image').style.width; 282 | this._documentRef.getElementById('container').style.width = this._documentRef.getElementById('image').style.height; 283 | } 284 | } 285 | 286 | private _resetImage(): void { 287 | this.rotate = 0; 288 | this._documentRef.getElementById('image').style.transform = `rotate(${this.rotate}deg)`; 289 | this._documentRef.getElementById('image').style.webkitTransform = `rotate(${this.rotate}deg)`; 290 | } 291 | 292 | private _calcTransformPoint(): void { 293 | let height = parseInt(this._documentRef.getElementById('image').style.height, 10); 294 | let width = parseInt(this._documentRef.getElementById('image').style.width, 10); 295 | let temp = this.rotate % 360; 296 | if (temp < 0) { 297 | temp = 360 + temp; 298 | } 299 | if (temp === 90) { 300 | this._documentRef.getElementById('image').style.transformOrigin = (height / 2) + 'px ' + (height / 2) + 'px'; 301 | } else if (temp === 180) { 302 | this._documentRef.getElementById('image').style.transformOrigin = (width / 2) + 'px ' + (height / 2) + 'px'; 303 | } else if (temp === 270) { 304 | this._documentRef.getElementById('image').style.transformOrigin = (width / 2) + 'px ' + (width / 2) + 'px'; 305 | } 306 | } 307 | 308 | public nextImage(): void { 309 | if (this.album.length === 1) { 310 | return; 311 | } else if (this.currentImageIndex === this.album.length - 1) { 312 | this._changeImage(0); 313 | } else { 314 | this._changeImage(this.currentImageIndex + 1); 315 | } 316 | } 317 | 318 | public prevImage(): void { 319 | if (this.album.length === 1) { 320 | return; 321 | } else if (this.currentImageIndex === 0 && this.album.length > 1) { 322 | this._changeImage(this.album.length - 1); 323 | } else { 324 | this._changeImage(this.currentImageIndex - 1); 325 | } 326 | } 327 | 328 | private _validateInputData(): boolean { 329 | if (this.album && 330 | this.album instanceof Array && 331 | this.album.length > 0) { 332 | for (let i = 0; i < this.album.length; i++) { 333 | // check whether each _nside 334 | // album has src data or not 335 | if (this.album[i].src) { 336 | continue; 337 | } 338 | 339 | throw new Error('One of the album data does not have source data'); 340 | } 341 | } else { 342 | throw new Error('No album data or album data is not correct in type'); 343 | } 344 | 345 | // to prevent data understand as string 346 | // convert it to number 347 | if (isNaN(this.currentImageIndex)) { 348 | throw new Error('Current image index is not a number'); 349 | } else { 350 | this.currentImageIndex = Number(this.currentImageIndex); 351 | } 352 | 353 | return true; 354 | } 355 | 356 | private _registerImageLoadingEvent(): void { 357 | const preloader = new Image(); 358 | 359 | preloader.onload = () => { 360 | this._onLoadImageSuccess(); 361 | } 362 | 363 | const src: any = this.album[this.currentImageIndex].src; 364 | preloader.src = this._sanitizer.sanitize(SecurityContext.URL, src); 365 | } 366 | 367 | /** 368 | * Fire when the image is loaded 369 | */ 370 | private _onLoadImageSuccess(): void { 371 | if (!this.options.disableKeyboardNav) { 372 | // unbind keyboard event during transition 373 | this._disableKeyboardNav(); 374 | } 375 | 376 | let imageHeight; 377 | let imageWidth; 378 | let maxImageHeight; 379 | let maxImageWidth; 380 | let windowHeight; 381 | let windowWidth; 382 | let naturalImageWidth; 383 | let naturalImageHeight; 384 | 385 | // set default width and height of image to be its natural 386 | imageWidth = naturalImageWidth = this._imageElem.nativeElement.naturalWidth; 387 | imageHeight = naturalImageHeight = this._imageElem.nativeElement.naturalHeight; 388 | if (this.options.fitImageInViewPort) { 389 | windowWidth = this._windowRef.innerWidth; 390 | windowHeight = this._windowRef.innerHeight; 391 | maxImageWidth = windowWidth - this._cssValue.containerLeftPadding - 392 | this._cssValue.containerRightPadding - this._cssValue.imageBorderWidthLeft - 393 | this._cssValue.imageBorderWidthRight - 20; 394 | maxImageHeight = windowHeight - this._cssValue.containerTopPadding - 395 | this._cssValue.containerTopPadding - this._cssValue.imageBorderWidthTop - 396 | this._cssValue.imageBorderWidthBottom - 120; 397 | if (naturalImageWidth > maxImageWidth || naturalImageHeight > maxImageHeight) { 398 | if ((naturalImageWidth / maxImageWidth) > (naturalImageHeight / maxImageHeight)) { 399 | imageWidth = maxImageWidth; 400 | imageHeight = Math.round(naturalImageHeight / (naturalImageWidth / imageWidth)); 401 | } else { 402 | imageHeight = maxImageHeight; 403 | imageWidth = Math.round(naturalImageWidth / (naturalImageHeight / imageHeight)); 404 | } 405 | } 406 | 407 | this._rendererRef.setStyle(this._imageElem.nativeElement, 'width', `${imageWidth}px`); 408 | this._rendererRef.setStyle(this._imageElem.nativeElement, 'height', `${imageHeight}px`); 409 | } 410 | 411 | this._sizeContainer(imageWidth, imageHeight); 412 | 413 | if (this.options.centerVertically) { 414 | this._centerVertically(imageWidth, imageHeight); 415 | } 416 | } 417 | 418 | private _centerVertically(imageWidth: number, imageHeight: number): void { 419 | const scrollOffset = this._documentRef.documentElement.scrollTop; 420 | const windowHeight = this._windowRef.innerHeight; 421 | 422 | const viewOffset = windowHeight / 2 - imageHeight / 2; 423 | const topDistance = scrollOffset + viewOffset; 424 | 425 | this._rendererRef.setStyle(this._lightboxElem.nativeElement, 'top', `${topDistance}px`); 426 | } 427 | 428 | private _sizeContainer(imageWidth: number, imageHeight: number): void { 429 | const oldWidth = this._outerContainerElem.nativeElement.offsetWidth; 430 | const oldHeight = this._outerContainerElem.nativeElement.offsetHeight; 431 | const newWidth = imageWidth + this._cssValue.containerRightPadding + this._cssValue.containerLeftPadding + 432 | this._cssValue.imageBorderWidthLeft + this._cssValue.imageBorderWidthRight; 433 | const newHeight = imageHeight + this._cssValue.containerTopPadding + this._cssValue.containerBottomPadding + 434 | this._cssValue.imageBorderWidthTop + this._cssValue.imageBorderWidthBottom; 435 | 436 | // make sure that distances are large enough for transitionend event to be fired, at least 5px. 437 | if (Math.abs(oldWidth - newWidth) + Math.abs(oldHeight - newHeight) > 5) { 438 | this._rendererRef.setStyle(this._outerContainerElem.nativeElement, 'width', `${newWidth}px`); 439 | this._rendererRef.setStyle(this._outerContainerElem.nativeElement, 'height', `${newHeight}px`); 440 | 441 | // bind resize event to outer container 442 | // use enableTransition to prevent infinite loader 443 | if (this.options.enableTransition) { 444 | this._event.transitions = []; 445 | ['transitionend', 'webkitTransitionEnd', 'oTransitionEnd', 'MSTransitionEnd'].forEach(eventName => { 446 | this._event.transitions.push( 447 | this._rendererRef.listen(this._outerContainerElem.nativeElement, eventName, (event: any) => { 448 | if (event.target === event.currentTarget) { 449 | this._postResize(newWidth, newHeight); 450 | } 451 | }) 452 | ); 453 | }); 454 | } else { 455 | this._postResize(newWidth, newHeight); 456 | } 457 | } else { 458 | this._postResize(newWidth, newHeight); 459 | } 460 | } 461 | 462 | private _postResize(newWidth: number, newHeight: number): void { 463 | // unbind resize event 464 | if (Array.isArray(this._event.transitions)) { 465 | this._event.transitions.forEach((eventHandler: any) => { 466 | eventHandler(); 467 | }); 468 | 469 | this._event.transitions = []; 470 | } 471 | 472 | this._rendererRef.setStyle(this._dataContainerElem.nativeElement, 'width', `${newWidth}px`); 473 | this._showImage(); 474 | } 475 | 476 | private _showImage(): void { 477 | this.ui.showReloader = false; 478 | this._updateNav(); 479 | this._updateDetails(); 480 | if (!this.options.disableKeyboardNav) { 481 | this._enableKeyboardNav(); 482 | } 483 | } 484 | 485 | private _prepareComponent(): void { 486 | // add css3 animation 487 | this._addCssAnimation(); 488 | 489 | // position the image according to user's option 490 | this._positionLightBox(); 491 | 492 | // update controls visibility on next view generation 493 | setTimeout(() => { 494 | this.ui.showZoomButton = this.options.showZoom; 495 | this.ui.showRotateButton = this.options.showRotate; 496 | this.ui.showDownloadButton = this.options.showDownloadButton; 497 | }, 0); 498 | } 499 | 500 | private _positionLightBox(): void { 501 | // @see https://stackoverflow.com/questions/3464876/javascript-get-window-x-y-position-for-scroll 502 | const top = (this._windowRef.pageYOffset || this._documentRef.documentElement.scrollTop) + 503 | this.options.positionFromTop; 504 | const left = this._windowRef.pageXOffset || this._documentRef.documentElement.scrollLeft; 505 | 506 | if (!this.options.centerVertically) { 507 | this._rendererRef.setStyle(this._lightboxElem.nativeElement, 'top', `${top}px`); 508 | } 509 | 510 | this._rendererRef.setStyle(this._lightboxElem.nativeElement, 'left', `${left}px`); 511 | this._rendererRef.setStyle(this._lightboxElem.nativeElement, 'display', 'block'); 512 | 513 | // disable scrolling of the page while open 514 | if (this.options.disableScrolling) { 515 | this._rendererRef.addClass(this._documentRef.documentElement, 'lb-disable-scrolling'); 516 | } 517 | } 518 | 519 | /** 520 | * addCssAnimation add css3 classes for animate lightbox 521 | */ 522 | private _addCssAnimation(): void { 523 | const resizeDuration = this.options.resizeDuration; 524 | const fadeDuration = this.options.fadeDuration; 525 | 526 | this._rendererRef.setStyle(this._lightboxElem.nativeElement, 527 | '-webkit-animation-duration', `${fadeDuration}s`); 528 | this._rendererRef.setStyle(this._lightboxElem.nativeElement, 529 | 'animation-duration', `${fadeDuration}s`); 530 | this._rendererRef.setStyle(this._outerContainerElem.nativeElement, 531 | '-webkit-transition-duration', `${resizeDuration}s`); 532 | this._rendererRef.setStyle(this._outerContainerElem.nativeElement, 533 | 'transition-duration', `${resizeDuration}s`); 534 | this._rendererRef.setStyle(this._dataContainerElem.nativeElement, 535 | '-webkit-animation-duration', `${fadeDuration}s`); 536 | this._rendererRef.setStyle(this._dataContainerElem.nativeElement, 537 | 'animation-duration', `${fadeDuration}s`); 538 | this._rendererRef.setStyle(this._imageElem.nativeElement, 539 | '-webkit-animation-duration', `${fadeDuration}s`); 540 | this._rendererRef.setStyle(this._imageElem.nativeElement, 541 | 'animation-duration', `${fadeDuration}s`); 542 | this._rendererRef.setStyle(this._captionElem.nativeElement, 543 | '-webkit-animation-duration', `${fadeDuration}s`); 544 | this._rendererRef.setStyle(this._captionElem.nativeElement, 545 | 'animation-duration', `${fadeDuration}s`); 546 | this._rendererRef.setStyle(this._numberElem.nativeElement, 547 | '-webkit-animation-duration', `${fadeDuration}s`); 548 | this._rendererRef.setStyle(this._numberElem.nativeElement, 549 | 'animation-duration', `${fadeDuration}s`); 550 | } 551 | 552 | private _end(): void { 553 | this.ui.classList = 'lightbox animation fadeOut'; 554 | if (this.options.disableScrolling) { 555 | this._rendererRef.removeClass(this._documentRef.documentElement, 'lb-disable-scrolling'); 556 | } 557 | setTimeout(() => { 558 | this.cmpRef.destroy(); 559 | }, this.options.fadeDuration * 1000); 560 | } 561 | 562 | private _updateDetails(): void { 563 | // update the caption 564 | if (typeof this.album[this.currentImageIndex].caption !== 'undefined' && 565 | this.album[this.currentImageIndex].caption !== '') { 566 | this.ui.showCaption = true; 567 | } 568 | 569 | // update the page number if user choose to do so 570 | // does not perform numbering the page if the 571 | // array length in album <= 1 572 | if (this.album.length > 1 && this.options.showImageNumberLabel) { 573 | this.ui.showPageNumber = true; 574 | this.content.pageNumber = this._albumLabel(); 575 | } 576 | } 577 | 578 | private _albumLabel(): string { 579 | // due to {this.currentImageIndex} is set from 0 to {this.album.length} - 1 580 | return this.options.albumLabel.replace(/%1/g, Number(this.currentImageIndex + 1)).replace(/%2/g, this.album.length); 581 | } 582 | 583 | private _changeImage(newIndex: number): void { 584 | this._resetImage(); 585 | this.currentImageIndex = newIndex; 586 | this._hideImage(); 587 | this._registerImageLoadingEvent(); 588 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.CHANGE_PAGE, data: newIndex }); 589 | } 590 | 591 | private _hideImage(): void { 592 | this.ui.showReloader = true; 593 | this.ui.showArrowNav = false; 594 | this.ui.showLeftArrow = false; 595 | this.ui.showRightArrow = false; 596 | this.ui.showPageNumber = false; 597 | this.ui.showCaption = false; 598 | } 599 | 600 | private _updateNav(): void { 601 | let alwaysShowNav = false; 602 | 603 | // check to see the browser support touch event 604 | try { 605 | this._documentRef.createEvent('TouchEvent'); 606 | alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false; 607 | } catch (e) { 608 | // noop 609 | } 610 | 611 | // initially show the arrow nav 612 | // which is the parent of both left and right nav 613 | this._showArrowNav(); 614 | if (this.album.length > 1) { 615 | if (this.options.wrapAround) { 616 | if (alwaysShowNav) { 617 | // alternatives this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1'); 618 | this._rendererRef.setStyle(this._leftArrowElem.nativeElement, 'opacity', '1'); 619 | this._rendererRef.setStyle(this._rightArrowElem.nativeElement, 'opacity', '1'); 620 | } 621 | 622 | // alternatives this.$lightbox.find('.lb-prev, .lb-next').show(); 623 | this._showLeftArrowNav(); 624 | this._showRightArrowNav(); 625 | } else { 626 | if (this.currentImageIndex > 0) { 627 | // alternatives this.$lightbox.find('.lb-prev').show(); 628 | this._showLeftArrowNav(); 629 | if (alwaysShowNav) { 630 | // alternatives this.$lightbox.find('.lb-prev').css('opacity', '1'); 631 | this._rendererRef.setStyle(this._leftArrowElem.nativeElement, 'opacity', '1'); 632 | } 633 | } 634 | 635 | if (this.currentImageIndex < this.album.length - 1) { 636 | // alternatives this.$lightbox.find('.lb-next').show(); 637 | this._showRightArrowNav(); 638 | if (alwaysShowNav) { 639 | // alternatives this.$lightbox.find('.lb-next').css('opacity', '1'); 640 | this._rendererRef.setStyle(this._rightArrowElem.nativeElement, 'opacity', '1'); 641 | } 642 | } 643 | } 644 | } 645 | } 646 | 647 | private _showLeftArrowNav(): void { 648 | this.ui.showLeftArrow = true; 649 | } 650 | 651 | private _showRightArrowNav(): void { 652 | this.ui.showRightArrow = true; 653 | } 654 | 655 | private _showArrowNav(): void { 656 | this.ui.showArrowNav = (this.album.length !== 1); 657 | } 658 | 659 | private _enableKeyboardNav(): void { 660 | this._event.keyup = this._rendererRef.listen('document', 'keyup', (event: any) => { 661 | this._keyboardAction(event); 662 | }); 663 | } 664 | 665 | private _disableKeyboardNav(): void { 666 | if (this._event.keyup) { 667 | this._event.keyup(); 668 | } 669 | } 670 | 671 | private _keyboardAction($event: any): void { 672 | const KEYCODE_ESC = 27; 673 | const KEYCODE_LEFTARROW = 37; 674 | const KEYCODE_RIGHTARROW = 39; 675 | const keycode = $event.keyCode; 676 | const key = String.fromCharCode(keycode).toLowerCase(); 677 | 678 | if (keycode === KEYCODE_ESC || key.match(/x|o|c/)) { 679 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.CLOSE, data: null }); 680 | } else if (key === 'p' || keycode === KEYCODE_LEFTARROW) { 681 | if (this.currentImageIndex !== 0) { 682 | this._changeImage(this.currentImageIndex - 1); 683 | } else if (this.options.wrapAround && this.album.length > 1) { 684 | this._changeImage(this.album.length - 1); 685 | } 686 | } else if (key === 'n' || keycode === KEYCODE_RIGHTARROW) { 687 | if (this.currentImageIndex !== this.album.length - 1) { 688 | this._changeImage(this.currentImageIndex + 1); 689 | } else if (this.options.wrapAround && this.album.length > 1) { 690 | this._changeImage(0); 691 | } 692 | } 693 | } 694 | 695 | private _getCssStyleValue(elem: any, propertyName: string): number { 696 | return parseFloat(this._windowRef 697 | .getComputedStyle(elem.nativeElement, null) 698 | .getPropertyValue(propertyName)); 699 | } 700 | 701 | private _onReceivedEvent(event: IEvent): void { 702 | switch (event.id) { 703 | case LIGHTBOX_EVENT.CLOSE: 704 | this._end(); 705 | break; 706 | default: 707 | break; 708 | } 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /src/lightbox.css: -------------------------------------------------------------------------------- 1 | html.lb-disable-scrolling { 2 | overflow: hidden; 3 | } 4 | 5 | .lightboxOverlay { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | z-index: 9999; 10 | background-color: black; 11 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 12 | opacity: 0.8; 13 | } 14 | 15 | .lightbox { 16 | position: absolute; 17 | left: 0; 18 | width: 100%; 19 | z-index: 10000; 20 | text-align: center; 21 | line-height: 0; 22 | font-weight: normal; 23 | box-sizing: content-box; 24 | outline: none; 25 | } 26 | 27 | .lightbox .lb-image { 28 | height: auto; 29 | max-width: inherit; 30 | max-height: none; 31 | border-radius: 3px; 32 | } 33 | 34 | .lightbox a img { 35 | border: none; 36 | } 37 | 38 | .lb-outerContainer { 39 | position: relative; 40 | zoom: 1; 41 | width: 250px; 42 | height: 250px; 43 | margin: 0 auto; 44 | border-radius: 4px; 45 | 46 | /* Background color behind image. 47 | This is visible during transitions. */ 48 | background-color: white; 49 | } 50 | 51 | .lb-outerContainer:after { 52 | content: ""; 53 | display: table; 54 | clear: both; 55 | } 56 | 57 | .lb-loader { 58 | position: absolute; 59 | top: 43%; 60 | left: 0; 61 | height: 25%; 62 | width: 100%; 63 | text-align: center; 64 | line-height: 0; 65 | } 66 | 67 | .lb-cancel { 68 | display: block; 69 | width: 32px; 70 | height: 32px; 71 | margin: 0 auto; 72 | background: url('data:image/gif;base64,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') no-repeat; 73 | } 74 | 75 | .lb-nav { 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | height: 100%; 80 | width: 100%; 81 | z-index: 10; 82 | } 83 | 84 | .lb-container > .nav { 85 | left: 0; 86 | } 87 | 88 | .lb-nav a { 89 | outline: none; 90 | background-image: url('data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='); 91 | } 92 | 93 | .lb-prev, .lb-next { 94 | height: 100%; 95 | cursor: pointer; 96 | display: block; 97 | } 98 | 99 | .lb-nav a.lb-prev { 100 | width: 34%; 101 | left: 0; 102 | float: left; 103 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAYAAADsvzj/AAAFF0lEQVR4Ac2ZW0xcVRSGPTNnhlPKcCsUAeeChkEVxhutDQwzMANaqamNWgpaH+yDIaZp1cRHbgH0gTsxkmDCI/hiRAqgD5qYRgKQ8II6TE00wfgGAcIdKeM/ydrNZIezxxg9m518gRxWmn6s9a9zhvNQJBL5T/gfjokwA5Uw0zWFeHBOugiTsAArfSWZky+iABVowAZSwRkiDSTRz1iHlJMmogATsIDTIAPYgRs8SeTTtXSQSLVKFNkivIQKksDDJFCsquqLmqZdAa/i+yCuPQ1cJHOKjdpJEWGdsIFs8BQoy83NvTEzMzO3t7f318HBweHc3Nxdj8dznWQeIWmpIryENUaiCPgdDsfN+fn5XyLcWV5eDlmt1gBqHgOpbAHIFmESySAHeECF0+m8hd/+vcgxZ3d39wBj9grqCkA6iaiyRBRunJhEpcvl+nBhYeG3iM7Z2dnZgkg1ZSgNqLI6wgebSVTZ7faPlpaW/tSTWF9f36ivr+9AbQkF3iZRhAs2dSInJ+eDUCj0h0Biq7S09BPUBkEhyAKJssKusE6QRCGoQLDfn56eDulJrK6ubgeDwS7UXgTPAztIkXUfUbhxKgLlyMRtBPtXPYm1tbXdqqoqJnEOOGhbJQCTkSJ8sJlEMNoJrFhdicPDw6PKyspe1FaD85yE2YBnLUGwSSIrK+s2bnZLehIbGxubfr+/B7WXSMJJ42QlCcVAES7YJJGdnR0dp7BgnLZKSko6qBPngIvrBEkYIKIT7PLoOKET4TjB7kbty+A8SaRxmcAxQEQn2BUI9q3Z2dl7gk7sINhRiZeoE87jMmGECB/s3JhgR8dJV2Jzc3Pb5/N1UieKKdgsEyaAY5wIk2Dj5GHBRifCgmBHb3adLBNsO3HBNkxEAWZwCmSCx4EPwb4ZJ9jbCHYXSRQDpyDYhomoNFIOUIRMvINO/KQnsbKyshMIBD5D7RVwgQWblzBahD2Sp5jN5jzM+9uLi4s/60mEw+FNbKcvUH8DVIECcAZoXLCliaRaLBbX8PBwb0RwRkZGfkftx+BdUM4+KInDbdxoWUCKoih5CQkJgYGBgS/xs6PjRPb394+ampp+RP174CIoBGcpYypQZIqYY+4dz4DLvb29Y6LONDY2fou6OuAF+SCDZCgj8kQSQDqNihfU9vX1TYlkGhoa7qDuDVBKMpQVrjMG30fYCs6gAHuRmdqurq5JkUxLS8sEaq+CMq4zJGOgCB2Fk8kHJSaTqaazs3Pi2MzQaWtrm0RtDfDFyCQyGUNFOJlEkMlkwLWenp5vRDKtra1TNGYsM5mcjKEifGeYjBfUQUaYmebm5omYzLjFC8C4zyNqTGfcNDZ1/2ABjKHudZLXkTFARJAZN/CqqnqNMqN7Ojo6vqMF4ONkVFmvFUQLQNiZ7u7u76PZAn6S4TJjrIhoAdT+iwXAdQYYKCJaAG/iPhNvAYyj7jXwAngUpAGrDBF+ATCZAuBXFOX60NDQ3TiPM1/hyfoyPf7kgNNSXyvwmSGZMk3T3hocHPwhzlPzJLLFnpZT5PztV5wZNyilbTZFmTnZrxU4GZWXATV4ap4kmeNELlEticjsSHyZq/39/V/j374P2Lk/Pj5+BznxUuDlj1acJ4B8cAH/4er29vbPR0dH58fGxubx/ac2my1Ab3iz5Yc9/gJIB05QCJ4Fz9FXD3gC5HIfi+WKCGQ0GpuzwA7yCDtdS+b/SCFfRPwaQqPxSSaS6JrlwUjR+RtEvCM0ct4sLQAAAABJRU5ErkJggg==') left 48% no-repeat; 104 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); 105 | opacity: 0; 106 | -webkit-transition: opacity 0.6s; 107 | -moz-transition: opacity 0.6s; 108 | -o-transition: opacity 0.6s; 109 | transition: opacity 0.6s; 110 | } 111 | 112 | .lb-nav a.lb-prev:hover { 113 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 114 | opacity: 1; 115 | } 116 | 117 | .lb-nav a.lb-next { 118 | width: 64%; 119 | right: 0; 120 | float: right; 121 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAYAAADsvzj/AAAFDUlEQVR4Ac2ZS0xcVRjHvTN3hisw0GIRZ3AeLWHQWqdVsRqgA86AUmpqoy20Whd2YYhprJq45BVAF7yJkQQTluDGiEhBF5qYRsIjYYMKQxNNMO4gQHgjZfxP8pF8ufEe0qQ5pyf5BTKcWfzyff/vnHt5xLQ0wgbsQCfswEY80BWPxx8I5sUlHMBJP0nm4RfRWAUMkAqOgseII8AFDNqjPYwiGuEAySADeEEuOEkE6bNjIIX22riQchHWSo+SRACc1nU9ahjGG+ASfn8Vn+WT0BNUMV0so04kFTwJTodCoeuTk5N3dnd397a3t/8dHx+fzM7OvoG/nQPPADdwscqoF2HBPgJynE5nZGFhYTZuWlNTU3/4fL6b2FMMnmUyTpJRLqKTSAbIQyu9vrW1tRv/n4Uqzfv9/g+x7xUQAh6QxmVUV0SnKRWESMXm5uZ63GJNT0//GQgEPsHeUibD20xTLeKioBdUV1e3rKysrFrJzM3N/eP1ej/F3jImIxgAcsOeDLLAKRAtLCz8HDKWlZmdnf3b4/F8zCojGADyz5F04AUvgPJoNNq2tLS0YSUzNjY2iwHwEWXmFHCzymiqRGwgiaaXD7wIysvKytqWl5e3rGQwAO4iM7ewt4SmmYfLqLpr2U0yZ0FFaWlp597e3r6VDEbzXapMlGQEA0COiEYyTmozP8lcKC4u7lhdXV2zksGhOZeVlXWLy5gHgDwRJsMqE6A2qygoKGhBm60L2izmdruZjGkAyBShxTNzlGTOgvMYAO2iAYDKxKjNSgQDQI6IRWb8VJnXMADaUZlNK5mJiYl5DAC6AQgGgCwRWjaWGR/IB+fD4XDr2trahqDN5lEZ3mbZ5gEgW4QPAD6aK3BotmIArAsqE2MDIMTajGTkinAZ3mb5NAAS58zGIQPgJvaGwVMgk5597ECTLcJl+AB4GVyKRCJfLi4uijLzGzLzHrWYj1pMVyXCB4BBz/J5oAzcwDT7OhaLWZ4zMzMzvyNX79rt9uOUNyewqRSxsbzk0Jh9H3w2MDDwV1yw+vv7Ox0OR4C+q1REAzr1+ON0TpSDD+rq6n7d2dmxusbs9/T0fJOUlBTRNO2gIg6lGSGJYyAXFIFrtbW1P4oq0dnZOYR9F8EZdqaoCDtVgrJBEoXgck1Nzfciia6urlHsu0rSOSADJEkXYRK8EufAlYaGhtsiiba2thFk4kAij75Po1fiOcIkkplEGFQ2NTWNCBz2W1tbb9tstkrsLaDvcQlN5hWFS2SyTFxubGwcFUl0dHT8gH1VTCITJHMJWSLmYAcPMlFfXy9sJ0gkMnGNpEnCXAkJIhYSReAtBHvosGCTRBgEWSV0qc8jPNhMIgyutLS0/CSSSGRC1/Uqkg5aZUKGiDkTQVAMqtrb238+RGJUHGyZb1F4Je4/2FfFwZYr4qRb7QnwEngTwR4+5JxIZOJtcbDlv2lMAR5wBjfUi7h2fCuS6Ovru6Np2nVqvzwmQcFW9+43HeSg10twix0RSfT29v5iGMY7dMLniTOh+N8KghN7lKZTIQgKMiG/IkwkCJELFiL7uMWOYE+lWUL8elRNa51APoqGh4cTN9p7TOJed3f3d4nz5P4l1ITdDU66XK5Ic3PzF0NDQ1ODg4NT+P0rCFbQM3qu4MRWLsIfX7PB0yAEngPP089TwA8yBMFWKmJ+qZBGj7FecJzw0mfpwBBLqBexseAbIBWkESnAEPybQLnIf4JfIzSb+FymAAAAAElFTkSuQmCC') right 48% no-repeat; 122 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); 123 | opacity: 0; 124 | -webkit-transition: opacity 0.6s; 125 | -moz-transition: opacity 0.6s; 126 | -o-transition: opacity 0.6s; 127 | transition: opacity 0.6s; 128 | } 129 | 130 | .lb-nav a.lb-next:hover { 131 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 132 | opacity: 1; 133 | } 134 | 135 | .lb-dataContainer { 136 | margin: 0 auto; 137 | padding-top: 10px; 138 | zoom: 1; 139 | width: 100%; 140 | -moz-border-radius-bottomleft: 4px; 141 | -webkit-border-bottom-left-radius: 4px; 142 | border-bottom-left-radius: 4px; 143 | -moz-border-radius-bottomright: 4px; 144 | -webkit-border-bottom-right-radius: 4px; 145 | border-bottom-right-radius: 4px; 146 | } 147 | 148 | .lb-dataContainer:after { 149 | content: ""; 150 | display: table; 151 | clear: both; 152 | } 153 | 154 | .lb-data { 155 | padding: 0 4px; 156 | color: #ccc; 157 | } 158 | 159 | .lb-data .lb-details { 160 | max-width: 80%; 161 | float: left; 162 | text-align: left; 163 | line-height: 1.1em; 164 | } 165 | 166 | .lb-data .lb-caption { 167 | font-size: 13px; 168 | font-weight: bold; 169 | line-height: 1em; 170 | } 171 | 172 | .lb-data .lb-caption a { 173 | color: #4ae; 174 | } 175 | 176 | .lb-data .lb-number { 177 | display: block; 178 | clear: left; 179 | padding-bottom: 1em; 180 | font-size: 12px; 181 | color: #999999; 182 | } 183 | 184 | .lb-data .lb-controlContainer { 185 | float: right; 186 | } 187 | 188 | .lb-data .lb-turnContainer { 189 | float: left; 190 | margin-right: 5px; 191 | } 192 | 193 | .lb-data .lb-zoomContainer { 194 | float: right; 195 | margin-right: 5px; 196 | } 197 | 198 | .lb-data .lb-downloadContainer { 199 | float: right; 200 | margin-right: 5px; 201 | } 202 | 203 | .lb-data .lb-closeContainer { 204 | float: right; 205 | } 206 | 207 | .lb-data .lb-close { 208 | display: block; 209 | float: right; 210 | width: 30px; 211 | height: 30px; 212 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAAAPFBMVEX///8AAAD9/f2CgoKAgIAAAAAAAAAAAABLS0sAAAAAAACqqqqqqqq6urpKSkpISEgAAAC7u7u5ubn////zbsMcAAAAE3RSTlMASv6rqwAWS5YMC7/AyZWVFcrJCYaKfAAAAHhJREFUeF590kkOgCAQRFEaFVGc+/53FYmbz6JqBbyQMFSYuoQuV+iTflnstI7ssLXRvMWRaEMs84e2uVckuZe6knL0hiSPObXhj6ChzoEkIolIIpKIO4joICAIeDd7QGIfCCjOKe9HEk8mnxpIAup/F31RPZP9fAG3IAyBSJe0igAAAABJRU5ErkJggg==') top right no-repeat; 213 | text-align: right; 214 | outline: none; 215 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 216 | opacity: 0.7; 217 | -webkit-transition: opacity 0.2s; 218 | -moz-transition: opacity 0.2s; 219 | -o-transition: opacity 0.2s; 220 | transition: opacity 0.2s; 221 | } 222 | 223 | .lb-data .lb-turnLeft { 224 | display: block; 225 | float: left; 226 | width: 30px; 227 | height: 30px; 228 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAACIElEQVRIicWWPWsUURSG3xM/4hYpjLIhphJiIYugNloIiRoU/QO20UJrU1gE/EAbRReCGkQQEbFLCi0EG5NKEIKYItFSkBhSmMLIipolj8WeWa/rbrIzmdm8MNy599xznjN37pe0QbKwAlyTdFjSoJkt1Nhyko5L6pe0S1KPmxYkzUmalDRpZqXYWQDLVHQuaNsNPAFKrK2fwCOgNy440nlgK1AEftUE/wJMAM/8mQDma/qUgVvAlrjgK8CboF4C7gAHAKvjZ24r1ozMFNAdB7wSvI8BXTFGLe8+kT4A+WbBkd4DF4Czazr/H2so+ICpVYe9DjjUyzhgj3cp8L+RBPwbGI4L9pjjHmMZ2JMkRiIB3cA3hz9oGdjhdx38A+hoJXhf8NtOS1Jbi9gzkhb9/WgVTGUD6GnktV6ZGZLeebVQBUsalTQHDGYFlzTvZWcIPuhlX4bgz14uSdJmr0xLOqTKEZeVRiR9lfS82gJ0AAPApgzBGyMaHTTAKeBTFhMMuOxr+Go944gbl4DOFKFdwPeGWyZQ8MNgBSikCL7v0DKwN2qPZrXMbBY4ImmHmc2mBZbU7mXRzD42k+kx4OR6qUCOBlemep138vf2MAZsjwHKA/eA60mybAfeBifKzSb9hn1y4ok3fVcLg2wDbgMzwAlva/PJ8gp4CvQH/fcHiZZZ7aqTIJle/tV0YMsBD4HHaa6KEH4ReAG8Bs6kDshafwDkXu6L86KiLgAAAABJRU5ErkJggg==') top right no-repeat; 229 | text-align: right; 230 | outline: none; 231 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 232 | opacity: 0.7; 233 | -webkit-transition: opacity 0.2s; 234 | -moz-transition: opacity 0.2s; 235 | -o-transition: opacity 0.2s; 236 | transition: opacity 0.2s; 237 | } 238 | 239 | .lb-data .lb-turnRight { 240 | display: block; 241 | float: right; 242 | width: 30px; 243 | height: 30px; 244 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAACGUlEQVRIicWXPWgUQRTH/3OJiSCEgJg7FcEmGLkmNoKCYESLYGNrqaDBSkQri4ARDPgBQRBU8CNgE8TCwkIhIqiVYLAQg5UoOZA0Eg1RE/xZ7DsymZzu7HJ7GVgG5r3/+82+3Z33Vlqj4WIdgQ2SBuzaJqlsphlJNUkvJE065xYC3WZJ9yW9ds6NRO8M6AXuAD9JHz+Ae8B2T3/cbIuxwA7gMrAUBJ8BJoEHdj0HaoHPL+CKxThZX4yBbgHeBHdyFegHVj0ewAG7gGvAvKd7BQxHgYEy8METTwA9UWlK9BXgkaf/kwq21Lz1BGdigabvAY4BQ8BUkP7/gi95fueyQE3/JISlgoEdLL9IE1mhFuM88Dsr+LbZvwGVPOA8O+0CFgw81hKogQ97Gam2glmyeb/Ns865960E1+9yqkgYsDUEb7S5ViD0hKQvwHUf/NXm2aLAkvYqqYa7JandFk9LeiZpvEDwZ5s/FshYPew4Pgh0hoa2lh0cAfiifcdnC4h9CvgEDDYy1o/M70C5gT4vtBuYs9ijjRx2ekXiVhPBfSQldvGfpyLLZfFus8AWdxDYk+ZUBTqaADsCHMgjLAEjJE1bltanC3homVsCNmUFV7yKNQdcANoidH4X8xIopWkaBRllZXvb79n2AePAU+Am0G7rh4Bpy9T6zFAPULVP7Qawzlt/x8rRmxuScUNHSZr5xyRdZfTv0JqOvwtaARgflgIWAAAAAElFTkSuQmCC') top right no-repeat; 245 | text-align: right; 246 | outline: none; 247 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 248 | opacity: 0.7; 249 | -webkit-transition: opacity 0.2s; 250 | -moz-transition: opacity 0.2s; 251 | -o-transition: opacity 0.2s; 252 | transition: opacity 0.2s; 253 | } 254 | 255 | .lb-data .lb-zoomOut { 256 | display: block; 257 | float: left; 258 | width: 30px; 259 | height: 30px; 260 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAB7UlEQVRIieXWvWsUURTG4XPjJpgmlSFKRAtTWGjAj0ajiSKKFopg/AtELbQTAiI2IaazEhsLK0XEVisbISD4gY2FlQQixiAYxESiBn0sdoKTQHZ3dte18FR3uO97fufcmbmciH8UqVYhUkT0RER3RMxGxMeU0uJfqQoJJ/EAc5bHDzzGWXQ0E9qPZ/iMGziCXnRgPfZgDFN4i2PNgJ7IOryFdVW0a3EZ3zHSCHQfvuFCQd8hzONcPdAuzGC0sLnsP5UVvb2o8RpeY0094CzHXTwqYmjHLIbrhWZ5tuAX+mo1LL2jzkbAWa4XuFRJ05Zb74iIlymlhUbBETERETsrCUq59YaImM5vYlNEXKkBdCelNJF7fh/lRlaNtkqbDUbF6zjf8YeI2LXMmdJURJyvA9obETOVBPmOX0XE7mZ8XBExGBHPa1KihE843QgRffiJrUVMo3iDUnX1qjnu4WFRUxemMVYndBgLha/MzLw3M18s6DucXUDXC0NzSY7jC26ju4q2E1exmA0I8xhqBL4NT7MCbuIoNuYGgQGM4x0mMZJBmwJP2VBwPysgHwt4gjNLvyCGVsAH64avKKRHeSTabJU5awV8rmnwGgscyJ3QVxz8b+EHWgnf789MPon2VsIHlGfv/pZBc/DWdVopfgMdyEl3/DM14wAAAABJRU5ErkJggg==') top right no-repeat; 261 | text-align: right; 262 | outline: none; 263 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 264 | opacity: 0.7; 265 | -webkit-transition: opacity 0.2s; 266 | -moz-transition: opacity 0.2s; 267 | -o-transition: opacity 0.2s; 268 | transition: opacity 0.2s; 269 | } 270 | 271 | .lb-data .lb-zoomIn { 272 | display: block; 273 | float: right; 274 | width: 30px; 275 | height: 30px; 276 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAB+UlEQVRIieXWv29NcRzG8c+XttGlk6aEMOhAQhM/FkpLhDD4kai/QFgYpQliaRobi1gMJoRYmSxNmkhIWVibJkQjQiNaKRpehh7p0bTnntt7XYNnOiff5znv53vvPd/7ifhHSmWNSBHRERHtETEREe9TSjN/pRUSjuMBJv2p73iM02ipJ7QLT/EJ13EQa9CCVdiJQbzGKA7XA3o02+FNrKzgXYEL+Ib+WqC78RVnq8ztxxTOLAXahncYqDo8mz+Rld5SbXAQL7G8wDOCTQXrd/CoGmgzJtBXwQfbC9Y34Cc6i56zLHfdExEtEVG+7QJKKY1GxPOIOFYWvDUiRlJK07WAMw1HxLYiQ1PuenVEjOcXsS4iLi2Qu4gPufvbKaXh3P3bmN3IolpWtFijyh3HOI+hEr7CH1fmuYZ7RZ78jl9ExA60lmparJ6IeFbKiSZ8xMkKvkqvUyd+YGPpmhjAqxoPkLt4WBqahdowjsGqgnP5PkxXfWRm4V1Z+FyVuQPZn8TVqqG5hxzBZ9xCewVvKy5jJvv+p9BbC3wznmQFbuAQ1uYGgW5cwRuMoT+D1gWesqHgflYgr2kM4dTvVxC98+A9S4bPK9JhdiRab5E5ax58sm7wkgW7c5/QF+z7b+F7GwnfY24mH0NzI+HdZmfvroZBc/DG7bRIvwDiiW2v3ei28wAAAABJRU5ErkJggg==') top right no-repeat; 277 | text-align: right; 278 | outline: none; 279 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 280 | opacity: 0.7; 281 | -webkit-transition: opacity 0.2s; 282 | -moz-transition: opacity 0.2s; 283 | -o-transition: opacity 0.2s; 284 | transition: opacity 0.2s; 285 | } 286 | 287 | .lb-data .lb-download { 288 | display: block; 289 | float: right; 290 | width: 30px; 291 | height: 30px; 292 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAaUlEQVR4Ae3VgQVAIQCE4UZplDa70Rrt3gQ5eDry/wRIn1QNoka2l22FsW6C5JwAAQIE6IeF5+HR287tw9x5YzdSedcKKDXOTcZ0UQFTQOnta59RVUxAFTABVcAElAZRuEX1DxYQEV3oA58RWgFolpBxAAAAAElFTkSuQmCC') right no-repeat; 293 | text-align: right; 294 | outline: none; 295 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 296 | opacity: 0.7; 297 | -webkit-transition: opacity 0.2s; 298 | -moz-transition: opacity 0.2s; 299 | -o-transition: opacity 0.2s; 300 | transition: opacity 0.2s; 301 | } 302 | 303 | .lb-data .lb-close:hover { 304 | cursor: pointer; 305 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 306 | opacity: 1; 307 | } 308 | 309 | /* animation */ 310 | @keyframes fadeIn{ 311 | 0% {opacity: 0;} 312 | 100% {opacity: 1;} 313 | } 314 | 315 | @-webkit-keyframes fadeIn{ 316 | 0% {opacity: 0;} 317 | 100% {opacity: 1;} 318 | } 319 | 320 | @keyframes fadeOut{ 321 | 0% {opacity: 1;} 322 | 100% {opacity: 0;} 323 | } 324 | 325 | @-webkit-keyframes fadeOut{ 326 | 0% {opacity: 1;} 327 | 100% {opacity: 0;} 328 | } 329 | 330 | 331 | @keyframes fadeInOverlay{ 332 | 0% {opacity: 0;} 333 | 100% {opacity: 0.8;} 334 | } 335 | 336 | @-webkit-keyframes fadeInOverlay{ 337 | 0% {opacity: 0;} 338 | 100% {opacity: 0.8;} 339 | } 340 | 341 | @keyframes fadeOutOverlay{ 342 | 0% {opacity: 0.8;} 343 | 100% {opacity: 0;} 344 | } 345 | 346 | @-webkit-keyframes fadeOutOverlay{ 347 | 0% {opacity: 0.8;} 348 | 100% {opacity: 0;} 349 | } 350 | 351 | .fadeIn{ 352 | -webkit-animation-name: fadeIn; 353 | animation-name: fadeIn; 354 | } 355 | 356 | .fadeInOverlay{ 357 | -webkit-animation-name: fadeInOverlay; 358 | animation-name: fadeInOverlay; 359 | } 360 | 361 | .fadeOut{ 362 | -webkit-animation-name: fadeOut; 363 | animation-name: fadeOut; 364 | } 365 | 366 | .fadeOutOverlay{ 367 | -webkit-animation-name: fadeOutOverlay; 368 | animation-name: fadeOutOverlay; 369 | } 370 | 371 | .animation{ 372 | -webkit-animation-fill-mode: both; 373 | animation-fill-mode: both; 374 | } 375 | 376 | .transition{ 377 | /* For Safari 3.1 to 6.0 */ 378 | -webkit-transition-property: all; 379 | -webkit-transition-timing-function: ease; 380 | /* Standard syntax */ 381 | transition-property: all; 382 | transition-timing-function: ease; 383 | } 384 | 385 | .lb-image { 386 | -webkit-transition-duration: 0.5s; 387 | -moz-transition-duration: 0.5s; 388 | -o-transition-duration: 0.5s; 389 | transition-duration: 0.5s; 390 | -webkit-transition-property: -webkit-transform; 391 | -moz-transition-property: -moz-transform; 392 | -o-transition-property: -o-transform; 393 | transition-property: transform; 394 | } 395 | 396 | /* animation */ 397 | -------------------------------------------------------------------------------- /src/lightbox.module.ts: -------------------------------------------------------------------------------- 1 | import { FileSaverModule } from 'ngx-filesaver'; 2 | 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { LightboxConfig } from './lightbox-config.service'; 6 | import { LightboxEvent, LightboxWindowRef } from './lightbox-event.service'; 7 | import { LightboxOverlayComponent } from './lightbox-overlay.component'; 8 | import { LightboxComponent } from './lightbox.component'; 9 | import { Lightbox } from './lightbox.service'; 10 | 11 | @NgModule({ 12 | declarations: [ LightboxOverlayComponent, LightboxComponent ], 13 | providers: [ 14 | Lightbox, 15 | LightboxConfig, 16 | LightboxEvent, 17 | LightboxWindowRef 18 | ], 19 | entryComponents: [ LightboxOverlayComponent, LightboxComponent ], 20 | imports: [ FileSaverModule ] 21 | }) 22 | export class LightboxModule { } 23 | -------------------------------------------------------------------------------- /src/lightbox.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationRef, 3 | ComponentFactoryResolver, 4 | ComponentRef, 5 | Inject, 6 | Injectable, 7 | Injector 8 | } from '@angular/core'; 9 | import { LightboxComponent } from './lightbox.component'; 10 | import { LightboxConfig } from './lightbox-config.service'; 11 | import { LightboxEvent, LIGHTBOX_EVENT, IAlbum } from './lightbox-event.service'; 12 | import { LightboxOverlayComponent } from './lightbox-overlay.component'; 13 | import { DOCUMENT } from '@angular/common'; 14 | 15 | @Injectable() 16 | export class Lightbox { 17 | constructor( 18 | private _componentFactoryResolver: ComponentFactoryResolver, 19 | private _injector: Injector, 20 | private _applicationRef: ApplicationRef, 21 | private _lightboxConfig: LightboxConfig, 22 | private _lightboxEvent: LightboxEvent, 23 | @Inject(DOCUMENT) private _documentRef 24 | ) { } 25 | 26 | open(album: Array, curIndex = 0, options = {}): void { 27 | const overlayComponentRef = this._createComponent(LightboxOverlayComponent); 28 | const componentRef = this._createComponent(LightboxComponent); 29 | const newOptions: Partial = {}; 30 | 31 | // broadcast open event 32 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.OPEN }); 33 | Object.assign(newOptions, this._lightboxConfig, options); 34 | 35 | // attach input to lightbox 36 | componentRef.instance.album = album; 37 | componentRef.instance.currentImageIndex = curIndex; 38 | componentRef.instance.options = newOptions; 39 | componentRef.instance.cmpRef = componentRef; 40 | 41 | // attach input to overlay 42 | overlayComponentRef.instance.options = newOptions; 43 | overlayComponentRef.instance.cmpRef = overlayComponentRef; 44 | 45 | // FIXME: not sure why last event is broadcasted (which is CLOSED) and make 46 | // lightbox can not be opened the second time. 47 | // Need to timeout so that the OPEN event is set before component is initialized 48 | setTimeout(() => { 49 | this._applicationRef.attachView(overlayComponentRef.hostView); 50 | this._applicationRef.attachView(componentRef.hostView); 51 | overlayComponentRef.onDestroy(() => { 52 | this._applicationRef.detachView(overlayComponentRef.hostView); 53 | }); 54 | componentRef.onDestroy(() => { 55 | this._applicationRef.detachView(componentRef.hostView); 56 | }); 57 | 58 | const containerElement = newOptions.containerElementResolver(this._documentRef); 59 | containerElement.appendChild(overlayComponentRef.location.nativeElement); 60 | containerElement.appendChild(componentRef.location.nativeElement); 61 | }); 62 | } 63 | 64 | close(): void { 65 | if (this._lightboxEvent) { 66 | this._lightboxEvent.broadcastLightboxEvent({ id: LIGHTBOX_EVENT.CLOSE }); 67 | } 68 | } 69 | 70 | _createComponent(ComponentClass: any): ComponentRef { 71 | const factory = this._componentFactoryResolver.resolveComponentFactory(ComponentClass); 72 | const component = factory.create(this._injector); 73 | 74 | return component; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tsconfig-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./compiled", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "noUnusedLocals": true, 11 | "target": "es2015", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "demo/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "noUnusedLocals": true, 10 | "target": "es2015", 11 | "skipLibCheck": true, 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "src/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "label-position": true, 16 | "max-line-length": [ 17 | true, 18 | 140 19 | ], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | "static-before-instance", 24 | "variables-before-functions" 25 | ], 26 | "no-arg": true, 27 | "no-bitwise": true, 28 | "no-console": [ 29 | true, 30 | "debug", 31 | "info", 32 | "time", 33 | "timeEnd", 34 | "trace" 35 | ], 36 | "no-construct": true, 37 | "no-debugger": true, 38 | "no-duplicate-variable": true, 39 | "no-empty": false, 40 | "no-eval": true, 41 | "no-inferrable-types": true, 42 | "no-shadowed-variable": true, 43 | "no-string-literal": false, 44 | "no-switch-case-fall-through": true, 45 | "no-trailing-whitespace": true, 46 | "no-unused-expression": true, 47 | "no-var-keyword": true, 48 | "object-literal-sort-keys": false, 49 | "one-line": [ 50 | true, 51 | "check-open-brace", 52 | "check-catch", 53 | "check-else", 54 | "check-whitespace" 55 | ], 56 | "quotemark": [ 57 | true, 58 | "single" 59 | ], 60 | "radix": true, 61 | "semicolon": [ 62 | "always" 63 | ], 64 | "triple-equals": [ 65 | true, 66 | "allow-null-check" 67 | ], 68 | "typedef-whitespace": [ 69 | true, 70 | { 71 | "call-signature": "nospace", 72 | "index-signature": "nospace", 73 | "parameter": "nospace", 74 | "property-declaration": "nospace", 75 | "variable-declaration": "nospace" 76 | } 77 | ], 78 | "variable-name": false, 79 | "whitespace": [ 80 | true, 81 | "check-branch", 82 | "check-decl", 83 | "check-operator", 84 | "check-separator", 85 | "check-type" 86 | ] 87 | } 88 | } --------------------------------------------------------------------------------