├── .browserslistrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── logo.png ├── .gitignore ├── .huskyrc.json ├── .npmignore ├── .storybook ├── main.js ├── manager.js └── theme.js ├── LICENSE ├── README.md ├── babel.config.js ├── config └── storybook │ ├── addons.js │ ├── config.js │ └── preview-head.html ├── jest.config.js ├── package.json ├── postcss.config.js ├── src └── components │ └── VueImageKit.vue ├── stories └── index.stories.js ├── tests └── unit │ ├── .eslintrc.js │ ├── __snapshots__ │ └── vue-image-kit.spec.js.snap │ └── vue-image-kit.spec.js ├── vue-image-kit.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: circleci/node 6 | 7 | working_directory: ~/app 8 | 9 | orbs: 10 | codecov: codecov/codecov@1.0.4 11 | 12 | jobs: 13 | commitlint: 14 | docker: 15 | - image: williamlauze/circleci-commitlint:latest 16 | working_directory: ~/app 17 | 18 | steps: 19 | - checkout 20 | - run: 21 | name: Lint commit messages 22 | command: /bin/sh /.scripts/commitlint_range.sh 23 | 24 | checkout: 25 | <<: *defaults 26 | 27 | steps: 28 | - restore_cache: 29 | name: Restore Repository Cache 30 | keys: 31 | - repo-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }} 32 | - repo-{{ .Branch }} 33 | - repo-master 34 | - repo- 35 | 36 | - checkout 37 | 38 | - save_cache: 39 | name: Save Repository Cache 40 | key: repo-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }} 41 | paths: 42 | - . 43 | 44 | - persist_to_workspace: 45 | root: . 46 | paths: 47 | - . 48 | 49 | install-dependencies: 50 | <<: *defaults 51 | 52 | steps: 53 | - attach_workspace: 54 | at: . 55 | 56 | - restore_cache: 57 | name: Restore yarn Package Cache 58 | keys: 59 | - yarn-{{ checksum "yarn.lock" }} 60 | - yarn- 61 | 62 | - run: 63 | name: Install Dependencies 64 | command: yarn install 65 | 66 | - save_cache: 67 | name: Save yarn Package Cache 68 | key: yarn-{{ checksum "yarn.lock" }} 69 | paths: 70 | - node_modules 71 | 72 | - persist_to_workspace: 73 | root: . 74 | paths: 75 | - node_modules 76 | 77 | lint: 78 | <<: *defaults 79 | 80 | steps: 81 | - attach_workspace: 82 | at: . 83 | 84 | - run: 85 | name: Add Yarn Binary Folder To $PATH 86 | command: export PATH="$PATH:`yarn global bin`" 87 | 88 | - run: 89 | name: Lint JS 90 | command: yarn lint 91 | 92 | test: 93 | <<: *defaults 94 | 95 | steps: 96 | - attach_workspace: 97 | at: . 98 | 99 | - run: 100 | name: Add Yarn Binary Folder To $PATH 101 | command: export PATH="$PATH:`yarn global bin`" 102 | 103 | - run: 104 | name: Run tests 105 | command: yarn test:unit 106 | 107 | coverage: 108 | <<: *defaults 109 | 110 | steps: 111 | - attach_workspace: 112 | at: . 113 | 114 | - run: 115 | name: Add Yarn Binary Folder To $PATH 116 | command: export PATH="$PATH:`yarn global bin`" 117 | 118 | - run: 119 | name: Generate and upload coverage report 120 | command: yarn test:unit --coverage --coverageReporters=json --collectCoverage=true --coverageDirectory=/home/circleci/app/coverage 121 | 122 | - store_test_results: 123 | path: ~/app/coverage 124 | 125 | - store_artifacts: 126 | path: ~/app/coverage 127 | destination: ~/app/coverage 128 | 129 | - codecov/upload: 130 | token: 84850ad5-b40a-4ebb-975b-ee2c5b99d28e 131 | file: ~/app/coverage/*.json 132 | flags: frontend 133 | 134 | workflows: 135 | version: 2 136 | 137 | main: 138 | jobs: 139 | - checkout 140 | - commitlint: 141 | filters: 142 | branches: 143 | ignore: master 144 | - install-dependencies: 145 | requires: 146 | - checkout 147 | - test: 148 | requires: 149 | - install-dependencies 150 | - coverage: 151 | requires: 152 | - install-dependencies 153 | - lint: 154 | requires: 155 | - install-dependencies -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | '**/tests/unit/*.spec.{j,t}s?(x)' 21 | ], 22 | env: { 23 | jest: true 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: guastallaigor 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 8 | 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 21 | 22 | **Expected behavior** 23 | 24 | 25 | **Screenshots** 26 | 27 | 28 | **Environment:** 29 | - OS: 30 | - Browser: 31 | - Build environment (i.e. NodeJS): 32 | 33 | **Suggestion(s) for fixing this issue** 34 | 35 | 36 | **Additional context** 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 8 | 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guastallaigor/vue-image-kit/53d1645b0cd9aeb97b48a34e44dcda4e8fdb2a4f/.github/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Static 27 | storybook-static 28 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-push": [ 4 | "npm run lint && npm run test:unit" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # User & editor settings 2 | .editorconfig 3 | .huskyrc 4 | .vscode 5 | jest.config.js 6 | postcss.config.js 7 | 8 | # Build files 9 | .circleci 10 | .github 11 | .storybook 12 | commitlint.config.js 13 | node_modules 14 | 15 | # Documentation files 16 | storybook-static 17 | config 18 | stories 19 | tests 20 | coverage 21 | .eslintrc.js 22 | .huskyrc.json -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.js'], 3 | logLevel: 'debug', 4 | addons: [ 5 | '@storybook/addon-controls', 6 | '@storybook/addon-docs', 7 | '@storybook/addon-a11y' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons' 2 | import theme from './theme' 3 | 4 | addons.setConfig({ theme }) 5 | -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming/create' 2 | 3 | export default create({ 4 | base: 'light', 5 | 6 | colorPrimary: '#41b883', 7 | colorSecondary: '#060a0e', 8 | 9 | // UI 10 | appBg: '#152232', 11 | appContentBg: '#0b1622', 12 | appBorderColor: '#CBD5E0', 13 | appBorderRadius: 4, 14 | 15 | // Typography 16 | fontBase: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Open Sans", sans-serif', 17 | fontCode: 'monospace', 18 | 19 | // Text colors 20 | textColor: '#94A3B8', 21 | textInverseColor: 'black', 22 | 23 | // Toolbar default and active colors 24 | barTextColor: 'silver', 25 | barSelectedColor: '#FFFFFF', 26 | barBg: '#152232', 27 | 28 | // Form colors 29 | inputBg: '#152232', 30 | inputBorder: '#152232', 31 | inputTextColor: '#FFFFFF', 32 | inputBorderRadius: 4, 33 | 34 | brandTitle: 'Vue Image Kit', 35 | brandUrl: 'https://github.com/guastallaigor/vue-image-kit', 36 | brandImage: 'https://raw.githubusercontent.com/guastallaigor/vue-image-kit/master/.github/logo.png' 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Igor Guastalla de Lima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Vue Image Kit

4 |
5 |

6 | Vue.js Image Kit Component with Lazy Load built in and Responsive Images 7 |

8 |

9 | The inspiration comes from this and a talk from @derevandal in @femug 10 |

11 | 12 |

13 | Build 14 | Code Coverage 15 | Total Downloads 16 | Latest Release 17 | Style standard 18 | Netlify Status 19 |

20 | 21 | > **Note:** 22 | > This is an unofficial project. 23 | > I do not work or am I affiliated with Image Kit 24 | 25 | ## Demo 26 | 27 | [![Edit Checkbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-image-kit-keeh1) 28 | 29 | ## Storybook 30 | 31 | Go to https://vue-image-kit.netlify.app 32 | 33 | ## How does it work 34 | 35 | This component uses the [Image Kit Real-time URL-based image transformation](https://imagekit.io/features/image-resize-smart-crop-responsive-dpr-client-hints), so you will need to have your images over [Image Kit](https://imagekit.io/) for it to work 36 | 37 | For more informations about [Image Kit](https://imagekit.io/), consult their website 38 | 39 | ## How to install 40 | 41 | ### npm 42 | 43 | ```bash 44 | $ npm install vue-image-kit --save 45 | ``` 46 | 47 | ### yarn (recommended) 48 | 49 | ```bash 50 | $ yarn add vue-image-kit 51 | ``` 52 | 53 | ## Quick start 54 | 55 | ### Vue.js 56 | 57 | You can import in your `main.js` file 58 | 59 | ```js 60 | import Vue from "vue"; 61 | import VueImageKit from "vue-image-kit"; 62 | 63 | Vue.use(VueImageKit); 64 | ``` 65 | 66 | Or locally in any component 67 | 68 | ```js 69 | import { VueImageKit } from "vue-image-kit"; 70 | // In v0.2+ you don't need the brackets above 71 | 72 | export default { 73 | components: { 74 | VueImageKit, 75 | }, 76 | }; 77 | ``` 78 | 79 | ### Nuxt.js 80 | 81 | You can import as a Nuxt.js plugin 82 | 83 | `~/plugins/vue-image-kit.js` 84 | 85 | ```js 86 | import Vue from "vue"; 87 | import VueImageKit from "vue-image-kit"; 88 | 89 | Vue.use(VueImageKit); 90 | ``` 91 | 92 | and then import it in your `nuxt.config.js` file 93 | 94 | ```js 95 | plugins: [{ src: "~/plugins/vue-image-kit", mode: "client" }]; 96 | ``` 97 | 98 | ## Basic usage 99 | 100 | ```html 101 | 110 | ``` 111 | 112 | ## Props 113 | 114 | | Property name | Type | Default | Required | Description | 115 | | --------------- | ------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 116 | | hash | String | null | true | Images hash. Example: Take this image -> https://ik.imagekit.io/6xhf1gnexgdgk/lion_BllLvaqVn.jpg, the hash is '6xhf1gnexgdgk' | 117 | | src | String | null | true | Images source. Example: Take this image -> https://ik.imagekit.io/6xhf1gnexgdgk/lion_BllLvaqVn.jpg, the source is 'lion_BllLvaqVn.jpg' | 118 | | placeholder | String | '' | false | Images placeholder. Here you can pass a link | 119 | | backgroundColor | String | '' | false | Background color of the images placeholder | 120 | | srcset | Array | [320, 480, 800] | false | Array of numbers that will define the images srcset attribute. Each number correspond to one of the images width | 121 | | sizes | Array | [] | false | Array of numbers that will define the images sizes attribute. Each number correspond to one of the images max-width. Empty by default, which gets each of the images srcset prop | 122 | | defaultSize | Number | 1080 | true | Images default size. Must be larger than the largest srcset and sizes | 123 | | customTransform | String | '' | false | Use this to append any extra image kit transform that you want | 124 | | width | Number | null | false | Images width. Width number in pixels. It will be set with inline style | 125 | | height | Number | null | false | Images height. Height number in pixels. It will be set with inline style | 126 | | alt | String | '' | false | Images alt attribute | 127 | | lazyLoad | Boolean | true | false | If you don't want to use the built in lazy load, you can set this to false, then the image will not be lazy loaded, and you can setup your own lazy load | 128 | 129 | ## Development 130 | 131 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/guastallaigor/vue-image-kit/issues) 132 | 133 | > **Note**: Contributions are very welcomed, however is very important to open a new issue using the issue template **before** you start working on anything, so we can discuss it before hand 134 | 135 | Fork the project and enter this commands in your terminal 136 | 137 | ```sh 138 | git clone https://github.com/YOUR_GITHUB_USERNAME/vue-image-kit.git 139 | cd vue-image-kit 140 | yarn 141 | ``` 142 | 143 | ### Storybook 144 | 145 | For visual testing, this project contains storybook which you can run by doing the next command 146 | 147 | ```sh 148 | $ yarn storybook 149 | ``` 150 | 151 | ### Jest 152 | 153 | Before making the PR, if you changed something that needs to be tested, please make the tests inside the `tests/unit` folder 154 | 155 | To run the tests, you can use the next command 156 | 157 | ```sh 158 | $ yarn test:unit 159 | ``` 160 | 161 | ### Commitlint 162 | 163 | This project follows the [commitlint](https://github.com/conventional-changelog/commitlint) guidelines, with minor changes 164 | 165 | You can commit using `npm run commit` to help you with that 166 | 167 | There's a `pre-push` hook that runs all the unit tests before you can push it 168 | 169 | If an error occurs, you can use the `npm run commit:retry` command that runs the previous `npm run commit` that you already filled 170 | 171 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/C1C63QCB8) 172 | 173 | ## License 174 | 175 | MIT © [guastallaigor](https://github.com/guastallaigor/vue-image-kit/blob/master/LICENSE) 176 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | } 4 | -------------------------------------------------------------------------------- /config/storybook/addons.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import '@storybook/addon-knobs/register' 3 | import '@storybook/addon-links/register' 4 | import '@storybook/addon-notes/register' 5 | import '@storybook/addon-backgrounds/register' 6 | -------------------------------------------------------------------------------- /config/storybook/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { configure } from '@storybook/vue' 3 | 4 | const req = require.context('../../stories', true, /.stories.js$/) 5 | 6 | function loadStories () { 7 | req.keys().forEach(filename => req(filename)) 8 | } 9 | 10 | configure(loadStories, module) 11 | -------------------------------------------------------------------------------- /config/storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest', 3 | coverageDirectory: './coverage/', 4 | collectCoverage: true 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-image-kit", 3 | "version": "0.2.2", 4 | "private": false, 5 | "description": "Vue.js Image Kit Component with Lazy Load built in and Responsive Images", 6 | "homepage": "https://github.com/guastallaigor/vue-image-kit#readme", 7 | "author": "Igor Guastalla de Lima", 8 | "scripts": { 9 | "lint": "vue-cli-service lint", 10 | "storybook": "start-storybook -p 6006 -s public", 11 | "build-storybook": "build-storybook", 12 | "test:unit": "vue-cli-service test:unit", 13 | "test:watch": "vue-cli-service test:unit --watch", 14 | "commit": "git-cz", 15 | "commit:retry": "git-cz --retry", 16 | "commitmsg": "commitlint -e" 17 | }, 18 | "dependencies": { 19 | "vue": "^2.6.12" 20 | }, 21 | "devDependencies": { 22 | "@commitlint/cli": "^11.0.0", 23 | "@commitlint/config-conventional": "^11.0.0", 24 | "@storybook/addon-a11y": "^6.1.6", 25 | "@storybook/addon-actions": "^6.1.6", 26 | "@storybook/addon-backgrounds": "^6.1.6", 27 | "@storybook/addon-controls": "^6.1.6", 28 | "@storybook/addon-docs": "^6.1.6", 29 | "@storybook/addon-links": "^6.1.6", 30 | "@storybook/addons": "^6.1.6", 31 | "@storybook/theming": "^6.1.6", 32 | "@storybook/vue": "^6.1.6", 33 | "@vue/cli-plugin-babel": "^4.5.9", 34 | "@vue/cli-plugin-eslint": "^4.5.9", 35 | "@vue/cli-plugin-unit-jest": "^4.5.9", 36 | "@vue/cli-service": "^4.5.9", 37 | "@vue/eslint-config-standard": "^5.1.2", 38 | "@vue/test-utils": "^1.1.1", 39 | "babel-eslint": "^10.1.0", 40 | "commitizen": "^4.2.2", 41 | "core-js": "3.8.0", 42 | "eslint": "^7.14.0", 43 | "eslint-plugin-import": "^2.22.1", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-promise": "^4.2.1", 46 | "eslint-plugin-standard": "^5.0.0", 47 | "eslint-plugin-vue": "^5.0.0s", 48 | "husky": "^4.3.0", 49 | "jest-canvas-mock": "^2.3.0", 50 | "vue-cli-plugin-storybook": "^2.0.0", 51 | "vue-template-compiler": "^2.6.12" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/guastallaigor/vue-image-kit/issues" 55 | }, 56 | "files": [ 57 | "src/*", 58 | "*.json", 59 | "*.js" 60 | ], 61 | "keywords": [ 62 | "vue", 63 | "vuejs", 64 | "image", 65 | "lazy-load", 66 | "kit", 67 | "component", 68 | "image-kit" 69 | ], 70 | "license": "MIT", 71 | "main": "./vue-image-kit.js", 72 | "repository": { 73 | "type": "git", 74 | "url": "https://github.com:guastallaigor/vue-image-kit.git" 75 | }, 76 | "commitlint": { 77 | "extends": [ 78 | "@commitlint/config-conventional" 79 | ], 80 | "rules": { 81 | "subject-case": [ 82 | 0, 83 | "never", 84 | "sentence-case" 85 | ], 86 | "subject-full-stop": [ 87 | 0, 88 | "never", 89 | "." 90 | ] 91 | } 92 | }, 93 | "config": { 94 | "commitizen": { 95 | "path": "./node_modules/cz-conventional-changelog" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/VueImageKit.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 202 | 203 | 228 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import VueImageKit from '../src/components/VueImageKit.vue' 2 | 3 | export default { 4 | title: 'VueImageKit', 5 | component: VueImageKit, 6 | parameters: { 7 | a11y: { 8 | element: '#root', 9 | config: {}, 10 | options: {}, 11 | manual: true, 12 | }, 13 | controls: { hideNoControlsWarning: true }, 14 | docs: { 15 | inlineStories: true 16 | }, 17 | backgrounds: [ 18 | { name: 'Blue', value: 'blue' }, 19 | { name: 'Green', value: 'green' }, 20 | { name: 'Yellow', value: 'yellow' }, 21 | { name: 'Orange', value: 'orange' }, 22 | { name: 'Red', value: 'red' }, 23 | { name: 'Purple', value: 'purple' }, 24 | { name: 'Black', value: 'black' }, 25 | { name: 'White', value: 'white', default: true } 26 | ] 27 | }, 28 | argTypes: { 29 | hash: { 30 | control: 'text' 31 | }, 32 | src: { 33 | control: 'text' 34 | }, 35 | placeholder: { 36 | control: 'text' 37 | }, 38 | backgroundColor: { 39 | control: 'text' 40 | }, 41 | srcset: { 42 | control: 'array' 43 | }, 44 | sizes: { 45 | control: 'array' 46 | }, 47 | defaultSize: { 48 | control: 'number' 49 | }, 50 | customTransform: { 51 | control: 'text' 52 | }, 53 | width: { 54 | control: 'number' 55 | }, 56 | height: { 57 | control: 'number' 58 | }, 59 | alt: { 60 | control: 'text' 61 | }, 62 | lazyLoad: { 63 | control: 'boolean' 64 | } 65 | } 66 | } 67 | 68 | export const DefaultComponent = () => ({ 69 | components: { VueImageKit }, 70 | props: { 71 | hash: { 72 | type: String, 73 | default: '6xhf1gnexgdgk' 74 | }, 75 | src: { 76 | type: String, 77 | default: 'lion_BllLvaqVn.jpg' 78 | }, 79 | placeholder: { 80 | type: String, 81 | default: '' 82 | }, 83 | backgroundColor: { 84 | type: String, 85 | default: 'Background' 86 | }, 87 | srcset: { 88 | type: Array, 89 | default: [320, 480, 800] 90 | }, 91 | sizes: { 92 | type: Array, 93 | default: [] 94 | }, 95 | defaultSize: { 96 | type: Number, 97 | default: 1080 98 | }, 99 | customTransform: { 100 | type: String, 101 | default: '' 102 | }, 103 | width: { 104 | type: Number, 105 | default: 1400 106 | }, 107 | height: { 108 | type: Number, 109 | default: 800 110 | }, 111 | alt: { 112 | type: String, 113 | default: '' 114 | }, 115 | lazyLoad: { 116 | type: Boolean, 117 | default: true 118 | } 119 | }, 120 | template: `
121 |
 
122 | 136 |
` 137 | }) 138 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/vue-image-kit.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`When I create the VueImageKit component should match snapshot 1`] = ` 4 |
5 | 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /tests/unit/vue-image-kit.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, shallowMount, createLocalVue } from '@vue/test-utils' 2 | import VueImageKit from '../../src/components/VueImageKit' 3 | import 'jest-canvas-mock' 4 | 5 | function setupIntersectionObserverMock ({ 6 | observe = () => null, 7 | unobserve = () => null 8 | } = {}) { 9 | class IntersectionObserver { 10 | observe = observe 11 | unobserve = unobserve 12 | disconnect () {} 13 | } 14 | Object.defineProperty( 15 | window, 16 | 'IntersectionObserver', 17 | { writable: true, configurable: true, value: IntersectionObserver } 18 | ) 19 | Object.defineProperty( 20 | global, 21 | 'IntersectionObserver', 22 | { writable: true, configurable: true, value: IntersectionObserver } 23 | ) 24 | } 25 | 26 | describe('When I create the VueImageKit component', () => { 27 | const item = { hash: '6xhf1gnexgdgk', src: 'lion_BllLvaqVn.jpg' } 28 | let timeout 29 | 30 | beforeEach(() => { 31 | timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms)) 32 | setupIntersectionObserverMock() 33 | }) 34 | 35 | const createComponent = (propsData = {}) => { 36 | return shallowMount(VueImageKit, { propsData }) 37 | } 38 | 39 | it('should validate all props', () => { 40 | const consoleLog = console.error 41 | console.error = jest.fn() 42 | const wrapper = createComponent(item) 43 | const hash = wrapper.vm.$options.props.hash 44 | expect(hash.required).toBeTruthy() 45 | expect(hash.type).toBe(String) 46 | expect(hash.default).toBe(undefined) 47 | const src = wrapper.vm.$options.props.src 48 | expect(src.required).toBeTruthy() 49 | expect(src.type).toBe(String) 50 | expect(src.default).toBe(undefined) 51 | const placeholder = wrapper.vm.$options.props.placeholder 52 | expect(placeholder.required).toBeFalsy() 53 | expect(placeholder.type).toBe(String) 54 | expect(placeholder.default).toBe('') 55 | const backgroundColor = wrapper.vm.$options.props.backgroundColor 56 | expect(backgroundColor.required).toBeFalsy() 57 | expect(backgroundColor.type).toBe(String) 58 | expect(backgroundColor.default).toBe('') 59 | const srcset = wrapper.vm.$options.props.srcset 60 | expect(srcset.required).toBeFalsy() 61 | expect(srcset.type).toBe(Array) 62 | const defaultSrcset = { default: () => [320, 480, 800] } 63 | expect(JSON.stringify(srcset.default)).toEqual(JSON.stringify(defaultSrcset.default)) 64 | const sizes = wrapper.vm.$options.props.sizes 65 | expect(sizes.required).toBeFalsy() 66 | expect(sizes.type).toBe(Array) 67 | expect(JSON.stringify(sizes.default)).toBeFalsy() 68 | const defaultSize = wrapper.vm.$options.props.defaultSize 69 | expect(defaultSize.required).toBeFalsy() 70 | expect(defaultSize.type).toBe(Number) 71 | expect(defaultSize.default).toBe(1080) 72 | const customTransform = wrapper.vm.$options.props.customTransform 73 | expect(customTransform.required).toBeFalsy() 74 | expect(customTransform.type).toBe(String) 75 | expect(customTransform.default).toBe('') 76 | const width = wrapper.vm.$options.props.width 77 | expect(width.required).toBeFalsy() 78 | expect(width.type).toBe(Number) 79 | expect(width.default).toBe(null) 80 | const height = wrapper.vm.$options.props.height 81 | expect(height.required).toBeFalsy() 82 | expect(height.type).toBe(Number) 83 | expect(height.default).toBe(null) 84 | const alt = wrapper.vm.$options.props.alt 85 | expect(alt.required).toBeFalsy() 86 | expect(alt.type).toBe(String) 87 | expect(alt.default).toBe('') 88 | const lazyLoad = wrapper.vm.$options.props.lazyLoad 89 | expect(lazyLoad.required).toBeFalsy() 90 | expect(lazyLoad.type).toBe(Boolean) 91 | expect(lazyLoad.default).toBe(true) 92 | const component = wrapper.find('.vue-image-kit') 93 | expect(component.exists()).toBe(true) 94 | console.error = consoleLog 95 | }) 96 | 97 | it('should have a width, height and a placeholder', () => { 98 | const wrapper = createComponent({ ...item, width: 300, height: 300, placeholder: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png' }) 99 | const found = wrapper.find('.vue-image-kit > .vue-image-kit__placeholder > img') 100 | expect(found.exists()).toBe(true) 101 | const expected = { 102 | src: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png', 103 | alt: 'Placeholder', 104 | style: 'width: 300px; height: 300px;' 105 | } 106 | expect(JSON.stringify(found.attributes())).toBe(JSON.stringify(expected)) 107 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 108 | expect(main.exists()).toBe(true) 109 | expect(main.attributes().style).toBe('width: 300px; height: 300px;') 110 | }) 111 | 112 | it('should have a alt attribute', () => { 113 | const wrapper = createComponent({ ...item, alt: 'Lion image' }) 114 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 115 | expect(main.exists()).toBe(true) 116 | expect(main.attributes().alt).toBe('Lion image') 117 | }) 118 | 119 | it('should have background color in the placeholder', async () => { 120 | const wrapper = createComponent({ ...item, width: 300, height: 300, placeholder: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png', backgroundColor: '#f40' }) 121 | const placeholder = wrapper.find('.vue-image-kit > .vue-image-kit__placeholder') 122 | expect(placeholder.exists()).toBe(true) 123 | expect(placeholder.attributes().style).toBe('background-color: rgb(255, 68, 0);') // #f40 124 | }) 125 | 126 | it('should have different srcset from default', async () => { 127 | const wrapper = createComponent({ ...item, srcset: [400, 600, 900] }) 128 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 129 | expect(main.exists()).toBe(true) 130 | wrapper.vm.triggerIntersection({ isIntersecting: true }) 131 | await wrapper.vm.$nextTick() 132 | const expected = 'https://ik.imagekit.io/6xhf1gnexgdgk/tr:w-400/lion_BllLvaqVn.jpg 400w, https://ik.imagekit.io/6xhf1gnexgdgk/tr:w-600/lion_BllLvaqVn.jpg 600w, https://ik.imagekit.io/6xhf1gnexgdgk/tr:w-900/lion_BllLvaqVn.jpg 900w' 133 | expect(main.attributes().srcset).toBe(expected) 134 | }) 135 | 136 | it('should have different sizes from default', async () => { 137 | const wrapper = createComponent({ ...item, srcset: [200, 250, 300] }) 138 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 139 | expect(main.exists()).toBe(true) 140 | wrapper.vm.triggerIntersection({ isIntersecting: true }) 141 | await wrapper.vm.$nextTick() 142 | const expected = '(max-width: 200px) 200px, (max-width: 250px) 250px, (max-width: 300px) 300px 1080px' 143 | expect(main.attributes().sizes).toBe(expected) 144 | }) 145 | 146 | it('should have different sizes and srcset from default', async () => { 147 | const wrapper = createComponent({ ...item, srcset: [210, 220, 230], sizes: [210, 220, 230] }) 148 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 149 | expect(main.exists()).toBe(true) 150 | wrapper.vm.triggerIntersection({ isIntersecting: true }) 151 | await wrapper.vm.$nextTick() 152 | const expected = '(max-width: 210px) 210px, (max-width: 220px) 220px, (max-width: 230px) 230px 1080px' 153 | expect(main.attributes().sizes).toBe(expected) 154 | const expectedSrcset = 'https://ik.imagekit.io/6xhf1gnexgdgk/tr:w-210/lion_BllLvaqVn.jpg 210w, https://ik.imagekit.io/6xhf1gnexgdgk/tr:w-220/lion_BllLvaqVn.jpg 220w, https://ik.imagekit.io/6xhf1gnexgdgk/tr:w-230/lion_BllLvaqVn.jpg 230w' 155 | expect(main.attributes().srcset).toBe(expectedSrcset) 156 | }) 157 | 158 | it('should have different default size', () => { 159 | const wrapper = createComponent({ ...item, defaultSize: 1366 }) 160 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 161 | expect(main.exists()).toBe(true) 162 | expect(main.attributes().sizes).toContain('1366px') 163 | }) 164 | 165 | it('should not have lazy load', async () => { 166 | const wrapper = createComponent({ ...item, lazyLoad: false }) 167 | const main = wrapper.find('.vue-image-kit') 168 | expect(main.exists()).toBe(true) 169 | await timeout(1) 170 | expect(main.classes()).toContain('vue-image-kit--loaded') 171 | const placeholder = wrapper.find('.vue-image-kit__placeholder') 172 | expect(placeholder.exists()).toBe(false) 173 | expect(wrapper.vm.showCanvas).toBe(false) 174 | expect(wrapper.vm.$refs.normalLoad).toBeInstanceOf(HTMLImageElement) 175 | }) 176 | 177 | it('should have a custom transform', async () => { 178 | // https://imagekit.io/features/advanced-image-manipulation-blur-rotate-crop-background-radius 179 | const customTransform = 'c-at_max,bl-1:r-20,bg-FFCFA1' 180 | const wrapper = createComponent({ ...item, customTransform }) 181 | const main = wrapper.find('.vue-image-kit > .vue-image-kit__img') 182 | expect(main.exists()).toBe(true) 183 | wrapper.vm.triggerIntersection({ isIntersecting: true }) 184 | await wrapper.vm.$nextTick() 185 | expect(main.attributes().srcset).toContain(customTransform) 186 | }) 187 | 188 | it('should disconnect the observer on destroy', () => { 189 | const wrapper = createComponent({ ...item }) 190 | wrapper.vm.timeOut = 300 191 | expect(wrapper.vm.observer).toStrictEqual(new IntersectionObserver({ observe: () => jest.fn(), unobserve: () => jest.fn() })) 192 | wrapper.destroy() 193 | expect(wrapper.vm.observer).toStrictEqual(new IntersectionObserver({ observe: () => jest.fn(), unobserve: () => jest.fn() })) 194 | const disconnect = jest.fn() 195 | expect(JSON.stringify(wrapper.vm.observer.disconnect)).toBe(JSON.stringify(disconnect)) 196 | expect(wrapper.exists()).toBeFalsy() 197 | }) 198 | 199 | it('should clear the timeout on disconnect', (done) => { 200 | const wrapper = createComponent({ ...item, width: 300, height: 300, placeholder: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png' }) 201 | expect(wrapper.exists()).toBe(true) 202 | wrapper.destroy() 203 | setTimeout(() => { 204 | expect(wrapper.vm.timeOut).toBe(null) 205 | done() 206 | }, 500) 207 | }) 208 | 209 | it('should trigger intersection', async () => { 210 | const localVue = createLocalVue() 211 | const wrapper = mount(VueImageKit, { 212 | propsData: { ...item, width: 300, height: 300, placeholder: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png' }, 213 | localVue, 214 | stubs: { 215 | transition: false 216 | } 217 | }) 218 | wrapper.vm.observer.disconnect = jest.fn() 219 | expect(wrapper.exists()).toBe(true) 220 | wrapper.vm.triggerIntersection({ isIntersecting: true }) 221 | expect(wrapper.vm.timeOut).toBeNull() 222 | expect(wrapper.vm.showCanvas).toBeFalsy() 223 | const img = wrapper.find('.vue-image-kit__img') 224 | expect(img.exists()).toBe(true) 225 | const placeholder = wrapper.find('.vue-image-kit__placeholder') 226 | expect(placeholder.exists()).toBe(true) 227 | expect(wrapper.vm.$el.querySelector('.vue-image-kit__placeholder')).toBeDefined() 228 | wrapper.vm.$el.querySelector('.vue-image-kit__img').onload() 229 | await wrapper.vm.$nextTick() 230 | const placeholderAgain = wrapper.find('.vue-image-kit__placeholder') 231 | expect(placeholderAgain.exists()).toBe(false) 232 | expect(wrapper.vm.timeOut).not.toBeNull() 233 | await timeout(350) 234 | expect(wrapper.vm.$el.querySelector('.vue-image-kit__placeholder')).toBeNull() 235 | }) 236 | 237 | it('should trigger intersection - method testing', () => { 238 | const localVue = createLocalVue() 239 | const wrapper = mount(VueImageKit, { 240 | propsData: { ...item, width: 300, height: 300, placeholder: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png' }, 241 | localVue, 242 | stubs: { 243 | transition: false 244 | } 245 | }) 246 | const observe = jest.fn() 247 | const unobserve = jest.fn() 248 | const disconnect = jest.fn() 249 | const takeRecords = jest.fn() 250 | window.IntersectionObserver = jest.fn(() => ({ 251 | observe, 252 | unobserve, 253 | disconnect, 254 | takeRecords 255 | })) 256 | expect(wrapper.exists()).toBe(true) 257 | wrapper.vm.initLazyLoad() 258 | wrapper.vm.triggerIntersection = jest.fn() 259 | wrapper.vm.setImgAttributes = jest.fn() 260 | expect(wrapper.vm.observer).not.toBeNull() 261 | expect(wrapper.vm.showCanvas).toBeTruthy() 262 | const img = wrapper.find('.vue-image-kit__img') 263 | expect(img.exists()).toBe(true) 264 | const placeholder = wrapper.find('.vue-image-kit__placeholder') 265 | expect(placeholder.exists()).toBe(true) 266 | expect(wrapper.vm.$el.querySelector('.vue-image-kit__placeholder')).toBeDefined() 267 | const observerCallback = window.IntersectionObserver.mock.calls[0][0] 268 | const mockEntry = { isIntersecting: true } 269 | observerCallback([mockEntry]) 270 | expect(wrapper.vm.observer.observe).toHaveBeenCalled() 271 | expect(wrapper.vm.triggerIntersection).toHaveBeenCalledTimes(1) 272 | }) 273 | 274 | it('should not trigger intersection', () => { 275 | const localVue = createLocalVue() 276 | const wrapper = mount(VueImageKit, { 277 | propsData: { ...item, width: 300, height: 300, placeholder: 'https://ik.imagekit.io/6xhf1gnexgdgk/igor2_HJhiHMa54.png' }, 278 | localVue, 279 | stubs: { 280 | transition: false 281 | } 282 | }) 283 | const observe = jest.fn() 284 | const unobserve = jest.fn() 285 | const disconnect = jest.fn() 286 | const takeRecords = jest.fn() 287 | window.IntersectionObserver = jest.fn(() => ({ 288 | observe, 289 | unobserve, 290 | disconnect, 291 | takeRecords 292 | })) 293 | expect(wrapper.exists()).toBe(true) 294 | wrapper.vm.initLazyLoad() 295 | wrapper.vm.setImgAttributes = jest.fn() 296 | wrapper.vm.triggerIntersection = jest.fn() 297 | expect(wrapper.vm.observer).not.toBeNull() 298 | expect(wrapper.vm.showCanvas).toBeTruthy() 299 | const observerCallback = window.IntersectionObserver.mock.calls[0][0] 300 | const mockEntry = { isIntersecting: false } 301 | observerCallback([mockEntry]) 302 | expect(wrapper.vm.setImgAttributes).not.toHaveBeenCalled() 303 | expect(wrapper.vm.observer.disconnect).not.toHaveBeenCalled() 304 | }) 305 | 306 | it('should match snapshot', () => { 307 | const wrapper = createComponent({ 308 | ...item, 309 | placeholder: '', 310 | backgroundColor: '', 311 | width: null, 312 | height: null, 313 | alt: '', 314 | srcset: [320, 480, 800], 315 | sizes: [], 316 | defaultSize: 1080, 317 | customTransform: '' 318 | }) 319 | 320 | expect(wrapper.html()).toMatchSnapshot() 321 | }) 322 | }) 323 | -------------------------------------------------------------------------------- /vue-image-kit.js: -------------------------------------------------------------------------------- 1 | // Import vue component 2 | import VueImageKit from './src/components/VueImageKit.vue' 3 | 4 | // install function executed by Vue.use() 5 | const install = function (Vue) { 6 | if (install.installed) return 7 | install.installed = true 8 | Vue.component('VueImageKit', VueImageKit) 9 | } 10 | 11 | // Create module definition for Vue.use() 12 | const plugin = { 13 | install 14 | } 15 | 16 | // To auto-install when vue is found 17 | // eslint-disable-next-line no-redeclare 18 | /* global window, global */ 19 | let GlobalVue = null 20 | 21 | if (typeof window !== 'undefined') { 22 | GlobalVue = window.Vue 23 | } else if (typeof global !== 'undefined') { 24 | GlobalVue = global.Vue 25 | } 26 | 27 | if (GlobalVue) { 28 | GlobalVue.use(plugin) 29 | } 30 | 31 | // Inject install function into component - allows component 32 | // to be registered via Vue.use() as well as Vue.component() 33 | VueImageKit.install = install 34 | 35 | // Export component by default 36 | export default VueImageKit 37 | 38 | // Export single (backwards compatibility) 39 | export { VueImageKit } 40 | --------------------------------------------------------------------------------