├── .all-contributorsrc ├── .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 ├── .prettierrc ├── .renovaterc.json ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bower.json ├── config └── karma │ ├── karma-ci.config.js │ ├── karma-local.config.js │ └── karma.config.js ├── dist ├── Drift.js ├── Drift.min.js ├── Drift.min.js.map ├── drift-basic.css └── drift-basic.min.css ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── css │ └── drift-basic.css └── js │ ├── BoundingBox.js │ ├── Drift-browser.js │ ├── Drift.js │ ├── Trigger.js │ ├── ZoomPane.js │ ├── injectBaseStylesheet.js │ └── util │ ├── debounce.js │ ├── dom.js │ └── throwIfMissing.js ├── test ├── .eslintrc.json ├── helpers.js ├── testBoundingBox.js ├── testDrift.js ├── testTrigger.js └── testZoomPane.js ├── tests.webpack.js └── webpack.config.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "paulstraw", 10 | "name": "Paul Straw", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/117288?v=4", 12 | "profile": "https://paulstraw.com", 13 | "contributions": [ 14 | "doc", 15 | "code", 16 | "maintenance" 17 | ] 18 | }, 19 | { 20 | "login": "sherwinski", 21 | "name": "sherwinski", 22 | "avatar_url": "https://avatars3.githubusercontent.com/u/15919091?v=4", 23 | "profile": "https://github.com/sherwinski", 24 | "contributions": [ 25 | "code", 26 | "doc", 27 | "maintenance" 28 | ] 29 | }, 30 | { 31 | "login": "frederickfogerty", 32 | "name": "Frederick Fogerty", 33 | "avatar_url": "https://avatars0.githubusercontent.com/u/615334?v=4", 34 | "profile": "https://github.com/frederickfogerty", 35 | "contributions": [ 36 | "code", 37 | "doc", 38 | "maintenance" 39 | ] 40 | }, 41 | { 42 | "login": "jayeb", 43 | "name": "Jason Eberle", 44 | "avatar_url": "https://avatars2.githubusercontent.com/u/609840?v=4", 45 | "profile": "http://jayeb.com", 46 | "contributions": [ 47 | "code", 48 | "doc", 49 | "maintenance" 50 | ] 51 | }, 52 | { 53 | "login": "luqven", 54 | "name": "Luis H. Ball Jr.", 55 | "avatar_url": "https://avatars.githubusercontent.com/u/16711614?v=4", 56 | "profile": "http://www.luisball.com", 57 | "contributions": [ 58 | "maintenance" 59 | ] 60 | } 61 | ], 62 | "contributorsPerLine": 7, 63 | "projectName": "drift", 64 | "projectOwner": "imgix", 65 | "repoType": "github", 66 | "repoHost": "https://github.com", 67 | "skipCi": true 68 | } 69 | -------------------------------------------------------------------------------- /.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:current 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": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>imgix/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | ### [1.4.4](https://github.com/imgix/drift/compare/v1.4.3...v1.4.4) (2021-05-21) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * document undefined in no browser environment ([23305b5](https://github.com/imgix/drift/commit/23305b5904c5c63df3d4894e51e8d20df0968da9)) 11 | * remove inline styles individually ([40f41be](https://github.com/imgix/drift/commit/40f41be6409986d2d3ab66c1b09b042481d2a5a1)) 12 | 13 | ### [1.4.3](https://github.com/imgix/drift/compare/v1.4.2...v1.4.3) (2021-03-02) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * add postcss dependency to fix build error ([#606](https://github.com/imgix/drift/issues/606)) ([a48f0ac](https://github.com/imgix/drift/commit/a48f0ac3e88fac5d4ced75636b70097167d8a9c9)) 19 | * revert "refactor: favor directly modifying DOM attributes over setAttribute" ([#614](https://github.com/imgix/drift/issues/614)) ([2da753d](https://github.com/imgix/drift/commit/2da753d28adee1f6a213437484babe0b8adcbf18)) 20 | 21 | ### [1.4.2](https://github.com/imgix/drift/compare/v1.4.1...v1.4.2) (2021-02-18) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * correct demo url on readme.md file ([#580](https://github.com/imgix/drift/issues/580)) ([72fa50e](https://github.com/imgix/drift/commit/72fa50ef984322169842cdb1cc2bc22239f4abc7)) 27 | * favor directly modifying DOM attributes over using `setAttribute()` ([598](https://github.com/imgix/drift/pull/598)) 28 | 29 | ### [1.4.1](https://github.com/imgix/drift/compare/v1.4.0...v1.4.1) (2020-10-09) 30 | 31 | * chore: syncs dependency updates 32 | * docs: syncs README updates 33 | 34 | ## [1.4.0](https://github.com/imgix/drift/compare/v1.3.5...v1.4.0) (2019-09-06) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * do not add drift-loading class after first hover ([#317](https://github.com/imgix/drift/issues/317)) ([750c4cf](https://github.com/imgix/drift/commit/750c4cf)) 40 | 41 | 42 | ### Features 43 | 44 | * **mobile:** add touch delay to allow scroll on mobile ([#315](https://github.com/imgix/drift/issues/315)) ([ceb6101](https://github.com/imgix/drift/commit/ceb6101)) 45 | 46 | ### [1.3.5](https://github.com/imgix/drift/compare/v1.3.4...v1.3.5) (2019-08-02) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * prevent zoom on mobile when handleTouch is set to false ([#293](https://github.com/imgix/drift/issues/293)) ([d1ea511](https://github.com/imgix/drift/commit/d1ea511)) 52 | * remove lingering DOM elements on destroy() ([#291](https://github.com/imgix/drift/issues/291)) ([e217752](https://github.com/imgix/drift/commit/e217752)) 53 | 54 | 55 | ## [1.3.4](https://github.com/imgix/drift/compare/v1.3.3...v1.3.4) (2019-04-06) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * ensure that handleTouch & injectBaseStyles can be disabled ([#221](https://github.com/imgix/drift/issues/221)) ([346f3bf](https://github.com/imgix/drift/commit/346f3bf)), fixes [#220](https://github.com/imgix/drift/issues/220) 61 | 62 | 63 | 64 | 65 | ## [1.3.3](https://github.com/imgix/drift/compare/v1.3.2...v1.3.3) (2018-11-02) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * stop loupe being shown in incorrect position on mouseenter ([#113](https://github.com/imgix/drift/issues/113)) ([710dfd7](https://github.com/imgix/drift/commit/710dfd7)) 71 | 72 | 73 | 74 | 75 | ## [1.3.2](https://github.com/imgix/drift/compare/v1.3.1...v1.3.2) (2018-09-29) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * export Drift's public API ([#83](https://github.com/imgix/drift/issues/83)) ([82052c4](https://github.com/imgix/drift/commit/82052c4)), closes [#81](https://github.com/imgix/drift/issues/81) 81 | 82 | 83 | 84 | 85 | ## [1.3.1](https://github.com/imgix/drift/compare/v1.3.0...v1.3.1) (2018-08-08) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * update closure file test to include more src files ([#78](https://github.com/imgix/drift/issues/78)) ([0a1aeca](https://github.com/imgix/drift/commit/0a1aeca)) 91 | 92 | 93 | 94 | 95 | # [1.3.0](https://github.com/imgix/drift/compare/v1.2.2...v1.3.0) (2018-07-16) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * **zoom-pane:** set min and max values correctly when image is smaller than container ([#69](https://github.com/imgix/drift/issues/69)) ([03f9e26](https://github.com/imgix/drift/commit/03f9e26)) 101 | 102 | 103 | ### Features 104 | 105 | * add lib and es6 bundles, use closure compiler, remove gulp ([#70](https://github.com/imgix/drift/issues/70)) ([e48daa7](https://github.com/imgix/drift/commit/e48daa7)) 106 | -------------------------------------------------------------------------------- /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:/drift.git 68 | # Navigate to the newly cloned directory 69 | cd drift 70 | # Assign the original repo to a remote called "upstream" 71 | git remote add upstream https://github.com/imgix/drift 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 | ### Using ES6 and NPM scripts 111 | 112 | To install all development dependencies, in the project's root directory, run 113 | 114 | ``` 115 | npm install 116 | ``` 117 | 118 | **Please note: the build process assumes Java is installed locally.** 119 | 120 | Once you're configured, building the JavaScript from the command line is easy: 121 | 122 | ``` 123 | npm run build # build Drift from source 124 | npm run build:watch # watch for changes and build automatically 125 | npm run test:watch # watch for changes and test automatically 126 | npm run test:local # run the test against local browsers only (Chrome, Safari, Firefox) 127 | ``` 128 | 129 | Please note: in order to run tests in-browser (with `npm run test:local`), Chrome and Firefox should be installed locally. 130 | -------------------------------------------------------------------------------- /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 | # Drift 2 | 3 | Drift adds easy "zoom on hover" functionality to your site's images, all with lightweight, no-dependency JavaScript. 4 | 5 | [![npm version](https://img.shields.io/npm/v/drift-zoom.svg)](https://www.npmjs.com/package/drift-zoom) 6 | [![Build Status](https://travis-ci.org/imgix/drift.svg?branch=main)](https://travis-ci.org/imgix/drift) 7 | [![npm](https://img.shields.io/npm/dm/drift-zoom.svg)](https://www.npmjs.com/package/drift-zoom) 8 | [![License](https://img.shields.io/github/license/imgix/drift)](https://github.com/imgix/drift/blob/main/LICENSE.md) 9 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 10 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 11 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimgix%2Fdrift.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimgix%2Fdrift?ref=badge_shield) 12 | 13 | --- 14 | 15 | 16 | 17 | - [Installation](#installation) 18 | - [Basic Usage](#basic-usage) 19 | - [Demo](#demo-) 20 | - [Options / Defaults](#options--defaults) 21 | - [API](#api) 22 | * [`Drift#disable`](#drift%23disable) 23 | * [`Drift#enable`](#drift%23enable) 24 | * [`Drift#setZoomImageURL(imageURL)`](#drift%23setzoomimageurlimageurl) 25 | - [Theming](#theming) 26 | - [FAQs/Examples](#faqsexamples) 27 | * [Disabling on Mobile](#disabling-on-mobile) 28 | + [CSS Solution (Recommended)](#css-solution-recommended) 29 | + [JS Solution](#js-solution) 30 | * [Use Drift with Multiple Images on the Same Page](#use-drift-with-multiple-images-on-the-same-page) 31 | - [Browser Support](#browser-support) 32 | - [Contributors ✨](#contributors-) 33 | - [Meta](#meta) 34 | - [License](#license) 35 | 36 | ## Installation 37 | 38 | - **NPM**: `npm install drift-zoom` 39 | - **Bower**: `bower install drift` 40 | - **Manual**: [Download](https://github.com/imgix/drift/archive/main.zip) and use `dist/Drift.min.js` or `dist/Drift.js` 41 | 42 | If you're using the pre-built version of Drift, it will automatically make `window.Drift` available for your use when included on your page. 43 | 44 | If you prefer to use `require` statements and a build tool like Browserify, here are a couple examples to help: 45 | 46 | ```javascript 47 | var Drift = require('drift-zoom'); 48 | 49 | new Drift(…); 50 | ``` 51 | 52 | If your project uses ES6, you can do the following instead: 53 | 54 | ```javascript 55 | import Drift from 'drift-zoom'; 56 | 57 | new Drift(…); 58 | ``` 59 | 60 | ## Basic Usage 61 | 62 | Once you've installed Drift 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=drift). 63 | Here's an example of a basic implementation: 64 | 65 | ```html 66 | 67 | 68 |

This is a simple description of the dog picture.

