├── .babelrc ├── .circleci └── config.yml ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── pull_request_template.md └── workflows │ ├── add-issue-to-project.yml │ └── add-pr-to-project.yml ├── .gitignore ├── .prettierignore ├── .renovaterc.json ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bower.json ├── build └── banner.js ├── config └── karma │ ├── karma-ci.config.js │ ├── karma-local.config.js │ └── karma.config.js ├── dist ├── luminous-basic.css ├── luminous-basic.min.css ├── luminous.js ├── luminous.min.js └── luminous.min.js.map ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── css │ └── luminous-basic.css └── js │ ├── Lightbox.js │ ├── Luminous.js │ ├── LuminousGallery.js │ ├── injectBaseStylesheet.js │ ├── lum-browser.js │ ├── lum.js │ └── util │ ├── dom.js │ └── throwIfMissing.js ├── test ├── .eslintrc.json ├── testLightbox.js ├── testLuminous.js └── testLuminousGallery.js ├── tests.webpack.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": false }]], 3 | "plugins": [], 4 | "env": { 5 | "commonjs": { 6 | "presets": ["env"] 7 | }, 8 | "test": { 9 | "presets": ["env"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.1.0 5 | browser-tools: circleci/browser-tools@1.4.1 6 | 7 | jobs: 8 | test: 9 | parameters: 10 | version: 11 | default: "current" 12 | description: Node.JS version to install 13 | type: string 14 | docker: 15 | - image: cimg/node:<>-browsers 16 | resource_class: large 17 | steps: 18 | - checkout 19 | - browser-tools/install-browser-tools: 20 | install-geckodriver: false 21 | - run: echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> $BASH_ENV 22 | - run: npm install 23 | - node/install-packages: 24 | pkg-manager: npm 25 | - run: 26 | name: "Test that build is successful" 27 | command: npm run build 28 | - run: npm run test:ci 29 | - run: npm run lint 30 | 31 | deploy: 32 | docker: 33 | - image: cimg/node:17.9-browsers 34 | steps: 35 | - checkout 36 | - run: echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> $BASH_ENV 37 | - run: npm install 38 | - node/install-packages 39 | - run: npx semantic-release 40 | 41 | workflows: 42 | test: 43 | jobs: 44 | - test: 45 | matrix: 46 | parameters: 47 | version: 48 | - "current" 49 | - "lts" 50 | 51 | - deploy: 52 | requires: 53 | - test 54 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "google", "prettier"], 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": "2015", 6 | "sourceType": "module" 7 | }, 8 | "env": { 9 | "browser": true 10 | }, 11 | "rules": { 12 | "require-jsdoc": [ 13 | "warn", 14 | { 15 | "require": { 16 | "FunctionDeclaration": true, 17 | "MethodDefinition": true, 18 | "ClassDeclaration": true 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners for repo 2 | * @imgix/imgix-sdk-team 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your issue has been discussed before. Github issue search can be used for this: https://github.com/imgix/vue/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | - [ ] Please ensure the problem has been isolated and reduced. This link explains more: http://css-tricks.com/6263-reduced-test-cases/ 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. Please strive to reach the **root problem** of your issue to avoid the XY problem. See more: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem 14 | 15 | **To Reproduce** 16 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Thus, the contributors need a way to reproduce your issue - if we can't reproduce your issue, we can't help you! Also, please be as detailed as possible. 17 | 18 | [a link to a codesandox or repl.it; here is a link to a codesandbox with @imgix/vue installed which can be forked: https://codesandbox.io/s/vue-imgix-base-codesandbox-bhz8n] 19 | 20 | [alternatively, please provide a code example] 21 | 22 | ```js 23 | // A *self-contained* demonstration of the problem follows... 24 | // This should be able to be dropped into a file with @imgix/vue installed and just work 25 | ``` 26 | 27 | Steps to reproduce the behaviour: 28 | 29 | 1. Go to '...' 30 | 2. Click on '....' 31 | 3. Scroll down to '....' 32 | 4. See error 33 | 34 | **Expected behaviour** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Screenshots** 38 | If applicable, add screenshots to help explain your problem. 39 | 40 | **Information:** 41 | 42 | - @imgix/vue version: [e.g. v1.0] 43 | - browser version: [include link from [https://www.whatsmybrowser.org/](https://www.whatsmybrowser.org/) or details about the OS used and browser version] 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your feature has already been discussed. Github issue search can be used for this: https://github.com/imgix/vue/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | - [ ] Please take a moment to find out whether your idea fits with the scope and aims of the project 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of how this feature would function. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the project 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your question has already been discussed. Github issue search can be used for this: https://github.com/imgix/vue/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | 11 | **Question** 12 | A clear and concise description of your question 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Description 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ## Checklist 18 | 19 | 22 | 23 | 24 | 25 | - [ ] Read the [contributing guidelines](CONTRIBUTING.md). 26 | - [ ] Each commit follows the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) spec format. 27 | - [ ] Update the readme (if applicable). 28 | - [ ] Update or add any necessary API documentation (if applicable) 29 | - [ ] All existing unit tests are still passing (if applicable). 30 | 31 | 32 | 33 | - [ ] Add some [steps](#steps-to-test) so we can test your bug fix or feature (if applicable). 34 | - [ ] Add new passing unit tests to cover the code introduced by your PR (if applicable). 35 | - [ ] Any breaking changes are specified on the commit on which they are introduced with `BREAKING CHANGE` in the body of the commit. 36 | - [ ] If this is a big feature with breaking changes, consider opening an issue to discuss first. This is completely up to you, but please keep in mind that your PR might not be accepted. 37 | -------------------------------------------------------------------------------- /.github/workflows/add-issue-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/imgix/projects/4 16 | github-token: ${{ secrets.GH_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/add-pr-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add PR to project 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add PR to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/imgix/projects/4 16 | github-token: ${{ secrets.GH_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### PLACE CUSTOM IGNORES HERE 2 | lib 3 | es 4 | .env 5 | 6 | 7 | 8 | 9 | # DO NOT CHANGE ANYTHING BELOW THIS LINE 10 | # ------------------------------------------- 11 | 12 | # Created by https://www.gitignore.io/api/node,bower,macos,windows,sublimetext,visualstudiocode 13 | 14 | ### Bower ### 15 | bower_components 16 | .bower-cache 17 | .bower-registry 18 | .bower-tmp 19 | 20 | ### macOS ### 21 | *.DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### Node ### 48 | # Logs 49 | logs 50 | *.log 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | *.pid.lock 60 | 61 | # Directory for instrumented libs generated by jscoverage/JSCover 62 | lib-cov 63 | 64 | # Coverage directory used by tools like istanbul 65 | coverage 66 | 67 | # nyc test coverage 68 | .nyc_output 69 | 70 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 71 | .grunt 72 | 73 | # Bower dependency directory (https://bower.io/) 74 | 75 | # node-waf configuration 76 | .lock-wscript 77 | 78 | # Compiled binary addons (http://nodejs.org/api/addons.html) 79 | build/Release 80 | 81 | # Dependency directories 82 | node_modules/ 83 | jspm_packages/ 84 | 85 | # Typescript v1 declaration files 86 | typings/ 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional REPL history 95 | .node_repl_history 96 | 97 | # Output of 'npm pack' 98 | *.tgz 99 | 100 | # Yarn Integrity file 101 | .yarn-integrity 102 | 103 | # dotenv environment variables file 104 | .env 105 | 106 | ### SublimeText ### 107 | # cache files for sublime text 108 | *.tmlanguage.cache 109 | *.tmPreferences.cache 110 | *.stTheme.cache 111 | 112 | # workspace files are user-specific 113 | *.sublime-workspace 114 | 115 | # project files should be checked into the repository, unless a significant 116 | # proportion of contributors will probably not be using SublimeText 117 | # *.sublime-project 118 | 119 | # sftp configuration file 120 | sftp-config.json 121 | 122 | # Package control specific files 123 | Package Control.last-run 124 | Package Control.ca-list 125 | Package Control.ca-bundle 126 | Package Control.system-ca-bundle 127 | Package Control.cache/ 128 | Package Control.ca-certs/ 129 | Package Control.merged-ca-bundle 130 | Package Control.user-ca-bundle 131 | oscrypto-ca-bundle.crt 132 | bh_unicode_properties.cache 133 | 134 | # Sublime-github package stores a github token in this file 135 | # https://packagecontrol.io/packages/sublime-github 136 | GitHub.sublime-settings 137 | 138 | ### VisualStudioCode ### 139 | .vscode/* 140 | !.vscode/settings.json 141 | !.vscode/tasks.json 142 | !.vscode/launch.json 143 | !.vscode/extensions.json 144 | .history 145 | 146 | ### Windows ### 147 | # Windows thumbnail cache files 148 | Thumbs.db 149 | ehthumbs.db 150 | ehthumbs_vista.db 151 | 152 | # Folder config file 153 | Desktop.ini 154 | 155 | # Recycle Bin used on file shares 156 | $RECYCLE.BIN/ 157 | 158 | # Windows Installer files 159 | *.cab 160 | *.msi 161 | *.msm 162 | *.msp 163 | 164 | # Windows shortcuts 165 | *.lnk 166 | 167 | 168 | # End of https://www.gitignore.io/api/node,bower,macos,windows,sublimetext,visualstudiocode 169 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | package.json 3 | bower.json 4 | es 5 | lib 6 | dist 7 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>imgix/renovate-config"] 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.3.4](https://github.com/imgix/luminous/compare/v2.3.3...v2.3.4) (2021-07-13) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * updated demo url on readme file ([#522](https://github.com/imgix/luminous/issues/522)) ([cae0218](https://github.com/imgix/luminous/commit/cae02188731e2e931dfd988c7bed1cefc50d06d5)) 11 | 12 | ### [2.3.3](https://github.com/imgix/luminous/compare/v2.3.2...v2.3.3) (2020-11-02) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * allow "closeWithEscape" and "injectBaseStyles" to be overriden when initializing ([fd7dde8](https://github.com/imgix/luminous/commit/fd7dde8aacdbc33771f36b2f2c3485e42b6d9fdc)) 18 | 19 | 20 | ## [2.3.2](https://github.com/imgix/luminous/compare/v2.3.1...v2.3.2) (2018-12-02) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * fix arrow navigation ([#121](https://github.com/imgix/luminous/issues/121)) ([2be022a](https://github.com/imgix/luminous/commit/2be022a)) 26 | 27 | 28 | 29 | 30 | ## [2.3.1](https://github.com/imgix/luminous/compare/v2.3.0...v2.3.1) (2018-10-15) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * support shadow roots for lightboxes ([#79](https://github.com/imgix/luminous/issues/79)) ([700dbfc](https://github.com/imgix/luminous/commit/700dbfc)) 36 | 37 | 38 | 39 | 40 | # [2.3.0](https://github.com/imgix/luminous/compare/v2.2.2...v2.3.0) (2018-10-11) 41 | 42 | 43 | ### Features 44 | 45 | * add close button ([#76](https://github.com/imgix/luminous/issues/76)) ([4548cbe](https://github.com/imgix/luminous/commit/4548cbe)), closes [#72](https://github.com/imgix/luminous/issues/72) 46 | * add shadow dom support for base styles ([#75](https://github.com/imgix/luminous/issues/75)) ([9f12eda](https://github.com/imgix/luminous/commit/9f12eda)) 47 | 48 | 49 | 50 | 51 | ## [2.2.2](https://github.com/imgix/luminous/compare/v2.2.1...v2.2.2) (2018-09-29) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * export public API ([#73](https://github.com/imgix/luminous/issues/73)) ([799eaa1](https://github.com/imgix/luminous/commit/799eaa1)) 57 | 58 | 59 | 60 | 61 | ## [2.2.1](https://github.com/imgix/luminous/compare/v2.2.0...v2.2.1) (2018-08-08) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * update closure file test to include more src files ([#71](https://github.com/imgix/luminous/issues/71)) ([77df615](https://github.com/imgix/luminous/commit/77df615)) 67 | 68 | 69 | 70 | 71 | # [2.2.0](https://github.com/imgix/luminous/compare/v2.1.1...v2.2.0) (2018-07-28) 72 | 73 | 74 | ### Features 75 | 76 | * **gallery:** implement destroy() on LuminousGallery ([#70](https://github.com/imgix/luminous/issues/70)) ([057afb4](https://github.com/imgix/luminous/commit/057afb4)) 77 | 78 | 79 | 80 | 81 | ## [2.1.1](https://github.com/imgix/luminous/compare/v2.1.0...v2.1.1) (2018-07-23) 82 | 83 | 84 | 85 | 86 | # [2.1.0](https://github.com/imgix/luminous/compare/v2.0.1...v2.1.0) (2018-07-23) 87 | 88 | 89 | ### Features 90 | 91 | * add lib and es6 bundles, use closure compiler, remove gulp ([#69](https://github.com/imgix/luminous/issues/69)) ([28be831](https://github.com/imgix/luminous/commit/28be831)) 92 | 93 | 94 | 95 | 96 | ## 2.0.1 (2018-06-26) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * use flexbox and auto margins to center images on mobile ([#60](https://github.com/imgix/luminous/issues/60)) ([262aa47](https://github.com/imgix/luminous/commit/262aa47)), closes [#44](https://github.com/imgix/luminous/issues/44) 102 | * use the right lettercase, add npm start ([#58](https://github.com/imgix/luminous/issues/58)) ([4e6b95b](https://github.com/imgix/luminous/commit/4e6b95b)) 103 | 104 | 105 | 106 | 107 | # 2.0.0 (2018-03-23) 108 | 109 | 110 | 111 | 112 | ## 1.0.1 (2016-10-19) 113 | 114 | 115 | 116 | 117 | # 1.0.0 (2016-09-07) 118 | 119 | 120 | 121 | 122 | ## 0.3.2 (2016-08-04) 123 | 124 | 125 | 126 | 127 | ## 0.3.1 (2016-08-03) 128 | 129 | 130 | 131 | 132 | # 0.3.0 (2016-06-12) 133 | 134 | 135 | 136 | 137 | ## 0.2.7 (2016-05-14) 138 | 139 | 140 | 141 | 142 | ## 0.2.6 (2016-05-02) 143 | 144 | 145 | 146 | 147 | ## 0.2.5 (2016-04-21) 148 | 149 | 150 | 151 | 152 | ## 0.2.4 (2016-03-15) 153 | 154 | 155 | 156 | 157 | ## 0.2.3 (2016-01-17) 158 | 159 | 160 | 161 | 162 | ## 0.2.2 (2016-01-14) 163 | 164 | 165 | 166 | 167 | ## 0.2.1 (2016-01-14) 168 | 169 | 170 | 171 | 172 | # 0.2.0 (2016-01-13) 173 | 174 | 175 | 176 | 177 | ## 0.1.3 (2016-01-08) 178 | 179 | 180 | 181 | 182 | ## 0.1.2 (2015-12-30) 183 | 184 | 185 | 186 | 187 | ## 0.1.1 (2015-12-29) 188 | 189 | 190 | 191 | 192 | # 0.1.0 (2015-12-26) 193 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please read the imgix [Code of Conduct](https://github.com/imgix/code-of-conduct). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you for investing your time in contributing to this project! Please take a moment to review this document in order to streamline the contribution process for you and any reviewers involved. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ## New contributor guide 10 | 11 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: 12 | 13 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 14 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 15 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 16 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 17 | 18 | ## Opening a Pull Request 19 | 20 | _To help the project's maintainers and community quickly understand the nature of your pull request, please be sure to do the following:_ 21 | 22 | 1. Include a descriptive Pull Request title. 23 | 2. Provide a detailed description that explains the nature of the change(s) introduced. This is not only helpful for your reviewer, but also for future users who may need to revisit your Pull Request for context purposes. Screenshots/video captures are helpful here! 24 | 3. Make incremental, modular changes, with a clean commit history. This helps reviewers understand your contribution more easily and maintain project quality. 25 | 26 | ### Checklist 27 | 28 | Check to see that you have completed each of the following before requesting a review of your Pull Request: 29 | 30 | - [ ] All existing unit tests are still passing (if applicable) 31 | - [ ] Add new passing unit tests to cover the code introduced by your PR 32 | - [ ] Update the README 33 | - [ ] Update or add any necessary API documentation 34 | - [ ] All commits in the branch adhere to the [conventional commit](#conventional-commit-spec) format: e.g. `fix: bug #issue-number` 35 | 36 | ## Conventional Commit Spec 37 | 38 | Commits should be in the format `(): `. This allows our team to leverage tooling for automatic releases and changelog generation. An example of a commit in this format might be: `docs(readme): fix typo in documentation` 39 | 40 | `type` can be any of the follow: 41 | 42 | - `feat`: a feature, or breaking change 43 | - `fix`: a bug-fix 44 | - `test`: Adding missing tests or correcting existing tests 45 | - `docs`: documentation only changes (readme, changelog, contributing guide) 46 | - `refactor`: a code change that neither fixes a bug nor adds a feature 47 | - `chore`: reoccurring tasks for project maintainability (example scopes: release, deps) 48 | - `config`: changes to tooling configurations used in the project 49 | - `build`: changes that affect the build system or external dependencies (example scopes: npm, bundler, gradle) 50 | - `ci`: changes to CI configuration files and scripts (example scopes: travis) 51 | - `perf`: a code change that improves performance 52 | - `style`: changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 53 | 54 | `scope` is optional, and can be anything. 55 | `description` should be a short description of the change, written in the imperative-mood. 56 | 57 | ### Example workflow 58 | 59 | Follow this process if you'd like your work considered for inclusion in the 60 | project: 61 | 62 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 63 | and configure the remotes: 64 | 65 | ```bash 66 | # Clone your fork of the repo into the current directory 67 | git clone git@github.com:/luminous.git 68 | # Navigate to the newly cloned directory 69 | cd luminous 70 | # Assign the original repo to a remote called "upstream" 71 | git remote add upstream https://github.com/imgix/luminous 72 | ``` 73 | 74 | 2. If you cloned a while ago, get the latest changes from upstream: 75 | 76 | ```bash 77 | git checkout 78 | git pull upstream 79 | ``` 80 | 81 | 3. Create a new topic branch (off the main project development branch) to 82 | contain your feature, change, or fix: 83 | 84 | ```bash 85 | git checkout -b 86 | ``` 87 | 88 | 4. Commit your changes in logical chunks. Use Git's 89 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 90 | feature to tidy up your commits before making them public. 91 | 92 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 93 | 94 | ```bash 95 | git pull [--rebase] upstream 96 | ``` 97 | 98 | 6. Push your topic branch up to your fork: 99 | 100 | ```bash 101 | git push origin 102 | ``` 103 | 104 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 105 | with a clear title and description. 106 | 107 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 108 | license your work under the same license as that used by the project. 109 | 110 | ## Code Conventions 111 | 112 | 1. Make all changes to files under `./src`, **not** `./dist`. 113 | 2. Use [Gulp](#gulp) to build the JavaScript files. 114 | 3. Use [Prettier](https://prettier.io/) for code formatting. Code will automatically be formatted upon submitting a PR. 115 | 116 | 117 | 118 | ### Using ES6 and Gulp 119 | 120 | To install all development dependencies, in the project's root directory, run 121 | 122 | ``` 123 | npm install 124 | ``` 125 | 126 | Once you're configured, building the JavaScript from the command line is easy: 127 | 128 | ``` 129 | npm run build # build Luminous from source 130 | npm run build:watch # watch for changes and build automatically 131 | npm run test:watch # watch for changes and test automatically 132 | npm run test:local # run the test against local browsers only (Chrome, Safari, Firefox) 133 | ``` 134 | 135 | Please note: in order to run tests in-browser (with `gulp test-local`), Chrome and Firefox should be installed locally. If you want to run `gulp test-full`, `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` must be defined in your shell. 136 | 137 | ### Cutting a release 138 | 139 | Ensure all commits and PR titles are correctly described using the [Conventional Commits Specification](https://conventionalcommits.org/). Update src/Luminous.js with new version number (can be found using `npm run release -- --dry-run`) 140 | 141 | ```sh 142 | npm install # update dependencies to latest 143 | npm run release # build code, bump package version according to commit messages, and generate changelog 144 | git push --follow-tags origin main # push to github and publish 145 | npm publish # publish to npm 146 | ``` 147 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2024, Zebrafish Labs 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luminous 2 | 3 | `Luminous` is a simple, lightweight, no-dependencies JavaScript image lightbox. 4 | 5 | [![npm version](https://img.shields.io/npm/v/luminous-lightbox.svg)](https://www.npmjs.com/package/luminous-lightbox) 6 | [![Build Status](https://travis-ci.org/imgix/luminous.svg?branch=main)](https://travis-ci.org/imgix/luminous) 7 | [![npm](https://img.shields.io/npm/dm/luminous-lightbox.svg)](https://www.npmjs.com/package/luminous-lightbox) 8 | [![License](https://img.shields.io/github/license/imgix/luminous)](https://github.com/imgix/luminous/blob/main/LICENSE.md) 9 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 10 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimgix%2Fluminous.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimgix%2Fluminous?ref=badge_shield) 11 | 12 | --- 13 | 14 | - [Installation](#installation) 15 | - [Usage](#usage) 16 | * [LuminousGallery Usage](#luminousgallery-usage) 17 | - [Options / Defaults](#options--defaults) 18 | * [LuminousGallery Options / Defaults](#luminousgallery-options--defaults) 19 | - [Theming](#theming) 20 | - [Browser Support](#browser-support) 21 | - [Meta](#meta) 22 | - [License](#license) 23 | 24 | 25 | 26 | ## Installation 27 | 28 | - **NPM**: `npm install luminous-lightbox` 29 | - **Bower**: `bower install luminous` 30 | - **Manual**: [Download](https://github.com/imgix/luminous/archive/main.zip) and use `dist/Luminous.min.js` or `dist/Luminous.js` 31 | 32 | If you're using the pre-built version of Luminous, it will automatically make `window.Luminous` and `window.LuminousGallery` available for your use when included on your page. 33 | 34 | If you prefer to use `require` statements and a build tool like Browserify, there are a couple other things to keep in mind. First, `require('luminous-lightbox')` gives you an object with `Luminous` and `LuminousGallery` keys. You can use it in the following ways: 35 | 36 | ```javascript 37 | var Luminous = require('luminous-lightbox').Luminous; 38 | 39 | new Luminous(…); 40 | ``` 41 | 42 | If your project uses ES6, you can do the following instead: 43 | 44 | ```javascript 45 | import { Luminous } from 'luminous-lightbox'; 46 | 47 | new Luminous(…); 48 | ``` 49 | 50 | 51 | 52 | ## Usage 53 | 54 | Once you've installed Luminous via one of the above methods, you're ready to get started. There are no dependencies, so you can just start making cool stuff. Check out the [announcement blog post](https://blog.imgix.com/2016/01/06/better-lightbox-zoom-viewer-with-imgix?utm_medium=referral&utm_source=github&utm_campaign=luminous), or take a peek at [our demo](https://codepen.io/imgix/pen/wMgOEL). Here's an example of a basic implementation: 55 | 56 | ```html 57 | 58 | A dog! 59 | 60 | ``` 61 | 62 | ```javascript 63 | new Luminous(document.querySelector("a")); 64 | ``` 65 | 66 | 67 | 68 | ### LuminousGallery Usage 69 | 70 | Luminous supports gallery-style navigation using the LuminousGallery class. It works nearly the same as Luminous, but has a slightly different method of instantiation. 71 | 72 | ```html 73 | 90 | ``` 91 | 92 | ```javascript 93 | new LuminousGallery(document.querySelectorAll(".gallery-demo")); 94 | ``` 95 | 96 | 97 | 98 | ## Options / Defaults 99 | 100 | Here's an example of using Luminous with a custom configuration. All of the listed options are displayed with their default value. 101 | 102 | ```javascript 103 | var options = { 104 | // Prefix for generated element class names (e.g. `my-ns` will 105 | // result in classes such as `my-ns-lightbox`. Default `lum-` 106 | // prefixed classes will always be added as well. 107 | namespace: null, 108 | // Which attribute to pull the lightbox image source from. 109 | sourceAttribute: "href", 110 | // Captions can be a literal string, or a function that receives the Luminous instance's trigger element as an argument and returns a string. Supports HTML, so use caution when dealing with user input. 111 | caption: null, 112 | // The event to listen to on the _trigger_ element: triggers opening. 113 | openTrigger: "click", 114 | // The event to listen to on the _lightbox_ element: triggers closing. 115 | closeTrigger: "click", 116 | // Allow closing by pressing escape. 117 | closeWithEscape: true, 118 | // Automatically close when the page is scrolled. 119 | closeOnScroll: false, 120 | // Disable close button 121 | showCloseButton: false, 122 | // A node to append the lightbox element to. 123 | appendToNode: document.body, 124 | // A selector defining what to append the lightbox element to. 125 | // This will take precedence over `appendToNode`. 126 | appendToSelector: null, 127 | // If present (and a function), this will be called 128 | // whenever the lightbox is opened. 129 | onOpen: null, 130 | // If present (and a function), this will be called 131 | // whenever the lightbox is closed. 132 | onClose: null, 133 | // When true, adds the `imgix-fluid` class to the `img` 134 | // inside the lightbox. See https://github.com/imgix/imgix.js 135 | // for more information. 136 | includeImgixJSClass: false, 137 | // Add base styles to the page. See the "Theming" 138 | // section of README.md for more information. 139 | injectBaseStyles: true 140 | }; 141 | 142 | new Luminous(document.querySelector("a"), options); 143 | ``` 144 | 145 | 146 | 147 | ### LuminousGallery Options / Defaults 148 | 149 | LuminousGallery supports two sets of options arguments. The first set is specific to the gallery itself, and the second specifies the options that get passed to its child Luminous instances. 150 | 151 | ```javascript 152 | var galleryOpts = { 153 | // Whether pressing the arrow keys should move to the next/previous slide. 154 | arrowNavigation: true, 155 | // A callback triggered when the image changes that is passed the image HTML element 156 | onChange: ({ imgEl }) => { … }, 157 | }; 158 | 159 | var luminousOpts = { 160 | // These options have the same defaults and potential values as the Luminous class. 161 | }; 162 | 163 | new LuminousGallery(document.querySelectorAll("a"), galleryOpts, luminousOpts); 164 | ``` 165 | 166 | 167 | 168 | ## Theming 169 | 170 | By default, Luminous injects an extremely basic set of styles into the page via the `injectBaseStyles` option. You will almost certainly want to extend these basic styles for a prettier, more usable experience that matches your site. If you need to do something very out of the ordinary, or just prefer to include the default styles in CSS yourself, you can pass `injectBaseStyles: false` when instantiating a new instance of Luminous. Please note that if you disable the included base styles, you will still need to provide an animation for `.lum-lightbox.lum-opening` and `.lum-lightbox.lum-closing` (this can be a "noop" style animation, as seen in the base styles source). 171 | 172 | There is also an included basic theme (`luminous-basic.css`) that may meet your needs, or at least give a good example of how to build out your own custom styles. This can either be included in your site's CSS via `@import "node_modules/luminous-lightbox/dist/luminous-basic.css";` or as a linked stylesheet in your HTML. 173 | 174 | Additionally, the `namespace` option can be used as a way to easily apply different themes to specific instances of Luminous. 175 | 176 | 177 | 178 | ## Browser Support 179 | 180 | We support the latest version of Google Chrome (which [automatically updates](https://support.google.com/chrome/answer/95414) whenever it detects that a new version of the browser is available). We also support the current and previous major releases of desktop Firefox, Internet Explorer, and Safari on a rolling basis. Mobile support is tested on the most recent minor version of the current and previous major release for the default browser on iOS and Android (e.g., iOS 9.2 and 8.4). Each time a new version is released, we begin supporting that version and stop supporting the third most recent version. 181 | 182 | 183 | 184 | ## Meta 185 | 186 | Luminous was made by [imgix](https://imgix.com?utm_medium=referral&utm_source=github&utm_campaign=luminous). It's licensed under the BSD 2-Clause license (see the [license file](https://github.com/imgix/luminous/blob/main/LICENSE.md) for more info). Any contribution is absolutely welcome, but please review the [contribution guidelines](https://github.com/imgix/luminous/blob/main/CONTRIBUTING.md) before getting started. 187 | 188 | ## License 189 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimgix%2Fluminous.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimgix%2Fluminous?ref=badge_large) 190 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luminous", 3 | "version": "2.3.4", 4 | "description": "A simple, lightweight, no-dependencies JavaScript image lightbox.", 5 | "main": [ 6 | "dist/Luminous.min.js", 7 | "dist/luminous-basic.css" 8 | ], 9 | "authors": [ 10 | "imgix" 11 | ], 12 | "license": "BSD-2", 13 | "keywords": [ 14 | "javascript", 15 | "lightbox", 16 | "image", 17 | "images" 18 | ], 19 | "homepage": "https://github.com/imgix/luminous", 20 | "moduleType": [], 21 | "ignore": [ 22 | "**/.*", 23 | ".github", 24 | "node_modules", 25 | "src", 26 | "test", 27 | "CONTRIBUTING.md", 28 | "index.html", 29 | "config", 30 | "package.json", 31 | "postcss.config.js", 32 | "webpack.config.js" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /build/banner.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../package.json'); 2 | const year = new Date().getFullYear(); 3 | 4 | const banner = `Luminous v${pkg.version} 5 | Copyright 2015-${year}, Zebrafish Labs 6 | Licensed under BSD-2 (https://github.com/imgix/luminous/blob/main/LICENSE.md)`; 7 | 8 | module.exports = banner; 9 | -------------------------------------------------------------------------------- /config/karma/karma-ci.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./karma.config").ci; 2 | -------------------------------------------------------------------------------- /config/karma/karma-local.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./karma.config").local; 2 | -------------------------------------------------------------------------------- /config/karma/karma.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require("../../webpack.config"); 2 | 3 | const webpackConfigAugmented = { 4 | ...webpackConfig, 5 | module: { 6 | ...webpackConfig.module, 7 | rules: [ 8 | // ...webpackConfig.module.rules, 9 | { 10 | test: /(\.jsx|\.js)$/, 11 | loader: "babel-loader", 12 | exclude: /(node_modules|bower_components)/ 13 | } 14 | ] 15 | } 16 | }; 17 | 18 | const baseConfig = { 19 | frameworks: ["jasmine"], 20 | files: ["../../tests.webpack.js"], 21 | preprocessors: { 22 | "../../tests.webpack.js": "webpack" 23 | }, 24 | webpack: webpackConfigAugmented, 25 | webpackMiddleware: { 26 | stats: "errors-only" 27 | }, 28 | concurrency: 5, 29 | captureTimeout: 90000, 30 | browserConnectTimeout: 3000, 31 | browserNoActivityTimeout: 15000 32 | }; 33 | 34 | const stringConfig = JSON.stringify(baseConfig); 35 | 36 | /** 37 | * Local testing - Chrome and FF, headlessly 38 | */ 39 | 40 | const localConfig = karmaConfig => { 41 | const config = { 42 | ...baseConfig, 43 | browsers: ["ChromeHeadless", "FirefoxHeadless"], 44 | customLaunchers: { 45 | FirefoxHeadless: { 46 | base: "Firefox", 47 | flags: ["-headless"] 48 | } 49 | } 50 | }; 51 | 52 | karmaConfig.set(config); 53 | }; 54 | 55 | /** 56 | * CI testing - Chrome, Firefox 57 | */ 58 | 59 | const ciConfig = karmaConfig => { 60 | const config = { 61 | ...baseConfig, 62 | browsers: ["ChromeTravis", "FirefoxHeadless"], 63 | customLaunchers: { 64 | ChromeTravis: { 65 | base: "ChromeHeadless", 66 | flags: ["--no-sandbox"] 67 | }, 68 | FirefoxHeadless: { 69 | base: "Firefox", 70 | flags: ["-headless"] 71 | } 72 | }, 73 | client: { 74 | mocha: { 75 | timeout: 20000 // 20 seconds 76 | } 77 | } 78 | }; 79 | 80 | karmaConfig.set(config); 81 | }; 82 | 83 | /** 84 | * SauceLabs configuration - not supported 85 | */ 86 | 87 | var fullConfig = JSON.parse(stringConfig); 88 | fullConfig.reporters = ["progress", "saucelabs"]; 89 | fullConfig.saucelabs = { testName: "Luminous Tests" }; 90 | fullConfig.browsers = [ 91 | "sl_chrome", 92 | "sl_safari_9", 93 | "sl_safari_8", 94 | "sl_firefox_42", 95 | "sl_firefox_41", 96 | "sl_ie_11", 97 | "sl_ie_10", 98 | "sl_ios_9", 99 | "sl_ios_8", 100 | "sl_android_5", 101 | "sl_android_4" 102 | ]; 103 | fullConfig.customLaunchers = { 104 | sl_chrome: { base: "SauceLabs", browserName: "Chrome" }, 105 | sl_safari_9: { base: "SauceLabs", browserName: "Safari", version: 9 }, 106 | sl_safari_8: { base: "SauceLabs", browserName: "Safari", version: 8 }, 107 | sl_firefox_42: { base: "SauceLabs", browserName: "Firefox", version: 42 }, 108 | sl_firefox_41: { base: "SauceLabs", browserName: "Firefox", version: 41 }, 109 | sl_ie_11: { 110 | base: "SauceLabs", 111 | browserName: "Internet Explorer", 112 | version: "11" 113 | }, 114 | sl_ie_10: { 115 | base: "SauceLabs", 116 | browserName: "Internet Explorer", 117 | version: "10" 118 | }, 119 | sl_ios_9: { base: "SauceLabs", browserName: "iPhone", version: "9.2" }, 120 | sl_ios_8: { base: "SauceLabs", browserName: "iPhone", version: "8.4" }, 121 | sl_android_5: { base: "SauceLabs", browserName: "Android", version: "5.1" }, 122 | sl_android_4: { base: "SauceLabs", browserName: "Android", version: "4.4" } 123 | }; 124 | 125 | exports.local = localConfig; 126 | exports.ci = ciConfig; 127 | exports.full = karmaConfig => karmaConfig.set(fullConfig); 128 | -------------------------------------------------------------------------------- /dist/luminous-basic.css: -------------------------------------------------------------------------------- 1 | @keyframes lum-fade { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 100% { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | @keyframes lum-fadeZoom { 11 | 0% { 12 | transform: scale(0.5); 13 | opacity: 0; 14 | } 15 | 100% { 16 | transform: scale(1); 17 | opacity: 1; 18 | } 19 | } 20 | 21 | @keyframes lum-loader-rotate { 22 | 0% { 23 | transform: translate(-50%, -50%) rotate(0); 24 | } 25 | 50% { 26 | transform: translate(-50%, -50%) rotate(-180deg); 27 | } 28 | 100% { 29 | transform: translate(-50%, -50%) rotate(-360deg); 30 | } 31 | } 32 | 33 | @keyframes lum-loader-before { 34 | 0% { 35 | transform: scale(1); 36 | } 37 | 10% { 38 | transform: scale(1.2) translateX(6px); 39 | } 40 | 25% { 41 | transform: scale(1.3) translateX(8px); 42 | } 43 | 40% { 44 | transform: scale(1.2) translateX(6px); 45 | } 46 | 50% { 47 | transform: scale(1); 48 | } 49 | 60% { 50 | transform: scale(0.8) translateX(6px); 51 | } 52 | 75% { 53 | transform: scale(0.7) translateX(8px); 54 | } 55 | 90% { 56 | transform: scale(0.8) translateX(6px); 57 | } 58 | 100% { 59 | transform: scale(1); 60 | } 61 | } 62 | 63 | @keyframes lum-loader-after { 64 | 0% { 65 | transform: scale(1); 66 | } 67 | 10% { 68 | transform: scale(1.2) translateX(-6px); 69 | } 70 | 25% { 71 | transform: scale(1.3) translateX(-8px); 72 | } 73 | 40% { 74 | transform: scale(1.2) translateX(-6px); 75 | } 76 | 50% { 77 | transform: scale(1); 78 | } 79 | 60% { 80 | transform: scale(0.8) translateX(-6px); 81 | } 82 | 75% { 83 | transform: scale(0.7) translateX(-8px); 84 | } 85 | 90% { 86 | transform: scale(0.8) translateX(-6px); 87 | } 88 | 100% { 89 | transform: scale(1); 90 | } 91 | } 92 | 93 | .lum-lightbox { 94 | background: rgba(0, 0, 0, 0.6); 95 | } 96 | 97 | .lum-lightbox-inner { 98 | top: 2.5%; 99 | right: 2.5%; 100 | bottom: 2.5%; 101 | left: 2.5%; 102 | } 103 | 104 | .lum-lightbox-inner img { 105 | position: relative; 106 | } 107 | 108 | .lum-lightbox-inner .lum-lightbox-caption { 109 | margin: 0 auto; 110 | color: #fff; 111 | max-width: 700px; 112 | text-align: center; 113 | } 114 | 115 | .lum-loading .lum-lightbox-loader { 116 | display: block; 117 | position: absolute; 118 | top: 50%; 119 | left: 50%; 120 | transform: translate(-50%, -50%); 121 | width: 66px; 122 | height: 20px; 123 | animation: lum-loader-rotate 1800ms infinite linear; 124 | } 125 | 126 | .lum-lightbox-loader:before, 127 | .lum-lightbox-loader:after { 128 | content: ""; 129 | display: block; 130 | width: 20px; 131 | height: 20px; 132 | position: absolute; 133 | top: 50%; 134 | margin-top: -10px; 135 | border-radius: 20px; 136 | background: rgba(255, 255, 255, 0.9); 137 | } 138 | 139 | .lum-lightbox-loader:before { 140 | left: 0; 141 | animation: lum-loader-before 1800ms infinite linear; 142 | } 143 | 144 | .lum-lightbox-loader:after { 145 | right: 0; 146 | animation: lum-loader-after 1800ms infinite linear; 147 | animation-delay: -900ms; 148 | } 149 | 150 | .lum-lightbox.lum-opening { 151 | animation: lum-fade 180ms ease-out; 152 | } 153 | 154 | .lum-lightbox.lum-opening .lum-lightbox-inner { 155 | animation: lum-fadeZoom 180ms ease-out; 156 | } 157 | 158 | .lum-lightbox.lum-closing { 159 | animation: lum-fade 300ms ease-in; 160 | animation-direction: reverse; 161 | } 162 | 163 | .lum-lightbox.lum-closing .lum-lightbox-inner { 164 | animation: lum-fadeZoom 300ms ease-in; 165 | animation-direction: reverse; 166 | } 167 | 168 | .lum-img { 169 | transition: opacity 120ms ease-out; 170 | } 171 | 172 | .lum-loading .lum-img { 173 | opacity: 0; 174 | } 175 | 176 | .lum-gallery-button { 177 | overflow: hidden; 178 | text-indent: 150%; 179 | white-space: nowrap; 180 | background: transparent; 181 | border: 0; 182 | margin: 0; 183 | padding: 0; 184 | outline: 0; 185 | position: absolute; 186 | top: 50%; 187 | transform: translateY(-50%); 188 | height: 100px; 189 | max-height: 100%; 190 | width: 60px; 191 | cursor: pointer; 192 | } 193 | 194 | .lum-close-button { 195 | position: absolute; 196 | right: 5px; 197 | top: 5px; 198 | width: 32px; 199 | height: 32px; 200 | opacity: 0.3; 201 | } 202 | .lum-close-button:hover { 203 | opacity: 1; 204 | } 205 | .lum-close-button:before, 206 | .lum-close-button:after { 207 | position: absolute; 208 | left: 15px; 209 | content: " "; 210 | height: 33px; 211 | width: 2px; 212 | background-color: #fff; 213 | } 214 | .lum-close-button:before { 215 | transform: rotate(45deg); 216 | } 217 | .lum-close-button:after { 218 | transform: rotate(-45deg); 219 | } 220 | 221 | .lum-previous-button { 222 | left: 12px; 223 | } 224 | 225 | .lum-next-button { 226 | right: 12px; 227 | } 228 | 229 | .lum-gallery-button:after { 230 | content: ""; 231 | display: block; 232 | position: absolute; 233 | top: 50%; 234 | width: 36px; 235 | height: 36px; 236 | border-top: 4px solid rgba(255, 255, 255, 0.8); 237 | } 238 | 239 | .lum-previous-button:after { 240 | transform: translateY(-50%) rotate(-45deg); 241 | border-left: 4px solid rgba(255, 255, 255, 0.8); 242 | box-shadow: -2px 0 rgba(0, 0, 0, 0.2); 243 | left: 12%; 244 | border-radius: 3px 0 0 0; 245 | } 246 | 247 | .lum-next-button:after { 248 | transform: translateY(-50%) rotate(45deg); 249 | border-right: 4px solid rgba(255, 255, 255, 0.8); 250 | box-shadow: 2px 0 rgba(0, 0, 0, 0.2); 251 | right: 12%; 252 | border-radius: 0 3px 0 0; 253 | } 254 | 255 | /* This media query makes screens less than 460px wide display in a "fullscreen"-esque mode. Users can then scroll around inside the lightbox to see the entire image. */ 256 | @media (max-width: 460px) { 257 | .lum-lightbox-image-wrapper { 258 | display: flex; 259 | overflow: auto; 260 | -webkit-overflow-scrolling: touch; 261 | } 262 | 263 | .lum-lightbox-caption { 264 | width: 100%; 265 | position: absolute; 266 | bottom: 0; 267 | } 268 | 269 | /* Used to centre the image in the container, respecting overflow: https://stackoverflow.com/a/33455342/515634 */ 270 | .lum-lightbox-position-helper { 271 | margin: auto; 272 | } 273 | 274 | .lum-lightbox-inner img { 275 | max-width: none; 276 | max-height: none; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /dist/luminous-basic.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Luminous v2.3.5 3 | * Copyright 2015-2021, Zebrafish Labs 4 | * Licensed under BSD-2 (https://github.com/imgix/luminous/blob/main/LICENSE.md) 5 | */@keyframes a{0%{opacity:0}to{opacity:1}}@keyframes b{0%{opacity:0;transform:scale(.5)}to{opacity:1;transform:scale(1)}}@keyframes c{0%{transform:translate(-50%,-50%) rotate(0)}50%{transform:translate(-50%,-50%) rotate(-180deg)}to{transform:translate(-50%,-50%) rotate(-1turn)}}@keyframes d{0%{transform:scale(1)}10%{transform:scale(1.2) translateX(6px)}25%{transform:scale(1.3) translateX(8px)}40%{transform:scale(1.2) translateX(6px)}50%{transform:scale(1)}60%{transform:scale(.8) translateX(6px)}75%{transform:scale(.7) translateX(8px)}90%{transform:scale(.8) translateX(6px)}to{transform:scale(1)}}@keyframes e{0%{transform:scale(1)}10%{transform:scale(1.2) translateX(-6px)}25%{transform:scale(1.3) translateX(-8px)}40%{transform:scale(1.2) translateX(-6px)}50%{transform:scale(1)}60%{transform:scale(.8) translateX(-6px)}75%{transform:scale(.7) translateX(-8px)}90%{transform:scale(.8) translateX(-6px)}to{transform:scale(1)}}.lum-lightbox{background:rgba(0,0,0,.6)}.lum-lightbox-inner{bottom:2.5%;left:2.5%;right:2.5%;top:2.5%}.lum-lightbox-inner img{position:relative}.lum-lightbox-inner .lum-lightbox-caption{color:#fff;margin:0 auto;max-width:700px;text-align:center}.lum-loading .lum-lightbox-loader{animation:c 1.8s linear infinite;display:block;height:20px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:66px}.lum-lightbox-loader:after,.lum-lightbox-loader:before{background:hsla(0,0%,100%,.9);border-radius:20px;content:"";display:block;height:20px;margin-top:-10px;position:absolute;top:50%;width:20px}.lum-lightbox-loader:before{animation:d 1.8s linear infinite;left:0}.lum-lightbox-loader:after{animation:e 1.8s linear infinite;animation-delay:-.9s;right:0}.lum-lightbox.lum-opening{animation:a .18s ease-out}.lum-lightbox.lum-opening .lum-lightbox-inner{animation:b .18s ease-out}.lum-lightbox.lum-closing{animation:a .3s ease-in;animation-direction:reverse}.lum-lightbox.lum-closing .lum-lightbox-inner{animation:b .3s ease-in;animation-direction:reverse}.lum-img{transition:opacity .12s ease-out}.lum-loading .lum-img{opacity:0}.lum-gallery-button{background:transparent;border:0;cursor:pointer;height:100px;margin:0;max-height:100%;outline:0;overflow:hidden;padding:0;position:absolute;text-indent:150%;top:50%;transform:translateY(-50%);white-space:nowrap;width:60px}.lum-close-button{height:32px;opacity:.3;position:absolute;right:5px;top:5px;width:32px}.lum-close-button:hover{opacity:1}.lum-close-button:after,.lum-close-button:before{background-color:#fff;content:" ";height:33px;left:15px;position:absolute;width:2px}.lum-close-button:before{transform:rotate(45deg)}.lum-close-button:after{transform:rotate(-45deg)}.lum-previous-button{left:12px}.lum-next-button{right:12px}.lum-gallery-button:after{border-top:4px solid hsla(0,0%,100%,.8);content:"";display:block;height:36px;position:absolute;top:50%;width:36px}.lum-previous-button:after{border-left:4px solid hsla(0,0%,100%,.8);border-radius:3px 0 0 0;box-shadow:-2px 0 rgba(0,0,0,.2);left:12%;transform:translateY(-50%) rotate(-45deg)}.lum-next-button:after{border-radius:0 3px 0 0;border-right:4px solid hsla(0,0%,100%,.8);box-shadow:2px 0 rgba(0,0,0,.2);right:12%;transform:translateY(-50%) rotate(45deg)}@media (max-width:460px){.lum-lightbox-image-wrapper{-webkit-overflow-scrolling:touch;display:flex;overflow:auto}.lum-lightbox-caption{bottom:0;position:absolute;width:100%}.lum-lightbox-position-helper{margin:auto}.lum-lightbox-inner img{max-height:none;max-width:none}} -------------------------------------------------------------------------------- /dist/luminous.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Luminous v2.3.5 3 | * Copyright 2015-2021, Zebrafish Labs 4 | * Licensed under BSD-2 (https://github.com/imgix/luminous/blob/main/LICENSE.md) 5 | */ 6 | var l="function"==typeof Object.defineProperties?Object.defineProperty:function(t,i,e){t!=Array.prototype&&t!=Object.prototype&&(t[i]=e.value)},n="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function p(){p=function(){},n.Symbol||(n.Symbol=q)}var r=0;function q(t){return"jscomp_symbol_"+(t||"")+r++}for(var t=n,u=["Object","assign"],v=0;v=this.a.length?this.a[0]:this.a[t]},n.prototype.ka=function(t){return 0>(t=Array.prototype.indexOf.call(this.a,t)-1)?this.a[this.a.length-1]:this.a[t]},n.prototype.M=function(t){t=t.j;var i=this.f.M;i&&"function"==typeof i&&i({j:t})},n.prototype.l=function(){this.g.forEach((function(t){return t.l()}))},n.prototype.destroy=n.prototype.l,window.LuminousGallery=n,window.Luminous=o}]); 7 | //# sourceMappingURL=luminous.min.js.map -------------------------------------------------------------------------------- /dist/luminous.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Luminous v2.3.5 3 | * Copyright 2015-2021, Zebrafish Labs 4 | * Licensed under BSD-2 (https://github.com/imgix/luminous/blob/main/LICENSE.md) 5 | */ 6 | var l="function"==typeof Object.defineProperties?Object.defineProperty:function(t,i,e){t!=Array.prototype&&t!=Object.prototype&&(t[i]=e.value)},n="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function p(){p=function(){},n.Symbol||(n.Symbol=q)}var r=0;function q(t){return"jscomp_symbol_"+(t||"")+r++}for(var t=n,u=["Object","assign"],v=0;v=this.a.length?this.a[0]:this.a[t]},n.prototype.ka=function(t){return 0>(t=Array.prototype.indexOf.call(this.a,t)-1)?this.a[this.a.length-1]:this.a[t]},n.prototype.M=function(t){t=t.j;var i=this.f.M;i&&"function"==typeof i&&i({j:t})},n.prototype.l=function(){this.g.forEach((function(t){return t.l()}))},n.prototype.destroy=n.prototype.l,window.LuminousGallery=n,window.Luminous=o}]); 7 | //# sourceMappingURL=luminous.min.js.map -------------------------------------------------------------------------------- /dist/luminous.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:/// [synthetic:util/defineproperty] ","webpack:/// [synthetic:util/global] ","webpack:/// [synthetic:es6/symbol] ","webpack:/// [synthetic:util/polyfill] ","webpack:/// [synthetic:es6/object/assign] ","webpack:/// [synthetic:util/owns] ","webpack:///luminous.min.js","webpack:///webpack/bootstrap","webpack:///./src/js/LuminousGallery.js","webpack:///./src/js/Luminous.js","webpack:///./src/js/injectBaseStylesheet.js","webpack:///./src/js/Lightbox.js","webpack:///./src/js/util/dom.js","webpack:///./src/js/util/throwIfMissing.js","webpack:///./src/js/lum-browser.js"],"names":["$jscomp.defineProperty","$jscomp.global","$jscomp.initSymbol","$jscomp.Symbol","$jscomp.symbolCounter_","$jscomp.SYMBOL_PREFIX","modules","__webpack_require__","moduleId","installedModules","exports","module","i","l","call","m","c","d","__webpack_require__.d","name","getter","o","Object","defineProperty","enumerable","get","r","__webpack_require__.r","Symbol","toStringTag","value","t","__webpack_require__.t","mode","__esModule","ns","create","key","bind","n","__webpack_require__.n","getDefault","getModuleExports","__webpack_require__.o","object","property","prototype","hasOwnProperty","p","s","__webpack_exports__","constructor","LuminousGallery_LuminousGallery","triggers","options","luminousOpts","settings","assign","optionsDefaults","arrowNavigation","onChange","_constructLuminousInstances","Luminous_Luminous","trigger","destroy","open","close","_handleKeyup","isOpen","isDOMElement","TypeError","rootNode","document","getRootNode","appendToNode","body","namespace","sourceAttribute","caption","openTrigger","closeTrigger","closeWithEscape","closeOnScroll","closeButtonEnabled","appendToSelector","onOpen","onClose","includeImgixJSClass","injectBaseStyles","_gallery","_arrowNavigation","injectionRoot","node","head","querySelector","styleEl","createElement","type","classList","add","appendChild","createTextNode","RULES","insertBefore","firstChild","_buildLightbox","_bindEventListeners","Lightbox_Lightbox","_sizeImgWrapperEl","showNext","showPrevious","_completeOpen","_completeClose","_handleKeydown","_handleClose","parentEl","throwIfMissing","triggerEl","currentTrigger","openClasses","_buildClasses","openingClasses","closingClasses","elementBuilt","hasBeenLoaded","obj","HAS_SHADOW","ShadowRoot","HAS_DOM_2","HTMLElement","nodeType","nodeName","addClasses","el","classNames","forEach","className","removeClasses","remove","Error","HAS_ANIMATION","style","e","preventDefault","addEventListener","closeButtonEl","suffix","classes","push","_buildElement","innerEl","loaderEl","imgWrapperEl","positionHelperEl","imgEl","captionEl","_setUpGalleryElements","_updateImgSrc","_updateCaption","_buildGalleryButton","fn","btn","innerText","stopPropagation","width","this","clientWidth","maxWidth","height","clientHeight","maxHeight","captionType","innerHTML","imageURL","getAttribute","loadingClasses","onload","this.imgEl.onload","setAttribute","LEFT_ARROW","keyCode","RIGHT_ARROW","nextTrigger","previousTrigger","window","removeEventListener","removeChild","lightbox","_unbindEvents","luminousInstances","triggerLen","length","lum","nextTriggerIndex","Array","indexOf","prevTriggerIndex","instance"],"mappings":"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","file":"luminous.min.js","sourcesContent":[null,null,null,null,null,null,"/******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId]) {\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\ti: moduleId,\n/******/ \t\t\tl: false,\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.l = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// define getter function for harmony exports\n/******/ \t__webpack_require__.d = function(exports, name, getter) {\n/******/ \t\tif(!__webpack_require__.o(exports, name)) {\n/******/ \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// define __esModule on exports\n/******/ \t__webpack_require__.r = function(exports) {\n/******/ \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n/******/ \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n/******/ \t\t}\n/******/ \t\tObject.defineProperty(exports, '__esModule', { value: true });\n/******/ \t};\n/******/\n/******/ \t// create a fake namespace object\n/******/ \t// mode & 1: value is a module id, require it\n/******/ \t// mode & 2: merge all properties of value into the ns\n/******/ \t// mode & 4: return value when already ns object\n/******/ \t// mode & 8|1: behave like require\n/******/ \t__webpack_require__.t = function(value, mode) {\n/******/ \t\tif(mode & 1) value = __webpack_require__(value);\n/******/ \t\tif(mode & 8) return value;\n/******/ \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n/******/ \t\tvar ns = Object.create(null);\n/******/ \t\t__webpack_require__.r(ns);\n/******/ \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n/******/ \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n/******/ \t\treturn ns;\n/******/ \t};\n/******/\n/******/ \t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t__webpack_require__.n = function(module) {\n/******/ \t\tvar getter = module && module.__esModule ?\n/******/ \t\t\tfunction getDefault() { return module['default']; } :\n/******/ \t\t\tfunction getModuleExports() { return module; };\n/******/ \t\t__webpack_require__.d(getter, 'a', getter);\n/******/ \t\treturn getter;\n/******/ \t};\n/******/\n/******/ \t// Object.prototype.hasOwnProperty.call\n/******/ \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"\";\n/******/\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(__webpack_require__.s = 0);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n// ESM COMPAT FLAG\n__webpack_require__.r(__webpack_exports__);\n\n// CONCATENATED MODULE: ./src/js/util/dom.js\n// This is not really a perfect check, but works fine.\n// From http://stackoverflow.com/questions/384286\nconst HAS_DOM_2 = typeof HTMLElement === \"object\";\nconst HAS_SHADOW = typeof ShadowRoot !== \"undefined\";\n\n/**\n * Determines whether an object is a DOM element or not.\n * @param {!Object} obj Object to check\n * @return {boolean} True if object is an element\n */\nfunction isDOMElement(obj) {\n if (HAS_SHADOW && obj instanceof ShadowRoot) {\n return true;\n }\n return HAS_DOM_2\n ? obj instanceof HTMLElement\n : obj &&\n typeof obj === \"object\" &&\n obj !== null &&\n obj.nodeType === 1 &&\n typeof obj.nodeName === \"string\";\n}\n\n/**\n * Adds an array of classes to an element\n * @param {!Element} el Element to add classes to\n * @param {!Array} classNames Class names to add\n * @return {void}\n */\nfunction addClasses(el, classNames) {\n classNames.forEach(function(className) {\n el.classList.add(className);\n });\n}\n\n/**\n * Removes an array of classes from an element\n * @param {!Element} el Element to remove classes from\n * @param {!Array} classNames Classes to remove\n * @return {void}\n */\nfunction removeClasses(el, classNames) {\n classNames.forEach(function(className) {\n el.classList.remove(className);\n });\n}\n\n// CONCATENATED MODULE: ./src/js/injectBaseStylesheet.js\n/* UNMINIFIED RULES\n\n@keyframes lum-noop {\n 0% { zoom: 1; }\n}\n\n.lum-lightbox {\n position: fixed;\n display: none;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n}\n\n.lum-lightbox.lum-open {\n display: block;\n}\n\n.lum-lightbox.lum-opening, .lum-lightbox.lum-closing {\n animation: lum-noop 1ms;\n}\n\n.lum-lightbox-inner {\n position: absolute;\n top: 0%;\n right: 0%;\n bottom: 0%;\n left: 0%;\n\n overflow: hidden;\n}\n\n.lum-lightbox-loader {\n display: none;\n}\n\n.lum-lightbox-inner img {\n max-width: 100%;\n max-height: 100%;\n}\n\n.lum-lightbox-image-wrapper {\n vertical-align: middle;\n display: table-cell;\n text-align: center;\n}\n*/\n\nconst RULES = `@keyframes lum-noop{0%{zoom:1}}.lum-lightbox{position:fixed;display:none;top:0;right:0;bottom:0;left:0}.lum-lightbox.lum-open{display:block}.lum-lightbox.lum-closing,.lum-lightbox.lum-opening{animation:lum-noop 1ms}.lum-lightbox-inner{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden}.lum-lightbox-loader{display:none}.lum-lightbox-inner img{max-width:100%;max-height:100%}.lum-lightbox-image-wrapper{vertical-align:middle;display:table-cell;text-align:center}`;\n\n/**\n * Injects the base stylesheet needed to display the lightbox\n * element.\n * If `node` is the document, the stylesheet will be appended to ``.\n * @param {!Node} node Node to append stylesheet to\n * @return {void}\n */\nfunction injectBaseStylesheet(node) {\n if (!node || node === document) {\n node = document.head;\n }\n\n if (node.querySelector(\".lum-base-styles\")) {\n return;\n }\n\n const styleEl = document.createElement(\"style\");\n styleEl.type = \"text/css\";\n styleEl.classList.add(\"lum-base-styles\");\n\n styleEl.appendChild(document.createTextNode(RULES));\n\n node.insertBefore(styleEl, node.firstChild);\n}\n\n// CONCATENATED MODULE: ./src/js/util/throwIfMissing.js\n/**\n * Throws a missing parameter error\n */\nfunction throwIfMissing() {\n throw new Error(\"Missing parameter\");\n}\n\n// CONCATENATED MODULE: ./src/js/Lightbox.js\n\n\n\nconst LEFT_ARROW = 37;\nconst RIGHT_ARROW = 39;\n\n// All officially-supported browsers have this, but it's easy to\n// account for, just in case.\nconst HAS_ANIMATION =\n typeof document === \"undefined\"\n ? false\n : \"animation\" in document.createElement(\"div\").style;\n\n/**\n * Represents the default lightbox implementation\n */\nclass Lightbox_Lightbox {\n /**\n * Constructor\n * @param {Object=} options Lightbox options\n */\n constructor(options = {}) {\n this._sizeImgWrapperEl = this._sizeImgWrapperEl.bind(this);\n this.showNext = this.showNext.bind(this);\n this.showPrevious = this.showPrevious.bind(this);\n this._completeOpen = this._completeOpen.bind(this);\n this._completeClose = this._completeClose.bind(this);\n this._handleKeydown = this._handleKeydown.bind(this);\n this._handleClose = this._handleClose.bind(this);\n\n const {\n namespace = null,\n parentEl = throwIfMissing(),\n triggerEl = throwIfMissing(),\n sourceAttribute = throwIfMissing(),\n caption = null,\n includeImgixJSClass = false,\n _gallery = null,\n _arrowNavigation = null,\n closeButtonEnabled = true,\n closeTrigger = \"click\"\n } = options;\n\n this.settings = {\n namespace,\n parentEl,\n triggerEl,\n sourceAttribute,\n caption,\n includeImgixJSClass,\n _gallery,\n _arrowNavigation,\n closeButtonEnabled,\n onClose: options.onClose,\n closeTrigger\n };\n\n if (!isDOMElement(this.settings.parentEl)) {\n throw new TypeError(\n \"`new Lightbox` requires a DOM element passed as `parentEl`.\"\n );\n }\n\n this.currentTrigger = this.settings.triggerEl;\n\n this.openClasses = this._buildClasses(\"open\");\n this.openingClasses = this._buildClasses(\"opening\");\n this.closingClasses = this._buildClasses(\"closing\");\n\n this.hasBeenLoaded = false;\n this.elementBuilt = false;\n }\n\n /**\n * Handles closing of the lightbox\n * @param {!Event} e Event that triggered closing\n * @return {void}\n * @protected\n */\n _handleClose(e) {\n if (e && typeof e.preventDefault === \"function\") {\n e.preventDefault();\n }\n\n const onClose = this.settings.onClose;\n if (onClose && typeof onClose === \"function\") {\n onClose();\n }\n }\n\n /**\n * Binds event listeners to the trigger element\n * @return {void}\n * @protected\n */\n _bindEventListeners() {\n this.el.addEventListener(this.settings.closeTrigger, this._handleClose);\n if (this.closeButtonEl) {\n this.closeButtonEl.addEventListener(\"click\", this._handleClose);\n }\n }\n\n /**\n * Builds a class list using the namespace and suffix, if any.\n * @param {string} suffix Suffix to add to each class\n * @return {!Array} Class list\n * @protected\n */\n _buildClasses(suffix) {\n const classes = [`lum-${suffix}`];\n\n const ns = this.settings.namespace;\n if (ns) {\n classes.push(`${ns}-${suffix}`);\n }\n\n return classes;\n }\n\n /**\n * Creates the lightbox element\n * @return {void}\n * @protected\n */\n _buildElement() {\n this.el = document.createElement(\"div\");\n addClasses(this.el, this._buildClasses(\"lightbox\"));\n\n this.innerEl = document.createElement(\"div\");\n addClasses(this.innerEl, this._buildClasses(\"lightbox-inner\"));\n this.el.appendChild(this.innerEl);\n\n const loaderEl = document.createElement(\"div\");\n addClasses(loaderEl, this._buildClasses(\"lightbox-loader\"));\n this.innerEl.appendChild(loaderEl);\n\n this.imgWrapperEl = document.createElement(\"div\");\n addClasses(this.imgWrapperEl, this._buildClasses(\"lightbox-image-wrapper\"));\n this.innerEl.appendChild(this.imgWrapperEl);\n\n const positionHelperEl = document.createElement(\"span\");\n addClasses(\n positionHelperEl,\n this._buildClasses(\"lightbox-position-helper\")\n );\n this.imgWrapperEl.appendChild(positionHelperEl);\n\n this.imgEl = document.createElement(\"img\");\n addClasses(this.imgEl, this._buildClasses(\"img\"));\n positionHelperEl.appendChild(this.imgEl);\n\n this.captionEl = document.createElement(\"p\");\n addClasses(this.captionEl, this._buildClasses(\"lightbox-caption\"));\n positionHelperEl.appendChild(this.captionEl);\n\n if (this.settings.closeButtonEnabled) {\n this.closeButtonEl = document.createElement(\"div\");\n addClasses(this.closeButtonEl, this._buildClasses(\"close-button\"));\n this.el.appendChild(this.closeButtonEl);\n }\n\n if (this.settings._gallery) {\n this._setUpGalleryElements();\n }\n\n this.settings.parentEl.appendChild(this.el);\n\n this._updateImgSrc();\n this._updateCaption();\n\n if (this.settings.includeImgixJSClass) {\n this.imgEl.classList.add(\"imgix-fluid\");\n }\n }\n\n /**\n * Creates gallery elements such as previous/next buttons\n * @return {void}\n * @protected\n */\n _setUpGalleryElements() {\n this._buildGalleryButton(\"previous\", this.showPrevious);\n this._buildGalleryButton(\"next\", this.showNext);\n }\n\n /**\n * Creates a gallery button\n * @param {string} name Name of button\n * @param {!Function} fn Click handler\n * @return {void}\n * @protected\n */\n _buildGalleryButton(name, fn) {\n const btn = document.createElement(\"button\");\n this[`${name}Button`] = btn;\n\n btn.innerText = name;\n addClasses(btn, this._buildClasses(`${name}-button`));\n addClasses(btn, this._buildClasses(\"gallery-button\"));\n this.innerEl.appendChild(btn);\n\n btn.addEventListener(\n \"click\",\n e => {\n e.stopPropagation();\n\n fn();\n },\n false\n );\n }\n\n /**\n * Sizes the image wrapper\n * @return {void}\n * @protected\n */\n _sizeImgWrapperEl() {\n const style = this.imgWrapperEl.style;\n style.width = `${this.innerEl.clientWidth}px`;\n style.maxWidth = `${this.innerEl.clientWidth}px`;\n style.height = `${this.innerEl.clientHeight -\n this.captionEl.clientHeight}px`;\n style.maxHeight = `${this.innerEl.clientHeight -\n this.captionEl.clientHeight}px`;\n }\n\n /**\n * Updates caption from settings\n * @return {void}\n * @protected\n */\n _updateCaption() {\n const captionType = typeof this.settings.caption;\n let caption = \"\";\n\n if (captionType === \"string\") {\n caption = this.settings.caption;\n } else if (captionType === \"function\") {\n caption = this.settings.caption(this.currentTrigger);\n }\n\n this.captionEl.innerHTML = caption;\n }\n\n /**\n * Updates image element from the trigger element's attributes\n * @return {void}\n * @protected\n */\n _updateImgSrc() {\n const imageURL = this.currentTrigger.getAttribute(\n this.settings.sourceAttribute\n );\n\n if (!imageURL) {\n throw new Error(\n `No image URL was found in the ${\n this.settings.sourceAttribute\n } attribute of the trigger.`\n );\n }\n\n const loadingClasses = this._buildClasses(\"loading\");\n\n if (!this.hasBeenLoaded) {\n addClasses(this.el, loadingClasses);\n }\n\n this.imgEl.onload = () => {\n removeClasses(this.el, loadingClasses);\n this.hasBeenLoaded = true;\n };\n\n this.imgEl.setAttribute(\"src\", imageURL);\n }\n\n /**\n * Handles key up/down events for moving between items\n * @param {!Event} e Keyboard event\n * @return {void}\n * @protected\n */\n _handleKeydown(e) {\n if (e.keyCode == LEFT_ARROW) {\n this.showPrevious();\n } else if (e.keyCode == RIGHT_ARROW) {\n this.showNext();\n }\n }\n\n /**\n * Shows the next item if in a gallery\n * @return {void}\n */\n showNext() {\n if (!this.settings._gallery) {\n return;\n }\n\n this.currentTrigger = this.settings._gallery.nextTrigger(\n this.currentTrigger\n );\n this._updateImgSrc();\n this._updateCaption();\n this._sizeImgWrapperEl();\n this.settings._gallery.onChange({ imgEl: this.imgEl });\n }\n\n /**\n * Shows the previous item if in a gallery\n * @return {void}\n */\n showPrevious() {\n if (!this.settings._gallery) {\n return;\n }\n\n this.currentTrigger = this.settings._gallery.previousTrigger(\n this.currentTrigger\n );\n this._updateImgSrc();\n this._updateCaption();\n this._sizeImgWrapperEl();\n this.settings._gallery.onChange({ imgEl: this.imgEl });\n }\n\n /**\n * Opens the lightbox\n * @return {void}\n */\n open() {\n if (!this.elementBuilt) {\n this._buildElement();\n this._bindEventListeners();\n this.elementBuilt = true;\n }\n\n // When opening, always reset to the trigger we were passed\n this.currentTrigger = this.settings.triggerEl;\n\n // Make sure to re-set the `img` `src`, in case it's been changed\n // by someone/something else.\n this._updateImgSrc();\n this._updateCaption();\n\n addClasses(this.el, this.openClasses);\n\n this._sizeImgWrapperEl();\n window.addEventListener(\"resize\", this._sizeImgWrapperEl, false);\n\n if (this.settings._arrowNavigation) {\n window.addEventListener(\"keydown\", this._handleKeydown, false);\n }\n\n if (HAS_ANIMATION) {\n this.el.addEventListener(\"animationend\", this._completeOpen, false);\n addClasses(this.el, this.openingClasses);\n }\n }\n\n /**\n * Closes the lightbox\n * @return {void}\n */\n close() {\n window.removeEventListener(\"resize\", this._sizeImgWrapperEl, false);\n\n if (this.settings._arrowNavigation) {\n window.removeEventListener(\"keydown\", this._handleKeydown, false);\n }\n\n if (HAS_ANIMATION) {\n this.el.addEventListener(\"animationend\", this._completeClose, false);\n addClasses(this.el, this.closingClasses);\n } else {\n removeClasses(this.el, this.openClasses);\n }\n }\n\n /**\n * Handles animations on completion of opening the lightbox\n * @return {void}\n * @protected\n */\n _completeOpen() {\n this.el.removeEventListener(\"animationend\", this._completeOpen, false);\n\n removeClasses(this.el, this.openingClasses);\n }\n\n /**\n * Handles animations on completion of closing the lightbox\n * @return {void}\n * @protected\n */\n _completeClose() {\n this.el.removeEventListener(\"animationend\", this._completeClose, false);\n\n removeClasses(this.el, this.openClasses);\n removeClasses(this.el, this.closingClasses);\n }\n\n /**\n * Destroys the lightbox\n * @return {void}\n */\n destroy() {\n if (this.el) {\n this.settings.parentEl.removeChild(this.el);\n }\n }\n}\n\n// CONCATENATED MODULE: ./src/js/Luminous.js\n\n\n\n\n/**\n * Represents the default luminous lightbox\n */\nclass Luminous_Luminous {\n /**\n * Constructor\n * @param {!Element} trigger Trigger element to open lightbox\n * @param {Object=} options Luminous options\n */\n constructor(trigger, options = {}) {\n this.VERSION = \"2.3.5\";\n this.destroy = this.destroy.bind(this);\n this.open = this.open.bind(this);\n this.close = this.close.bind(this);\n this._handleKeyup = this._handleKeyup.bind(this);\n\n this.isOpen = false;\n\n this.trigger = trigger;\n\n if (!isDOMElement(this.trigger)) {\n throw new TypeError(\n \"`new Luminous` requires a DOM element as its first argument.\"\n );\n }\n\n let rootNode = document;\n if (\"getRootNode\" in this.trigger) {\n rootNode = this.trigger.getRootNode();\n }\n // Prefix for generated element class names (e.g. `my-ns` will\n // result in classes such as `my-ns-lightbox`. Default `lum-`\n // prefixed classes will always be added as well.\n const namespace = options[\"namespace\"] || null;\n // Which attribute to pull the lightbox image source from.\n const sourceAttribute = options[\"sourceAttribute\"] || \"href\";\n // Captions can be a literal string, or a function that receives the Luminous instance's trigger element as an argument and returns a string. Supports HTML, so use caution when dealing with user input.\n const caption = options[\"caption\"] || null;\n // The event to listen to on the _trigger_ element: triggers opening.\n const openTrigger = options[\"openTrigger\"] || \"click\";\n // The event to listen to on the _lightbox_ element: triggers closing.\n const closeTrigger = options[\"closeTrigger\"] || \"click\";\n // Allow closing by pressing escape.\n const closeWithEscape = \"closeWithEscape\" in options ? !!options[\"closeWithEscape\"] : true;\n // Automatically close when the page is scrolled.\n const closeOnScroll = options[\"closeOnScroll\"] || false;\n const closeButtonEnabled =\n options[\"showCloseButton\"] != null ? options[\"showCloseButton\"] : true;\n const appendToNode =\n options[\"appendToNode\"] ||\n (rootNode === document ? document.body : rootNode);\n // A selector defining what to append the lightbox element to.\n const appendToSelector = options[\"appendToSelector\"] || null;\n // If present (and a function), this will be called\n // whenever the lightbox is opened.\n const onOpen = options[\"onOpen\"] || null;\n // If present (and a function), this will be called\n // whenever the lightbox is closed.\n const onClose = options[\"onClose\"] || null;\n // When true, adds the `imgix-fluid` class to the `img`\n // inside the lightbox. See https://github.com/imgix/imgix.js\n // for more information.\n const includeImgixJSClass = options[\"includeImgixJSClass\"] || false;\n // Add base styles to the page. See the \"Theming\"\n // section of README.md for more information.\n const injectBaseStyles = \"injectBaseStyles\" in options ? !!options[\"injectBaseStyles\"] : true;\n // Internal use only!\n const _gallery = options[\"_gallery\"] || null;\n const _arrowNavigation = options[\"_arrowNavigation\"] || null;\n\n this.settings = {\n namespace,\n sourceAttribute,\n caption,\n openTrigger,\n closeTrigger,\n closeWithEscape,\n closeOnScroll,\n closeButtonEnabled,\n appendToNode,\n appendToSelector,\n onOpen,\n onClose,\n includeImgixJSClass,\n injectBaseStyles,\n _gallery,\n _arrowNavigation\n };\n\n let injectionRoot = document.body;\n if (appendToNode && \"getRootNode\" in appendToNode) {\n injectionRoot = appendToNode.getRootNode();\n }\n\n if (this.settings.injectBaseStyles) {\n injectBaseStylesheet(injectionRoot);\n }\n\n this._buildLightbox();\n this._bindEventListeners();\n }\n\n /**\n * Opens the lightbox\n * @param {Event=} e Event which triggered opening\n * @return {void}\n */\n open(e) {\n if (e && typeof e.preventDefault === \"function\") {\n e.preventDefault();\n }\n\n this.lightbox.open();\n\n if (this.settings.closeOnScroll) {\n window.addEventListener(\"scroll\", this.close, false);\n }\n\n const onOpen = this.settings.onOpen;\n if (onOpen && typeof onOpen === \"function\") {\n onOpen();\n }\n\n this.isOpen = true;\n }\n\n /**\n * Closes the lightbox\n * @param {Event=} e Event which triggered closing\n * @return {void}\n */\n close(e) {\n if (this.settings.closeOnScroll) {\n window.removeEventListener(\"scroll\", this.close, false);\n }\n\n this.lightbox.close();\n\n const onClose = this.settings.onClose;\n if (onClose && typeof onClose === \"function\") {\n onClose();\n }\n\n this.isOpen = false;\n }\n\n /**\n * Builds the internal lightbox instance\n * @protected\n * @return {void}\n */\n _buildLightbox() {\n let parentEl = this.settings.appendToNode;\n\n if (this.settings.appendToSelector) {\n parentEl = document.querySelector(this.settings.appendToSelector);\n }\n\n this.lightbox = new Lightbox_Lightbox({\n namespace: this.settings.namespace,\n parentEl: parentEl,\n triggerEl: this.trigger,\n sourceAttribute: this.settings.sourceAttribute,\n caption: this.settings.caption,\n includeImgixJSClass: this.settings.includeImgixJSClass,\n closeButtonEnabled: this.settings.closeButtonEnabled,\n _gallery: this.settings._gallery,\n _arrowNavigation: this.settings._arrowNavigation,\n closeTrigger: this.settings.closeTrigger,\n onClose: this.close\n });\n }\n\n /**\n * Binds lightbox events to the trigger element\n * @protected\n * @return {void}\n */\n _bindEventListeners() {\n this.trigger.addEventListener(this.settings.openTrigger, this.open, false);\n\n if (this.settings.closeWithEscape) {\n window.addEventListener(\"keyup\", this._handleKeyup, false);\n }\n }\n\n /**\n * Unbinds all events\n * @protected\n * @return {void}\n */\n _unbindEvents() {\n this.trigger.removeEventListener(\n this.settings.openTrigger,\n this.open,\n false\n );\n if (this.lightbox.el) {\n this.lightbox.el.removeEventListener(\n this.settings.closeTrigger,\n this.close,\n false\n );\n }\n\n if (this.settings.closeWithEscape) {\n window.removeEventListener(\"keyup\", this._handleKeyup, false);\n }\n }\n\n /**\n * Handles key up events and closes lightbox when esc is pressed\n * @param {!Event} e Keyboard event\n * @return {void}\n * @protected\n */\n _handleKeyup(e) {\n if (this.isOpen && e.keyCode === 27) {\n this.close();\n }\n }\n\n /**\n * Destroys internal lightbox and unbinds events\n * @return {void}\n */\n destroy() {\n this._unbindEvents();\n this.lightbox.destroy();\n }\n}\n\n/* eslint-disable no-self-assign */\nLuminous_Luminous.prototype[\"open\"] = Luminous_Luminous.prototype.open;\nLuminous_Luminous.prototype[\"close\"] = Luminous_Luminous.prototype.close;\nLuminous_Luminous.prototype[\"destroy\"] = Luminous_Luminous.prototype.destroy;\n/* eslint-enable no-self-assign */\n\n// CONCATENATED MODULE: ./src/js/LuminousGallery.js\n\n\n/**\n * Represents a gallery-style lightbox\n */\nclass LuminousGallery_LuminousGallery {\n /**\n * Constructor\n * @param {!Array} triggers Array of trigger elements\n * @param {Object=} options Gallery options\n * @param {Object=} luminousOpts Luminous options\n */\n constructor(triggers, options = {}, luminousOpts = {}) {\n const optionsDefaults = {\n arrowNavigation: true,\n onChange: null,\n };\n\n this.settings = Object.assign({}, optionsDefaults, options);\n\n this.triggers = triggers;\n this.luminousOpts = luminousOpts;\n this.luminousOpts[\"_gallery\"] = this;\n this.luminousOpts[\"_arrowNavigation\"] = this.settings[\"arrowNavigation\"];\n this._constructLuminousInstances();\n }\n\n /**\n * Creates internal luminous instances\n * @protected\n * @return {void}\n */\n _constructLuminousInstances() {\n this.luminousInstances = [];\n\n const triggerLen = this.triggers.length;\n for (let i = 0; i < triggerLen; i++) {\n const trigger = this.triggers[i];\n const lum = new Luminous_Luminous(trigger, this.luminousOpts);\n this.luminousInstances.push(lum);\n }\n }\n\n /**\n * Determines the next trigger element\n * @param {!Element} trigger Current trigger element\n * @return {!Element}\n */\n nextTrigger(trigger) {\n const nextTriggerIndex =\n Array.prototype.indexOf.call(this.triggers, trigger) + 1;\n\n return nextTriggerIndex >= this.triggers.length\n ? this.triggers[0]\n : this.triggers[nextTriggerIndex];\n }\n\n /**\n * Determines the previous trigger element\n * @param {!Element} trigger Current trigger element\n * @return {!Element}\n */\n previousTrigger(trigger) {\n const prevTriggerIndex =\n Array.prototype.indexOf.call(this.triggers, trigger) - 1;\n\n return prevTriggerIndex < 0\n ? this.triggers[this.triggers.length - 1]\n : this.triggers[prevTriggerIndex];\n }\n\n /**\n * Callback called when current image is changed\n * @param {Object} params\n * @param {!Element} params.imgEl New image element\n */\n onChange({ imgEl }) {\n const onChange = this.settings.onChange;\n if (onChange && typeof onChange === \"function\") {\n onChange({ imgEl });\n }\n }\n\n /**\n * Destroys the internal luminous instances\n * @return {void}\n */\n destroy() {\n this.luminousInstances.forEach(instance => instance.destroy());\n }\n}\n\n/* eslint-disable-next-line no-self-assign */\nLuminousGallery_LuminousGallery.prototype[\"destroy\"] = LuminousGallery_LuminousGallery.prototype.destroy;\n\n// CONCATENATED MODULE: ./src/js/lum-browser.js\n// This file is used for the standalone browser build\n\n\n\n\nwindow[\"LuminousGallery\"] = LuminousGallery_LuminousGallery;\nwindow[\"Luminous\"] = Luminous_Luminous;\n\n\n/***/ })\n/******/ ]);"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","import Luminous from \"./Luminous\";\n\n/**\n * Represents a gallery-style lightbox\n */\nexport default class LuminousGallery {\n /**\n * Constructor\n * @param {!Array} triggers Array of trigger elements\n * @param {Object=} options Gallery options\n * @param {Object=} luminousOpts Luminous options\n */\n constructor(triggers, options = {}, luminousOpts = {}) {\n const optionsDefaults = {\n arrowNavigation: true,\n onChange: null,\n };\n\n this.settings = Object.assign({}, optionsDefaults, options);\n\n this.triggers = triggers;\n this.luminousOpts = luminousOpts;\n this.luminousOpts[\"_gallery\"] = this;\n this.luminousOpts[\"_arrowNavigation\"] = this.settings[\"arrowNavigation\"];\n this._constructLuminousInstances();\n }\n\n /**\n * Creates internal luminous instances\n * @protected\n * @return {void}\n */\n _constructLuminousInstances() {\n this.luminousInstances = [];\n\n const triggerLen = this.triggers.length;\n for (let i = 0; i < triggerLen; i++) {\n const trigger = this.triggers[i];\n const lum = new Luminous(trigger, this.luminousOpts);\n this.luminousInstances.push(lum);\n }\n }\n\n /**\n * Determines the next trigger element\n * @param {!Element} trigger Current trigger element\n * @return {!Element}\n */\n nextTrigger(trigger) {\n const nextTriggerIndex =\n Array.prototype.indexOf.call(this.triggers, trigger) + 1;\n\n return nextTriggerIndex >= this.triggers.length\n ? this.triggers[0]\n : this.triggers[nextTriggerIndex];\n }\n\n /**\n * Determines the previous trigger element\n * @param {!Element} trigger Current trigger element\n * @return {!Element}\n */\n previousTrigger(trigger) {\n const prevTriggerIndex =\n Array.prototype.indexOf.call(this.triggers, trigger) - 1;\n\n return prevTriggerIndex < 0\n ? this.triggers[this.triggers.length - 1]\n : this.triggers[prevTriggerIndex];\n }\n\n /**\n * Callback called when current image is changed\n * @param {Object} params\n * @param {!Element} params.imgEl New image element\n */\n onChange({ imgEl }) {\n const onChange = this.settings.onChange;\n if (onChange && typeof onChange === \"function\") {\n onChange({ imgEl });\n }\n }\n\n /**\n * Destroys the internal luminous instances\n * @return {void}\n */\n destroy() {\n this.luminousInstances.forEach(instance => instance.destroy());\n }\n}\n\n/* eslint-disable-next-line no-self-assign */\nLuminousGallery.prototype[\"destroy\"] = LuminousGallery.prototype.destroy;\n","import { isDOMElement } from \"./util/dom\";\nimport injectBaseStylesheet from \"./injectBaseStylesheet\";\nimport Lightbox from \"./Lightbox\";\n\n/**\n * Represents the default luminous lightbox\n */\nexport default class Luminous {\n /**\n * Constructor\n * @param {!Element} trigger Trigger element to open lightbox\n * @param {Object=} options Luminous options\n */\n constructor(trigger, options = {}) {\n this.VERSION = \"2.3.5\";\n this.destroy = this.destroy.bind(this);\n this.open = this.open.bind(this);\n this.close = this.close.bind(this);\n this._handleKeyup = this._handleKeyup.bind(this);\n\n this.isOpen = false;\n\n this.trigger = trigger;\n\n if (!isDOMElement(this.trigger)) {\n throw new TypeError(\n \"`new Luminous` requires a DOM element as its first argument.\"\n );\n }\n\n let rootNode = document;\n if (\"getRootNode\" in this.trigger) {\n rootNode = this.trigger.getRootNode();\n }\n // Prefix for generated element class names (e.g. `my-ns` will\n // result in classes such as `my-ns-lightbox`. Default `lum-`\n // prefixed classes will always be added as well.\n const namespace = options[\"namespace\"] || null;\n // Which attribute to pull the lightbox image source from.\n const sourceAttribute = options[\"sourceAttribute\"] || \"href\";\n // Captions can be a literal string, or a function that receives the Luminous instance's trigger element as an argument and returns a string. Supports HTML, so use caution when dealing with user input.\n const caption = options[\"caption\"] || null;\n // The event to listen to on the _trigger_ element: triggers opening.\n const openTrigger = options[\"openTrigger\"] || \"click\";\n // The event to listen to on the _lightbox_ element: triggers closing.\n const closeTrigger = options[\"closeTrigger\"] || \"click\";\n // Allow closing by pressing escape.\n const closeWithEscape = \"closeWithEscape\" in options ? !!options[\"closeWithEscape\"] : true;\n // Automatically close when the page is scrolled.\n const closeOnScroll = options[\"closeOnScroll\"] || false;\n const closeButtonEnabled =\n options[\"showCloseButton\"] != null ? options[\"showCloseButton\"] : true;\n const appendToNode =\n options[\"appendToNode\"] ||\n (rootNode === document ? document.body : rootNode);\n // A selector defining what to append the lightbox element to.\n const appendToSelector = options[\"appendToSelector\"] || null;\n // If present (and a function), this will be called\n // whenever the lightbox is opened.\n const onOpen = options[\"onOpen\"] || null;\n // If present (and a function), this will be called\n // whenever the lightbox is closed.\n const onClose = options[\"onClose\"] || null;\n // When true, adds the `imgix-fluid` class to the `img`\n // inside the lightbox. See https://github.com/imgix/imgix.js\n // for more information.\n const includeImgixJSClass = options[\"includeImgixJSClass\"] || false;\n // Add base styles to the page. See the \"Theming\"\n // section of README.md for more information.\n const injectBaseStyles = \"injectBaseStyles\" in options ? !!options[\"injectBaseStyles\"] : true;\n // Internal use only!\n const _gallery = options[\"_gallery\"] || null;\n const _arrowNavigation = options[\"_arrowNavigation\"] || null;\n\n this.settings = {\n namespace,\n sourceAttribute,\n caption,\n openTrigger,\n closeTrigger,\n closeWithEscape,\n closeOnScroll,\n closeButtonEnabled,\n appendToNode,\n appendToSelector,\n onOpen,\n onClose,\n includeImgixJSClass,\n injectBaseStyles,\n _gallery,\n _arrowNavigation\n };\n\n let injectionRoot = document.body;\n if (appendToNode && \"getRootNode\" in appendToNode) {\n injectionRoot = appendToNode.getRootNode();\n }\n\n if (this.settings.injectBaseStyles) {\n injectBaseStylesheet(injectionRoot);\n }\n\n this._buildLightbox();\n this._bindEventListeners();\n }\n\n /**\n * Opens the lightbox\n * @param {Event=} e Event which triggered opening\n * @return {void}\n */\n open(e) {\n if (e && typeof e.preventDefault === \"function\") {\n e.preventDefault();\n }\n\n this.lightbox.open();\n\n if (this.settings.closeOnScroll) {\n window.addEventListener(\"scroll\", this.close, false);\n }\n\n const onOpen = this.settings.onOpen;\n if (onOpen && typeof onOpen === \"function\") {\n onOpen();\n }\n\n this.isOpen = true;\n }\n\n /**\n * Closes the lightbox\n * @param {Event=} e Event which triggered closing\n * @return {void}\n */\n close(e) {\n if (this.settings.closeOnScroll) {\n window.removeEventListener(\"scroll\", this.close, false);\n }\n\n this.lightbox.close();\n\n const onClose = this.settings.onClose;\n if (onClose && typeof onClose === \"function\") {\n onClose();\n }\n\n this.isOpen = false;\n }\n\n /**\n * Builds the internal lightbox instance\n * @protected\n * @return {void}\n */\n _buildLightbox() {\n let parentEl = this.settings.appendToNode;\n\n if (this.settings.appendToSelector) {\n parentEl = document.querySelector(this.settings.appendToSelector);\n }\n\n this.lightbox = new Lightbox({\n namespace: this.settings.namespace,\n parentEl: parentEl,\n triggerEl: this.trigger,\n sourceAttribute: this.settings.sourceAttribute,\n caption: this.settings.caption,\n includeImgixJSClass: this.settings.includeImgixJSClass,\n closeButtonEnabled: this.settings.closeButtonEnabled,\n _gallery: this.settings._gallery,\n _arrowNavigation: this.settings._arrowNavigation,\n closeTrigger: this.settings.closeTrigger,\n onClose: this.close\n });\n }\n\n /**\n * Binds lightbox events to the trigger element\n * @protected\n * @return {void}\n */\n _bindEventListeners() {\n this.trigger.addEventListener(this.settings.openTrigger, this.open, false);\n\n if (this.settings.closeWithEscape) {\n window.addEventListener(\"keyup\", this._handleKeyup, false);\n }\n }\n\n /**\n * Unbinds all events\n * @protected\n * @return {void}\n */\n _unbindEvents() {\n this.trigger.removeEventListener(\n this.settings.openTrigger,\n this.open,\n false\n );\n if (this.lightbox.el) {\n this.lightbox.el.removeEventListener(\n this.settings.closeTrigger,\n this.close,\n false\n );\n }\n\n if (this.settings.closeWithEscape) {\n window.removeEventListener(\"keyup\", this._handleKeyup, false);\n }\n }\n\n /**\n * Handles key up events and closes lightbox when esc is pressed\n * @param {!Event} e Keyboard event\n * @return {void}\n * @protected\n */\n _handleKeyup(e) {\n if (this.isOpen && e.keyCode === 27) {\n this.close();\n }\n }\n\n /**\n * Destroys internal lightbox and unbinds events\n * @return {void}\n */\n destroy() {\n this._unbindEvents();\n this.lightbox.destroy();\n }\n}\n\n/* eslint-disable no-self-assign */\nLuminous.prototype[\"open\"] = Luminous.prototype.open;\nLuminous.prototype[\"close\"] = Luminous.prototype.close;\nLuminous.prototype[\"destroy\"] = Luminous.prototype.destroy;\n/* eslint-enable no-self-assign */\n","/* UNMINIFIED RULES\n\n@keyframes lum-noop {\n 0% { zoom: 1; }\n}\n\n.lum-lightbox {\n position: fixed;\n display: none;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n}\n\n.lum-lightbox.lum-open {\n display: block;\n}\n\n.lum-lightbox.lum-opening, .lum-lightbox.lum-closing {\n animation: lum-noop 1ms;\n}\n\n.lum-lightbox-inner {\n position: absolute;\n top: 0%;\n right: 0%;\n bottom: 0%;\n left: 0%;\n\n overflow: hidden;\n}\n\n.lum-lightbox-loader {\n display: none;\n}\n\n.lum-lightbox-inner img {\n max-width: 100%;\n max-height: 100%;\n}\n\n.lum-lightbox-image-wrapper {\n vertical-align: middle;\n display: table-cell;\n text-align: center;\n}\n*/\n\nconst RULES = `@keyframes lum-noop{0%{zoom:1}}.lum-lightbox{position:fixed;display:none;top:0;right:0;bottom:0;left:0}.lum-lightbox.lum-open{display:block}.lum-lightbox.lum-closing,.lum-lightbox.lum-opening{animation:lum-noop 1ms}.lum-lightbox-inner{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden}.lum-lightbox-loader{display:none}.lum-lightbox-inner img{max-width:100%;max-height:100%}.lum-lightbox-image-wrapper{vertical-align:middle;display:table-cell;text-align:center}`;\n\n/**\n * Injects the base stylesheet needed to display the lightbox\n * element.\n * If `node` is the document, the stylesheet will be appended to ``.\n * @param {!Node} node Node to append stylesheet to\n * @return {void}\n */\nexport default function injectBaseStylesheet(node) {\n if (!node || node === document) {\n node = document.head;\n }\n\n if (node.querySelector(\".lum-base-styles\")) {\n return;\n }\n\n const styleEl = document.createElement(\"style\");\n styleEl.type = \"text/css\";\n styleEl.classList.add(\"lum-base-styles\");\n\n styleEl.appendChild(document.createTextNode(RULES));\n\n node.insertBefore(styleEl, node.firstChild);\n}\n","import { isDOMElement, addClasses, removeClasses } from \"./util/dom\";\nimport throwIfMissing from \"./util/throwIfMissing\";\n\nconst LEFT_ARROW = 37;\nconst RIGHT_ARROW = 39;\n\n// All officially-supported browsers have this, but it's easy to\n// account for, just in case.\nconst HAS_ANIMATION =\n typeof document === \"undefined\"\n ? false\n : \"animation\" in document.createElement(\"div\").style;\n\n/**\n * Represents the default lightbox implementation\n */\nexport default class Lightbox {\n /**\n * Constructor\n * @param {Object=} options Lightbox options\n */\n constructor(options = {}) {\n this._sizeImgWrapperEl = this._sizeImgWrapperEl.bind(this);\n this.showNext = this.showNext.bind(this);\n this.showPrevious = this.showPrevious.bind(this);\n this._completeOpen = this._completeOpen.bind(this);\n this._completeClose = this._completeClose.bind(this);\n this._handleKeydown = this._handleKeydown.bind(this);\n this._handleClose = this._handleClose.bind(this);\n\n const {\n namespace = null,\n parentEl = throwIfMissing(),\n triggerEl = throwIfMissing(),\n sourceAttribute = throwIfMissing(),\n caption = null,\n includeImgixJSClass = false,\n _gallery = null,\n _arrowNavigation = null,\n closeButtonEnabled = true,\n closeTrigger = \"click\"\n } = options;\n\n this.settings = {\n namespace,\n parentEl,\n triggerEl,\n sourceAttribute,\n caption,\n includeImgixJSClass,\n _gallery,\n _arrowNavigation,\n closeButtonEnabled,\n onClose: options.onClose,\n closeTrigger\n };\n\n if (!isDOMElement(this.settings.parentEl)) {\n throw new TypeError(\n \"`new Lightbox` requires a DOM element passed as `parentEl`.\"\n );\n }\n\n this.currentTrigger = this.settings.triggerEl;\n\n this.openClasses = this._buildClasses(\"open\");\n this.openingClasses = this._buildClasses(\"opening\");\n this.closingClasses = this._buildClasses(\"closing\");\n\n this.hasBeenLoaded = false;\n this.elementBuilt = false;\n }\n\n /**\n * Handles closing of the lightbox\n * @param {!Event} e Event that triggered closing\n * @return {void}\n * @protected\n */\n _handleClose(e) {\n if (e && typeof e.preventDefault === \"function\") {\n e.preventDefault();\n }\n\n const onClose = this.settings.onClose;\n if (onClose && typeof onClose === \"function\") {\n onClose();\n }\n }\n\n /**\n * Binds event listeners to the trigger element\n * @return {void}\n * @protected\n */\n _bindEventListeners() {\n this.el.addEventListener(this.settings.closeTrigger, this._handleClose);\n if (this.closeButtonEl) {\n this.closeButtonEl.addEventListener(\"click\", this._handleClose);\n }\n }\n\n /**\n * Builds a class list using the namespace and suffix, if any.\n * @param {string} suffix Suffix to add to each class\n * @return {!Array} Class list\n * @protected\n */\n _buildClasses(suffix) {\n const classes = [`lum-${suffix}`];\n\n const ns = this.settings.namespace;\n if (ns) {\n classes.push(`${ns}-${suffix}`);\n }\n\n return classes;\n }\n\n /**\n * Creates the lightbox element\n * @return {void}\n * @protected\n */\n _buildElement() {\n this.el = document.createElement(\"div\");\n addClasses(this.el, this._buildClasses(\"lightbox\"));\n\n this.innerEl = document.createElement(\"div\");\n addClasses(this.innerEl, this._buildClasses(\"lightbox-inner\"));\n this.el.appendChild(this.innerEl);\n\n const loaderEl = document.createElement(\"div\");\n addClasses(loaderEl, this._buildClasses(\"lightbox-loader\"));\n this.innerEl.appendChild(loaderEl);\n\n this.imgWrapperEl = document.createElement(\"div\");\n addClasses(this.imgWrapperEl, this._buildClasses(\"lightbox-image-wrapper\"));\n this.innerEl.appendChild(this.imgWrapperEl);\n\n const positionHelperEl = document.createElement(\"span\");\n addClasses(\n positionHelperEl,\n this._buildClasses(\"lightbox-position-helper\")\n );\n this.imgWrapperEl.appendChild(positionHelperEl);\n\n this.imgEl = document.createElement(\"img\");\n addClasses(this.imgEl, this._buildClasses(\"img\"));\n positionHelperEl.appendChild(this.imgEl);\n\n this.captionEl = document.createElement(\"p\");\n addClasses(this.captionEl, this._buildClasses(\"lightbox-caption\"));\n positionHelperEl.appendChild(this.captionEl);\n\n if (this.settings.closeButtonEnabled) {\n this.closeButtonEl = document.createElement(\"div\");\n addClasses(this.closeButtonEl, this._buildClasses(\"close-button\"));\n this.el.appendChild(this.closeButtonEl);\n }\n\n if (this.settings._gallery) {\n this._setUpGalleryElements();\n }\n\n this.settings.parentEl.appendChild(this.el);\n\n this._updateImgSrc();\n this._updateCaption();\n\n if (this.settings.includeImgixJSClass) {\n this.imgEl.classList.add(\"imgix-fluid\");\n }\n }\n\n /**\n * Creates gallery elements such as previous/next buttons\n * @return {void}\n * @protected\n */\n _setUpGalleryElements() {\n this._buildGalleryButton(\"previous\", this.showPrevious);\n this._buildGalleryButton(\"next\", this.showNext);\n }\n\n /**\n * Creates a gallery button\n * @param {string} name Name of button\n * @param {!Function} fn Click handler\n * @return {void}\n * @protected\n */\n _buildGalleryButton(name, fn) {\n const btn = document.createElement(\"button\");\n this[`${name}Button`] = btn;\n\n btn.innerText = name;\n addClasses(btn, this._buildClasses(`${name}-button`));\n addClasses(btn, this._buildClasses(\"gallery-button\"));\n this.innerEl.appendChild(btn);\n\n btn.addEventListener(\n \"click\",\n e => {\n e.stopPropagation();\n\n fn();\n },\n false\n );\n }\n\n /**\n * Sizes the image wrapper\n * @return {void}\n * @protected\n */\n _sizeImgWrapperEl() {\n const style = this.imgWrapperEl.style;\n style.width = `${this.innerEl.clientWidth}px`;\n style.maxWidth = `${this.innerEl.clientWidth}px`;\n style.height = `${this.innerEl.clientHeight -\n this.captionEl.clientHeight}px`;\n style.maxHeight = `${this.innerEl.clientHeight -\n this.captionEl.clientHeight}px`;\n }\n\n /**\n * Updates caption from settings\n * @return {void}\n * @protected\n */\n _updateCaption() {\n const captionType = typeof this.settings.caption;\n let caption = \"\";\n\n if (captionType === \"string\") {\n caption = this.settings.caption;\n } else if (captionType === \"function\") {\n caption = this.settings.caption(this.currentTrigger);\n }\n\n this.captionEl.innerHTML = caption;\n }\n\n /**\n * Updates image element from the trigger element's attributes\n * @return {void}\n * @protected\n */\n _updateImgSrc() {\n const imageURL = this.currentTrigger.getAttribute(\n this.settings.sourceAttribute\n );\n\n if (!imageURL) {\n throw new Error(\n `No image URL was found in the ${\n this.settings.sourceAttribute\n } attribute of the trigger.`\n );\n }\n\n const loadingClasses = this._buildClasses(\"loading\");\n\n if (!this.hasBeenLoaded) {\n addClasses(this.el, loadingClasses);\n }\n\n this.imgEl.onload = () => {\n removeClasses(this.el, loadingClasses);\n this.hasBeenLoaded = true;\n };\n\n this.imgEl.setAttribute(\"src\", imageURL);\n }\n\n /**\n * Handles key up/down events for moving between items\n * @param {!Event} e Keyboard event\n * @return {void}\n * @protected\n */\n _handleKeydown(e) {\n if (e.keyCode == LEFT_ARROW) {\n this.showPrevious();\n } else if (e.keyCode == RIGHT_ARROW) {\n this.showNext();\n }\n }\n\n /**\n * Shows the next item if in a gallery\n * @return {void}\n */\n showNext() {\n if (!this.settings._gallery) {\n return;\n }\n\n this.currentTrigger = this.settings._gallery.nextTrigger(\n this.currentTrigger\n );\n this._updateImgSrc();\n this._updateCaption();\n this._sizeImgWrapperEl();\n this.settings._gallery.onChange({ imgEl: this.imgEl });\n }\n\n /**\n * Shows the previous item if in a gallery\n * @return {void}\n */\n showPrevious() {\n if (!this.settings._gallery) {\n return;\n }\n\n this.currentTrigger = this.settings._gallery.previousTrigger(\n this.currentTrigger\n );\n this._updateImgSrc();\n this._updateCaption();\n this._sizeImgWrapperEl();\n this.settings._gallery.onChange({ imgEl: this.imgEl });\n }\n\n /**\n * Opens the lightbox\n * @return {void}\n */\n open() {\n if (!this.elementBuilt) {\n this._buildElement();\n this._bindEventListeners();\n this.elementBuilt = true;\n }\n\n // When opening, always reset to the trigger we were passed\n this.currentTrigger = this.settings.triggerEl;\n\n // Make sure to re-set the `img` `src`, in case it's been changed\n // by someone/something else.\n this._updateImgSrc();\n this._updateCaption();\n\n addClasses(this.el, this.openClasses);\n\n this._sizeImgWrapperEl();\n window.addEventListener(\"resize\", this._sizeImgWrapperEl, false);\n\n if (this.settings._arrowNavigation) {\n window.addEventListener(\"keydown\", this._handleKeydown, false);\n }\n\n if (HAS_ANIMATION) {\n this.el.addEventListener(\"animationend\", this._completeOpen, false);\n addClasses(this.el, this.openingClasses);\n }\n }\n\n /**\n * Closes the lightbox\n * @return {void}\n */\n close() {\n window.removeEventListener(\"resize\", this._sizeImgWrapperEl, false);\n\n if (this.settings._arrowNavigation) {\n window.removeEventListener(\"keydown\", this._handleKeydown, false);\n }\n\n if (HAS_ANIMATION) {\n this.el.addEventListener(\"animationend\", this._completeClose, false);\n addClasses(this.el, this.closingClasses);\n } else {\n removeClasses(this.el, this.openClasses);\n }\n }\n\n /**\n * Handles animations on completion of opening the lightbox\n * @return {void}\n * @protected\n */\n _completeOpen() {\n this.el.removeEventListener(\"animationend\", this._completeOpen, false);\n\n removeClasses(this.el, this.openingClasses);\n }\n\n /**\n * Handles animations on completion of closing the lightbox\n * @return {void}\n * @protected\n */\n _completeClose() {\n this.el.removeEventListener(\"animationend\", this._completeClose, false);\n\n removeClasses(this.el, this.openClasses);\n removeClasses(this.el, this.closingClasses);\n }\n\n /**\n * Destroys the lightbox\n * @return {void}\n */\n destroy() {\n if (this.el) {\n this.settings.parentEl.removeChild(this.el);\n }\n }\n}\n","// This is not really a perfect check, but works fine.\n// From http://stackoverflow.com/questions/384286\nconst HAS_DOM_2 = typeof HTMLElement === \"object\";\nconst HAS_SHADOW = typeof ShadowRoot !== \"undefined\";\n\n/**\n * Determines whether an object is a DOM element or not.\n * @param {!Object} obj Object to check\n * @return {boolean} True if object is an element\n */\nexport function isDOMElement(obj) {\n if (HAS_SHADOW && obj instanceof ShadowRoot) {\n return true;\n }\n return HAS_DOM_2\n ? obj instanceof HTMLElement\n : obj &&\n typeof obj === \"object\" &&\n obj !== null &&\n obj.nodeType === 1 &&\n typeof obj.nodeName === \"string\";\n}\n\n/**\n * Adds an array of classes to an element\n * @param {!Element} el Element to add classes to\n * @param {!Array} classNames Class names to add\n * @return {void}\n */\nexport function addClasses(el, classNames) {\n classNames.forEach(function(className) {\n el.classList.add(className);\n });\n}\n\n/**\n * Removes an array of classes from an element\n * @param {!Element} el Element to remove classes from\n * @param {!Array} classNames Classes to remove\n * @return {void}\n */\nexport function removeClasses(el, classNames) {\n classNames.forEach(function(className) {\n el.classList.remove(className);\n });\n}\n","/**\n * Throws a missing parameter error\n */\nexport default function throwIfMissing() {\n throw new Error(\"Missing parameter\");\n}\n","// This file is used for the standalone browser build\n\nimport Luminous from \"./Luminous\";\nimport LuminousGallery from \"./LuminousGallery\";\n\nwindow[\"LuminousGallery\"] = LuminousGallery;\nwindow[\"Luminous\"] = Luminous;\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | Luminous Playground 14 | 19 | 75 | 76 | 77 | 78 | 82 | Mountainous landscape 86 | 87 | 88 |
89 |

Luminous Demo

90 |

91 | This is a demo of Luminous, a simple, lightweight, no-dependencies 92 | JavaScript image lightbox from imgix. 93 | This demo uses the simple included theme, but it's very easy to extend 94 | and customize to fit your needs. You can 95 | learn more and download it here. 98 |

99 |
100 | 101 | 136 | 137 | 138 | imgix 147 | 148 | 149 | 150 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luminous-lightbox", 3 | "version": "2.4.0", 4 | "description": "A simple, lightweight, no-dependencies JavaScript image lightbox.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/imgix/luminous.git" 8 | }, 9 | "keywords": [ 10 | "javascript", 11 | "lightbox" 12 | ], 13 | "author": "imgix", 14 | "license": "BSD-2", 15 | "bugs": { 16 | "url": "https://github.com/imgix/luminous/issues" 17 | }, 18 | "homepage": "https://github.com/imgix/luminous#readme", 19 | "contributors": [ 20 | "Frederick Fogerty (https://github.com/frederickfogerty)", 21 | "Jakub Duras (https://duras.me)", 22 | "Gwendolen Lynch (https://github.com/GwendolenLynch)" 23 | ], 24 | "main": "lib/lum.js", 25 | "module": "es/lum.js", 26 | "jsnext:main": "es/lum.js", 27 | "scripts": { 28 | "build": "npm run clean && run-p build:css:** build:js:**", 29 | "build:serial": "npm run clean && run-s build:css:** build:js:**", 30 | "build:css:dev": "mkdir -p dist && cp src/css/luminous-basic.css dist/", 31 | "build:css:prod": "mkdir -p dist && postcss src/css/luminous-basic.css > dist/luminous-basic.min.css", 32 | "build:js:dist:prod": "webpack --env build && cp dist/luminous.min.js dist/luminous.js", 33 | "build:js:lib:commonjs": "cross-env BABEL_ENV=commonjs babel src/js --out-dir lib --source-maps", 34 | "build:js:lib:es": "cross-env BABEL_ENV=es babel src/js --out-dir es --source-maps", 35 | "build:watch": "webpack --progress --colors --watch --env dev", 36 | "clean": "rimraf lib es dist", 37 | "dev": "npm run build:watch", 38 | "format:check": "prettier --list-different \"{src,test}/**/*.js\"", 39 | "format": "prettier --write \"{src,test}/**/*.js\"", 40 | "prepare": "npm run build", 41 | "prepublishOnly": "npm run build", 42 | "release": "npm run build && git add dist src/js/Luminous.js && standard-version -a", 43 | "start": "npm run dev", 44 | "test:watch": "karma start config/karma/karma-local.config.js", 45 | "test:local": "karma start config/karma/karma-local.config.js --singleRun", 46 | "test:ci": "karma start config/karma/karma-ci.config.js --singleRun", 47 | "test": "npm run test:local", 48 | "lint": "eslint \"{src,test}/**/*.js\"", 49 | "release:dryRun": "npx node-env-run --exec 'semantic-release --dryRun'", 50 | "release:publish": "semantic-release" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "7.22.9", 54 | "@babel/core": "7.22.9", 55 | "@babel/plugin-proposal-class-properties": "7.18.6", 56 | "@babel/preset-env": "7.22.9", 57 | "@google/semantic-release-replace-plugin": "1.2.0", 58 | "@semantic-release/changelog": "6.0.3", 59 | "@semantic-release/commit-analyzer": "9.0.2", 60 | "@semantic-release/git": "10.0.1", 61 | "@semantic-release/github": "8.0.7", 62 | "@semantic-release/npm": "9.0.2", 63 | "babel-eslint": "10.1.0", 64 | "babel-loader": "8.3.0", 65 | "babel-preset-env": "7.0.0-beta.3", 66 | "chai": "4.3.7", 67 | "closure-webpack-plugin": "2.6.1", 68 | "cross-env": "7.0.3", 69 | "cssnano": "6.0.1", 70 | "cssnano-preset-advanced": "5.3.10", 71 | "eslint": "7.32.0", 72 | "eslint-config-google": "0.14.0", 73 | "eslint-config-prettier": "8.8.0", 74 | "google-closure-compiler-js": "20200719.0.0", 75 | "jasmine-core": "4.6.0", 76 | "karma": "6.4.2", 77 | "karma-chrome-launcher": "3.2.0", 78 | "karma-firefox-launcher": "2.1.2", 79 | "karma-jasmine": "4.0.2", 80 | "karma-webpack": "4.0.2", 81 | "npm-run-all": "4.1.5", 82 | "postcss": "8.4.24", 83 | "postcss-banner": "4.0.1", 84 | "postcss-cli": "9.1.0", 85 | "prettier": "2.8.8", 86 | "rimraf": "3.0.2", 87 | "semantic-release": "19.0.5", 88 | "standard-version": "9.5.0", 89 | "webpack": "4.46.0", 90 | "webpack-cli": "4.10.0", 91 | "webpack-closure-compiler": "2.1.6", 92 | "yargs": "17.7.2" 93 | }, 94 | "release": { 95 | "branches": [ 96 | "main", 97 | { 98 | "name": "next", 99 | "prerelease": "rc" 100 | }, 101 | { 102 | "name": "beta", 103 | "prerelease": true 104 | }, 105 | { 106 | "name": "alpha", 107 | "prerelease": true 108 | } 109 | ], 110 | "plugins": [ 111 | "@semantic-release/commit-analyzer", 112 | "@semantic-release/release-notes-generator", 113 | [ 114 | "@google/semantic-release-replace-plugin", 115 | { 116 | "replacements": [ 117 | { 118 | "files": [ 119 | "src/js/Luminous.js" 120 | ], 121 | "from": "this.VERSION = \".*\"", 122 | "to": "this.VERSION = \"${nextRelease.version}\"", 123 | "results": [ 124 | { 125 | "file": "src/js/Luminous.js", 126 | "hasChanged": true, 127 | "numMatches": 1, 128 | "numReplacements": 1 129 | } 130 | ], 131 | "countMatches": true 132 | } 133 | ] 134 | } 135 | ], 136 | "@semantic-release/changelog", 137 | "@semantic-release/npm", 138 | [ 139 | "@semantic-release/git", 140 | { 141 | "assets": [ 142 | "src/**", 143 | "dist/**", 144 | "package.json", 145 | "changelog.md" 146 | ], 147 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes} [skip ci]" 148 | } 149 | ], 150 | [ 151 | "@semantic-release/github", 152 | { 153 | "assets": [ 154 | { 155 | "path": "dist/Luminous.min.js", 156 | "label": "Minified build" 157 | }, 158 | { 159 | "path": "dist/Luminous.js", 160 | "label": "Standard build" 161 | }, 162 | { 163 | "path": "dist/luminous-basic.css", 164 | "label": "CSS" 165 | }, 166 | { 167 | "path": "dist/luminous-basic.min.css", 168 | "label": "Minified CSS" 169 | } 170 | ] 171 | } 172 | ] 173 | ] 174 | }, 175 | "browserslist": [ 176 | "ie 11", 177 | "last 2 edge versions", 178 | "last 2 Chrome versions", 179 | "last 2 Firefox versions", 180 | "last 2 Safari versions", 181 | "last 2 iOS versions", 182 | "last 2 Android versions" 183 | ], 184 | "files": [ 185 | "bower.json", 186 | "CHANGELOG.md", 187 | "dist", 188 | "lib", 189 | "es" 190 | ] 191 | } 192 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const licenseBanner = require("./build/banner"); 2 | 3 | module.exports = { 4 | plugins: [ 5 | require("cssnano")({ 6 | preset: "advanced" 7 | }) 8 | , 9 | require("postcss-banner")({ 10 | banner: licenseBanner, 11 | important: true 12 | }) 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /src/css/luminous-basic.css: -------------------------------------------------------------------------------- 1 | @keyframes lum-fade { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 100% { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | @keyframes lum-fadeZoom { 11 | 0% { 12 | transform: scale(0.5); 13 | opacity: 0; 14 | } 15 | 100% { 16 | transform: scale(1); 17 | opacity: 1; 18 | } 19 | } 20 | 21 | @keyframes lum-loader-rotate { 22 | 0% { 23 | transform: translate(-50%, -50%) rotate(0); 24 | } 25 | 50% { 26 | transform: translate(-50%, -50%) rotate(-180deg); 27 | } 28 | 100% { 29 | transform: translate(-50%, -50%) rotate(-360deg); 30 | } 31 | } 32 | 33 | @keyframes lum-loader-before { 34 | 0% { 35 | transform: scale(1); 36 | } 37 | 10% { 38 | transform: scale(1.2) translateX(6px); 39 | } 40 | 25% { 41 | transform: scale(1.3) translateX(8px); 42 | } 43 | 40% { 44 | transform: scale(1.2) translateX(6px); 45 | } 46 | 50% { 47 | transform: scale(1); 48 | } 49 | 60% { 50 | transform: scale(0.8) translateX(6px); 51 | } 52 | 75% { 53 | transform: scale(0.7) translateX(8px); 54 | } 55 | 90% { 56 | transform: scale(0.8) translateX(6px); 57 | } 58 | 100% { 59 | transform: scale(1); 60 | } 61 | } 62 | 63 | @keyframes lum-loader-after { 64 | 0% { 65 | transform: scale(1); 66 | } 67 | 10% { 68 | transform: scale(1.2) translateX(-6px); 69 | } 70 | 25% { 71 | transform: scale(1.3) translateX(-8px); 72 | } 73 | 40% { 74 | transform: scale(1.2) translateX(-6px); 75 | } 76 | 50% { 77 | transform: scale(1); 78 | } 79 | 60% { 80 | transform: scale(0.8) translateX(-6px); 81 | } 82 | 75% { 83 | transform: scale(0.7) translateX(-8px); 84 | } 85 | 90% { 86 | transform: scale(0.8) translateX(-6px); 87 | } 88 | 100% { 89 | transform: scale(1); 90 | } 91 | } 92 | 93 | .lum-lightbox { 94 | background: rgba(0, 0, 0, 0.6); 95 | } 96 | 97 | .lum-lightbox-inner { 98 | top: 2.5%; 99 | right: 2.5%; 100 | bottom: 2.5%; 101 | left: 2.5%; 102 | } 103 | 104 | .lum-lightbox-inner img { 105 | position: relative; 106 | } 107 | 108 | .lum-lightbox-inner .lum-lightbox-caption { 109 | margin: 0 auto; 110 | color: #fff; 111 | max-width: 700px; 112 | text-align: center; 113 | } 114 | 115 | .lum-loading .lum-lightbox-loader { 116 | display: block; 117 | position: absolute; 118 | top: 50%; 119 | left: 50%; 120 | transform: translate(-50%, -50%); 121 | width: 66px; 122 | height: 20px; 123 | animation: lum-loader-rotate 1800ms infinite linear; 124 | } 125 | 126 | .lum-lightbox-loader:before, 127 | .lum-lightbox-loader:after { 128 | content: ""; 129 | display: block; 130 | width: 20px; 131 | height: 20px; 132 | position: absolute; 133 | top: 50%; 134 | margin-top: -10px; 135 | border-radius: 20px; 136 | background: rgba(255, 255, 255, 0.9); 137 | } 138 | 139 | .lum-lightbox-loader:before { 140 | left: 0; 141 | animation: lum-loader-before 1800ms infinite linear; 142 | } 143 | 144 | .lum-lightbox-loader:after { 145 | right: 0; 146 | animation: lum-loader-after 1800ms infinite linear; 147 | animation-delay: -900ms; 148 | } 149 | 150 | .lum-lightbox.lum-opening { 151 | animation: lum-fade 180ms ease-out; 152 | } 153 | 154 | .lum-lightbox.lum-opening .lum-lightbox-inner { 155 | animation: lum-fadeZoom 180ms ease-out; 156 | } 157 | 158 | .lum-lightbox.lum-closing { 159 | animation: lum-fade 300ms ease-in; 160 | animation-direction: reverse; 161 | } 162 | 163 | .lum-lightbox.lum-closing .lum-lightbox-inner { 164 | animation: lum-fadeZoom 300ms ease-in; 165 | animation-direction: reverse; 166 | } 167 | 168 | .lum-img { 169 | transition: opacity 120ms ease-out; 170 | } 171 | 172 | .lum-loading .lum-img { 173 | opacity: 0; 174 | } 175 | 176 | .lum-gallery-button { 177 | overflow: hidden; 178 | text-indent: 150%; 179 | white-space: nowrap; 180 | background: transparent; 181 | border: 0; 182 | margin: 0; 183 | padding: 0; 184 | outline: 0; 185 | position: absolute; 186 | top: 50%; 187 | transform: translateY(-50%); 188 | height: 100px; 189 | max-height: 100%; 190 | width: 60px; 191 | cursor: pointer; 192 | } 193 | 194 | .lum-close-button { 195 | position: absolute; 196 | right: 5px; 197 | top: 5px; 198 | width: 32px; 199 | height: 32px; 200 | opacity: 0.3; 201 | } 202 | .lum-close-button:hover { 203 | opacity: 1; 204 | } 205 | .lum-close-button:before, 206 | .lum-close-button:after { 207 | position: absolute; 208 | left: 15px; 209 | content: " "; 210 | height: 33px; 211 | width: 2px; 212 | background-color: #fff; 213 | } 214 | .lum-close-button:before { 215 | transform: rotate(45deg); 216 | } 217 | .lum-close-button:after { 218 | transform: rotate(-45deg); 219 | } 220 | 221 | .lum-previous-button { 222 | left: 12px; 223 | } 224 | 225 | .lum-next-button { 226 | right: 12px; 227 | } 228 | 229 | .lum-gallery-button:after { 230 | content: ""; 231 | display: block; 232 | position: absolute; 233 | top: 50%; 234 | width: 36px; 235 | height: 36px; 236 | border-top: 4px solid rgba(255, 255, 255, 0.8); 237 | } 238 | 239 | .lum-previous-button:after { 240 | transform: translateY(-50%) rotate(-45deg); 241 | border-left: 4px solid rgba(255, 255, 255, 0.8); 242 | box-shadow: -2px 0 rgba(0, 0, 0, 0.2); 243 | left: 12%; 244 | border-radius: 3px 0 0 0; 245 | } 246 | 247 | .lum-next-button:after { 248 | transform: translateY(-50%) rotate(45deg); 249 | border-right: 4px solid rgba(255, 255, 255, 0.8); 250 | box-shadow: 2px 0 rgba(0, 0, 0, 0.2); 251 | right: 12%; 252 | border-radius: 0 3px 0 0; 253 | } 254 | 255 | /* This media query makes screens less than 460px wide display in a "fullscreen"-esque mode. Users can then scroll around inside the lightbox to see the entire image. */ 256 | @media (max-width: 460px) { 257 | .lum-lightbox-image-wrapper { 258 | display: flex; 259 | overflow: auto; 260 | -webkit-overflow-scrolling: touch; 261 | } 262 | 263 | .lum-lightbox-caption { 264 | width: 100%; 265 | position: absolute; 266 | bottom: 0; 267 | } 268 | 269 | /* Used to centre the image in the container, respecting overflow: https://stackoverflow.com/a/33455342/515634 */ 270 | .lum-lightbox-position-helper { 271 | margin: auto; 272 | } 273 | 274 | .lum-lightbox-inner img { 275 | max-width: none; 276 | max-height: none; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/js/Lightbox.js: -------------------------------------------------------------------------------- 1 | import { isDOMElement, addClasses, removeClasses } from "./util/dom"; 2 | import throwIfMissing from "./util/throwIfMissing"; 3 | 4 | const LEFT_ARROW = 37; 5 | const RIGHT_ARROW = 39; 6 | 7 | // All officially-supported browsers have this, but it's easy to 8 | // account for, just in case. 9 | const HAS_ANIMATION = 10 | typeof document === "undefined" 11 | ? false 12 | : "animation" in document.createElement("div").style; 13 | 14 | /** 15 | * Represents the default lightbox implementation 16 | */ 17 | export default class Lightbox { 18 | /** 19 | * Constructor 20 | * @param {Object=} options Lightbox options 21 | */ 22 | constructor(options = {}) { 23 | this._sizeImgWrapperEl = this._sizeImgWrapperEl.bind(this); 24 | this.showNext = this.showNext.bind(this); 25 | this.showPrevious = this.showPrevious.bind(this); 26 | this._completeOpen = this._completeOpen.bind(this); 27 | this._completeClose = this._completeClose.bind(this); 28 | this._handleKeydown = this._handleKeydown.bind(this); 29 | this._handleClose = this._handleClose.bind(this); 30 | 31 | const { 32 | namespace = null, 33 | parentEl = throwIfMissing(), 34 | triggerEl = throwIfMissing(), 35 | sourceAttribute = throwIfMissing(), 36 | caption = null, 37 | includeImgixJSClass = false, 38 | _gallery = null, 39 | _arrowNavigation = null, 40 | closeButtonEnabled = true, 41 | closeTrigger = "click" 42 | } = options; 43 | 44 | this.settings = { 45 | namespace, 46 | parentEl, 47 | triggerEl, 48 | sourceAttribute, 49 | caption, 50 | includeImgixJSClass, 51 | _gallery, 52 | _arrowNavigation, 53 | closeButtonEnabled, 54 | onClose: options.onClose, 55 | closeTrigger 56 | }; 57 | 58 | if (!isDOMElement(this.settings.parentEl)) { 59 | throw new TypeError( 60 | "`new Lightbox` requires a DOM element passed as `parentEl`." 61 | ); 62 | } 63 | 64 | this.currentTrigger = this.settings.triggerEl; 65 | 66 | this.openClasses = this._buildClasses("open"); 67 | this.openingClasses = this._buildClasses("opening"); 68 | this.closingClasses = this._buildClasses("closing"); 69 | 70 | this.hasBeenLoaded = false; 71 | this.elementBuilt = false; 72 | } 73 | 74 | /** 75 | * Handles closing of the lightbox 76 | * @param {!Event} e Event that triggered closing 77 | * @return {void} 78 | * @protected 79 | */ 80 | _handleClose(e) { 81 | if (e && typeof e.preventDefault === "function") { 82 | e.preventDefault(); 83 | } 84 | 85 | const onClose = this.settings.onClose; 86 | if (onClose && typeof onClose === "function") { 87 | onClose(); 88 | } 89 | } 90 | 91 | /** 92 | * Binds event listeners to the trigger element 93 | * @return {void} 94 | * @protected 95 | */ 96 | _bindEventListeners() { 97 | this.el.addEventListener(this.settings.closeTrigger, this._handleClose); 98 | if (this.closeButtonEl) { 99 | this.closeButtonEl.addEventListener("click", this._handleClose); 100 | } 101 | } 102 | 103 | /** 104 | * Builds a class list using the namespace and suffix, if any. 105 | * @param {string} suffix Suffix to add to each class 106 | * @return {!Array} Class list 107 | * @protected 108 | */ 109 | _buildClasses(suffix) { 110 | const classes = [`lum-${suffix}`]; 111 | 112 | const ns = this.settings.namespace; 113 | if (ns) { 114 | classes.push(`${ns}-${suffix}`); 115 | } 116 | 117 | return classes; 118 | } 119 | 120 | /** 121 | * Creates the lightbox element 122 | * @return {void} 123 | * @protected 124 | */ 125 | _buildElement() { 126 | this.el = document.createElement("div"); 127 | addClasses(this.el, this._buildClasses("lightbox")); 128 | 129 | this.innerEl = document.createElement("div"); 130 | addClasses(this.innerEl, this._buildClasses("lightbox-inner")); 131 | this.el.appendChild(this.innerEl); 132 | 133 | const loaderEl = document.createElement("div"); 134 | addClasses(loaderEl, this._buildClasses("lightbox-loader")); 135 | this.innerEl.appendChild(loaderEl); 136 | 137 | this.imgWrapperEl = document.createElement("div"); 138 | addClasses(this.imgWrapperEl, this._buildClasses("lightbox-image-wrapper")); 139 | this.innerEl.appendChild(this.imgWrapperEl); 140 | 141 | const positionHelperEl = document.createElement("span"); 142 | addClasses( 143 | positionHelperEl, 144 | this._buildClasses("lightbox-position-helper") 145 | ); 146 | this.imgWrapperEl.appendChild(positionHelperEl); 147 | 148 | this.imgEl = document.createElement("img"); 149 | addClasses(this.imgEl, this._buildClasses("img")); 150 | positionHelperEl.appendChild(this.imgEl); 151 | 152 | this.captionEl = document.createElement("p"); 153 | addClasses(this.captionEl, this._buildClasses("lightbox-caption")); 154 | positionHelperEl.appendChild(this.captionEl); 155 | 156 | if (this.settings.closeButtonEnabled) { 157 | this.closeButtonEl = document.createElement("div"); 158 | addClasses(this.closeButtonEl, this._buildClasses("close-button")); 159 | this.el.appendChild(this.closeButtonEl); 160 | } 161 | 162 | if (this.settings._gallery) { 163 | this._setUpGalleryElements(); 164 | } 165 | 166 | this.settings.parentEl.appendChild(this.el); 167 | 168 | this._updateImgSrc(); 169 | this._updateCaption(); 170 | 171 | if (this.settings.includeImgixJSClass) { 172 | this.imgEl.classList.add("imgix-fluid"); 173 | } 174 | } 175 | 176 | /** 177 | * Creates gallery elements such as previous/next buttons 178 | * @return {void} 179 | * @protected 180 | */ 181 | _setUpGalleryElements() { 182 | this._buildGalleryButton("previous", this.showPrevious); 183 | this._buildGalleryButton("next", this.showNext); 184 | } 185 | 186 | /** 187 | * Creates a gallery button 188 | * @param {string} name Name of button 189 | * @param {!Function} fn Click handler 190 | * @return {void} 191 | * @protected 192 | */ 193 | _buildGalleryButton(name, fn) { 194 | const btn = document.createElement("button"); 195 | this[`${name}Button`] = btn; 196 | 197 | btn.innerText = name; 198 | addClasses(btn, this._buildClasses(`${name}-button`)); 199 | addClasses(btn, this._buildClasses("gallery-button")); 200 | this.innerEl.appendChild(btn); 201 | 202 | btn.addEventListener( 203 | "click", 204 | e => { 205 | e.stopPropagation(); 206 | 207 | fn(); 208 | }, 209 | false 210 | ); 211 | } 212 | 213 | /** 214 | * Sizes the image wrapper 215 | * @return {void} 216 | * @protected 217 | */ 218 | _sizeImgWrapperEl() { 219 | const style = this.imgWrapperEl.style; 220 | style.width = `${this.innerEl.clientWidth}px`; 221 | style.maxWidth = `${this.innerEl.clientWidth}px`; 222 | style.height = `${this.innerEl.clientHeight - 223 | this.captionEl.clientHeight}px`; 224 | style.maxHeight = `${this.innerEl.clientHeight - 225 | this.captionEl.clientHeight}px`; 226 | } 227 | 228 | /** 229 | * Updates caption from settings 230 | * @return {void} 231 | * @protected 232 | */ 233 | _updateCaption() { 234 | const captionType = typeof this.settings.caption; 235 | let caption = ""; 236 | 237 | if (captionType === "string") { 238 | caption = this.settings.caption; 239 | } else if (captionType === "function") { 240 | caption = this.settings.caption(this.currentTrigger); 241 | } 242 | 243 | this.captionEl.innerHTML = caption; 244 | } 245 | 246 | /** 247 | * Updates image element from the trigger element's attributes 248 | * @return {void} 249 | * @protected 250 | */ 251 | _updateImgSrc() { 252 | const imageURL = this.currentTrigger.getAttribute( 253 | this.settings.sourceAttribute 254 | ); 255 | 256 | if (!imageURL) { 257 | throw new Error( 258 | `No image URL was found in the ${ 259 | this.settings.sourceAttribute 260 | } attribute of the trigger.` 261 | ); 262 | } 263 | 264 | const loadingClasses = this._buildClasses("loading"); 265 | 266 | if (!this.hasBeenLoaded) { 267 | addClasses(this.el, loadingClasses); 268 | } 269 | 270 | this.imgEl.onload = () => { 271 | removeClasses(this.el, loadingClasses); 272 | this.hasBeenLoaded = true; 273 | }; 274 | 275 | this.imgEl.setAttribute("src", imageURL); 276 | } 277 | 278 | /** 279 | * Handles key up/down events for moving between items 280 | * @param {!Event} e Keyboard event 281 | * @return {void} 282 | * @protected 283 | */ 284 | _handleKeydown(e) { 285 | if (e.keyCode == LEFT_ARROW) { 286 | this.showPrevious(); 287 | } else if (e.keyCode == RIGHT_ARROW) { 288 | this.showNext(); 289 | } 290 | } 291 | 292 | /** 293 | * Shows the next item if in a gallery 294 | * @return {void} 295 | */ 296 | showNext() { 297 | if (!this.settings._gallery) { 298 | return; 299 | } 300 | 301 | this.currentTrigger = this.settings._gallery.nextTrigger( 302 | this.currentTrigger 303 | ); 304 | this._updateImgSrc(); 305 | this._updateCaption(); 306 | this._sizeImgWrapperEl(); 307 | this.settings._gallery.onChange({ imgEl: this.imgEl }); 308 | } 309 | 310 | /** 311 | * Shows the previous item if in a gallery 312 | * @return {void} 313 | */ 314 | showPrevious() { 315 | if (!this.settings._gallery) { 316 | return; 317 | } 318 | 319 | this.currentTrigger = this.settings._gallery.previousTrigger( 320 | this.currentTrigger 321 | ); 322 | this._updateImgSrc(); 323 | this._updateCaption(); 324 | this._sizeImgWrapperEl(); 325 | this.settings._gallery.onChange({ imgEl: this.imgEl }); 326 | } 327 | 328 | /** 329 | * Opens the lightbox 330 | * @return {void} 331 | */ 332 | open() { 333 | if (!this.elementBuilt) { 334 | this._buildElement(); 335 | this._bindEventListeners(); 336 | this.elementBuilt = true; 337 | } 338 | 339 | // When opening, always reset to the trigger we were passed 340 | this.currentTrigger = this.settings.triggerEl; 341 | 342 | // Make sure to re-set the `img` `src`, in case it's been changed 343 | // by someone/something else. 344 | this._updateImgSrc(); 345 | this._updateCaption(); 346 | 347 | addClasses(this.el, this.openClasses); 348 | 349 | this._sizeImgWrapperEl(); 350 | window.addEventListener("resize", this._sizeImgWrapperEl, false); 351 | 352 | if (this.settings._arrowNavigation) { 353 | window.addEventListener("keydown", this._handleKeydown, false); 354 | } 355 | 356 | if (HAS_ANIMATION) { 357 | this.el.addEventListener("animationend", this._completeOpen, false); 358 | addClasses(this.el, this.openingClasses); 359 | } 360 | } 361 | 362 | /** 363 | * Closes the lightbox 364 | * @return {void} 365 | */ 366 | close() { 367 | window.removeEventListener("resize", this._sizeImgWrapperEl, false); 368 | 369 | if (this.settings._arrowNavigation) { 370 | window.removeEventListener("keydown", this._handleKeydown, false); 371 | } 372 | 373 | if (HAS_ANIMATION) { 374 | this.el.addEventListener("animationend", this._completeClose, false); 375 | addClasses(this.el, this.closingClasses); 376 | } else { 377 | removeClasses(this.el, this.openClasses); 378 | } 379 | } 380 | 381 | /** 382 | * Handles animations on completion of opening the lightbox 383 | * @return {void} 384 | * @protected 385 | */ 386 | _completeOpen() { 387 | this.el.removeEventListener("animationend", this._completeOpen, false); 388 | 389 | removeClasses(this.el, this.openingClasses); 390 | } 391 | 392 | /** 393 | * Handles animations on completion of closing the lightbox 394 | * @return {void} 395 | * @protected 396 | */ 397 | _completeClose() { 398 | this.el.removeEventListener("animationend", this._completeClose, false); 399 | 400 | removeClasses(this.el, this.openClasses); 401 | removeClasses(this.el, this.closingClasses); 402 | } 403 | 404 | /** 405 | * Destroys the lightbox 406 | * @return {void} 407 | */ 408 | destroy() { 409 | if (this.el) { 410 | this.settings.parentEl.removeChild(this.el); 411 | } 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/js/Luminous.js: -------------------------------------------------------------------------------- 1 | import { isDOMElement } from "./util/dom"; 2 | import injectBaseStylesheet from "./injectBaseStylesheet"; 3 | import Lightbox from "./Lightbox"; 4 | 5 | /** 6 | * Represents the default luminous lightbox 7 | */ 8 | export default class Luminous { 9 | /** 10 | * Constructor 11 | * @param {!Element} trigger Trigger element to open lightbox 12 | * @param {Object=} options Luminous options 13 | */ 14 | constructor(trigger, options = {}) { 15 | this.VERSION = "2.4.0"; 16 | this.destroy = this.destroy.bind(this); 17 | this.open = this.open.bind(this); 18 | this.close = this.close.bind(this); 19 | this._handleKeyup = this._handleKeyup.bind(this); 20 | 21 | this.isOpen = false; 22 | 23 | this.trigger = trigger; 24 | 25 | if (!isDOMElement(this.trigger)) { 26 | throw new TypeError( 27 | "`new Luminous` requires a DOM element as its first argument." 28 | ); 29 | } 30 | 31 | let rootNode = document; 32 | if ("getRootNode" in this.trigger) { 33 | rootNode = this.trigger.getRootNode(); 34 | } 35 | // Prefix for generated element class names (e.g. `my-ns` will 36 | // result in classes such as `my-ns-lightbox`. Default `lum-` 37 | // prefixed classes will always be added as well. 38 | const namespace = options["namespace"] || null; 39 | // Which attribute to pull the lightbox image source from. 40 | const sourceAttribute = options["sourceAttribute"] || "href"; 41 | // Captions can be a literal string, or a function that receives the Luminous instance's trigger element as an argument and returns a string. Supports HTML, so use caution when dealing with user input. 42 | const caption = options["caption"] || null; 43 | // The event to listen to on the _trigger_ element: triggers opening. 44 | const openTrigger = options["openTrigger"] || "click"; 45 | // The event to listen to on the _lightbox_ element: triggers closing. 46 | const closeTrigger = options["closeTrigger"] || "click"; 47 | // Allow closing by pressing escape. 48 | const closeWithEscape = "closeWithEscape" in options ? !!options["closeWithEscape"] : true; 49 | // Automatically close when the page is scrolled. 50 | const closeOnScroll = options["closeOnScroll"] || false; 51 | const closeButtonEnabled = 52 | options["showCloseButton"] != null ? options["showCloseButton"] : true; 53 | const appendToNode = 54 | options["appendToNode"] || 55 | (rootNode === document ? document.body : rootNode); 56 | // A selector defining what to append the lightbox element to. 57 | const appendToSelector = options["appendToSelector"] || null; 58 | // If present (and a function), this will be called 59 | // whenever the lightbox is opened. 60 | const onOpen = options["onOpen"] || null; 61 | // If present (and a function), this will be called 62 | // whenever the lightbox is closed. 63 | const onClose = options["onClose"] || null; 64 | // When true, adds the `imgix-fluid` class to the `img` 65 | // inside the lightbox. See https://github.com/imgix/imgix.js 66 | // for more information. 67 | const includeImgixJSClass = options["includeImgixJSClass"] || false; 68 | // Add base styles to the page. See the "Theming" 69 | // section of README.md for more information. 70 | const injectBaseStyles = "injectBaseStyles" in options ? !!options["injectBaseStyles"] : true; 71 | // Internal use only! 72 | const _gallery = options["_gallery"] || null; 73 | const _arrowNavigation = options["_arrowNavigation"] || null; 74 | 75 | this.settings = { 76 | namespace, 77 | sourceAttribute, 78 | caption, 79 | openTrigger, 80 | closeTrigger, 81 | closeWithEscape, 82 | closeOnScroll, 83 | closeButtonEnabled, 84 | appendToNode, 85 | appendToSelector, 86 | onOpen, 87 | onClose, 88 | includeImgixJSClass, 89 | injectBaseStyles, 90 | _gallery, 91 | _arrowNavigation 92 | }; 93 | 94 | let injectionRoot = document.body; 95 | if (appendToNode && "getRootNode" in appendToNode) { 96 | injectionRoot = appendToNode.getRootNode(); 97 | } 98 | 99 | if (this.settings.injectBaseStyles) { 100 | injectBaseStylesheet(injectionRoot); 101 | } 102 | 103 | this._buildLightbox(); 104 | this._bindEventListeners(); 105 | } 106 | 107 | /** 108 | * Opens the lightbox 109 | * @param {Event=} e Event which triggered opening 110 | * @return {void} 111 | */ 112 | open(e) { 113 | if (e && typeof e.preventDefault === "function") { 114 | e.preventDefault(); 115 | } 116 | 117 | this.lightbox.open(); 118 | 119 | if (this.settings.closeOnScroll) { 120 | window.addEventListener("scroll", this.close, false); 121 | } 122 | 123 | const onOpen = this.settings.onOpen; 124 | if (onOpen && typeof onOpen === "function") { 125 | onOpen(); 126 | } 127 | 128 | this.isOpen = true; 129 | } 130 | 131 | /** 132 | * Closes the lightbox 133 | * @param {Event=} e Event which triggered closing 134 | * @return {void} 135 | */ 136 | close(e) { 137 | if (this.settings.closeOnScroll) { 138 | window.removeEventListener("scroll", this.close, false); 139 | } 140 | 141 | this.lightbox.close(); 142 | 143 | const onClose = this.settings.onClose; 144 | if (onClose && typeof onClose === "function") { 145 | onClose(); 146 | } 147 | 148 | this.isOpen = false; 149 | } 150 | 151 | /** 152 | * Builds the internal lightbox instance 153 | * @protected 154 | * @return {void} 155 | */ 156 | _buildLightbox() { 157 | let parentEl = this.settings.appendToNode; 158 | 159 | if (this.settings.appendToSelector) { 160 | parentEl = document.querySelector(this.settings.appendToSelector); 161 | } 162 | 163 | this.lightbox = new Lightbox({ 164 | namespace: this.settings.namespace, 165 | parentEl: parentEl, 166 | triggerEl: this.trigger, 167 | sourceAttribute: this.settings.sourceAttribute, 168 | caption: this.settings.caption, 169 | includeImgixJSClass: this.settings.includeImgixJSClass, 170 | closeButtonEnabled: this.settings.closeButtonEnabled, 171 | _gallery: this.settings._gallery, 172 | _arrowNavigation: this.settings._arrowNavigation, 173 | closeTrigger: this.settings.closeTrigger, 174 | onClose: this.close 175 | }); 176 | } 177 | 178 | /** 179 | * Binds lightbox events to the trigger element 180 | * @protected 181 | * @return {void} 182 | */ 183 | _bindEventListeners() { 184 | this.trigger.addEventListener(this.settings.openTrigger, this.open, false); 185 | 186 | if (this.settings.closeWithEscape) { 187 | window.addEventListener("keyup", this._handleKeyup, false); 188 | } 189 | } 190 | 191 | /** 192 | * Unbinds all events 193 | * @protected 194 | * @return {void} 195 | */ 196 | _unbindEvents() { 197 | this.trigger.removeEventListener( 198 | this.settings.openTrigger, 199 | this.open, 200 | false 201 | ); 202 | if (this.lightbox.el) { 203 | this.lightbox.el.removeEventListener( 204 | this.settings.closeTrigger, 205 | this.close, 206 | false 207 | ); 208 | } 209 | 210 | if (this.settings.closeWithEscape) { 211 | window.removeEventListener("keyup", this._handleKeyup, false); 212 | } 213 | } 214 | 215 | /** 216 | * Handles key up events and closes lightbox when esc is pressed 217 | * @param {!Event} e Keyboard event 218 | * @return {void} 219 | * @protected 220 | */ 221 | _handleKeyup(e) { 222 | if (this.isOpen && e.keyCode === 27) { 223 | this.close(); 224 | } 225 | } 226 | 227 | /** 228 | * Destroys internal lightbox and unbinds events 229 | * @return {void} 230 | */ 231 | destroy() { 232 | this._unbindEvents(); 233 | this.lightbox.destroy(); 234 | } 235 | } 236 | 237 | /* eslint-disable no-self-assign */ 238 | Luminous.prototype["open"] = Luminous.prototype.open; 239 | Luminous.prototype["close"] = Luminous.prototype.close; 240 | Luminous.prototype["destroy"] = Luminous.prototype.destroy; 241 | /* eslint-enable no-self-assign */ 242 | -------------------------------------------------------------------------------- /src/js/LuminousGallery.js: -------------------------------------------------------------------------------- 1 | import Luminous from "./Luminous"; 2 | 3 | /** 4 | * Represents a gallery-style lightbox 5 | */ 6 | export default class LuminousGallery { 7 | /** 8 | * Constructor 9 | * @param {!Array} triggers Array of trigger elements 10 | * @param {Object=} options Gallery options 11 | * @param {Object=} luminousOpts Luminous options 12 | */ 13 | constructor(triggers, options = {}, luminousOpts = {}) { 14 | const optionsDefaults = { 15 | arrowNavigation: true, 16 | onChange: null, 17 | }; 18 | 19 | this.settings = Object.assign({}, optionsDefaults, options); 20 | 21 | this.triggers = triggers; 22 | this.luminousOpts = luminousOpts; 23 | this.luminousOpts["_gallery"] = this; 24 | this.luminousOpts["_arrowNavigation"] = this.settings["arrowNavigation"]; 25 | this._constructLuminousInstances(); 26 | } 27 | 28 | /** 29 | * Creates internal luminous instances 30 | * @protected 31 | * @return {void} 32 | */ 33 | _constructLuminousInstances() { 34 | this.luminousInstances = []; 35 | 36 | const triggerLen = this.triggers.length; 37 | for (let i = 0; i < triggerLen; i++) { 38 | const trigger = this.triggers[i]; 39 | const lum = new Luminous(trigger, this.luminousOpts); 40 | this.luminousInstances.push(lum); 41 | } 42 | } 43 | 44 | /** 45 | * Determines the next trigger element 46 | * @param {!Element} trigger Current trigger element 47 | * @return {!Element} 48 | */ 49 | nextTrigger(trigger) { 50 | const nextTriggerIndex = 51 | Array.prototype.indexOf.call(this.triggers, trigger) + 1; 52 | 53 | return nextTriggerIndex >= this.triggers.length 54 | ? this.triggers[0] 55 | : this.triggers[nextTriggerIndex]; 56 | } 57 | 58 | /** 59 | * Determines the previous trigger element 60 | * @param {!Element} trigger Current trigger element 61 | * @return {!Element} 62 | */ 63 | previousTrigger(trigger) { 64 | const prevTriggerIndex = 65 | Array.prototype.indexOf.call(this.triggers, trigger) - 1; 66 | 67 | return prevTriggerIndex < 0 68 | ? this.triggers[this.triggers.length - 1] 69 | : this.triggers[prevTriggerIndex]; 70 | } 71 | 72 | /** 73 | * Callback called when current image is changed 74 | * @param {Object} params 75 | * @param {!Element} params.imgEl New image element 76 | */ 77 | onChange({ imgEl }) { 78 | const onChange = this.settings.onChange; 79 | if (onChange && typeof onChange === "function") { 80 | onChange({ imgEl }); 81 | } 82 | } 83 | 84 | /** 85 | * Destroys the internal luminous instances 86 | * @return {void} 87 | */ 88 | destroy() { 89 | this.luminousInstances.forEach(instance => instance.destroy()); 90 | } 91 | } 92 | 93 | /* eslint-disable-next-line no-self-assign */ 94 | LuminousGallery.prototype["destroy"] = LuminousGallery.prototype.destroy; 95 | -------------------------------------------------------------------------------- /src/js/injectBaseStylesheet.js: -------------------------------------------------------------------------------- 1 | /* UNMINIFIED RULES 2 | 3 | @keyframes lum-noop { 4 | 0% { zoom: 1; } 5 | } 6 | 7 | .lum-lightbox { 8 | position: fixed; 9 | display: none; 10 | top: 0; 11 | right: 0; 12 | bottom: 0; 13 | left: 0; 14 | } 15 | 16 | .lum-lightbox.lum-open { 17 | display: block; 18 | } 19 | 20 | .lum-lightbox.lum-opening, .lum-lightbox.lum-closing { 21 | animation: lum-noop 1ms; 22 | } 23 | 24 | .lum-lightbox-inner { 25 | position: absolute; 26 | top: 0%; 27 | right: 0%; 28 | bottom: 0%; 29 | left: 0%; 30 | 31 | overflow: hidden; 32 | } 33 | 34 | .lum-lightbox-loader { 35 | display: none; 36 | } 37 | 38 | .lum-lightbox-inner img { 39 | max-width: 100%; 40 | max-height: 100%; 41 | } 42 | 43 | .lum-lightbox-image-wrapper { 44 | vertical-align: middle; 45 | display: table-cell; 46 | text-align: center; 47 | } 48 | */ 49 | 50 | const RULES = `@keyframes lum-noop{0%{zoom:1}}.lum-lightbox{position:fixed;display:none;top:0;right:0;bottom:0;left:0}.lum-lightbox.lum-open{display:block}.lum-lightbox.lum-closing,.lum-lightbox.lum-opening{animation:lum-noop 1ms}.lum-lightbox-inner{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden}.lum-lightbox-loader{display:none}.lum-lightbox-inner img{max-width:100%;max-height:100%}.lum-lightbox-image-wrapper{vertical-align:middle;display:table-cell;text-align:center}`; 51 | 52 | /** 53 | * Injects the base stylesheet needed to display the lightbox 54 | * element. 55 | * If `node` is the document, the stylesheet will be appended to ``. 56 | * @param {!Node} node Node to append stylesheet to 57 | * @return {void} 58 | */ 59 | export default function injectBaseStylesheet(node) { 60 | if (!node || node === document) { 61 | node = document.head; 62 | } 63 | 64 | if (node.querySelector(".lum-base-styles")) { 65 | return; 66 | } 67 | 68 | const styleEl = document.createElement("style"); 69 | styleEl.type = "text/css"; 70 | styleEl.classList.add("lum-base-styles"); 71 | 72 | styleEl.appendChild(document.createTextNode(RULES)); 73 | 74 | node.insertBefore(styleEl, node.firstChild); 75 | } 76 | -------------------------------------------------------------------------------- /src/js/lum-browser.js: -------------------------------------------------------------------------------- 1 | // This file is used for the standalone browser build 2 | 3 | import Luminous from "./Luminous"; 4 | import LuminousGallery from "./LuminousGallery"; 5 | 6 | window["LuminousGallery"] = LuminousGallery; 7 | window["Luminous"] = Luminous; 8 | -------------------------------------------------------------------------------- /src/js/lum.js: -------------------------------------------------------------------------------- 1 | // This file exports the Luminous exports in the ES6 Module Spec, which is compatible with the commonjs spec 2 | import Luminous from "./Luminous"; 3 | import LuminousGallery from "./LuminousGallery"; 4 | 5 | export { Luminous, LuminousGallery }; 6 | -------------------------------------------------------------------------------- /src/js/util/dom.js: -------------------------------------------------------------------------------- 1 | // This is not really a perfect check, but works fine. 2 | // From http://stackoverflow.com/questions/384286 3 | const HAS_DOM_2 = typeof HTMLElement === "object"; 4 | const HAS_SHADOW = typeof ShadowRoot !== "undefined"; 5 | 6 | /** 7 | * Determines whether an object is a DOM element or not. 8 | * @param {!Object} obj Object to check 9 | * @return {boolean} True if object is an element 10 | */ 11 | export function isDOMElement(obj) { 12 | if (HAS_SHADOW && obj instanceof ShadowRoot) { 13 | return true; 14 | } 15 | return HAS_DOM_2 16 | ? obj instanceof HTMLElement 17 | : obj && 18 | typeof obj === "object" && 19 | obj !== null && 20 | obj.nodeType === 1 && 21 | typeof obj.nodeName === "string"; 22 | } 23 | 24 | /** 25 | * Adds an array of classes to an element 26 | * @param {!Element} el Element to add classes to 27 | * @param {!Array} classNames Class names to add 28 | * @return {void} 29 | */ 30 | export function addClasses(el, classNames) { 31 | classNames.forEach(function(className) { 32 | el.classList.add(className); 33 | }); 34 | } 35 | 36 | /** 37 | * Removes an array of classes from an element 38 | * @param {!Element} el Element to remove classes from 39 | * @param {!Array} classNames Classes to remove 40 | * @return {void} 41 | */ 42 | export function removeClasses(el, classNames) { 43 | classNames.forEach(function(className) { 44 | el.classList.remove(className); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/js/util/throwIfMissing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Throws a missing parameter error 3 | */ 4 | export default function throwIfMissing() { 5 | throw new Error("Missing parameter"); 6 | } 7 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "mocha": true, 5 | "jasmine": true 6 | }, 7 | "rules": { 8 | "require-jsdoc": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/testLightbox.js: -------------------------------------------------------------------------------- 1 | import Lightbox from "../src/js/Lightbox"; 2 | 3 | let lightbox = null; 4 | beforeEach(function() { 5 | const anchor = document.createElement("a"); 6 | anchor.href = "http://website.com/image.png"; 7 | anchor.classList.add("test-anchor"); 8 | 9 | document.body.appendChild(anchor); 10 | }); 11 | 12 | const deleteAllElementsByClassName = className => { 13 | const paras = document.getElementsByClassName(className); 14 | while (paras[0]) { 15 | paras[0].parentNode.removeChild(paras[0]); 16 | } 17 | }; 18 | afterEach(function() { 19 | const anchor = document.querySelector(".test-anchor"); 20 | 21 | if (lightbox) { 22 | try { 23 | lightbox.close(); 24 | lightbox.destroy(); 25 | } catch (_e) {} // eslint-disable-line no-empty 26 | lightbox = null; 27 | } 28 | deleteAllElementsByClassName("lum-lightbox"); 29 | document.body.removeChild(anchor); 30 | }); 31 | 32 | describe("Lightbox", () => { 33 | it("throws if no arguments are passed", () => { 34 | expect(() => { 35 | new Lightbox(); 36 | }).toThrowError(Error, "Missing parameter"); 37 | }); 38 | 39 | it("throws if required arguments are missing", () => { 40 | expect(() => { 41 | new Lightbox({ namespace: "test", parentEl: document.body }); 42 | }).toThrowError(Error, "Missing parameter"); 43 | }); 44 | 45 | it("does not throw if all required arguments are passed", () => { 46 | expect(() => { 47 | const triggerEl = document.querySelector(".test-anchor"); 48 | 49 | new Lightbox({ 50 | namespace: "test", 51 | parentEl: document.body, 52 | triggerEl: triggerEl, 53 | sourceAttribute: "href", 54 | caption: null 55 | }); 56 | }).not.toThrowError(); 57 | }); 58 | 59 | it("throws if passed `parentEl` is not a DOM element", () => { 60 | expect(() => { 61 | const triggerEl = document.querySelector(".test-anchor"); 62 | 63 | new Lightbox({ 64 | namespace: "test", 65 | parentEl: ".not-an-element", 66 | triggerEl: triggerEl, 67 | sourceAttribute: "href", 68 | caption: null 69 | }); 70 | }).toThrowError( 71 | TypeError, 72 | "`new Lightbox` requires a DOM element passed as `parentEl`." 73 | ); 74 | }); 75 | 76 | it("assigns the correct class to its element", () => { 77 | const triggerEl = document.querySelector(".test-anchor"); 78 | 79 | lightbox = new Lightbox({ 80 | namespace: "test-namespace", 81 | parentEl: document.body, 82 | triggerEl: triggerEl, 83 | sourceAttribute: "href", 84 | caption: null 85 | }); 86 | lightbox.open(); 87 | lightbox.close(); 88 | 89 | expect( 90 | document.body.querySelector(".test-namespace-lightbox") 91 | ).not.toBeNull(); 92 | }); 93 | 94 | it("appends its element to the specified `appendToEl`", () => { 95 | const demoDiv = document.createElement("div"); 96 | demoDiv.classList.add("demo-div"); 97 | document.body.appendChild(demoDiv); 98 | 99 | const triggerEl = document.querySelector(".test-anchor"); 100 | 101 | lightbox = new Lightbox({ 102 | namespace: "lum", 103 | parentEl: demoDiv, 104 | triggerEl: triggerEl, 105 | sourceAttribute: "href", 106 | caption: null 107 | }); 108 | lightbox.open(); 109 | lightbox.close(); 110 | 111 | expect( 112 | document.body.querySelector(".demo-div > .lum-lightbox") 113 | ).not.toBeNull(); 114 | }); 115 | 116 | it("cleans up its element when destroyed", () => { 117 | const triggerEl = document.querySelector(".test-anchor"); 118 | 119 | lightbox = new Lightbox({ 120 | namespace: "to-destroy", 121 | parentEl: document.body, 122 | triggerEl: triggerEl, 123 | sourceAttribute: "href", 124 | caption: null 125 | }); 126 | lightbox.open(); 127 | lightbox.close(); 128 | lightbox.destroy(); 129 | 130 | expect(document.body.querySelector(".to-destroy-lightbox")).toBeNull(); 131 | }); 132 | 133 | it("adds the `imgix-fluid` param if configured", () => { 134 | const triggerEl = document.querySelector(".test-anchor"); 135 | 136 | lightbox = new Lightbox({ 137 | namespace: "fluid", 138 | parentEl: document.body, 139 | triggerEl: triggerEl, 140 | sourceAttribute: "href", 141 | caption: null, 142 | includeImgixJSClass: true 143 | }); 144 | lightbox.open(); 145 | lightbox.close(); 146 | 147 | expect(lightbox.el.querySelector(".imgix-fluid")).not.toBeNull(); 148 | }); 149 | 150 | describe("Close button", () => { 151 | it("shows a close button when the lightbox is open", () => { 152 | const triggerEl = document.querySelector(".test-anchor"); 153 | 154 | lightbox = new Lightbox({ 155 | namespace: "lum", 156 | parentEl: document.body, 157 | triggerEl: triggerEl, 158 | sourceAttribute: "href", 159 | caption: null 160 | }); 161 | lightbox.open(); 162 | // lightbox.close(); 163 | 164 | expect(document.body.querySelector(".lum-close-button")).not.toBeNull(); 165 | }); 166 | it("the close button closes the lightbox", () => { 167 | const triggerEl = document.querySelector(".test-anchor"); 168 | 169 | let closed = false; 170 | lightbox = new Lightbox({ 171 | namespace: "lum", 172 | parentEl: document.body, 173 | triggerEl: triggerEl, 174 | sourceAttribute: "href", 175 | caption: null, 176 | onClose: () => { 177 | closed = true; 178 | } 179 | }); 180 | lightbox.open(); 181 | 182 | const closeButtonEl = document.body.querySelector(".lum-close-button"); 183 | if (!closeButtonEl) { 184 | throw new Error("Close button doesn't exist in DOM."); 185 | } 186 | closeButtonEl.click(); 187 | 188 | expect(closed).toBe(true); 189 | }); 190 | it("the close button can be disabled", () => { 191 | const triggerEl = document.querySelector(".test-anchor"); 192 | 193 | lightbox = new Lightbox({ 194 | namespace: "lum", 195 | parentEl: document.body, 196 | triggerEl: triggerEl, 197 | sourceAttribute: "href", 198 | caption: null, 199 | closeButtonEnabled: false 200 | }); 201 | lightbox.open(); 202 | // lightbox.close(); 203 | 204 | expect(document.body.querySelector(".lum-close-button")).toBeNull(); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /test/testLuminous.js: -------------------------------------------------------------------------------- 1 | import Luminous from "../src/js/Luminous"; 2 | 3 | beforeEach(function() { 4 | const anchor = document.createElement("a"); 5 | anchor.href = "http://website.com/image.png"; 6 | anchor.classList.add("test-anchor"); 7 | 8 | document.body.appendChild(anchor); 9 | }); 10 | 11 | afterEach(function() { 12 | const anchor = document.querySelector(".test-anchor"); 13 | 14 | document.body.removeChild(anchor); 15 | }); 16 | 17 | describe("Core", () => { 18 | it("throws if no arguments are passed", () => { 19 | expect(() => { 20 | new Luminous(); 21 | }).toThrowError( 22 | TypeError, 23 | "`new Luminous` requires a DOM element as its first argument." 24 | ); 25 | }); 26 | 27 | it("throws if the first argument is not a DOM element", () => { 28 | expect(() => { 29 | new Luminous(".some-selector"); 30 | }).toThrowError( 31 | TypeError, 32 | "`new Luminous` requires a DOM element as its first argument." 33 | ); 34 | }); 35 | 36 | it("returns an instance of `Luminous` when correctly instantiated", () => { 37 | const anchor = document.querySelector(".test-anchor"); 38 | const lum = new Luminous(anchor); 39 | 40 | expect(lum.constructor).toBe(Luminous); 41 | }); 42 | 43 | it("executes the `onOpen` callback when present", () => { 44 | let called = false; 45 | function openCallback() { 46 | called = true; 47 | } 48 | 49 | const anchor = document.querySelector(".test-anchor"); 50 | const lum = new Luminous(anchor, { onOpen: openCallback }); 51 | 52 | lum.open(); 53 | expect(called).toBe(true); 54 | }); 55 | 56 | it("executes the `onClose` callback when present", () => { 57 | let called = false; 58 | function closeCallback() { 59 | called = true; 60 | } 61 | 62 | const anchor = document.querySelector(".test-anchor"); 63 | const lum = new Luminous(anchor, { onClose: closeCallback }); 64 | 65 | lum.open(); 66 | lum.close(); 67 | expect(called).toBe(true); 68 | }); 69 | 70 | it("injects styles into shadow root if parented by one", () => { 71 | // TODO (43081j): remove when firefox ships with shadow DOM 72 | if (typeof ShadowRoot === "undefined") { 73 | return; 74 | } 75 | const container = document.createElement("div"); 76 | container.attachShadow({ mode: "open" }); 77 | const anchor = document.createElement("a"); 78 | anchor.href = "https://example.com/image.png"; 79 | anchor.classList.add("test-shadow-anchor"); 80 | 81 | container.shadowRoot.appendChild(anchor); 82 | document.body.appendChild(container); 83 | 84 | new Luminous(anchor); 85 | const styles = container.shadowRoot.querySelector("style.lum-base-styles"); 86 | expect(styles).not.toBe(null); 87 | }); 88 | 89 | it("appends to shadow dom if parented by one", () => { 90 | // TODO (43081j): remove when firefox ships with shadow DOM 91 | if (typeof ShadowRoot === "undefined") { 92 | return; 93 | } 94 | const container = document.createElement("div"); 95 | container.attachShadow({ mode: "open" }); 96 | const anchor = document.createElement("a"); 97 | anchor.href = "https://example.com/image.png"; 98 | anchor.classList.add("test-shadow-anchor"); 99 | 100 | container.shadowRoot.appendChild(anchor); 101 | document.body.appendChild(container); 102 | 103 | new Luminous(anchor); 104 | anchor.click(); 105 | 106 | const lightbox = container.shadowRoot.querySelector(".lum-lightbox"); 107 | expect(lightbox).not.toBe(null); 108 | }); 109 | }); 110 | 111 | describe("Configuration", () => { 112 | it("sets up settings object when no options are passed", () => { 113 | const anchor = document.querySelector(".test-anchor"); 114 | const lum = new Luminous(anchor); 115 | 116 | expect(lum.settings).toBeDefined(); 117 | }); 118 | 119 | it("applies proper setting defaults when no options are passed", () => { 120 | const anchor = document.querySelector(".test-anchor"); 121 | const lum = new Luminous(anchor); 122 | 123 | expect(lum.settings.sourceAttribute).toBe("href"); 124 | }); 125 | 126 | it("accepts custom settings", () => { 127 | const anchor = document.querySelector(".test-anchor"); 128 | const lum = new Luminous(anchor, { namespace: "not-the-default" }); 129 | 130 | expect(lum.settings.namespace).toBe("not-the-default"); 131 | }); 132 | 133 | it("leaves settings defaults in place when custom settings are passed", () => { 134 | const anchor = document.querySelector(".test-anchor"); 135 | const lum = new Luminous(anchor, { namespace: "it-does-not-matter" }); 136 | 137 | expect(lum.settings.openTrigger).toBe("click"); 138 | }); 139 | 140 | it("allows truthy defaults to be overridden ", () => { 141 | const anchor = document.querySelector(".test-anchor"); 142 | const lum = new Luminous(anchor, { closeWithEscape: false, injectBaseStyles: false }); 143 | 144 | expect(lum.settings.closeWithEscape).toBe(false); 145 | expect(lum.settings.injectBaseStyles).toBe(false); 146 | }); 147 | 148 | it("passes settings to Lightbox", () => { 149 | const anchor = document.querySelector(".test-anchor"); 150 | const settingsToMap = { 151 | namespace: "custom", 152 | sourceAttribute: "not-href", 153 | caption: "custom", 154 | includeImgixJSClass: true, 155 | showCloseButton: { 156 | value: false, 157 | lightboxKey: "closeButtonEnabled" 158 | } 159 | }; 160 | const isObject = v => typeof v === "object" && v != null; 161 | const clientSettings = Object.keys(settingsToMap).reduce((p, key) => { 162 | const valuePrimitiveOrObject = settingsToMap[key]; 163 | p[key] = isObject(valuePrimitiveOrObject) 164 | ? valuePrimitiveOrObject.value 165 | : valuePrimitiveOrObject; 166 | return p; 167 | }, {}); 168 | 169 | const lum = new Luminous(anchor, clientSettings); 170 | 171 | Object.keys(settingsToMap).forEach(settingKey => { 172 | const valuePrimitiveOrObject = settingsToMap[settingKey]; 173 | let expectedKey; 174 | let expectedValue; 175 | if (isObject(valuePrimitiveOrObject)) { 176 | const valueConfig = valuePrimitiveOrObject; 177 | expectedKey = valueConfig.lightboxKey || settingKey; 178 | expectedValue = 179 | "lightboxValue" in valueConfig 180 | ? valueConfig.lightboxValue 181 | : valueConfig.value; 182 | } else { 183 | expectedKey = settingKey; 184 | expectedValue = valuePrimitiveOrObject; 185 | } 186 | expect(lum.lightbox.settings[expectedKey]).toBe(expectedValue); 187 | }); 188 | }); 189 | }); 190 | 191 | describe("#destroy", () => { 192 | it("does not throw if the Lightbox instance has never been `#open`ed", () => { 193 | const anchor = document.querySelector(".test-anchor"); 194 | const lum = new Luminous(anchor); 195 | 196 | expect(function() { 197 | lum.destroy(); 198 | }).not.toThrow(); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/testLuminousGallery.js: -------------------------------------------------------------------------------- 1 | import LuminousGallery from "../src/js/LuminousGallery"; 2 | 3 | const genLink = idx => `http://website.com/image-${idx}.png`; 4 | 5 | const isChromeHeadless = /HeadlessChrome/.test(window.navigator.userAgent); 6 | 7 | beforeEach(function() { 8 | for (let index = 0; index < 3; index++) { 9 | const anchor = document.createElement("a"); 10 | anchor.href = genLink(index); 11 | anchor.classList.add("test-gallery-anchor"); 12 | 13 | document.body.appendChild(anchor); 14 | } 15 | }); 16 | 17 | afterEach(function() { 18 | const anchors = document.getElementsByClassName("test-gallery-anchor"); 19 | while (anchors.length > 0) { 20 | anchors[0].parentNode.removeChild(anchors[0]); 21 | } 22 | }); 23 | 24 | function openLuminous(index = 0) { 25 | const anchors = document.querySelectorAll(".test-gallery-anchor"); 26 | const anchor = [...anchors].filter( 27 | anchor => anchor.href === genLink(index) 28 | )[0]; 29 | if (!anchor) { 30 | throw new Error("Anchor not found in DOM."); 31 | } 32 | anchor.click(); 33 | } 34 | 35 | describe("LuminousGallery", () => { 36 | it("should navigate right when right arrow button pressed", () => { 37 | new LuminousGallery(document.querySelectorAll(".test-gallery-anchor"), { 38 | arrowNavigation: true 39 | }); 40 | 41 | openLuminous(0); 42 | 43 | const nextButtonEl = document.body.querySelector(".lum-next-button"); 44 | if (!nextButtonEl) { 45 | throw new Error("Navigation button not found in DOM."); 46 | } 47 | nextButtonEl.click(); 48 | 49 | expect(document.body.querySelector(".lum-img").src).toBe(genLink(1)); 50 | }); 51 | it("should navigate left when left arrow button pressed", () => { 52 | new LuminousGallery(document.querySelectorAll(".test-gallery-anchor"), { 53 | arrowNavigation: true 54 | }); 55 | 56 | openLuminous(1); 57 | 58 | const prevButtonEl = document.body.querySelector(".lum-previous-button"); 59 | if (!prevButtonEl) { 60 | throw new Error("Navigation button not found in DOM."); 61 | } 62 | prevButtonEl.click(); 63 | 64 | expect(document.body.querySelector(".lum-img").src).toBe(genLink(0)); 65 | }); 66 | it("should navigate right when right arrow key pressed", () => { 67 | // Broken on CI 68 | if (isChromeHeadless) { 69 | return; 70 | } 71 | new LuminousGallery(document.querySelectorAll(".test-gallery-anchor"), { 72 | arrowNavigation: true 73 | }); 74 | 75 | openLuminous(0); 76 | 77 | const event = new KeyboardEvent("keydown", { keyCode: 39 }); 78 | window.dispatchEvent(event); 79 | 80 | expect(document.body.querySelector(".lum-img").src).toBe(genLink(1)); 81 | }); 82 | it("should navigate left when left arrow key pressed", () => { 83 | // Broken on CI 84 | if (isChromeHeadless) { 85 | return; 86 | } 87 | new LuminousGallery(document.querySelectorAll(".test-gallery-anchor"), { 88 | arrowNavigation: true 89 | }); 90 | 91 | openLuminous(1); 92 | 93 | const event = new KeyboardEvent("keydown", { keyCode: 37 }); 94 | window.dispatchEvent(event); 95 | 96 | expect(document.body.querySelector(".lum-img").src).toBe(genLink(0)); 97 | }); 98 | it("should call onChange when current image moves right", () => { 99 | let current = null; 100 | new LuminousGallery(document.querySelectorAll(".test-gallery-anchor"), { 101 | arrowNavigation: true, 102 | onChange: ({ imgEl }) => current = imgEl, 103 | }); 104 | 105 | openLuminous(0); 106 | 107 | const nextButtonEl = document.body.querySelector(".lum-next-button"); 108 | if (!nextButtonEl) { 109 | throw new Error("Navigation button not found in DOM."); 110 | } 111 | nextButtonEl.click(); 112 | 113 | expect(current.src).toBe(genLink(1)); 114 | }); 115 | it("should call onChange when current image moves left", () => { 116 | let current = null; 117 | new LuminousGallery(document.querySelectorAll(".test-gallery-anchor"), { 118 | arrowNavigation: true, 119 | onChange: ({ imgEl }) => current = imgEl, 120 | }); 121 | 122 | openLuminous(1); 123 | 124 | const prevButtonEl = document.body.querySelector(".lum-previous-button"); 125 | if (!prevButtonEl) { 126 | throw new Error("Navigation button not found in DOM."); 127 | } 128 | prevButtonEl.click(); 129 | 130 | expect(current.src).toBe(genLink(0)); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | var context = require.context("./test", true, /\.js$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const env = require("yargs").argv.env; // use --env with webpack 2 4 | const ClosureCompilerPlugin = require("webpack-closure-compiler"); 5 | const licenseBanner = require("./build/banner"); 6 | 7 | let libraryName = "luminous"; 8 | 9 | let config; 10 | { 11 | let outputFile, mode; 12 | 13 | if (env === "build") { 14 | mode = "production"; 15 | outputFile = libraryName + ".min.js"; 16 | } else { 17 | mode = "development"; 18 | outputFile = libraryName + ".js"; 19 | } 20 | config = buildWithEnv(mode, outputFile); 21 | } 22 | 23 | function buildWithEnv(mode, outputFile) { 24 | const config = { 25 | mode: mode, 26 | entry: __dirname + "/src/js/lum-browser.js", 27 | devtool: "source-map", 28 | output: { 29 | path: __dirname + "/dist", 30 | filename: outputFile 31 | }, 32 | resolve: { 33 | modules: [path.resolve("./node_modules"), path.resolve("./src")], 34 | extensions: [".json", ".js"] 35 | }, 36 | module: { 37 | rules: [] 38 | }, 39 | plugins: [ 40 | new ClosureCompilerPlugin({ 41 | compiler: { 42 | language_in: "ECMASCRIPT6", 43 | language_out: "ECMASCRIPT5", 44 | compilation_level: "ADVANCED", 45 | create_source_map: true 46 | }, 47 | test: /^(?!.*tests\.webpack).*$/, 48 | concurrency: 3 49 | }), 50 | new webpack.BannerPlugin({ 51 | banner: licenseBanner 52 | }), 53 | ] 54 | }; 55 | 56 | return config; 57 | } 58 | 59 | module.exports = config; 60 | --------------------------------------------------------------------------------