69 | ``` 70 | 71 | ```javascript 72 | new Drift(document.querySelector("img"), { 73 | paneContainer: document.querySelector("p") 74 | }); 75 | ``` 76 | 77 | ## Demo 💻💻💻 78 | Take a peek at our [Demo Site](https://codepen.io/imgix/pen/WrRmLb). 79 | 80 | ## Options / Defaults 81 | 82 | Here's an example of using Drift with a custom configuration. All of the listed options are displayed with their default value. 83 | 84 | ```javascript 85 | var options = { 86 | // Prefix for generated element class names (e.g. `my-ns` will 87 | // result in classes such as `my-ns-pane`. Default `drift-` 88 | // prefixed classes will always be added as well. 89 | namespace: null, 90 | // Whether the ZoomPane should show whitespace when near the edges. 91 | showWhitespaceAtEdges: false, 92 | // Whether the inline ZoomPane should stay inside 93 | // the bounds of its image. 94 | containInline: false, 95 | // How much to offset the ZoomPane from the 96 | // interaction point when inline. 97 | inlineOffsetX: 0, 98 | inlineOffsetY: 0, 99 | // A DOM element to append the inline ZoomPane to. 100 | inlineContainer: document.body, 101 | // Which trigger attribute to pull the ZoomPane image source from. 102 | sourceAttribute: 'data-zoom', 103 | // How much to magnify the trigger by in the ZoomPane. 104 | // (e.g., `zoomFactor: 3` will result in a 900 px wide ZoomPane image 105 | // if the trigger is displayed at 300 px wide) 106 | zoomFactor: 3, 107 | // A DOM element to append the non-inline ZoomPane to. 108 | // Required if `inlinePane !== true`. 109 | paneContainer: document.body, 110 | // When to switch to an inline ZoomPane. This can be a boolean or 111 | // an integer. If `true`, the ZoomPane will always be inline, 112 | // if `false`, it will switch to inline when `windowWidth <= inlinePane` 113 | inlinePane: 375, 114 | // If `true`, touch events will trigger the zoom, like mouse events. 115 | handleTouch: true, 116 | // If present (and a function), this will be called 117 | // whenever the ZoomPane is shown. 118 | onShow: null, 119 | // If present (and a function), this will be called 120 | // whenever the ZoomPane is hidden. 121 | onHide: null, 122 | // Add base styles to the page. See the "Theming" 123 | // section of README.md for more information. 124 | injectBaseStyles: true, 125 | // An optional number that determines how long to wait before 126 | // showing the ZoomPane because of a `mouseenter` event. 127 | hoverDelay: 0, 128 | // An optional number that determines how long to wait before 129 | // showing the ZoomPane because of a `touchstart` event. 130 | // Setting this to a reasonable amount will allow users to execute 131 | // scroll-gestures events on touch-enabled devices with the image as 132 | // a starting point 133 | touchDelay: 0, 134 | // If true, a bounding box will show the area currently being previewed 135 | // during mouse hover 136 | hoverBoundingBox: false, 137 | // If true, a bounding box will show the area currently being previewed 138 | // during touch events 139 | touchBoundingBox: false, 140 | // A DOM element to append the bounding box to. 141 | boundingBoxContainer: document.body, 142 | // If true, the events related to handleTouch use passive listeners in 143 | // order to improve performance for touch devices. 144 | passive: false, 145 | }; 146 | 147 | new Drift(document.querySelector('img'), options); 148 | ``` 149 | 150 | ## API 151 | 152 | ### `Drift#disable` 153 | 154 | Disable your Drift instance. This will prevent your Drift instance from showing, but will not hide it if it's currently visible. 155 | 156 | ```javascript 157 | var drift = new Drift(document.querySelector("img"), { 158 | paneContainer: document.querySelector("p") 159 | }); 160 | 161 | document.querySelector(".disable-button").addEventListener("click", function() { 162 | drift.disable(); 163 | }); 164 | ``` 165 | 166 | ### `Drift#enable` 167 | 168 | Enable your Drift instance. 169 | 170 | ```javascript 171 | var drift = new Drift(document.querySelector("img"), { 172 | paneContainer: document.querySelector("p") 173 | }); 174 | 175 | document.querySelector(".enable-button").addEventListener("click", function() { 176 | drift.enable(); 177 | }); 178 | ``` 179 | 180 | ### `Drift#setZoomImageURL(imageURL)` 181 | 182 | Change the URL of the zoom image to the passed string. This only has a visible effect while your Drift is currently open. When opening, Drift always pulls the zoom image URL from the specified `sourceAttribute`. If you want to make a "permanent" change that will persist after the user leaves and re-enters your Drift trigger, you update its `sourceAttribute` as well (default `data-zoom`). For more information about this method, please see [issue #42](https://github.com/imgix/drift/issues/42). 183 | 184 | ```javascript 185 | var triggerEl = document.querySelector("img"); 186 | var drift = new Drift(triggerEl, { 187 | paneContainer: document.querySelector("p") 188 | }); 189 | 190 | var frames = ["https://mysite.com/frame1.jpg", "https://mysite.com/frame2.jpg", "https://mysite.com/frame3.jpg"]; 191 | 192 | var currentFrame = 0; 193 | 194 | setInterval(function() { 195 | currentFrame++; 196 | 197 | if (currentFrame > frames.length - 1) { 198 | currentFrame = 0; 199 | } 200 | 201 | drift.setZoomImageURL(frames[currentFrame]); 202 | triggerEl.setAttribute("data-zoom", frames[currentFrame]); 203 | }, 1200); 204 | ``` 205 | 206 | ## Theming 207 | 208 | By default, Drift injects an extremely basic set of styles into the page. You will almost certainly want to extend these basic styles for a prettier, more usable experience that matches your site. There is an included basic theme that may meet your needs, or at least give a good example of how to build out your own custom styles. The `namespace` option can be used as a way to easily apply different themes to specific instances of Drift. 209 | 210 | 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 Drift. Please note that if you disable the included base styles, you will still need to provide an animation for `.drift-window.drift-opening` and `.drift-window.drift-closing` (this can be a "noop" style animation, as seen in the base styles source). 211 | 212 | ## FAQs/Examples 213 | 214 | In this section we answer common questions about Drift. 215 | 216 | ### Disabling on Mobile 217 | 218 | If you would like the touch events not to fire on mobile for one reason or another, these two solutions should work for you. 219 | 220 | #### CSS Solution (Recommended) 221 | 222 | This solution places a transparent element over the image on mobiles to block touch events. Replace `1024px` in the media query with your mobile breakpoint. 223 | 224 | ```css 225 | .zoom-image { 226 | position: relative; 227 | } 228 | 229 | .zoom-image::before { 230 | content: ""; 231 | position: absolute; 232 | top: 0; 233 | left: 0; 234 | right: 0; 235 | bottom: 0; 236 | background: transparent; 237 | } 238 | 239 | @media only screen and (min-width: 1024px) { 240 | .zoom-image::before { 241 | display: none; 242 | } 243 | } 244 | ``` 245 | 246 | #### JS Solution 247 | 248 | This solution creates and destroys the Drift instance when the browser size changes. It depends on the library [responsive.js](https://www.responsivejs.com/) but can easily be altered to use vanilla JS. 249 | 250 | ```js 251 | const driftOptions = { 252 | paneContainer: paneContainer, 253 | inlinePane: false, 254 | handleTouch: false 255 | }; 256 | 257 | const handleChange = () => { 258 | requestAnimationFrame(() => { 259 | if (Responsive.is('mobile') && !!window.productZoom) { 260 | window.productZoom.destroy(); 261 | } else { 262 | window.productZoom = new Drift(img, driftOptions); 263 | } 264 | }) 265 | } 266 | 267 | window.addEventListener('resize', handleChange); 268 | window.addEventListener('load', handleChange); 269 | 270 | ``` 271 | 272 | ### Use Drift with Multiple Images on the Same Page 273 | 274 | This code will iterate over all elements on your page with the class `drift-img`, and will instantiate Drift for each element. You can update the query selector and pane as you see fit. 275 | 276 | ```js 277 | const driftImgs = document.querySelectorAll('.drift-img'); 278 | const pane = document.querySelector('.drift-pane'); 279 | driftImgs.map(img => { 280 | new Drift(img, { 281 | paneContainer: pane, 282 | inlinePane: false 283 | }); 284 | }); 285 | ``` 286 | 287 | ## Browser Support 288 | 289 | 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. 290 | 291 | ## Contributors ✨ 292 | 293 | 294 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 |

Paul Straw

📖 💻 🚧

sherwinski

💻 📖 🚧

Frederick Fogerty

💻 📖 🚧

Jason Eberle

💻 📖 🚧

Luis H. Ball Jr.

🚧
308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 316 | 317 | ## Meta 318 | 319 | Drift was made by [imgix](https://imgix.com?utm_medium=referral&utm_source=github&utm_campaign=drift). It's licensed under the BSD 2-Clause license (see the [license file](https://github.com/imgix/drift/blob/main/LICENSE.md) for more info). Any contribution is absolutely welcome, but please review the [contribution guidelines](https://github.com/imgix/drift/blob/main/CONTRIBUTING.md) before getting started. 320 | 321 | ## License 322 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimgix%2Fdrift.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimgix%2Fdrift?ref=badge_large) 323 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drift", 3 | "version": "1.4.4", 4 | "description": "Easily add \"zoom on hover\" functionality to your site's images. Lightweight, no-dependency JavaScript.", 5 | "main": [ 6 | "dist/Drift.min.js", 7 | "dist/drift-basic.css" 8 | ], 9 | "authors": [ 10 | "imgix" 11 | ], 12 | "license": "BSD-2", 13 | "keywords": [ 14 | "javascript", 15 | "zoom", 16 | "hover", 17 | "image", 18 | "images" 19 | ], 20 | "homepage": "https://github.com/imgix/drift", 21 | "moduleType": [], 22 | "ignore": [ 23 | "**/.*", 24 | ".github", 25 | "node_modules", 26 | "src", 27 | "test", 28 | "CONTRIBUTING.md", 29 | "index.html", 30 | "config", 31 | "package.json", 32 | "postcss.config.js", 33 | "webpack.config.js" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /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 | { 9 | test: /(\.jsx|\.js)$/, 10 | loader: "babel-loader", 11 | exclude: /(node_modules|bower_components)/ 12 | } 13 | ] 14 | } 15 | }; 16 | 17 | const baseConfig = { 18 | frameworks: ["jasmine"], 19 | files: ["../../tests.webpack.js"], 20 | preprocessors: { 21 | "../../tests.webpack.js": "webpack" 22 | }, 23 | webpack: webpackConfigAugmented, 24 | webpackMiddleware: { 25 | stats: "errors-only" 26 | }, 27 | concurrency: 5, 28 | captureTimeout: 90000, 29 | browserConnectTimeout: 3000, 30 | browserNoActivityTimeout: 15000 31 | }; 32 | 33 | const stringConfig = JSON.stringify(baseConfig); 34 | 35 | /** 36 | * Local testing - Chrome and FF, headlessly 37 | */ 38 | 39 | const localConfig = karmaConfig => { 40 | const config = { 41 | ...baseConfig, 42 | browsers: ["ChromeHeadless", "FirefoxHeadless"], 43 | customLaunchers: { 44 | FirefoxHeadless: { 45 | base: "Firefox", 46 | flags: ["-headless"] 47 | } 48 | } 49 | }; 50 | 51 | karmaConfig.set(config); 52 | }; 53 | 54 | /** 55 | * CI testing - Chrome, Firefox 56 | */ 57 | 58 | const ciConfig = karmaConfig => { 59 | const config = { 60 | ...baseConfig, 61 | browsers: ["ChromeTravis", "FirefoxHeadless"], 62 | customLaunchers: { 63 | ChromeTravis: { 64 | base: "ChromeHeadless", 65 | flags: ["--no-sandbox"] 66 | }, 67 | FirefoxHeadless: { 68 | base: "Firefox", 69 | flags: ["-headless"] 70 | } 71 | }, 72 | client: { 73 | mocha: { 74 | timeout: 20000 // 20 seconds 75 | } 76 | } 77 | }; 78 | 79 | karmaConfig.set(config); 80 | }; 81 | 82 | /** 83 | * SauceLabs configuration - not supported 84 | */ 85 | 86 | var fullConfig = JSON.parse(stringConfig); 87 | fullConfig.reporters = ["progress", "saucelabs"]; 88 | fullConfig.saucelabs = { testName: "Drift Tests" }; 89 | fullConfig.browsers = [ 90 | "sl_chrome", 91 | "sl_safari_9", 92 | "sl_safari_8", 93 | "sl_firefox_42", 94 | "sl_firefox_41", 95 | "sl_ie_11", 96 | "sl_ie_10", 97 | "sl_ios_9", 98 | "sl_ios_8", 99 | "sl_android_5", 100 | "sl_android_4" 101 | ]; 102 | fullConfig.customLaunchers = { 103 | sl_chrome: { base: "SauceLabs", browserName: "Chrome" }, 104 | sl_safari_9: { base: "SauceLabs", browserName: "Safari", version: 9 }, 105 | sl_safari_8: { base: "SauceLabs", browserName: "Safari", version: 8 }, 106 | sl_firefox_42: { base: "SauceLabs", browserName: "Firefox", version: 42 }, 107 | sl_firefox_41: { base: "SauceLabs", browserName: "Firefox", version: 41 }, 108 | sl_ie_11: { 109 | base: "SauceLabs", 110 | browserName: "Internet Explorer", 111 | version: "11" 112 | }, 113 | sl_ie_10: { 114 | base: "SauceLabs", 115 | browserName: "Internet Explorer", 116 | version: "10" 117 | }, 118 | sl_ios_9: { base: "SauceLabs", browserName: "iPhone", version: "9.2" }, 119 | sl_ios_8: { base: "SauceLabs", browserName: "iPhone", version: "8.4" }, 120 | sl_android_5: { base: "SauceLabs", browserName: "Android", version: "5.1" }, 121 | sl_android_4: { base: "SauceLabs", browserName: "Android", version: "4.4" } 122 | }; 123 | 124 | exports.local = localConfig; 125 | exports.ci = ciConfig; 126 | exports.full = karmaConfig => karmaConfig.set(fullConfig); 127 | -------------------------------------------------------------------------------- /dist/Drift.js: -------------------------------------------------------------------------------- 1 | (function(__wpcc){__wpcc.d=__wpcc.d||{};__wpcc.d.scope={};__wpcc.d.createTemplateTagFirstArg=function(a){return a.raw=a};__wpcc.d.createTemplateTagFirstArgWithRaw=function(a,b){a.raw=b;return a};__wpcc.d.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;bc.left+c.width+e&&(a=c.left+c.width-this.el.clientWidth+e);bc.top+c.height+f&&(b=c.top+c.height-this.el.clientHeight+f);this.el.style.left=a+"px";this.el.style.top=b+"px"};z.prototype._preventDefault=function(a){a.preventDefault()};z.prototype._preventDefaultAllowTouchScroll=function(a){this.settings.touchDelay&&this._isTouchEvent(a)&&!this.isShowing||a.preventDefault()};z.prototype._isTouchEvent=function(a){return!!a.touches}; 16 | z.prototype._bindEvents=function(){this.settings.el.addEventListener("mouseenter",this._handleEntry);this.settings.el.addEventListener("mouseleave",this._hide);this.settings.el.addEventListener("mousemove",this._handleMovement);var a={passive:this.settings.passive};this.settings.handleTouch?(this.settings.el.addEventListener("touchstart",this._handleEntry,a),this.settings.el.addEventListener("touchend",this._hide),this.settings.el.addEventListener("touchmove",this._handleMovement,a)):(this.settings.el.addEventListener("touchstart", 17 | this._preventDefault,a),this.settings.el.addEventListener("touchend",this._preventDefault),this.settings.el.addEventListener("touchmove",this._preventDefault,a))}; 18 | z.prototype._unbindEvents=function(){this.settings.el.removeEventListener("mouseenter",this._handleEntry);this.settings.el.removeEventListener("mouseleave",this._hide);this.settings.el.removeEventListener("mousemove",this._handleMovement);this.settings.handleTouch?(this.settings.el.removeEventListener("touchstart",this._handleEntry),this.settings.el.removeEventListener("touchend",this._hide),this.settings.el.removeEventListener("touchmove",this._handleMovement)):(this.settings.el.removeEventListener("touchstart", 19 | this._preventDefault),this.settings.el.removeEventListener("touchend",this._preventDefault),this.settings.el.removeEventListener("touchmove",this._preventDefault))};z.prototype._handleEntry=function(a){this._preventDefaultAllowTouchScroll(a);this._lastMovement=a;"mouseenter"==a.type&&this.settings.hoverDelay?this.entryTimeout=setTimeout(this._show,this.settings.hoverDelay):this.settings.touchDelay?this.entryTimeout=setTimeout(this._show,this.settings.touchDelay):this._show()}; 20 | z.prototype._show=function(){if(this.enabled){var a=this.settings.onShow;a&&"function"===typeof a&&a();this.settings.zoomPane.show(this.settings.el.getAttribute(this.settings.sourceAttribute),this.settings.el.clientWidth,this.settings.el.clientHeight);this._lastMovement&&((a=this._lastMovement.touches)&&this.settings.touchBoundingBox||!a&&this.settings.hoverBoundingBox)&&this.boundingBox.show(this.settings.zoomPane.el.clientWidth,this.settings.zoomPane.el.clientHeight);this._handleMovement()}}; 21 | z.prototype._hide=function(a){a&&this._preventDefaultAllowTouchScroll(a);this._lastMovement=null;this.entryTimeout&&clearTimeout(this.entryTimeout);this.boundingBox&&this.boundingBox.hide();(a=this.settings.onHide)&&"function"===typeof a&&a();this.settings.zoomPane.hide()}; 22 | z.prototype._handleMovement=function(a){if(a)this._preventDefaultAllowTouchScroll(a),this._lastMovement=a;else if(this._lastMovement)a=this._lastMovement;else return;if(a.touches){a=a.touches[0];var b=a.clientX;var c=a.clientY}else b=a.clientX,c=a.clientY;a=this.settings.el.getBoundingClientRect();b=(b-a.left)/this.settings.el.clientWidth;c=(c-a.top)/this.settings.el.clientHeight;this.boundingBox&&this.boundingBox.setPosition(b,c,a);this.settings.zoomPane.setPosition(b,c,a)}; 23 | __wpcc.d.global.Object.defineProperties(z.prototype,{isShowing:{configurable:!0,enumerable:!0,get:function(){return this.settings.zoomPane.isShowing}}});A.prototype._buildClasses=function(a){var b=["drift-"+a],c=this.settings.namespace;c&&b.push(c+"-"+a);return b};A.prototype._buildElement=function(){this.el=document.createElement("div");t(this.el,this._buildClasses("zoom-pane"));var a=document.createElement("div");t(a,this._buildClasses("zoom-pane-loader"));this.el.appendChild(a);this.imgEl=document.createElement("img");this.el.appendChild(this.imgEl)};A.prototype._setImageURL=function(a){this.imgEl.setAttribute("src",a)}; 24 | A.prototype._setImageSize=function(a,b){this.imgEl.style.width=a*this.settings.zoomFactor+"px";this.imgEl.style.height=b*this.settings.zoomFactor+"px"}; 25 | A.prototype.setPosition=function(a,b,c){var e=this.imgEl.offsetWidth,f=this.imgEl.offsetHeight,q=this.el.offsetWidth,k=this.el.offsetHeight,v=q/2-e*a,w=k/2-f*b,l=q-e,m=k-f,n=0c.left+c.width+p&& 26 | (a=c.left+c.width-q+p),bc.top+c.height+n&&(b=c.top+c.height-k+n)),this.el.style.left=a+"px",this.el.style.top=b+"px");this.settings.showWhitespaceAtEdges||(v>f?v=f:ve?w=e:wc.left+c.width+e&&(a=c.left+c.width-this.el.clientWidth+e);bc.top+c.height+f&&(b=c.top+c.height-this.el.clientHeight+f);this.el.style.left=a+"px";this.el.style.top=b+"px"};z.prototype._preventDefault=function(a){a.preventDefault()};z.prototype._preventDefaultAllowTouchScroll=function(a){this.settings.touchDelay&&this._isTouchEvent(a)&&!this.isShowing||a.preventDefault()};z.prototype._isTouchEvent=function(a){return!!a.touches}; 16 | z.prototype._bindEvents=function(){this.settings.el.addEventListener("mouseenter",this._handleEntry);this.settings.el.addEventListener("mouseleave",this._hide);this.settings.el.addEventListener("mousemove",this._handleMovement);var a={passive:this.settings.passive};this.settings.handleTouch?(this.settings.el.addEventListener("touchstart",this._handleEntry,a),this.settings.el.addEventListener("touchend",this._hide),this.settings.el.addEventListener("touchmove",this._handleMovement,a)):(this.settings.el.addEventListener("touchstart", 17 | this._preventDefault,a),this.settings.el.addEventListener("touchend",this._preventDefault),this.settings.el.addEventListener("touchmove",this._preventDefault,a))}; 18 | z.prototype._unbindEvents=function(){this.settings.el.removeEventListener("mouseenter",this._handleEntry);this.settings.el.removeEventListener("mouseleave",this._hide);this.settings.el.removeEventListener("mousemove",this._handleMovement);this.settings.handleTouch?(this.settings.el.removeEventListener("touchstart",this._handleEntry),this.settings.el.removeEventListener("touchend",this._hide),this.settings.el.removeEventListener("touchmove",this._handleMovement)):(this.settings.el.removeEventListener("touchstart", 19 | this._preventDefault),this.settings.el.removeEventListener("touchend",this._preventDefault),this.settings.el.removeEventListener("touchmove",this._preventDefault))};z.prototype._handleEntry=function(a){this._preventDefaultAllowTouchScroll(a);this._lastMovement=a;"mouseenter"==a.type&&this.settings.hoverDelay?this.entryTimeout=setTimeout(this._show,this.settings.hoverDelay):this.settings.touchDelay?this.entryTimeout=setTimeout(this._show,this.settings.touchDelay):this._show()}; 20 | z.prototype._show=function(){if(this.enabled){var a=this.settings.onShow;a&&"function"===typeof a&&a();this.settings.zoomPane.show(this.settings.el.getAttribute(this.settings.sourceAttribute),this.settings.el.clientWidth,this.settings.el.clientHeight);this._lastMovement&&((a=this._lastMovement.touches)&&this.settings.touchBoundingBox||!a&&this.settings.hoverBoundingBox)&&this.boundingBox.show(this.settings.zoomPane.el.clientWidth,this.settings.zoomPane.el.clientHeight);this._handleMovement()}}; 21 | z.prototype._hide=function(a){a&&this._preventDefaultAllowTouchScroll(a);this._lastMovement=null;this.entryTimeout&&clearTimeout(this.entryTimeout);this.boundingBox&&this.boundingBox.hide();(a=this.settings.onHide)&&"function"===typeof a&&a();this.settings.zoomPane.hide()}; 22 | z.prototype._handleMovement=function(a){if(a)this._preventDefaultAllowTouchScroll(a),this._lastMovement=a;else if(this._lastMovement)a=this._lastMovement;else return;if(a.touches){a=a.touches[0];var b=a.clientX;var c=a.clientY}else b=a.clientX,c=a.clientY;a=this.settings.el.getBoundingClientRect();b=(b-a.left)/this.settings.el.clientWidth;c=(c-a.top)/this.settings.el.clientHeight;this.boundingBox&&this.boundingBox.setPosition(b,c,a);this.settings.zoomPane.setPosition(b,c,a)}; 23 | __wpcc.d.global.Object.defineProperties(z.prototype,{isShowing:{configurable:!0,enumerable:!0,get:function(){return this.settings.zoomPane.isShowing}}});A.prototype._buildClasses=function(a){var b=["drift-"+a],c=this.settings.namespace;c&&b.push(c+"-"+a);return b};A.prototype._buildElement=function(){this.el=document.createElement("div");t(this.el,this._buildClasses("zoom-pane"));var a=document.createElement("div");t(a,this._buildClasses("zoom-pane-loader"));this.el.appendChild(a);this.imgEl=document.createElement("img");this.el.appendChild(this.imgEl)};A.prototype._setImageURL=function(a){this.imgEl.setAttribute("src",a)}; 24 | A.prototype._setImageSize=function(a,b){this.imgEl.style.width=a*this.settings.zoomFactor+"px";this.imgEl.style.height=b*this.settings.zoomFactor+"px"}; 25 | A.prototype.setPosition=function(a,b,c){var e=this.imgEl.offsetWidth,f=this.imgEl.offsetHeight,q=this.el.offsetWidth,k=this.el.offsetHeight,v=q/2-e*a,w=k/2-f*b,l=q-e,m=k-f,n=0c.left+c.width+p&& 26 | (a=c.left+c.width-q+p),bc.top+c.height+n&&(b=c.top+c.height-k+n)),this.el.style.left=a+"px",this.el.style.top=b+"px");this.settings.showWhitespaceAtEdges||(v>f?v=f:ve?w=e:w triggerRect.left + triggerRect.width + pageXOffset) {\n inlineLeft = triggerRect.left + triggerRect.width - this.el.clientWidth + pageXOffset;\n }\n\n if (inlineTop < triggerRect.top + pageYOffset) {\n inlineTop = triggerRect.top + pageYOffset;\n } else if (inlineTop + this.el.clientHeight > triggerRect.top + triggerRect.height + pageYOffset) {\n inlineTop = triggerRect.top + triggerRect.height - this.el.clientHeight + pageYOffset;\n }\n\n this.el.style.left = `${inlineLeft}px`;\n this.el.style.top = `${inlineTop}px`;\n }\n}\n","import throwIfMissing from \"1\"\nimport BoundingBox from \"5\"\n\nexport default class Trigger {\n constructor(options = {}) {\n this._show = this._show.bind(this);\n this._hide = this._hide.bind(this);\n this._handleEntry = this._handleEntry.bind(this);\n this._handleMovement = this._handleMovement.bind(this);\n\n const {\n el = throwIfMissing(),\n zoomPane = throwIfMissing(),\n sourceAttribute = throwIfMissing(),\n handleTouch = throwIfMissing(),\n onShow = null,\n onHide = null,\n hoverDelay = 0,\n touchDelay = 0,\n hoverBoundingBox = throwIfMissing(),\n touchBoundingBox = throwIfMissing(),\n namespace = null,\n zoomFactor = throwIfMissing(),\n boundingBoxContainer = throwIfMissing(),\n passive = false,\n } = options;\n\n this.settings = {\n el,\n zoomPane,\n sourceAttribute,\n handleTouch,\n onShow,\n onHide,\n hoverDelay,\n touchDelay,\n hoverBoundingBox,\n touchBoundingBox,\n namespace,\n zoomFactor,\n boundingBoxContainer,\n passive,\n };\n\n if (this.settings.hoverBoundingBox || this.settings.touchBoundingBox) {\n this.boundingBox = new BoundingBox({\n namespace: this.settings.namespace,\n zoomFactor: this.settings.zoomFactor,\n containerEl: this.settings.boundingBoxContainer,\n });\n }\n\n this.enabled = true;\n\n this._bindEvents();\n }\n\n get isShowing() {\n return this.settings.zoomPane.isShowing;\n }\n\n _preventDefault(event) {\n event.preventDefault();\n }\n\n _preventDefaultAllowTouchScroll(event) {\n if (!this.settings.touchDelay || !this._isTouchEvent(event) || this.isShowing) {\n event.preventDefault();\n }\n }\n\n _isTouchEvent(event) {\n return !!event.touches;\n }\n\n _bindEvents() {\n this.settings.el.addEventListener(\"mouseenter\", this._handleEntry);\n this.settings.el.addEventListener(\"mouseleave\", this._hide);\n this.settings.el.addEventListener(\"mousemove\", this._handleMovement);\n\n const isPassive = { passive: this.settings.passive };\n if (this.settings.handleTouch) {\n this.settings.el.addEventListener(\"touchstart\", this._handleEntry, isPassive);\n this.settings.el.addEventListener(\"touchend\", this._hide);\n this.settings.el.addEventListener(\"touchmove\", this._handleMovement, isPassive);\n } else {\n this.settings.el.addEventListener(\"touchstart\", this._preventDefault, isPassive);\n this.settings.el.addEventListener(\"touchend\", this._preventDefault);\n this.settings.el.addEventListener(\"touchmove\", this._preventDefault, isPassive);\n }\n }\n\n _unbindEvents() {\n this.settings.el.removeEventListener(\"mouseenter\", this._handleEntry);\n this.settings.el.removeEventListener(\"mouseleave\", this._hide);\n this.settings.el.removeEventListener(\"mousemove\", this._handleMovement);\n\n if (this.settings.handleTouch) {\n this.settings.el.removeEventListener(\"touchstart\", this._handleEntry);\n this.settings.el.removeEventListener(\"touchend\", this._hide);\n this.settings.el.removeEventListener(\"touchmove\", this._handleMovement);\n } else {\n this.settings.el.removeEventListener(\"touchstart\", this._preventDefault);\n this.settings.el.removeEventListener(\"touchend\", this._preventDefault);\n this.settings.el.removeEventListener(\"touchmove\", this._preventDefault);\n }\n }\n\n _handleEntry(e) {\n this._preventDefaultAllowTouchScroll(e);\n this._lastMovement = e;\n\n if (e.type == \"mouseenter\" && this.settings.hoverDelay) {\n this.entryTimeout = setTimeout(this._show, this.settings.hoverDelay);\n } else if (this.settings.touchDelay) {\n this.entryTimeout = setTimeout(this._show, this.settings.touchDelay);\n } else {\n this._show();\n }\n }\n\n _show() {\n if (!this.enabled) {\n return;\n }\n\n const onShow = this.settings.onShow;\n if (onShow && typeof onShow === \"function\") {\n onShow();\n }\n\n this.settings.zoomPane.show(\n this.settings.el.getAttribute(this.settings.sourceAttribute),\n this.settings.el.clientWidth,\n this.settings.el.clientHeight\n );\n\n if (this._lastMovement) {\n const touchActivated = this._lastMovement.touches;\n if ((touchActivated && this.settings.touchBoundingBox) || (!touchActivated && this.settings.hoverBoundingBox)) {\n this.boundingBox.show(this.settings.zoomPane.el.clientWidth, this.settings.zoomPane.el.clientHeight);\n }\n }\n\n this._handleMovement();\n }\n\n _hide(e) {\n if (e) {\n this._preventDefaultAllowTouchScroll(e);\n }\n\n this._lastMovement = null;\n\n if (this.entryTimeout) {\n clearTimeout(this.entryTimeout);\n }\n\n if (this.boundingBox) {\n this.boundingBox.hide();\n }\n\n const onHide = this.settings.onHide;\n if (onHide && typeof onHide === \"function\") {\n onHide();\n }\n\n this.settings.zoomPane.hide();\n }\n\n _handleMovement(e) {\n if (e) {\n this._preventDefaultAllowTouchScroll(e);\n this._lastMovement = e;\n } else if (this._lastMovement) {\n e = this._lastMovement;\n } else {\n return;\n }\n\n let movementX;\n let movementY;\n\n if (e.touches) {\n const firstTouch = e.touches[0];\n movementX = firstTouch.clientX;\n movementY = firstTouch.clientY;\n } else {\n movementX = e.clientX;\n movementY = e.clientY;\n }\n\n const el = this.settings.el;\n const rect = el.getBoundingClientRect();\n const offsetX = movementX - rect.left;\n const offsetY = movementY - rect.top;\n\n const percentageOffsetX = offsetX / this.settings.el.clientWidth;\n const percentageOffsetY = offsetY / this.settings.el.clientHeight;\n\n if (this.boundingBox) {\n this.boundingBox.setPosition(percentageOffsetX, percentageOffsetY, rect);\n }\n\n this.settings.zoomPane.setPosition(percentageOffsetX, percentageOffsetY, rect);\n }\n}\n","import throwIfMissing from \"1\"\nimport { addClasses, removeClasses } from \"0\"\n\nexport default class ZoomPane {\n constructor(options = {}) {\n // All officially-supported browsers have this, but it's easy to\n // account for, just in case.\n this.HAS_ANIMATION = false;\n if (typeof document !== \"undefined\") {\n const divStyle = document.createElement(\"div\").style;\n this.HAS_ANIMATION = \"animation\" in divStyle || \"webkitAnimation\" in divStyle;\n }\n\n this._completeShow = this._completeShow.bind(this);\n this._completeHide = this._completeHide.bind(this);\n this._handleLoad = this._handleLoad.bind(this);\n\n this.isShowing = false;\n\n const {\n container = null,\n zoomFactor = throwIfMissing(),\n inline = throwIfMissing(),\n namespace = null,\n showWhitespaceAtEdges = throwIfMissing(),\n containInline = throwIfMissing(),\n inlineOffsetX = 0,\n inlineOffsetY = 0,\n inlineContainer = document.body,\n } = options;\n\n this.settings = {\n container,\n zoomFactor,\n inline,\n namespace,\n showWhitespaceAtEdges,\n containInline,\n inlineOffsetX,\n inlineOffsetY,\n inlineContainer,\n };\n\n this.openClasses = this._buildClasses(\"open\");\n this.openingClasses = this._buildClasses(\"opening\");\n this.closingClasses = this._buildClasses(\"closing\");\n this.inlineClasses = this._buildClasses(\"inline\");\n this.loadingClasses = this._buildClasses(\"loading\");\n\n this._buildElement();\n }\n\n _buildClasses(suffix) {\n const classes = [`drift-${suffix}`];\n\n const ns = this.settings.namespace;\n if (ns) {\n classes.push(`${ns}-${suffix}`);\n }\n\n return classes;\n }\n\n _buildElement() {\n this.el = document.createElement(\"div\");\n addClasses(this.el, this._buildClasses(\"zoom-pane\"));\n\n const loaderEl = document.createElement(\"div\");\n addClasses(loaderEl, this._buildClasses(\"zoom-pane-loader\"));\n this.el.appendChild(loaderEl);\n\n this.imgEl = document.createElement(\"img\");\n this.el.appendChild(this.imgEl);\n }\n\n _setImageURL(imageURL) {\n this.imgEl.setAttribute(\"src\", imageURL);\n }\n\n _setImageSize(triggerWidth, triggerHeight) {\n this.imgEl.style.width = `${triggerWidth * this.settings.zoomFactor}px`;\n this.imgEl.style.height = `${triggerHeight * this.settings.zoomFactor}px`;\n }\n\n // `percentageOffsetX` and `percentageOffsetY` must be percentages\n // expressed as floats between `0' and `1`.\n setPosition(percentageOffsetX, percentageOffsetY, triggerRect) {\n const imgElWidth = this.imgEl.offsetWidth;\n const imgElHeight = this.imgEl.offsetHeight;\n const elWidth = this.el.offsetWidth;\n const elHeight = this.el.offsetHeight;\n\n const centreOfContainerX = elWidth / 2;\n const centreOfContainerY = elHeight / 2;\n\n const targetImgXToBeCentre = imgElWidth * percentageOffsetX;\n const targetImgYToBeCentre = imgElHeight * percentageOffsetY;\n\n let left = centreOfContainerX - targetImgXToBeCentre;\n let top = centreOfContainerY - targetImgYToBeCentre;\n\n const differenceBetweenContainerWidthAndImgWidth = elWidth - imgElWidth;\n const differenceBetweenContainerHeightAndImgHeight = elHeight - imgElHeight;\n const isContainerLargerThanImgX = differenceBetweenContainerWidthAndImgWidth > 0;\n const isContainerLargerThanImgY = differenceBetweenContainerHeightAndImgHeight > 0;\n\n const minLeft = isContainerLargerThanImgX ? differenceBetweenContainerWidthAndImgWidth / 2 : 0;\n const minTop = isContainerLargerThanImgY ? differenceBetweenContainerHeightAndImgHeight / 2 : 0;\n\n const maxLeft = isContainerLargerThanImgX\n ? differenceBetweenContainerWidthAndImgWidth / 2\n : differenceBetweenContainerWidthAndImgWidth;\n const maxTop = isContainerLargerThanImgY\n ? differenceBetweenContainerHeightAndImgHeight / 2\n : differenceBetweenContainerHeightAndImgHeight;\n\n if (this.el.parentElement === this.settings.inlineContainer) {\n // This may be needed in the future to deal with browser event\n // inconsistencies, but it's difficult to tell for sure.\n // let scrollX = isTouch ? 0 : window.scrollX;\n // let scrollY = isTouch ? 0 : window.scrollY;\n const scrollX = window.pageXOffset;\n const scrollY = window.pageYOffset;\n\n let inlineLeft =\n triggerRect.left + percentageOffsetX * triggerRect.width - elWidth / 2 + this.settings.inlineOffsetX + scrollX;\n let inlineTop =\n triggerRect.top + percentageOffsetY * triggerRect.height - elHeight / 2 + this.settings.inlineOffsetY + scrollY;\n\n if (this.settings.containInline) {\n if (inlineLeft < triggerRect.left + scrollX) {\n inlineLeft = triggerRect.left + scrollX;\n } else if (inlineLeft + elWidth > triggerRect.left + triggerRect.width + scrollX) {\n inlineLeft = triggerRect.left + triggerRect.width - elWidth + scrollX;\n }\n\n if (inlineTop < triggerRect.top + scrollY) {\n inlineTop = triggerRect.top + scrollY;\n } else if (inlineTop + elHeight > triggerRect.top + triggerRect.height + scrollY) {\n inlineTop = triggerRect.top + triggerRect.height - elHeight + scrollY;\n }\n }\n\n this.el.style.left = `${inlineLeft}px`;\n this.el.style.top = `${inlineTop}px`;\n }\n\n if (!this.settings.showWhitespaceAtEdges) {\n if (left > minLeft) {\n left = minLeft;\n } else if (left < maxLeft) {\n left = maxLeft;\n }\n\n if (top > minTop) {\n top = minTop;\n } else if (top < maxTop) {\n top = maxTop;\n }\n }\n\n this.imgEl.style.transform = `translate(${left}px, ${top}px)`;\n this.imgEl.style.webkitTransform = `translate(${left}px, ${top}px)`;\n }\n\n get _isInline() {\n const inline = this.settings.inline;\n\n return inline === true || (typeof inline === \"number\" && window.innerWidth <= inline);\n }\n\n _removeListenersAndResetClasses() {\n this.el.removeEventListener(\"animationend\", this._completeShow);\n this.el.removeEventListener(\"animationend\", this._completeHide);\n this.el.removeEventListener(\"webkitAnimationEnd\", this._completeShow);\n this.el.removeEventListener(\"webkitAnimationEnd\", this._completeHide);\n removeClasses(this.el, this.openClasses);\n removeClasses(this.el, this.closingClasses);\n }\n\n show(imageURL, triggerWidth, triggerHeight) {\n this._removeListenersAndResetClasses();\n this.isShowing = true;\n\n addClasses(this.el, this.openClasses);\n\n if (this.imgEl.getAttribute(\"src\") != imageURL) {\n addClasses(this.el, this.loadingClasses);\n this.imgEl.addEventListener(\"load\", this._handleLoad);\n this._setImageURL(imageURL);\n }\n\n this._setImageSize(triggerWidth, triggerHeight);\n\n if (this._isInline) {\n this._showInline();\n } else {\n this._showInContainer();\n }\n\n if (this.HAS_ANIMATION) {\n this.el.addEventListener(\"animationend\", this._completeShow);\n this.el.addEventListener(\"webkitAnimationEnd\", this._completeShow);\n addClasses(this.el, this.openingClasses);\n }\n }\n\n _showInline() {\n this.settings.inlineContainer.appendChild(this.el);\n addClasses(this.el, this.inlineClasses);\n }\n\n _showInContainer() {\n this.settings.container.appendChild(this.el);\n }\n\n hide() {\n this._removeListenersAndResetClasses();\n this.isShowing = false;\n\n if (this.HAS_ANIMATION) {\n this.el.addEventListener(\"animationend\", this._completeHide);\n this.el.addEventListener(\"webkitAnimationEnd\", this._completeHide);\n addClasses(this.el, this.closingClasses);\n } else {\n removeClasses(this.el, this.openClasses);\n removeClasses(this.el, this.inlineClasses);\n }\n }\n\n _completeShow() {\n this.el.removeEventListener(\"animationend\", this._completeShow);\n this.el.removeEventListener(\"webkitAnimationEnd\", this._completeShow);\n\n removeClasses(this.el, this.openingClasses);\n }\n\n _completeHide() {\n this.el.removeEventListener(\"animationend\", this._completeHide);\n this.el.removeEventListener(\"webkitAnimationEnd\", this._completeHide);\n\n removeClasses(this.el, this.openClasses);\n removeClasses(this.el, this.closingClasses);\n removeClasses(this.el, this.inlineClasses);\n\n this.el.style.left = \"\";\n this.el.style.top = \"\";\n\n // The window could have been resized above or below `inline`\n // limits since the ZoomPane was shown. Because of this, we\n // can't rely on `this._isInline` here.\n if (this.el.parentElement === this.settings.container) {\n this.settings.container.removeChild(this.el);\n } else if (this.el.parentElement === this.settings.inlineContainer) {\n this.settings.inlineContainer.removeChild(this.el);\n }\n }\n\n _handleLoad() {\n this.imgEl.removeEventListener(\"load\", this._handleLoad);\n removeClasses(this.el, this.loadingClasses);\n }\n}\n","import { isDOMElement } from \"0\"\nimport injectBaseStylesheet from \"3\"\n\nimport Trigger from \"4\"\nimport ZoomPane from \"6\"\n\nexport default class Drift {\n constructor(triggerEl, options = {}) {\n this.VERSION = \"1.5.0\";\n this.triggerEl = triggerEl;\n\n this.destroy = this.destroy.bind(this);\n\n if (!isDOMElement(this.triggerEl)) {\n throw new TypeError(\"`new Drift` requires a DOM element as its first argument.\");\n }\n\n // Prefix for generated element class names (e.g. `my-ns` will\n // result in classes such as `my-ns-pane`. Default `drift-`\n // prefixed classes will always be added as well.\n const namespace = options[\"namespace\"] || null;\n // Whether the ZoomPane should show whitespace when near the edges.\n const showWhitespaceAtEdges = options[\"showWhitespaceAtEdges\"] || false;\n // Whether the inline ZoomPane should stay inside\n // the bounds of its image.\n const containInline = options[\"containInline\"] || false;\n // How much to offset the ZoomPane from the\n // interaction point when inline.\n const inlineOffsetX = options[\"inlineOffsetX\"] || 0;\n const inlineOffsetY = options[\"inlineOffsetY\"] || 0;\n // A DOM element to append the inline ZoomPane to\n const inlineContainer = options[\"inlineContainer\"] || document.body;\n // Which trigger attribute to pull the ZoomPane image source from.\n const sourceAttribute = options[\"sourceAttribute\"] || \"data-zoom\";\n // How much to magnify the trigger by in the ZoomPane.\n // (e.g., `zoomFactor: 3` will result in a 900 px wide ZoomPane imag\n // if the trigger is displayed at 300 px wide)\n const zoomFactor = options[\"zoomFactor\"] || 3;\n // A DOM element to append the non-inline ZoomPane to.\n // Required if `inlinePane !== true`.\n const paneContainer = options[\"paneContainer\"] === undefined ? document.body : options[\"paneContainer\"];\n // When to switch to an inline ZoomPane. This can be a boolean or\n // an integer. If `true`, the ZoomPane will always be inline,\n // if `false`, it will switch to inline when `windowWidth <= inlinePane`\n const inlinePane = options[\"inlinePane\"] || 375;\n // If `true`, touch events will trigger the zoom, like mouse events.\n const handleTouch = \"handleTouch\" in options ? !!options[\"handleTouch\"] : true;\n // If present (and a function), this will be called\n // whenever the ZoomPane is shown.\n const onShow = options[\"onShow\"] || null;\n // If present (and a function), this will be called\n // whenever the ZoomPane is hidden.\n const onHide = options[\"onHide\"] || null;\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 // An optional number that determines how long to wait before\n // showing the ZoomPane because of a `mouseenter` event.\n const hoverDelay = options[\"hoverDelay\"] || 0;\n // An optional number that determines how long to wait before\n // showing the ZoomPane because of a `touchstart` event.\n // It's unlikely that you would want to use this option, since\n // \"tap and hold\" is much more intentional than a hover event.\n const touchDelay = options[\"touchDelay\"] || 0;\n // If true, a bounding box will show the area currently being previewed\n // during mouse hover\n const hoverBoundingBox = options[\"hoverBoundingBox\"] || false;\n // If true, a bounding box will show the area currently being previewed\n // during touch events\n const touchBoundingBox = options[\"touchBoundingBox\"] || false;\n // A DOM element to append the bounding box to.\n const boundingBoxContainer = options[\"boundingBoxContainer\"] || document.body;\n // If true, the events related to handleTouch use passive listeners in\n // order to improve performance for touch devices.\n const passive = options[\"passive\"] || false;\n\n if (inlinePane !== true && !isDOMElement(paneContainer)) {\n throw new TypeError(\"`paneContainer` must be a DOM element when `inlinePane !== true`\");\n }\n if (!isDOMElement(inlineContainer)) {\n throw new TypeError(\"`inlineContainer` must be a DOM element\");\n }\n\n this.settings = {\n namespace,\n showWhitespaceAtEdges,\n containInline,\n inlineOffsetX,\n inlineOffsetY,\n inlineContainer,\n sourceAttribute,\n zoomFactor,\n paneContainer,\n inlinePane,\n handleTouch,\n onShow,\n onHide,\n injectBaseStyles,\n hoverDelay,\n touchDelay,\n hoverBoundingBox,\n touchBoundingBox,\n boundingBoxContainer,\n passive,\n };\n\n if (this.settings.injectBaseStyles) {\n injectBaseStylesheet();\n }\n\n this._buildZoomPane();\n this._buildTrigger();\n }\n\n get isShowing() {\n return this.zoomPane.isShowing;\n }\n\n get zoomFactor() {\n return this.settings.zoomFactor;\n }\n\n set zoomFactor(zf) {\n this.settings.zoomFactor = zf;\n this.zoomPane.settings.zoomFactor = zf;\n this.trigger.settings.zoomFactor = zf;\n this.boundingBox.settings.zoomFactor = zf;\n }\n\n _buildZoomPane() {\n this.zoomPane = new ZoomPane({\n container: this.settings.paneContainer,\n zoomFactor: this.settings.zoomFactor,\n showWhitespaceAtEdges: this.settings.showWhitespaceAtEdges,\n containInline: this.settings.containInline,\n inline: this.settings.inlinePane,\n namespace: this.settings.namespace,\n inlineOffsetX: this.settings.inlineOffsetX,\n inlineOffsetY: this.settings.inlineOffsetY,\n inlineContainer: this.settings.inlineContainer,\n });\n }\n\n _buildTrigger() {\n this.trigger = new Trigger({\n el: this.triggerEl,\n zoomPane: this.zoomPane,\n handleTouch: this.settings.handleTouch,\n onShow: this.settings.onShow,\n onHide: this.settings.onHide,\n sourceAttribute: this.settings.sourceAttribute,\n hoverDelay: this.settings.hoverDelay,\n touchDelay: this.settings.touchDelay,\n hoverBoundingBox: this.settings.hoverBoundingBox,\n touchBoundingBox: this.settings.touchBoundingBox,\n namespace: this.settings.namespace,\n zoomFactor: this.settings.zoomFactor,\n boundingBoxContainer: this.settings.boundingBoxContainer,\n passive: this.settings.passive,\n });\n }\n\n setZoomImageURL(imageURL) {\n this.zoomPane._setImageURL(imageURL);\n }\n\n disable() {\n this.trigger.enabled = false;\n }\n\n enable() {\n this.trigger.enabled = true;\n }\n\n destroy() {\n this.trigger._hide();\n this.trigger._unbindEvents();\n }\n}\n\n// Public API\n/* eslint-disable no-self-assign */\nObject.defineProperty(Drift.prototype, \"isShowing\", {\n get: function () {\n return this.isShowing;\n },\n});\nObject.defineProperty(Drift.prototype, \"zoomFactor\", {\n get: function () {\n return this.zoomFactor;\n },\n set: function (value) {\n this.zoomFactor = value;\n },\n});\nDrift.prototype[\"setZoomImageURL\"] = Drift.prototype.setZoomImageURL;\nDrift.prototype[\"disable\"] = Drift.prototype.disable;\nDrift.prototype[\"enable\"] = Drift.prototype.enable;\nDrift.prototype[\"destroy\"] = Drift.prototype.destroy;\n/* eslint-enable no-self-assign */\n","/* UNMINIFIED RULES\n\nconst RULES = `\n@keyframes noop {\n 0% { zoom: 1; }\n}\n\n@-webkit-keyframes noop {\n 0% { zoom: 1; }\n}\n\n.drift-zoom-pane.drift-open {\n display: block;\n}\n\n.drift-zoom-pane.drift-opening, .drift-zoom-pane.drift-closing {\n animation: noop 1ms;\n -webkit-animation: noop 1ms;\n}\n\n.drift-zoom-pane {\n position: absolute;\n overflow: hidden;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n pointer-events: none;\n}\n\n.drift-zoom-pane-loader {\n display: none;\n}\n\n.drift-zoom-pane img {\n position: absolute;\n display: block;\n max-width: none;\n max-height: none;\n}\n\n.drift-bounding-box {\n position: absolute;\n pointer-events: none;\n}\n`;\n\n*/\n\nconst RULES =\n \".drift-bounding-box,.drift-zoom-pane{position:absolute;pointer-events:none}@keyframes noop{0%{zoom:1}}@-webkit-keyframes noop{0%{zoom:1}}.drift-zoom-pane.drift-open{display:block}.drift-zoom-pane.drift-closing,.drift-zoom-pane.drift-opening{animation:noop 1ms;-webkit-animation:noop 1ms}.drift-zoom-pane{overflow:hidden;width:100%;height:100%;top:0;left:0}.drift-zoom-pane-loader{display:none}.drift-zoom-pane img{position:absolute;display:block;max-width:none;max-height:none}\";\n\nexport default function injectBaseStylesheet() {\n if (document.querySelector(\".drift-base-styles\")) {\n return;\n }\n\n const styleEl = document.createElement(\"style\");\n styleEl.type = \"text/css\";\n styleEl.classList.add(\"drift-base-styles\");\n\n styleEl.appendChild(document.createTextNode(RULES));\n\n const head = document.head;\n head.insertBefore(styleEl, head.firstChild);\n}\n","// This file is used for the standalone browser build\n\nimport Drift from \"2\"\n\nwindow[\"Drift\"] = Drift;\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/drift-basic.css: -------------------------------------------------------------------------------- 1 | @keyframes drift-fadeZoomIn { 2 | 0% { 3 | transform: scale(1.5); 4 | opacity: 0; 5 | } 6 | 100% { 7 | transform: scale(1); 8 | opacity: 1; 9 | } 10 | } 11 | 12 | @keyframes drift-fadeZoomOut { 13 | 0% { 14 | transform: scale(1); 15 | opacity: 1; 16 | } 17 | 15% { 18 | transform: scale(1.1); 19 | opacity: 1; 20 | } 21 | 100% { 22 | transform: scale(0.5); 23 | opacity: 0; 24 | } 25 | } 26 | 27 | @keyframes drift-loader-rotate { 28 | 0% { 29 | transform: translate(-50%, -50%) rotate(0); 30 | } 31 | 50% { 32 | transform: translate(-50%, -50%) rotate(-180deg); 33 | } 34 | 100% { 35 | transform: translate(-50%, -50%) rotate(-360deg); 36 | } 37 | } 38 | 39 | @keyframes drift-loader-before { 40 | 0% { 41 | transform: scale(1); 42 | } 43 | 10% { 44 | transform: scale(1.2) translateX(6px); 45 | } 46 | 25% { 47 | transform: scale(1.3) translateX(8px); 48 | } 49 | 40% { 50 | transform: scale(1.2) translateX(6px); 51 | } 52 | 50% { 53 | transform: scale(1); 54 | } 55 | 60% { 56 | transform: scale(0.8) translateX(6px); 57 | } 58 | 75% { 59 | transform: scale(0.7) translateX(8px); 60 | } 61 | 90% { 62 | transform: scale(0.8) translateX(6px); 63 | } 64 | 100% { 65 | transform: scale(1); 66 | } 67 | } 68 | 69 | @keyframes drift-loader-after { 70 | 0% { 71 | transform: scale(1); 72 | } 73 | 10% { 74 | transform: scale(1.2) translateX(-6px); 75 | } 76 | 25% { 77 | transform: scale(1.3) translateX(-8px); 78 | } 79 | 40% { 80 | transform: scale(1.2) translateX(-6px); 81 | } 82 | 50% { 83 | transform: scale(1); 84 | } 85 | 60% { 86 | transform: scale(0.8) translateX(-6px); 87 | } 88 | 75% { 89 | transform: scale(0.7) translateX(-8px); 90 | } 91 | 90% { 92 | transform: scale(0.8) translateX(-6px); 93 | } 94 | 100% { 95 | transform: scale(1); 96 | } 97 | } 98 | 99 | @-webkit-keyframes drift-fadeZoomIn { 100 | 0% { 101 | -webkit-transform: scale(1.5); 102 | opacity: 0; 103 | } 104 | 100% { 105 | -webkit-transform: scale(1); 106 | opacity: 1; 107 | } 108 | } 109 | 110 | @-webkit-keyframes drift-fadeZoomOut { 111 | 0% { 112 | -webkit-transform: scale(1); 113 | opacity: 1; 114 | } 115 | 15% { 116 | -webkit-transform: scale(1.1); 117 | opacity: 1; 118 | } 119 | 100% { 120 | -webkit-transform: scale(0.5); 121 | opacity: 0; 122 | } 123 | } 124 | 125 | @-webkit-keyframes drift-loader-rotate { 126 | 0% { 127 | -webkit-transform: translate(-50%, -50%) rotate(0); 128 | } 129 | 50% { 130 | -webkit-transform: translate(-50%, -50%) rotate(-180deg); 131 | } 132 | 100% { 133 | -webkit-transform: translate(-50%, -50%) rotate(-360deg); 134 | } 135 | } 136 | 137 | @-webkit-keyframes drift-loader-before { 138 | 0% { 139 | -webkit-transform: scale(1); 140 | } 141 | 10% { 142 | -webkit-transform: scale(1.2) translateX(6px); 143 | } 144 | 25% { 145 | -webkit-transform: scale(1.3) translateX(8px); 146 | } 147 | 40% { 148 | -webkit-transform: scale(1.2) translateX(6px); 149 | } 150 | 50% { 151 | -webkit-transform: scale(1); 152 | } 153 | 60% { 154 | -webkit-transform: scale(0.8) translateX(6px); 155 | } 156 | 75% { 157 | -webkit-transform: scale(0.7) translateX(8px); 158 | } 159 | 90% { 160 | -webkit-transform: scale(0.8) translateX(6px); 161 | } 162 | 100% { 163 | -webkit-transform: scale(1); 164 | } 165 | } 166 | 167 | @-webkit-keyframes drift-loader-after { 168 | 0% { 169 | -webkit-transform: scale(1); 170 | } 171 | 10% { 172 | -webkit-transform: scale(1.2) translateX(-6px); 173 | } 174 | 25% { 175 | -webkit-transform: scale(1.3) translateX(-8px); 176 | } 177 | 40% { 178 | -webkit-transform: scale(1.2) translateX(-6px); 179 | } 180 | 50% { 181 | -webkit-transform: scale(1); 182 | } 183 | 60% { 184 | -webkit-transform: scale(0.8) translateX(-6px); 185 | } 186 | 75% { 187 | -webkit-transform: scale(0.7) translateX(-8px); 188 | } 189 | 90% { 190 | -webkit-transform: scale(0.8) translateX(-6px); 191 | } 192 | 100% { 193 | -webkit-transform: scale(1); 194 | } 195 | } 196 | 197 | .drift-zoom-pane { 198 | background: rgba(0, 0, 0, 0.5); 199 | /* This is required because of a bug that causes border-radius to not 200 | work with child elements in certain cases. */ 201 | transform: translate3d(0, 0, 0); 202 | -webkit-transform: translate3d(0, 0, 0); 203 | } 204 | 205 | .drift-zoom-pane.drift-opening { 206 | animation: drift-fadeZoomIn 180ms ease-out; 207 | -webkit-animation: drift-fadeZoomIn 180ms ease-out; 208 | } 209 | 210 | .drift-zoom-pane.drift-closing { 211 | animation: drift-fadeZoomOut 210ms ease-in; 212 | -webkit-animation: drift-fadeZoomOut 210ms ease-in; 213 | } 214 | 215 | .drift-zoom-pane.drift-inline { 216 | position: absolute; 217 | width: 150px; 218 | height: 150px; 219 | border-radius: 75px; 220 | box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3); 221 | } 222 | 223 | .drift-loading .drift-zoom-pane-loader { 224 | display: block; 225 | position: absolute; 226 | top: 50%; 227 | left: 50%; 228 | transform: translate(-50%, -50%); 229 | -webkit-transform: translate(-50%, -50%); 230 | width: 66px; 231 | height: 20px; 232 | animation: drift-loader-rotate 1800ms infinite linear; 233 | -webkit-animation: drift-loader-rotate 1800ms infinite linear; 234 | } 235 | 236 | .drift-zoom-pane-loader:before, 237 | .drift-zoom-pane-loader:after { 238 | content: ""; 239 | display: block; 240 | width: 20px; 241 | height: 20px; 242 | position: absolute; 243 | top: 50%; 244 | margin-top: -10px; 245 | border-radius: 20px; 246 | background: rgba(255, 255, 255, 0.9); 247 | } 248 | 249 | .drift-zoom-pane-loader:before { 250 | left: 0; 251 | animation: drift-loader-before 1800ms infinite linear; 252 | -webkit-animation: drift-loader-before 1800ms infinite linear; 253 | } 254 | 255 | .drift-zoom-pane-loader:after { 256 | right: 0; 257 | animation: drift-loader-after 1800ms infinite linear; 258 | -webkit-animation: drift-loader-after 1800ms infinite linear; 259 | animation-delay: -900ms; 260 | -webkit-animation-delay: -900ms; 261 | } 262 | 263 | .drift-bounding-box { 264 | background-color: rgba(0, 0, 0, 0.4); 265 | } 266 | -------------------------------------------------------------------------------- /dist/drift-basic.min.css: -------------------------------------------------------------------------------- 1 | @keyframes a{0%{opacity:0;transform:scale(1.5)}to{opacity:1;transform:scale(1)}}@keyframes b{0%{opacity:1;transform:scale(1)}15%{opacity:1;transform:scale(1.1)}to{opacity:0;transform:scale(.5)}}@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)}}.drift-zoom-pane{background:rgba(0,0,0,.5);transform:translateZ(0);-webkit-transform:translateZ(0)}.drift-zoom-pane.drift-opening{animation:a .18s ease-out;-webkit-animation:a .18s ease-out}.drift-zoom-pane.drift-closing{animation:b .21s ease-in;-webkit-animation:b .21s ease-in}.drift-zoom-pane.drift-inline{border-radius:75px;box-shadow:0 6px 18px rgba(0,0,0,.3);height:150px;position:absolute;width:150px}.drift-loading .drift-zoom-pane-loader{animation:c 1.8s linear infinite;-webkit-animation:c 1.8s linear infinite;display:block;height:20px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);-webkit-transform:translate(-50%,-50%);width:66px}.drift-zoom-pane-loader:after,.drift-zoom-pane-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}.drift-zoom-pane-loader:before{animation:d 1.8s linear infinite;-webkit-animation:d 1.8s linear infinite;left:0}.drift-zoom-pane-loader:after{animation:e 1.8s linear infinite;-webkit-animation:e 1.8s linear infinite;animation-delay:-.9s;-webkit-animation-delay:-.9s;right:0}.drift-bounding-box{background-color:rgba(0,0,0,.4)} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | Drift Playground 16 | 17 | 102 | 103 | 104 | 105 |
106 | 111 | 112 |
113 |
114 |

Drift Demo

115 |

116 | This is a demo of Drift, a simple, lightweight, no-dependencies JavaScript "zoom on hover" tool from 117 | imgix. Move your mouse over the image (or touch it) to see it in action. 118 |

119 |

120 | This demo uses the simple included theme, but it's very easy to extend and customize to fit your needs. You 121 | can learn more and download it here. 122 |

123 |

(Psst… try making your browser window smaller!)

124 |
125 | 126 | 127 | imgix 133 | 134 |
135 |
136 | 137 | 138 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drift-zoom", 3 | "version": "1.5.1", 4 | "description": "Easily add \"zoom on hover\" functionality to your site's images. Lightweight, no-dependency JavaScript.", 5 | "contributors": [ 6 | "Frederick Fogerty (https://github.com/frederickfogerty)", 7 | "Sherwin Heydarbeygi (https://github.com/sherwinski)" 8 | ], 9 | "main": "lib/Drift.js", 10 | "module": "es/Drift.js", 11 | "jsnext:main": "es/Drift.js", 12 | "scripts": { 13 | "build": "npm run clean && run-p build:css:** build:js:**", 14 | "build:serial": "npm run clean && run-s build:css:** build:js:**", 15 | "build:css:dev": "mkdir -p dist && cp src/css/drift-basic.css dist/", 16 | "build:css:prod": "mkdir -p dist && postcss src/css/drift-basic.css > dist/drift-basic.min.css", 17 | "build:js:dist:prod": "webpack --env build && cp dist/Drift.min.js dist/Drift.js", 18 | "build:js:lib:commonjs": "cross-env BABEL_ENV=commonjs babel src/js --out-dir lib --source-maps", 19 | "build:js:lib:es": "cross-env BABEL_ENV=es babel src/js --out-dir es --source-maps", 20 | "build:watch": "webpack --progress --color --watch --env dev", 21 | "clean": "rimraf lib es dist", 22 | "dev": "npm run build:watch", 23 | "format:check": "prettier --list-different \"{src,test}/**/*.js\"", 24 | "format": "prettier --write \"{src,test}/**/*.js\"", 25 | "lint": "eslint \"{src,test}/**/*.js\"", 26 | "prepare": "npm run build", 27 | "prepublishOnly": "npm run build", 28 | "release": "npm run build && git add dist src/js/Drift.js && standard-version -a", 29 | "start": "npm run dev", 30 | "test:watch": "karma start config/karma/karma-local.config.js", 31 | "test:local": "karma start config/karma/karma-local.config.js --singleRun", 32 | "test:ci": "karma start config/karma/karma-ci.config.js --singleRun", 33 | "test": "npm run test:local", 34 | "release:dryRun": "npx node-env-run --exec 'semantic-release --dryRun'", 35 | "release:publish": "semantic-release" 36 | }, 37 | "files": [ 38 | "lib", 39 | "es", 40 | "dist", 41 | "src" 42 | ], 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/imgix/drift.git" 46 | }, 47 | "keywords": [ 48 | "javascript", 49 | "zoom", 50 | "hover" 51 | ], 52 | "author": "imgix", 53 | "license": "BSD-2", 54 | "bugs": { 55 | "url": "https://github.com/imgix/drift/issues" 56 | }, 57 | "homepage": "https://github.com/imgix/drift", 58 | "devDependencies": { 59 | "@babel/cli": "7.22.9", 60 | "@babel/core": "7.22.9", 61 | "@babel/plugin-proposal-class-properties": "7.18.6", 62 | "@babel/preset-env": "7.22.9", 63 | "@google/semantic-release-replace-plugin": "1.2.7", 64 | "@semantic-release/changelog": "6.0.3", 65 | "@semantic-release/git": "10.0.1", 66 | "babel-eslint": "10.1.0", 67 | "babel-loader": "9.1.3", 68 | "babel-preset-env": "7.0.0-beta.3", 69 | "chai": "4.3.7", 70 | "closure-webpack-plugin": "2.6.1", 71 | "cross-env": "7.0.3", 72 | "cssnano": "6.0.1", 73 | "cssnano-preset-advanced": "6.0.1", 74 | "eslint": "7.32.0", 75 | "eslint-config-google": "0.14.0", 76 | "eslint-config-prettier": "8.8.0", 77 | "google-closure-compiler": "20230502.0.0", 78 | "google-closure-compiler-js": "20200719.0.0", 79 | "jasmine-core": "5.0.1", 80 | "karma": "6.4.2", 81 | "karma-chrome-launcher": "3.2.0", 82 | "karma-firefox-launcher": "2.1.2", 83 | "karma-jasmine": "5.1.0", 84 | "karma-webpack": "4.0.2", 85 | "npm-run-all": "4.1.5", 86 | "postcss": "8.4.36", 87 | "postcss-cli": "8.3.1", 88 | "prettier": "3.2.5", 89 | "rimraf": "5.0.5", 90 | "semantic-release": "21.0.7", 91 | "standard-version": "9.5.0", 92 | "webpack": "4.46.0", 93 | "webpack-cli": "5.1.4", 94 | "yargs": "17.7.2" 95 | }, 96 | "browserslist": [ 97 | "ie 11", 98 | "last 2 edge versions", 99 | "last 2 Chrome versions", 100 | "last 2 Firefox versions", 101 | "last 2 Safari versions", 102 | "last 2 iOS versions", 103 | "last 2 Android versions" 104 | ], 105 | "release": { 106 | "branches": [ 107 | "main", 108 | { 109 | "name": "next", 110 | "prerelease": "rc" 111 | }, 112 | { 113 | "name": "beta", 114 | "prerelease": true 115 | }, 116 | { 117 | "name": "alpha", 118 | "prerelease": true 119 | } 120 | ], 121 | "plugins": [ 122 | "@semantic-release/commit-analyzer", 123 | "@semantic-release/release-notes-generator", 124 | [ 125 | "@google/semantic-release-replace-plugin", 126 | { 127 | "replacements": [ 128 | { 129 | "files": [ 130 | "src/js/Drift.js" 131 | ], 132 | "from": "this.VERSION = \".*\"", 133 | "to": "this.VERSION = \"${nextRelease.version}\"", 134 | "results": [ 135 | { 136 | "file": "src/js/Drift.js", 137 | "hasChanged": true, 138 | "numMatches": 1, 139 | "numReplacements": 1 140 | } 141 | ], 142 | "countMatches": true 143 | } 144 | ] 145 | } 146 | ], 147 | "@semantic-release/changelog", 148 | "@semantic-release/npm", 149 | [ 150 | "@semantic-release/git", 151 | { 152 | "assets": [ 153 | "src/**", 154 | "dist/**", 155 | "package.json", 156 | "changelog.md" 157 | ], 158 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes} [skip ci]" 159 | } 160 | ], 161 | [ 162 | "@semantic-release/github", 163 | { 164 | "assets": [ 165 | { 166 | "path": "dist/Drift.js", 167 | "label": "Standard build" 168 | }, 169 | { 170 | "path": "dist/Drift.min.js", 171 | "label": "Minified build" 172 | }, 173 | { 174 | "path": "dist/drift-basic.css", 175 | "label": "Drift css" 176 | }, 177 | { 178 | "path": "dist/drift-basic.min.css", 179 | "label": "Minified Drift css" 180 | } 181 | ] 182 | } 183 | ] 184 | ] 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("cssnano")({ 4 | preset: "advanced" 5 | }) 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /src/css/drift-basic.css: -------------------------------------------------------------------------------- 1 | @keyframes drift-fadeZoomIn { 2 | 0% { 3 | transform: scale(1.5); 4 | opacity: 0; 5 | } 6 | 100% { 7 | transform: scale(1); 8 | opacity: 1; 9 | } 10 | } 11 | 12 | @keyframes drift-fadeZoomOut { 13 | 0% { 14 | transform: scale(1); 15 | opacity: 1; 16 | } 17 | 15% { 18 | transform: scale(1.1); 19 | opacity: 1; 20 | } 21 | 100% { 22 | transform: scale(0.5); 23 | opacity: 0; 24 | } 25 | } 26 | 27 | @keyframes drift-loader-rotate { 28 | 0% { 29 | transform: translate(-50%, -50%) rotate(0); 30 | } 31 | 50% { 32 | transform: translate(-50%, -50%) rotate(-180deg); 33 | } 34 | 100% { 35 | transform: translate(-50%, -50%) rotate(-360deg); 36 | } 37 | } 38 | 39 | @keyframes drift-loader-before { 40 | 0% { 41 | transform: scale(1); 42 | } 43 | 10% { 44 | transform: scale(1.2) translateX(6px); 45 | } 46 | 25% { 47 | transform: scale(1.3) translateX(8px); 48 | } 49 | 40% { 50 | transform: scale(1.2) translateX(6px); 51 | } 52 | 50% { 53 | transform: scale(1); 54 | } 55 | 60% { 56 | transform: scale(0.8) translateX(6px); 57 | } 58 | 75% { 59 | transform: scale(0.7) translateX(8px); 60 | } 61 | 90% { 62 | transform: scale(0.8) translateX(6px); 63 | } 64 | 100% { 65 | transform: scale(1); 66 | } 67 | } 68 | 69 | @keyframes drift-loader-after { 70 | 0% { 71 | transform: scale(1); 72 | } 73 | 10% { 74 | transform: scale(1.2) translateX(-6px); 75 | } 76 | 25% { 77 | transform: scale(1.3) translateX(-8px); 78 | } 79 | 40% { 80 | transform: scale(1.2) translateX(-6px); 81 | } 82 | 50% { 83 | transform: scale(1); 84 | } 85 | 60% { 86 | transform: scale(0.8) translateX(-6px); 87 | } 88 | 75% { 89 | transform: scale(0.7) translateX(-8px); 90 | } 91 | 90% { 92 | transform: scale(0.8) translateX(-6px); 93 | } 94 | 100% { 95 | transform: scale(1); 96 | } 97 | } 98 | 99 | @-webkit-keyframes drift-fadeZoomIn { 100 | 0% { 101 | -webkit-transform: scale(1.5); 102 | opacity: 0; 103 | } 104 | 100% { 105 | -webkit-transform: scale(1); 106 | opacity: 1; 107 | } 108 | } 109 | 110 | @-webkit-keyframes drift-fadeZoomOut { 111 | 0% { 112 | -webkit-transform: scale(1); 113 | opacity: 1; 114 | } 115 | 15% { 116 | -webkit-transform: scale(1.1); 117 | opacity: 1; 118 | } 119 | 100% { 120 | -webkit-transform: scale(0.5); 121 | opacity: 0; 122 | } 123 | } 124 | 125 | @-webkit-keyframes drift-loader-rotate { 126 | 0% { 127 | -webkit-transform: translate(-50%, -50%) rotate(0); 128 | } 129 | 50% { 130 | -webkit-transform: translate(-50%, -50%) rotate(-180deg); 131 | } 132 | 100% { 133 | -webkit-transform: translate(-50%, -50%) rotate(-360deg); 134 | } 135 | } 136 | 137 | @-webkit-keyframes drift-loader-before { 138 | 0% { 139 | -webkit-transform: scale(1); 140 | } 141 | 10% { 142 | -webkit-transform: scale(1.2) translateX(6px); 143 | } 144 | 25% { 145 | -webkit-transform: scale(1.3) translateX(8px); 146 | } 147 | 40% { 148 | -webkit-transform: scale(1.2) translateX(6px); 149 | } 150 | 50% { 151 | -webkit-transform: scale(1); 152 | } 153 | 60% { 154 | -webkit-transform: scale(0.8) translateX(6px); 155 | } 156 | 75% { 157 | -webkit-transform: scale(0.7) translateX(8px); 158 | } 159 | 90% { 160 | -webkit-transform: scale(0.8) translateX(6px); 161 | } 162 | 100% { 163 | -webkit-transform: scale(1); 164 | } 165 | } 166 | 167 | @-webkit-keyframes drift-loader-after { 168 | 0% { 169 | -webkit-transform: scale(1); 170 | } 171 | 10% { 172 | -webkit-transform: scale(1.2) translateX(-6px); 173 | } 174 | 25% { 175 | -webkit-transform: scale(1.3) translateX(-8px); 176 | } 177 | 40% { 178 | -webkit-transform: scale(1.2) translateX(-6px); 179 | } 180 | 50% { 181 | -webkit-transform: scale(1); 182 | } 183 | 60% { 184 | -webkit-transform: scale(0.8) translateX(-6px); 185 | } 186 | 75% { 187 | -webkit-transform: scale(0.7) translateX(-8px); 188 | } 189 | 90% { 190 | -webkit-transform: scale(0.8) translateX(-6px); 191 | } 192 | 100% { 193 | -webkit-transform: scale(1); 194 | } 195 | } 196 | 197 | .drift-zoom-pane { 198 | background: rgba(0, 0, 0, 0.5); 199 | /* This is required because of a bug that causes border-radius to not 200 | work with child elements in certain cases. */ 201 | transform: translate3d(0, 0, 0); 202 | -webkit-transform: translate3d(0, 0, 0); 203 | } 204 | 205 | .drift-zoom-pane.drift-opening { 206 | animation: drift-fadeZoomIn 180ms ease-out; 207 | -webkit-animation: drift-fadeZoomIn 180ms ease-out; 208 | } 209 | 210 | .drift-zoom-pane.drift-closing { 211 | animation: drift-fadeZoomOut 210ms ease-in; 212 | -webkit-animation: drift-fadeZoomOut 210ms ease-in; 213 | } 214 | 215 | .drift-zoom-pane.drift-inline { 216 | position: absolute; 217 | width: 150px; 218 | height: 150px; 219 | border-radius: 75px; 220 | box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3); 221 | } 222 | 223 | .drift-loading .drift-zoom-pane-loader { 224 | display: block; 225 | position: absolute; 226 | top: 50%; 227 | left: 50%; 228 | transform: translate(-50%, -50%); 229 | -webkit-transform: translate(-50%, -50%); 230 | width: 66px; 231 | height: 20px; 232 | animation: drift-loader-rotate 1800ms infinite linear; 233 | -webkit-animation: drift-loader-rotate 1800ms infinite linear; 234 | } 235 | 236 | .drift-zoom-pane-loader:before, 237 | .drift-zoom-pane-loader:after { 238 | content: ""; 239 | display: block; 240 | width: 20px; 241 | height: 20px; 242 | position: absolute; 243 | top: 50%; 244 | margin-top: -10px; 245 | border-radius: 20px; 246 | background: rgba(255, 255, 255, 0.9); 247 | } 248 | 249 | .drift-zoom-pane-loader:before { 250 | left: 0; 251 | animation: drift-loader-before 1800ms infinite linear; 252 | -webkit-animation: drift-loader-before 1800ms infinite linear; 253 | } 254 | 255 | .drift-zoom-pane-loader:after { 256 | right: 0; 257 | animation: drift-loader-after 1800ms infinite linear; 258 | -webkit-animation: drift-loader-after 1800ms infinite linear; 259 | animation-delay: -900ms; 260 | -webkit-animation-delay: -900ms; 261 | } 262 | 263 | .drift-bounding-box { 264 | background-color: rgba(0, 0, 0, 0.4); 265 | } 266 | -------------------------------------------------------------------------------- /src/js/BoundingBox.js: -------------------------------------------------------------------------------- 1 | import throwIfMissing from "./util/throwIfMissing"; 2 | import { addClasses, removeClasses } from "./util/dom"; 3 | 4 | export default class BoundingBox { 5 | constructor(options) { 6 | this.isShowing = false; 7 | 8 | const { namespace = null, zoomFactor = throwIfMissing(), containerEl = throwIfMissing() } = options; 9 | 10 | this.settings = { namespace, zoomFactor, containerEl }; 11 | 12 | this.openClasses = this._buildClasses("open"); 13 | 14 | this._buildElement(); 15 | } 16 | 17 | _buildClasses(suffix) { 18 | const classes = [`drift-${suffix}`]; 19 | 20 | const ns = this.settings.namespace; 21 | if (ns) { 22 | classes.push(`${ns}-${suffix}`); 23 | } 24 | 25 | return classes; 26 | } 27 | 28 | _buildElement() { 29 | this.el = document.createElement("div"); 30 | addClasses(this.el, this._buildClasses("bounding-box")); 31 | } 32 | 33 | show(zoomPaneWidth, zoomPaneHeight) { 34 | this.isShowing = true; 35 | 36 | this.settings.containerEl.appendChild(this.el); 37 | 38 | const style = this.el.style; 39 | style.width = `${Math.round(zoomPaneWidth / this.settings.zoomFactor)}px`; 40 | style.height = `${Math.round(zoomPaneHeight / this.settings.zoomFactor)}px`; 41 | 42 | addClasses(this.el, this.openClasses); 43 | } 44 | 45 | hide() { 46 | if (this.isShowing) { 47 | this.settings.containerEl.removeChild(this.el); 48 | } 49 | 50 | this.isShowing = false; 51 | 52 | removeClasses(this.el, this.openClasses); 53 | } 54 | 55 | setPosition(percentageOffsetX, percentageOffsetY, triggerRect) { 56 | const pageXOffset = window.pageXOffset; 57 | const pageYOffset = window.pageYOffset; 58 | 59 | let inlineLeft = triggerRect.left + percentageOffsetX * triggerRect.width - this.el.clientWidth / 2 + pageXOffset; 60 | let inlineTop = triggerRect.top + percentageOffsetY * triggerRect.height - this.el.clientHeight / 2 + pageYOffset; 61 | 62 | if (inlineLeft < triggerRect.left + pageXOffset) { 63 | inlineLeft = triggerRect.left + pageXOffset; 64 | } else if (inlineLeft + this.el.clientWidth > triggerRect.left + triggerRect.width + pageXOffset) { 65 | inlineLeft = triggerRect.left + triggerRect.width - this.el.clientWidth + pageXOffset; 66 | } 67 | 68 | if (inlineTop < triggerRect.top + pageYOffset) { 69 | inlineTop = triggerRect.top + pageYOffset; 70 | } else if (inlineTop + this.el.clientHeight > triggerRect.top + triggerRect.height + pageYOffset) { 71 | inlineTop = triggerRect.top + triggerRect.height - this.el.clientHeight + pageYOffset; 72 | } 73 | 74 | this.el.style.left = `${inlineLeft}px`; 75 | this.el.style.top = `${inlineTop}px`; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/js/Drift-browser.js: -------------------------------------------------------------------------------- 1 | // This file is used for the standalone browser build 2 | 3 | import Drift from "./Drift"; 4 | 5 | window["Drift"] = Drift; 6 | -------------------------------------------------------------------------------- /src/js/Drift.js: -------------------------------------------------------------------------------- 1 | import { isDOMElement } from "./util/dom"; 2 | import injectBaseStylesheet from "./injectBaseStylesheet"; 3 | 4 | import Trigger from "./Trigger"; 5 | import ZoomPane from "./ZoomPane"; 6 | 7 | export default class Drift { 8 | constructor(triggerEl, options = {}) { 9 | this.VERSION = "1.5.1"; 10 | this.triggerEl = triggerEl; 11 | 12 | this.destroy = this.destroy.bind(this); 13 | 14 | if (!isDOMElement(this.triggerEl)) { 15 | throw new TypeError("`new Drift` requires a DOM element as its first argument."); 16 | } 17 | 18 | // Prefix for generated element class names (e.g. `my-ns` will 19 | // result in classes such as `my-ns-pane`. Default `drift-` 20 | // prefixed classes will always be added as well. 21 | const namespace = options["namespace"] || null; 22 | // Whether the ZoomPane should show whitespace when near the edges. 23 | const showWhitespaceAtEdges = options["showWhitespaceAtEdges"] || false; 24 | // Whether the inline ZoomPane should stay inside 25 | // the bounds of its image. 26 | const containInline = options["containInline"] || false; 27 | // How much to offset the ZoomPane from the 28 | // interaction point when inline. 29 | const inlineOffsetX = options["inlineOffsetX"] || 0; 30 | const inlineOffsetY = options["inlineOffsetY"] || 0; 31 | // A DOM element to append the inline ZoomPane to 32 | const inlineContainer = options["inlineContainer"] || document.body; 33 | // Which trigger attribute to pull the ZoomPane image source from. 34 | const sourceAttribute = options["sourceAttribute"] || "data-zoom"; 35 | // How much to magnify the trigger by in the ZoomPane. 36 | // (e.g., `zoomFactor: 3` will result in a 900 px wide ZoomPane imag 37 | // if the trigger is displayed at 300 px wide) 38 | const zoomFactor = options["zoomFactor"] || 3; 39 | // A DOM element to append the non-inline ZoomPane to. 40 | // Required if `inlinePane !== true`. 41 | const paneContainer = options["paneContainer"] === undefined ? document.body : options["paneContainer"]; 42 | // When to switch to an inline ZoomPane. This can be a boolean or 43 | // an integer. If `true`, the ZoomPane will always be inline, 44 | // if `false`, it will switch to inline when `windowWidth <= inlinePane` 45 | const inlinePane = options["inlinePane"] || 375; 46 | // If `true`, touch events will trigger the zoom, like mouse events. 47 | const handleTouch = "handleTouch" in options ? !!options["handleTouch"] : true; 48 | // If present (and a function), this will be called 49 | // whenever the ZoomPane is shown. 50 | const onShow = options["onShow"] || null; 51 | // If present (and a function), this will be called 52 | // whenever the ZoomPane is hidden. 53 | const onHide = options["onHide"] || null; 54 | // Add base styles to the page. See the "Theming" 55 | // section of README.md for more information. 56 | const injectBaseStyles = "injectBaseStyles" in options ? !!options["injectBaseStyles"] : true; 57 | // An optional number that determines how long to wait before 58 | // showing the ZoomPane because of a `mouseenter` event. 59 | const hoverDelay = options["hoverDelay"] || 0; 60 | // An optional number that determines how long to wait before 61 | // showing the ZoomPane because of a `touchstart` event. 62 | // It's unlikely that you would want to use this option, since 63 | // "tap and hold" is much more intentional than a hover event. 64 | const touchDelay = options["touchDelay"] || 0; 65 | // If true, a bounding box will show the area currently being previewed 66 | // during mouse hover 67 | const hoverBoundingBox = options["hoverBoundingBox"] || false; 68 | // If true, a bounding box will show the area currently being previewed 69 | // during touch events 70 | const touchBoundingBox = options["touchBoundingBox"] || false; 71 | // A DOM element to append the bounding box to. 72 | const boundingBoxContainer = options["boundingBoxContainer"] || document.body; 73 | // If true, the events related to handleTouch use passive listeners in 74 | // order to improve performance for touch devices. 75 | const passive = options["passive"] || false; 76 | 77 | if (inlinePane !== true && !isDOMElement(paneContainer)) { 78 | throw new TypeError("`paneContainer` must be a DOM element when `inlinePane !== true`"); 79 | } 80 | if (!isDOMElement(inlineContainer)) { 81 | throw new TypeError("`inlineContainer` must be a DOM element"); 82 | } 83 | 84 | this.settings = { 85 | namespace, 86 | showWhitespaceAtEdges, 87 | containInline, 88 | inlineOffsetX, 89 | inlineOffsetY, 90 | inlineContainer, 91 | sourceAttribute, 92 | zoomFactor, 93 | paneContainer, 94 | inlinePane, 95 | handleTouch, 96 | onShow, 97 | onHide, 98 | injectBaseStyles, 99 | hoverDelay, 100 | touchDelay, 101 | hoverBoundingBox, 102 | touchBoundingBox, 103 | boundingBoxContainer, 104 | passive, 105 | }; 106 | 107 | if (this.settings.injectBaseStyles) { 108 | injectBaseStylesheet(); 109 | } 110 | 111 | this._buildZoomPane(); 112 | this._buildTrigger(); 113 | } 114 | 115 | get isShowing() { 116 | return this.zoomPane.isShowing; 117 | } 118 | 119 | get zoomFactor() { 120 | return this.settings.zoomFactor; 121 | } 122 | 123 | set zoomFactor(zf) { 124 | this.settings.zoomFactor = zf; 125 | this.zoomPane.settings.zoomFactor = zf; 126 | this.trigger.settings.zoomFactor = zf; 127 | this.boundingBox.settings.zoomFactor = zf; 128 | } 129 | 130 | _buildZoomPane() { 131 | this.zoomPane = new ZoomPane({ 132 | container: this.settings.paneContainer, 133 | zoomFactor: this.settings.zoomFactor, 134 | showWhitespaceAtEdges: this.settings.showWhitespaceAtEdges, 135 | containInline: this.settings.containInline, 136 | inline: this.settings.inlinePane, 137 | namespace: this.settings.namespace, 138 | inlineOffsetX: this.settings.inlineOffsetX, 139 | inlineOffsetY: this.settings.inlineOffsetY, 140 | inlineContainer: this.settings.inlineContainer, 141 | }); 142 | } 143 | 144 | _buildTrigger() { 145 | this.trigger = new Trigger({ 146 | el: this.triggerEl, 147 | zoomPane: this.zoomPane, 148 | handleTouch: this.settings.handleTouch, 149 | onShow: this.settings.onShow, 150 | onHide: this.settings.onHide, 151 | sourceAttribute: this.settings.sourceAttribute, 152 | hoverDelay: this.settings.hoverDelay, 153 | touchDelay: this.settings.touchDelay, 154 | hoverBoundingBox: this.settings.hoverBoundingBox, 155 | touchBoundingBox: this.settings.touchBoundingBox, 156 | namespace: this.settings.namespace, 157 | zoomFactor: this.settings.zoomFactor, 158 | boundingBoxContainer: this.settings.boundingBoxContainer, 159 | passive: this.settings.passive, 160 | }); 161 | } 162 | 163 | setZoomImageURL(imageURL) { 164 | this.zoomPane._setImageURL(imageURL); 165 | } 166 | 167 | disable() { 168 | this.trigger.enabled = false; 169 | } 170 | 171 | enable() { 172 | this.trigger.enabled = true; 173 | } 174 | 175 | destroy() { 176 | this.trigger._hide(); 177 | this.trigger._unbindEvents(); 178 | } 179 | } 180 | 181 | // Public API 182 | /* eslint-disable no-self-assign */ 183 | Object.defineProperty(Drift.prototype, "isShowing", { 184 | get: function () { 185 | return this.isShowing; 186 | }, 187 | }); 188 | Object.defineProperty(Drift.prototype, "zoomFactor", { 189 | get: function () { 190 | return this.zoomFactor; 191 | }, 192 | set: function (value) { 193 | this.zoomFactor = value; 194 | }, 195 | }); 196 | Drift.prototype["setZoomImageURL"] = Drift.prototype.setZoomImageURL; 197 | Drift.prototype["disable"] = Drift.prototype.disable; 198 | Drift.prototype["enable"] = Drift.prototype.enable; 199 | Drift.prototype["destroy"] = Drift.prototype.destroy; 200 | /* eslint-enable no-self-assign */ 201 | -------------------------------------------------------------------------------- /src/js/Trigger.js: -------------------------------------------------------------------------------- 1 | import throwIfMissing from "./util/throwIfMissing"; 2 | import BoundingBox from "./BoundingBox"; 3 | 4 | export default class Trigger { 5 | constructor(options = {}) { 6 | this._show = this._show.bind(this); 7 | this._hide = this._hide.bind(this); 8 | this._handleEntry = this._handleEntry.bind(this); 9 | this._handleMovement = this._handleMovement.bind(this); 10 | 11 | const { 12 | el = throwIfMissing(), 13 | zoomPane = throwIfMissing(), 14 | sourceAttribute = throwIfMissing(), 15 | handleTouch = throwIfMissing(), 16 | onShow = null, 17 | onHide = null, 18 | hoverDelay = 0, 19 | touchDelay = 0, 20 | hoverBoundingBox = throwIfMissing(), 21 | touchBoundingBox = throwIfMissing(), 22 | namespace = null, 23 | zoomFactor = throwIfMissing(), 24 | boundingBoxContainer = throwIfMissing(), 25 | passive = false, 26 | } = options; 27 | 28 | this.settings = { 29 | el, 30 | zoomPane, 31 | sourceAttribute, 32 | handleTouch, 33 | onShow, 34 | onHide, 35 | hoverDelay, 36 | touchDelay, 37 | hoverBoundingBox, 38 | touchBoundingBox, 39 | namespace, 40 | zoomFactor, 41 | boundingBoxContainer, 42 | passive, 43 | }; 44 | 45 | if (this.settings.hoverBoundingBox || this.settings.touchBoundingBox) { 46 | this.boundingBox = new BoundingBox({ 47 | namespace: this.settings.namespace, 48 | zoomFactor: this.settings.zoomFactor, 49 | containerEl: this.settings.boundingBoxContainer, 50 | }); 51 | } 52 | 53 | this.enabled = true; 54 | 55 | this._bindEvents(); 56 | } 57 | 58 | get isShowing() { 59 | return this.settings.zoomPane.isShowing; 60 | } 61 | 62 | _preventDefault(event) { 63 | event.preventDefault(); 64 | } 65 | 66 | _preventDefaultAllowTouchScroll(event) { 67 | if (!this.settings.touchDelay || !this._isTouchEvent(event) || this.isShowing) { 68 | event.preventDefault(); 69 | } 70 | } 71 | 72 | _isTouchEvent(event) { 73 | return !!event.touches; 74 | } 75 | 76 | _bindEvents() { 77 | this.settings.el.addEventListener("mouseenter", this._handleEntry); 78 | this.settings.el.addEventListener("mouseleave", this._hide); 79 | this.settings.el.addEventListener("mousemove", this._handleMovement); 80 | 81 | const isPassive = { passive: this.settings.passive }; 82 | if (this.settings.handleTouch) { 83 | this.settings.el.addEventListener("touchstart", this._handleEntry, isPassive); 84 | this.settings.el.addEventListener("touchend", this._hide); 85 | this.settings.el.addEventListener("touchmove", this._handleMovement, isPassive); 86 | } else { 87 | this.settings.el.addEventListener("touchstart", this._preventDefault, isPassive); 88 | this.settings.el.addEventListener("touchend", this._preventDefault); 89 | this.settings.el.addEventListener("touchmove", this._preventDefault, isPassive); 90 | } 91 | } 92 | 93 | _unbindEvents() { 94 | this.settings.el.removeEventListener("mouseenter", this._handleEntry); 95 | this.settings.el.removeEventListener("mouseleave", this._hide); 96 | this.settings.el.removeEventListener("mousemove", this._handleMovement); 97 | 98 | if (this.settings.handleTouch) { 99 | this.settings.el.removeEventListener("touchstart", this._handleEntry); 100 | this.settings.el.removeEventListener("touchend", this._hide); 101 | this.settings.el.removeEventListener("touchmove", this._handleMovement); 102 | } else { 103 | this.settings.el.removeEventListener("touchstart", this._preventDefault); 104 | this.settings.el.removeEventListener("touchend", this._preventDefault); 105 | this.settings.el.removeEventListener("touchmove", this._preventDefault); 106 | } 107 | } 108 | 109 | _handleEntry(e) { 110 | this._preventDefaultAllowTouchScroll(e); 111 | this._lastMovement = e; 112 | 113 | if (e.type == "mouseenter" && this.settings.hoverDelay) { 114 | this.entryTimeout = setTimeout(this._show, this.settings.hoverDelay); 115 | } else if (this.settings.touchDelay) { 116 | this.entryTimeout = setTimeout(this._show, this.settings.touchDelay); 117 | } else { 118 | this._show(); 119 | } 120 | } 121 | 122 | _show() { 123 | if (!this.enabled) { 124 | return; 125 | } 126 | 127 | const onShow = this.settings.onShow; 128 | if (onShow && typeof onShow === "function") { 129 | onShow(); 130 | } 131 | 132 | this.settings.zoomPane.show( 133 | this.settings.el.getAttribute(this.settings.sourceAttribute), 134 | this.settings.el.clientWidth, 135 | this.settings.el.clientHeight 136 | ); 137 | 138 | if (this._lastMovement) { 139 | const touchActivated = this._lastMovement.touches; 140 | if ((touchActivated && this.settings.touchBoundingBox) || (!touchActivated && this.settings.hoverBoundingBox)) { 141 | this.boundingBox.show(this.settings.zoomPane.el.clientWidth, this.settings.zoomPane.el.clientHeight); 142 | } 143 | } 144 | 145 | this._handleMovement(); 146 | } 147 | 148 | _hide(e) { 149 | if (e) { 150 | this._preventDefaultAllowTouchScroll(e); 151 | } 152 | 153 | this._lastMovement = null; 154 | 155 | if (this.entryTimeout) { 156 | clearTimeout(this.entryTimeout); 157 | } 158 | 159 | if (this.boundingBox) { 160 | this.boundingBox.hide(); 161 | } 162 | 163 | const onHide = this.settings.onHide; 164 | if (onHide && typeof onHide === "function") { 165 | onHide(); 166 | } 167 | 168 | this.settings.zoomPane.hide(); 169 | } 170 | 171 | _handleMovement(e) { 172 | if (e) { 173 | this._preventDefaultAllowTouchScroll(e); 174 | this._lastMovement = e; 175 | } else if (this._lastMovement) { 176 | e = this._lastMovement; 177 | } else { 178 | return; 179 | } 180 | 181 | let movementX; 182 | let movementY; 183 | 184 | if (e.touches) { 185 | const firstTouch = e.touches[0]; 186 | movementX = firstTouch.clientX; 187 | movementY = firstTouch.clientY; 188 | } else { 189 | movementX = e.clientX; 190 | movementY = e.clientY; 191 | } 192 | 193 | const el = this.settings.el; 194 | const rect = el.getBoundingClientRect(); 195 | const offsetX = movementX - rect.left; 196 | const offsetY = movementY - rect.top; 197 | 198 | const percentageOffsetX = offsetX / this.settings.el.clientWidth; 199 | const percentageOffsetY = offsetY / this.settings.el.clientHeight; 200 | 201 | if (this.boundingBox) { 202 | this.boundingBox.setPosition(percentageOffsetX, percentageOffsetY, rect); 203 | } 204 | 205 | this.settings.zoomPane.setPosition(percentageOffsetX, percentageOffsetY, rect); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/js/ZoomPane.js: -------------------------------------------------------------------------------- 1 | import throwIfMissing from "./util/throwIfMissing"; 2 | import { addClasses, removeClasses } from "./util/dom"; 3 | 4 | export default class ZoomPane { 5 | constructor(options = {}) { 6 | // All officially-supported browsers have this, but it's easy to 7 | // account for, just in case. 8 | this.HAS_ANIMATION = false; 9 | if (typeof document !== "undefined") { 10 | const divStyle = document.createElement("div").style; 11 | this.HAS_ANIMATION = "animation" in divStyle || "webkitAnimation" in divStyle; 12 | } 13 | 14 | this._completeShow = this._completeShow.bind(this); 15 | this._completeHide = this._completeHide.bind(this); 16 | this._handleLoad = this._handleLoad.bind(this); 17 | 18 | this.isShowing = false; 19 | 20 | const { 21 | container = null, 22 | zoomFactor = throwIfMissing(), 23 | inline = throwIfMissing(), 24 | namespace = null, 25 | showWhitespaceAtEdges = throwIfMissing(), 26 | containInline = throwIfMissing(), 27 | inlineOffsetX = 0, 28 | inlineOffsetY = 0, 29 | inlineContainer = document.body, 30 | } = options; 31 | 32 | this.settings = { 33 | container, 34 | zoomFactor, 35 | inline, 36 | namespace, 37 | showWhitespaceAtEdges, 38 | containInline, 39 | inlineOffsetX, 40 | inlineOffsetY, 41 | inlineContainer, 42 | }; 43 | 44 | this.openClasses = this._buildClasses("open"); 45 | this.openingClasses = this._buildClasses("opening"); 46 | this.closingClasses = this._buildClasses("closing"); 47 | this.inlineClasses = this._buildClasses("inline"); 48 | this.loadingClasses = this._buildClasses("loading"); 49 | 50 | this._buildElement(); 51 | } 52 | 53 | _buildClasses(suffix) { 54 | const classes = [`drift-${suffix}`]; 55 | 56 | const ns = this.settings.namespace; 57 | if (ns) { 58 | classes.push(`${ns}-${suffix}`); 59 | } 60 | 61 | return classes; 62 | } 63 | 64 | _buildElement() { 65 | this.el = document.createElement("div"); 66 | addClasses(this.el, this._buildClasses("zoom-pane")); 67 | 68 | const loaderEl = document.createElement("div"); 69 | addClasses(loaderEl, this._buildClasses("zoom-pane-loader")); 70 | this.el.appendChild(loaderEl); 71 | 72 | this.imgEl = document.createElement("img"); 73 | this.el.appendChild(this.imgEl); 74 | } 75 | 76 | _setImageURL(imageURL) { 77 | this.imgEl.setAttribute("src", imageURL); 78 | } 79 | 80 | _setImageSize(triggerWidth, triggerHeight) { 81 | this.imgEl.style.width = `${triggerWidth * this.settings.zoomFactor}px`; 82 | this.imgEl.style.height = `${triggerHeight * this.settings.zoomFactor}px`; 83 | } 84 | 85 | // `percentageOffsetX` and `percentageOffsetY` must be percentages 86 | // expressed as floats between `0' and `1`. 87 | setPosition(percentageOffsetX, percentageOffsetY, triggerRect) { 88 | const imgElWidth = this.imgEl.offsetWidth; 89 | const imgElHeight = this.imgEl.offsetHeight; 90 | const elWidth = this.el.offsetWidth; 91 | const elHeight = this.el.offsetHeight; 92 | 93 | const centreOfContainerX = elWidth / 2; 94 | const centreOfContainerY = elHeight / 2; 95 | 96 | const targetImgXToBeCentre = imgElWidth * percentageOffsetX; 97 | const targetImgYToBeCentre = imgElHeight * percentageOffsetY; 98 | 99 | let left = centreOfContainerX - targetImgXToBeCentre; 100 | let top = centreOfContainerY - targetImgYToBeCentre; 101 | 102 | const differenceBetweenContainerWidthAndImgWidth = elWidth - imgElWidth; 103 | const differenceBetweenContainerHeightAndImgHeight = elHeight - imgElHeight; 104 | const isContainerLargerThanImgX = differenceBetweenContainerWidthAndImgWidth > 0; 105 | const isContainerLargerThanImgY = differenceBetweenContainerHeightAndImgHeight > 0; 106 | 107 | const minLeft = isContainerLargerThanImgX ? differenceBetweenContainerWidthAndImgWidth / 2 : 0; 108 | const minTop = isContainerLargerThanImgY ? differenceBetweenContainerHeightAndImgHeight / 2 : 0; 109 | 110 | const maxLeft = isContainerLargerThanImgX 111 | ? differenceBetweenContainerWidthAndImgWidth / 2 112 | : differenceBetweenContainerWidthAndImgWidth; 113 | const maxTop = isContainerLargerThanImgY 114 | ? differenceBetweenContainerHeightAndImgHeight / 2 115 | : differenceBetweenContainerHeightAndImgHeight; 116 | 117 | if (this.el.parentElement === this.settings.inlineContainer) { 118 | // This may be needed in the future to deal with browser event 119 | // inconsistencies, but it's difficult to tell for sure. 120 | // let scrollX = isTouch ? 0 : window.scrollX; 121 | // let scrollY = isTouch ? 0 : window.scrollY; 122 | const scrollX = window.pageXOffset; 123 | const scrollY = window.pageYOffset; 124 | 125 | let inlineLeft = 126 | triggerRect.left + percentageOffsetX * triggerRect.width - elWidth / 2 + this.settings.inlineOffsetX + scrollX; 127 | let inlineTop = 128 | triggerRect.top + percentageOffsetY * triggerRect.height - elHeight / 2 + this.settings.inlineOffsetY + scrollY; 129 | 130 | if (this.settings.containInline) { 131 | if (inlineLeft < triggerRect.left + scrollX) { 132 | inlineLeft = triggerRect.left + scrollX; 133 | } else if (inlineLeft + elWidth > triggerRect.left + triggerRect.width + scrollX) { 134 | inlineLeft = triggerRect.left + triggerRect.width - elWidth + scrollX; 135 | } 136 | 137 | if (inlineTop < triggerRect.top + scrollY) { 138 | inlineTop = triggerRect.top + scrollY; 139 | } else if (inlineTop + elHeight > triggerRect.top + triggerRect.height + scrollY) { 140 | inlineTop = triggerRect.top + triggerRect.height - elHeight + scrollY; 141 | } 142 | } 143 | 144 | this.el.style.left = `${inlineLeft}px`; 145 | this.el.style.top = `${inlineTop}px`; 146 | } 147 | 148 | if (!this.settings.showWhitespaceAtEdges) { 149 | if (left > minLeft) { 150 | left = minLeft; 151 | } else if (left < maxLeft) { 152 | left = maxLeft; 153 | } 154 | 155 | if (top > minTop) { 156 | top = minTop; 157 | } else if (top < maxTop) { 158 | top = maxTop; 159 | } 160 | } 161 | 162 | this.imgEl.style.transform = `translate(${left}px, ${top}px)`; 163 | this.imgEl.style.webkitTransform = `translate(${left}px, ${top}px)`; 164 | } 165 | 166 | get _isInline() { 167 | const inline = this.settings.inline; 168 | 169 | return inline === true || (typeof inline === "number" && window.innerWidth <= inline); 170 | } 171 | 172 | _removeListenersAndResetClasses() { 173 | this.el.removeEventListener("animationend", this._completeShow); 174 | this.el.removeEventListener("animationend", this._completeHide); 175 | this.el.removeEventListener("webkitAnimationEnd", this._completeShow); 176 | this.el.removeEventListener("webkitAnimationEnd", this._completeHide); 177 | removeClasses(this.el, this.openClasses); 178 | removeClasses(this.el, this.closingClasses); 179 | } 180 | 181 | show(imageURL, triggerWidth, triggerHeight) { 182 | this._removeListenersAndResetClasses(); 183 | this.isShowing = true; 184 | 185 | addClasses(this.el, this.openClasses); 186 | 187 | if (this.imgEl.getAttribute("src") != imageURL) { 188 | addClasses(this.el, this.loadingClasses); 189 | this.imgEl.addEventListener("load", this._handleLoad); 190 | this._setImageURL(imageURL); 191 | } 192 | 193 | this._setImageSize(triggerWidth, triggerHeight); 194 | 195 | if (this._isInline) { 196 | this._showInline(); 197 | } else { 198 | this._showInContainer(); 199 | } 200 | 201 | if (this.HAS_ANIMATION) { 202 | this.el.addEventListener("animationend", this._completeShow); 203 | this.el.addEventListener("webkitAnimationEnd", this._completeShow); 204 | addClasses(this.el, this.openingClasses); 205 | } 206 | } 207 | 208 | _showInline() { 209 | this.settings.inlineContainer.appendChild(this.el); 210 | addClasses(this.el, this.inlineClasses); 211 | } 212 | 213 | _showInContainer() { 214 | this.settings.container.appendChild(this.el); 215 | } 216 | 217 | hide() { 218 | this._removeListenersAndResetClasses(); 219 | this.isShowing = false; 220 | 221 | if (this.HAS_ANIMATION) { 222 | this.el.addEventListener("animationend", this._completeHide); 223 | this.el.addEventListener("webkitAnimationEnd", this._completeHide); 224 | addClasses(this.el, this.closingClasses); 225 | } else { 226 | removeClasses(this.el, this.openClasses); 227 | removeClasses(this.el, this.inlineClasses); 228 | } 229 | } 230 | 231 | _completeShow() { 232 | this.el.removeEventListener("animationend", this._completeShow); 233 | this.el.removeEventListener("webkitAnimationEnd", this._completeShow); 234 | 235 | removeClasses(this.el, this.openingClasses); 236 | } 237 | 238 | _completeHide() { 239 | this.el.removeEventListener("animationend", this._completeHide); 240 | this.el.removeEventListener("webkitAnimationEnd", this._completeHide); 241 | 242 | removeClasses(this.el, this.openClasses); 243 | removeClasses(this.el, this.closingClasses); 244 | removeClasses(this.el, this.inlineClasses); 245 | 246 | this.el.style.left = ""; 247 | this.el.style.top = ""; 248 | 249 | // The window could have been resized above or below `inline` 250 | // limits since the ZoomPane was shown. Because of this, we 251 | // can't rely on `this._isInline` here. 252 | if (this.el.parentElement === this.settings.container) { 253 | this.settings.container.removeChild(this.el); 254 | } else if (this.el.parentElement === this.settings.inlineContainer) { 255 | this.settings.inlineContainer.removeChild(this.el); 256 | } 257 | } 258 | 259 | _handleLoad() { 260 | this.imgEl.removeEventListener("load", this._handleLoad); 261 | removeClasses(this.el, this.loadingClasses); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/js/injectBaseStylesheet.js: -------------------------------------------------------------------------------- 1 | /* UNMINIFIED RULES 2 | 3 | const RULES = ` 4 | @keyframes noop { 5 | 0% { zoom: 1; } 6 | } 7 | 8 | @-webkit-keyframes noop { 9 | 0% { zoom: 1; } 10 | } 11 | 12 | .drift-zoom-pane.drift-open { 13 | display: block; 14 | } 15 | 16 | .drift-zoom-pane.drift-opening, .drift-zoom-pane.drift-closing { 17 | animation: noop 1ms; 18 | -webkit-animation: noop 1ms; 19 | } 20 | 21 | .drift-zoom-pane { 22 | position: absolute; 23 | overflow: hidden; 24 | width: 100%; 25 | height: 100%; 26 | top: 0; 27 | left: 0; 28 | pointer-events: none; 29 | } 30 | 31 | .drift-zoom-pane-loader { 32 | display: none; 33 | } 34 | 35 | .drift-zoom-pane img { 36 | position: absolute; 37 | display: block; 38 | max-width: none; 39 | max-height: none; 40 | } 41 | 42 | .drift-bounding-box { 43 | position: absolute; 44 | pointer-events: none; 45 | } 46 | `; 47 | 48 | */ 49 | 50 | const RULES = 51 | ".drift-bounding-box,.drift-zoom-pane{position:absolute;pointer-events:none}@keyframes noop{0%{zoom:1}}@-webkit-keyframes noop{0%{zoom:1}}.drift-zoom-pane.drift-open{display:block}.drift-zoom-pane.drift-closing,.drift-zoom-pane.drift-opening{animation:noop 1ms;-webkit-animation:noop 1ms}.drift-zoom-pane{overflow:hidden;width:100%;height:100%;top:0;left:0}.drift-zoom-pane-loader{display:none}.drift-zoom-pane img{position:absolute;display:block;max-width:none;max-height:none}"; 52 | 53 | export default function injectBaseStylesheet() { 54 | if (document.querySelector(".drift-base-styles")) { 55 | return; 56 | } 57 | 58 | const styleEl = document.createElement("style"); 59 | styleEl.type = "text/css"; 60 | styleEl.classList.add("drift-base-styles"); 61 | 62 | styleEl.appendChild(document.createTextNode(RULES)); 63 | 64 | const head = document.head; 65 | head.insertBefore(styleEl, head.firstChild); 66 | } 67 | -------------------------------------------------------------------------------- /src/js/util/debounce.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // http://underscorejs.org/docs/underscore.html#section-83 3 | export default function debounce(func, wait, immediate) { 4 | let timeout; 5 | let args; 6 | let context; 7 | let timestamp; 8 | let result; 9 | 10 | const later = function () { 11 | const last = Date.now() - timestamp; 12 | 13 | if (last < wait && last >= 0) { 14 | timeout = setTimeout(later, wait - last); 15 | } else { 16 | timeout = null; 17 | 18 | if (!immediate) { 19 | result = func.apply(context, args); 20 | 21 | if (!timeout) { 22 | context = args = null; 23 | } 24 | } 25 | } 26 | }; 27 | 28 | return function () { 29 | context = this; 30 | args = arguments; 31 | timestamp = Date.now(); 32 | 33 | const callNow = immediate && !timeout; 34 | 35 | if (!timeout) { 36 | timeout = setTimeout(later, wait); 37 | } 38 | 39 | if (callNow) { 40 | result = func.apply(context, args); 41 | context = args = null; 42 | } 43 | 44 | return result; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /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 | 5 | export function isDOMElement(obj) { 6 | return HAS_DOM_2 7 | ? obj instanceof HTMLElement 8 | : obj && typeof obj === "object" && obj !== null && obj.nodeType === 1 && typeof obj.nodeName === "string"; 9 | } 10 | 11 | export function addClasses(el, classNames) { 12 | classNames.forEach(function (className) { 13 | el.classList.add(className); 14 | }); 15 | } 16 | 17 | export function removeClasses(el, classNames) { 18 | classNames.forEach(function (className) { 19 | el.classList.remove(className); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/js/util/throwIfMissing.js: -------------------------------------------------------------------------------- 1 | export default function throwIfMissing() { 2 | throw new Error("Missing parameter"); 3 | } 4 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import ZoomPane from "../src/js/ZoomPane"; 2 | 3 | export const mockEvent = { 4 | preventDefault: function () {}, 5 | }; 6 | 7 | export function defaultDriftConfig() { 8 | return { 9 | namespace: null, 10 | showWhitespaceAtEdges: false, 11 | containInline: false, 12 | inlineOffsetX: 0, 13 | inlineOffsetY: 0, 14 | inlineContainer: document.body, 15 | sourceAttribute: "data-zoom", 16 | zoomFactor: 3, 17 | paneContainer: document.body, 18 | inlinePane: 375, 19 | handleTouch: true, 20 | onShow: null, 21 | onHide: null, 22 | injectBaseStyles: true, 23 | hoverDelay: 0, 24 | touchDelay: 0, 25 | hoverBoundingBox: false, 26 | touchBoundingBox: false, 27 | boundingBoxContainer: document.body, 28 | passive: false, 29 | }; 30 | } 31 | 32 | export function zoomPaneOptions() { 33 | return { 34 | container: document.body, 35 | zoomFactor: 3, 36 | inline: 375, 37 | namespace: null, 38 | showWhitespaceAtEdges: false, 39 | containInline: false, 40 | inlineOffsetX: 0, 41 | inlineOffsetY: 0, 42 | }; 43 | } 44 | 45 | export function triggerOptions() { 46 | return { 47 | el: document.querySelector(".test-anchor"), 48 | zoomPane: new ZoomPane(zoomPaneOptions()), 49 | sourceAttribute: "data-zoom", 50 | handleTouch: true, 51 | onShow: null, 52 | onHide: null, 53 | hoverDelay: 0, 54 | touchDelay: 0, 55 | hoverBoundingBox: false, 56 | touchBoundingBox: false, 57 | namespace: null, 58 | zoomFactor: 3, 59 | boundingBoxContainer: document.body, 60 | passive: false, 61 | }; 62 | } 63 | 64 | export function boundingBoxOptions() { 65 | return { 66 | namespace: null, 67 | zoomFactor: 3, 68 | containerEl: document.querySelector(".test-anchor").offsetParent, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /test/testBoundingBox.js: -------------------------------------------------------------------------------- 1 | import BoundingBox from "../src/js/BoundingBox"; 2 | 3 | import { boundingBoxOptions } from "./helpers"; 4 | 5 | describe("BoundingBox", () => { 6 | it("returns an instance of `BoundingBox` when correctly instantiated", () => { 7 | const zoomPane = new BoundingBox(boundingBoxOptions()); 8 | 9 | expect(zoomPane.constructor).toBe(BoundingBox); 10 | }); 11 | 12 | it("requires `zoomFactor` option", () => { 13 | const opts = boundingBoxOptions(); 14 | delete opts.zoomFactor; 15 | 16 | expect(() => { 17 | new BoundingBox(opts); 18 | }).toThrowError(Error, "Missing parameter"); 19 | }); 20 | 21 | it("requires `containerEl` option", () => { 22 | const opts = boundingBoxOptions(); 23 | delete opts.containerEl; 24 | 25 | expect(() => { 26 | new BoundingBox(opts); 27 | }).toThrowError(Error, "Missing parameter"); 28 | }); 29 | 30 | it("builds its element", () => { 31 | const opts = boundingBoxOptions(); 32 | opts.namespace = "tb"; 33 | const zoomPane = new BoundingBox(opts); 34 | 35 | expect(zoomPane.el.classList.toString()).toBe("drift-bounding-box tb-bounding-box"); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/testDrift.js: -------------------------------------------------------------------------------- 1 | import Drift from "../src/js/Drift"; 2 | 3 | import { defaultDriftConfig } from "./helpers"; 4 | 5 | beforeEach(function () { 6 | const anchor = document.createElement("a"); 7 | anchor.classList.add("test-anchor"); 8 | anchor.setAttribute("href", "http://assets.imgix.net/test.png&w=400"); 9 | anchor.dataset.zoom = "http://assets.imgix.net/test.png&w=1200"; 10 | document.body.appendChild(anchor); 11 | }); 12 | 13 | afterEach(function () { 14 | const anchor = document.querySelector(".test-anchor"); 15 | 16 | document.body.removeChild(anchor); 17 | }); 18 | 19 | describe("Drift", () => { 20 | describe("core", () => { 21 | it("throws if no arguments are passed", () => { 22 | expect(() => { 23 | new Drift(); 24 | }).toThrowError(TypeError, "`new Drift` requires a DOM element as its first argument."); 25 | }); 26 | 27 | it("throws if the first argument is not a DOM element", () => { 28 | expect(() => { 29 | new Drift(".some-selector"); 30 | }).toThrowError(TypeError, "`new Drift` requires a DOM element as its first argument."); 31 | }); 32 | 33 | it("returns an instance of `Drift` when correctly instantiated", () => { 34 | const anchor = document.querySelector(".test-anchor"); 35 | const drift = new Drift(anchor); 36 | 37 | expect(drift.constructor).toBe(Drift); 38 | }); 39 | }); 40 | 41 | describe("configuration", () => { 42 | it("sets up settings object when no options are passed", () => { 43 | const anchor = document.querySelector(".test-anchor"); 44 | const drift = new Drift(anchor); 45 | 46 | expect(drift.settings).toBeDefined(); 47 | }); 48 | 49 | it("applies proper setting defaults when no options are passed", () => { 50 | const anchor = document.querySelector(".test-anchor"); 51 | const drift = new Drift(anchor); 52 | 53 | expect(drift.settings).toEqual(defaultDriftConfig()); 54 | }); 55 | 56 | it("accepts custom settings", () => { 57 | const anchor = document.querySelector(".test-anchor"); 58 | const drift = new Drift(anchor, { inlineOffsetX: 12, handleTouch: false, injectBaseStyles: false }); 59 | 60 | const expectedConfig = defaultDriftConfig(); 61 | expectedConfig.inlineOffsetX = 12; 62 | expectedConfig.handleTouch = false; 63 | expectedConfig.injectBaseStyles = false; 64 | 65 | expect(drift.settings).toEqual(expectedConfig); 66 | }); 67 | 68 | it("requires `paneContainer` setting when `inlinePane !== true`", () => { 69 | const anchor = document.querySelector(".test-anchor"); 70 | 71 | const conf = defaultDriftConfig(); 72 | conf.paneContainer = null; 73 | 74 | expect(() => { 75 | new Drift(anchor, conf); 76 | }).toThrowError(TypeError, "`paneContainer` must be a DOM element when `inlinePane !== true`"); 77 | }); 78 | 79 | it("requires `paneContainer` to be a DOM element when `inlinePane !== true`", () => { 80 | const anchor = document.querySelector(".test-anchor"); 81 | 82 | const conf = defaultDriftConfig(); 83 | conf.paneContainer = ".not-a-dom-element"; 84 | 85 | expect(() => { 86 | new Drift(anchor, conf); 87 | }).toThrowError(TypeError, "`paneContainer` must be a DOM element when `inlinePane !== true`"); 88 | }); 89 | 90 | it("allows `paneContainer` to be null when `inlinePane === true`", () => { 91 | const anchor = document.querySelector(".test-anchor"); 92 | 93 | const conf = defaultDriftConfig(); 94 | conf.paneContainer = null; 95 | conf.inlinePane = true; 96 | 97 | expect(() => { 98 | new Drift(anchor, conf); 99 | }).not.toThrow(); 100 | }); 101 | }); 102 | 103 | describe("public methods", () => { 104 | describe("#setZoomImageURL", () => { 105 | it("updates the `src` attribute of the ZoomPane's `imgEl`", () => { 106 | const anchor = document.querySelector(".test-anchor"); 107 | const drift = new Drift(anchor); 108 | 109 | drift.setZoomImageURL("test!"); 110 | 111 | expect(drift.zoomPane.imgEl.getAttribute("src")).toBe("test!"); 112 | }); 113 | }); 114 | 115 | describe("#enable", () => { 116 | it("sets `trigger.enabled` to `true`", () => { 117 | const anchor = document.querySelector(".test-anchor"); 118 | const drift = new Drift(anchor); 119 | 120 | drift.trigger.enabled = false; 121 | 122 | drift.enable(); 123 | 124 | expect(drift.trigger.enabled).toBe(true); 125 | }); 126 | }); 127 | 128 | describe("#disable", () => { 129 | it("sets `trigger.enabled` to `false`", () => { 130 | const anchor = document.querySelector(".test-anchor"); 131 | const drift = new Drift(anchor); 132 | 133 | drift.disable(); 134 | 135 | expect(drift.trigger.enabled).toBe(false); 136 | }); 137 | }); 138 | 139 | describe("#destroy", () => { 140 | it("should hide and unbind events", function () { 141 | const anchor = document.querySelector(".test-anchor"); 142 | const drift = new Drift(anchor); 143 | 144 | const hideSpy = spyOn(drift.trigger, "_hide"); 145 | const unbindEventsSpy = spyOn(drift.trigger, "_unbindEvents"); 146 | 147 | drift.destroy(); 148 | 149 | expect(hideSpy).toHaveBeenCalled(); 150 | expect(unbindEventsSpy).toHaveBeenCalled(); 151 | }); 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /test/testTrigger.js: -------------------------------------------------------------------------------- 1 | import Trigger from "../src/js/Trigger"; 2 | 3 | import { mockEvent, triggerOptions } from "./helpers"; 4 | 5 | beforeEach(function () { 6 | const anchor = document.createElement("a"); 7 | anchor.classList.add("test-anchor"); 8 | anchor.setAttribute("href", "http://assets.imgix.net/test.png&w=400"); 9 | anchor.dataset.zoom = "http://assets.imgix.net/test.png&w=1200"; 10 | document.body.appendChild(anchor); 11 | }); 12 | 13 | afterEach(function () { 14 | const anchor = document.querySelector(".test-anchor"); 15 | 16 | document.body.removeChild(anchor); 17 | }); 18 | 19 | describe("Trigger", () => { 20 | it("returns an instance of `Trigger` when correctly instantiated", () => { 21 | const trigger = new Trigger(triggerOptions()); 22 | 23 | expect(trigger.constructor).toBe(Trigger); 24 | }); 25 | 26 | it("requires `el` option", () => { 27 | const opts = triggerOptions(); 28 | delete opts.el; 29 | 30 | expect(() => { 31 | new Trigger(opts); 32 | }).toThrowError(Error, "Missing parameter"); 33 | }); 34 | 35 | it("requires `zoomPane` option", () => { 36 | const opts = triggerOptions(); 37 | delete opts.zoomPane; 38 | 39 | expect(() => { 40 | new Trigger(opts); 41 | }).toThrowError(Error, "Missing parameter"); 42 | }); 43 | 44 | it("requires `sourceAttribute` option", () => { 45 | const opts = triggerOptions(); 46 | delete opts.sourceAttribute; 47 | 48 | expect(() => { 49 | new Trigger(opts); 50 | }).toThrowError(Error, "Missing parameter"); 51 | }); 52 | 53 | it("requires `handleTouch` option", () => { 54 | const opts = triggerOptions(); 55 | delete opts.handleTouch; 56 | 57 | expect(() => { 58 | new Trigger(opts); 59 | }).toThrowError(Error, "Missing parameter"); 60 | }); 61 | 62 | it("executes the `onShow` callback when present", () => { 63 | let called = false; 64 | // e 65 | function showCallback() { 66 | called = true; 67 | } 68 | const opts = triggerOptions(); 69 | opts.onShow = showCallback; 70 | 71 | const trigger = new Trigger(opts); 72 | trigger._show(mockEvent); 73 | 74 | expect(called).toBe(true); 75 | }); 76 | 77 | it("executes the `onHide` callback when present", () => { 78 | let called = false; 79 | function hideCallback() { 80 | called = true; 81 | } 82 | 83 | const opts = triggerOptions(); 84 | opts.onHide = hideCallback; 85 | 86 | const trigger = new Trigger(opts); 87 | trigger._show(mockEvent); 88 | trigger._hide(mockEvent); 89 | 90 | expect(called).toBe(true); 91 | }); 92 | 93 | it("executes touchstart on mobile when handleTouch is set to true", () => { 94 | const opts = triggerOptions(); 95 | opts.handleTouch = true; 96 | const spy = spyOn(Trigger.prototype, "_handleEntry"); 97 | const trigger = new Trigger(opts); 98 | 99 | const event = new Event("touchstart"); 100 | 101 | trigger.settings.el.dispatchEvent(event); 102 | expect(spy).toHaveBeenCalled(); 103 | }); 104 | 105 | it("does not execute touchstart on mobile when handleTouch is set to false", () => { 106 | const opts = triggerOptions(); 107 | opts.handleTouch = false; 108 | const spy = spyOn(Trigger.prototype, "_handleEntry"); 109 | const trigger = new Trigger(opts); 110 | 111 | const event = new Event("touchstart"); 112 | 113 | trigger.settings.el.dispatchEvent(event); 114 | expect(spy).not.toHaveBeenCalled(); 115 | }); 116 | 117 | it("uses passive listeners for touchstart on mobile when passive is set to true", () => { 118 | const opts = triggerOptions(); 119 | opts.passive = true; 120 | 121 | const trigger = new Trigger(opts); 122 | 123 | const event = new Event("touchstart", { cancelable: true }); 124 | 125 | trigger.settings.el.dispatchEvent(event); 126 | expect(event.defaultPrevented).toBeFalse(); 127 | }); 128 | 129 | it("does not use passive listeners for touchstart on mobile when passive is set to false", () => { 130 | const opts = triggerOptions(); 131 | opts.passive = false; 132 | 133 | const trigger = new Trigger(opts); 134 | const event = new Event("touchstart", { cancelable: true }); 135 | 136 | trigger.settings.el.dispatchEvent(event); 137 | expect(event.defaultPrevented).toBeTrue(); 138 | }); 139 | 140 | it("does not use passive event listeners on mouse events", () => { 141 | const opts = triggerOptions(); 142 | opts.passive = false; 143 | 144 | const trigger = new Trigger(opts); 145 | const event = new Event("mouseenter", { cancelable: true }); 146 | 147 | trigger.settings.el.dispatchEvent(event); 148 | expect(event.defaultPrevented).toBeTrue(); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/testZoomPane.js: -------------------------------------------------------------------------------- 1 | import ZoomPane from "../src/js/ZoomPane"; 2 | 3 | import { zoomPaneOptions } from "./helpers"; 4 | 5 | describe("ZoomPane", () => { 6 | it("returns an instance of `ZoomPane` when correctly instantiated", () => { 7 | const zoomPane = new ZoomPane(zoomPaneOptions()); 8 | 9 | expect(zoomPane.constructor).toBe(ZoomPane); 10 | }); 11 | 12 | it("requires `zoomFactor` option", () => { 13 | const opts = zoomPaneOptions(); 14 | delete opts.zoomFactor; 15 | 16 | expect(() => { 17 | new ZoomPane(opts); 18 | }).toThrowError(Error, "Missing parameter"); 19 | }); 20 | 21 | it("requires `inline` option", () => { 22 | const opts = zoomPaneOptions(); 23 | delete opts.inline; 24 | 25 | expect(() => { 26 | new ZoomPane(opts); 27 | }).toThrowError(Error, "Missing parameter"); 28 | }); 29 | 30 | it("requires `showWhitespaceAtEdges` option", () => { 31 | const opts = zoomPaneOptions(); 32 | delete opts.showWhitespaceAtEdges; 33 | 34 | expect(() => { 35 | new ZoomPane(opts); 36 | }).toThrowError(Error, "Missing parameter"); 37 | }); 38 | 39 | it("requires `containInline` option", () => { 40 | const opts = zoomPaneOptions(); 41 | delete opts.containInline; 42 | 43 | expect(() => { 44 | new ZoomPane(opts); 45 | }).toThrowError(Error, "Missing parameter"); 46 | }); 47 | 48 | it("builds its element", () => { 49 | const opts = zoomPaneOptions(); 50 | opts.namespace = "tb"; 51 | const zoomPane = new ZoomPane(opts); 52 | 53 | expect(zoomPane.el.classList.toString()).toBe("drift-zoom-pane tb-zoom-pane"); 54 | }); 55 | 56 | it("creates an `img` element inside its main element", () => { 57 | const zoomPane = new ZoomPane(zoomPaneOptions()); 58 | 59 | expect(zoomPane.imgEl.parentElement).toBe(zoomPane.el); 60 | }); 61 | 62 | it("sets the `imgEl` `src` attribute when `#show` is called", () => { 63 | const zoomPane = new ZoomPane(zoomPaneOptions()); 64 | const testSrc = "http://assets.imgix.net/unsplash/pretty2.jpg"; 65 | 66 | zoomPane.show(testSrc, 400); 67 | 68 | expect(zoomPane.imgEl.getAttribute("src")).toBe(testSrc); 69 | }); 70 | 71 | it("sets the `imgEl` width attribute when `#show` is called", () => { 72 | const zoomPane = new ZoomPane(zoomPaneOptions()); 73 | const testSrc = "http://assets.imgix.net/unsplash/pretty2.jpg"; 74 | const triggerWidth = 400; 75 | 76 | zoomPane.show(testSrc, triggerWidth); 77 | 78 | expect(zoomPane.imgEl.style.width).toBe(`${triggerWidth * zoomPane.settings.zoomFactor}px`); 79 | }); 80 | 81 | it("does not add the drift-loading class after first hover", () => { 82 | const zoomPane = new ZoomPane(zoomPaneOptions()); 83 | const testSrc = "http://assets.imgix.net/unsplash/pretty2.jpg"; 84 | const triggerWidth = 400; 85 | 86 | zoomPane.show(testSrc, triggerWidth); 87 | expect(zoomPane.el.classList.toString()).toContain("drift-loading"); 88 | 89 | zoomPane.hide(); 90 | setTimeout(function () { 91 | expect(zoomPane.el.classList.toString()).not.toContain("drift-loading"); 92 | }, 1000); 93 | 94 | zoomPane.show(testSrc, triggerWidth); 95 | setTimeout(function () { 96 | expect(zoomPane.el.classList.toString()).not.toContain("drift-loading"); 97 | }, 1000); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | var context = require.context("./test", true, /\.js$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const env = require("yargs").argv.env; // use --env with webpack 2 3 | const ClosurePlugin = require("closure-webpack-plugin"); 4 | 5 | const isProduction = env === "build"; 6 | 7 | let libraryName = "Drift"; 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/Drift-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 | optimization: { 41 | concatenateModules: false, 42 | minimize: isProduction, 43 | minimizer: [ 44 | new ClosurePlugin( 45 | { 46 | mode: "AGGRESSIVE_BUNDLE", 47 | test: /^(?!.*tests\.webpack).*$/, 48 | }, 49 | { languageIn: "ECMASCRIPT6", languageOut: "ECMASCRIPT5" } 50 | ), 51 | ], 52 | }, 53 | }; 54 | 55 | return config; 56 | } 57 | 58 | module.exports = config; 59 | --------------------------------------------------------------------------------