├── .babelrc ├── .editorconfig ├── .github └── workflows │ └── deploy-demo.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── App.vue ├── app.js ├── index.ejs └── main.js ├── jest.config.js ├── package.json ├── publish-demo.js ├── src ├── VueSelectImage.vue ├── plugin.js └── vue-select-image.css ├── test ├── vue-select-image-logic.spec.js └── vue-select-image.spec.js ├── webpack.config.dist.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "entry" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy-demo.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Demo 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy_gh_pages: 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | with: 16 | persist-credentials: false 17 | fetch-depth: 0 18 | 19 | - name: Use node 12 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 12.x 23 | 24 | - name: Get yarn cache 25 | id: yarn-cache 26 | run: echo "::set-output name=dir::$(yarn cache dir)" 27 | 28 | - name: Cache Node.js modules 29 | uses: actions/cache@v1 30 | with: 31 | path: ${{ steps.yarn-cache.outputs.dir }} 32 | key: ${{ runner.OS }}-yarn-${{ hashFiles('**/yarn.lock') }} 33 | restore-keys: | 34 | ${{ runner.OS }}-yarn- 35 | 36 | - name: Install dependencies 37 | run: yarn 38 | 39 | - name: Build Demo UI 40 | run: yarn build:demo 41 | 42 | - name: GitHub Pages Deploy 43 | uses: peaceiris/actions-gh-pages@v3 44 | with: 45 | personal_token: ${{ secrets.PERSONAL_TOKEN }} 46 | publish_dir: ./dist-demo 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | test/coverage 4 | npm-debug.log 5 | yarn-error.log 6 | dist-demo/ 7 | dist/ 8 | 9 | # Editor directories and files 10 | .idea 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: '12' 3 | branches: 4 | only: 5 | - master 6 | - /^greenkeeper/.*$/ 7 | - /^feature/.*$/ 8 | - /^renovate/.*$/ 9 | cache: 10 | yarn: true 11 | directories: 12 | - node_modules 13 | install: 14 | - yarn install 15 | script: 16 | - yarn run test 17 | after_script: 18 | - npm install -g codecov 19 | - codecov 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to vue-select-image 2 | 3 | Looking to contribute something? **Here's how you can help.** 4 | 5 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone in the community. 6 | 7 | ## Using the issue tracker 8 | 9 | The issue tracker is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests), but please respect the following 10 | restrictions: 11 | 12 | * Please **do not** use the issue tracker for personal support requests. Please [email me](mailto:mazipanneh@gmail.com) or [send me a tweet](https://twitter.com/Maz_Ipan) as they are better places to get help. 13 | 14 | * Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. 15 | 16 | ## Bug reports 17 | 18 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful, so thanks! 19 | 20 | Guidelines for bug reports: 21 | 22 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 23 | 24 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository. 25 | 26 | 3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/). 27 | 28 | 29 | A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS experience the problem? Do other browsers show the bug differently? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 30 | 31 | Example: 32 | 33 | > Short and descriptive example bug report title 34 | > 35 | > A summary of the issue and the browser/OS environment in which it occurs. If 36 | > suitable, include the steps required to reproduce the bug. 37 | > 38 | > 1. This is the first step 39 | > 2. This is the second step 40 | > 3. Further steps, etc. 41 | > 42 | > `` - a link to the reduced test case 43 | > 44 | > Any other information you want to share that is relevant to the issue being 45 | > reported. This might include the lines of code that you have identified as 46 | > causing the bug, and potential solutions (and your opinions on their 47 | > merits). 48 | 49 | 50 | ## Feature requests 51 | 52 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. 53 | 54 | 55 | ## Pull requests 56 | 57 | Good pull requests-patches, improvements, new features-are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. 58 | 59 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. 60 | 61 | Please adhere to the [coding guidelines](#code-guidelines) used throughout the project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). 62 | 63 | Adhering to the following process is the best way to get your work included in the project: 64 | 65 | 1. [Fork](https://help.github.com/fork-a-repo/) the project, clone your fork, 66 | and configure the remotes: 67 | 68 | ```bash 69 | # Clone your fork of the repo into the current directory 70 | git clone https://github.com// 71 | # Navigate to the newly cloned directory 72 | cd 73 | # Assign the original repo to a remote called "upstream" 74 | git remote add upstream https://github.com/mazipan/vue-currency-filter 75 | ``` 76 | 77 | 2. If you cloned a while ago, get the latest changes from upstream: 78 | 79 | ```bash 80 | git checkout master 81 | git pull upstream master 82 | ``` 83 | 84 | 3. Create a new topic branch (off the main project development branch) to 85 | contain your feature, change, or fix: 86 | 87 | ```bash 88 | git checkout -b 89 | ``` 90 | 91 | 4. Make sure to update, or add to the tests when appropriate. **Patches and 92 | features will not be accepted without tests.** Run `npm test` to check that 93 | all tests pass after you've made changes. 94 | 95 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 96 | 97 | ```bash 98 | git pull [--rebase] upstream master 99 | ``` 100 | 101 | 6. Push your topic branch up to your fork: 102 | 103 | ```bash 104 | git push origin 105 | ``` 106 | 107 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 108 | with a clear title and description against the `master` branch. 109 | 110 | **IMPORTANT**: By submitting a patch, you agree to allow the project owners to 111 | license your work under the terms of the [MIT License](LICENSE). 112 | 113 | 114 | ## Code guidelines 115 | 116 | ### HTML 117 | 118 | - Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags). 119 | 120 | ### JS 121 | 122 | - No semicolons (in client-side JS) 123 | - 2 spaces (no tabs) 124 | - Don't use jQuery (no "$" allowed) 125 | 126 | ### Checking code 127 | 128 | Run `npm run dev` before committing to ensure your changes follow our coding standards. 129 | 130 | 131 | ## License 132 | 133 | By contributing your code, you agree to license your contribution under the [MIT License](LICENSE). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Irfan Maulana 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 | # :white_check_mark: Vue Select Image 2 | 3 | > Vue 2.x component for selecting image from list 4 | 5 | [![License](https://img.shields.io/github/license/mazipan/vue-select-image.svg?maxAge=3600)](https://github.com/mazipan/vue-select-image) ![minified](https://badgen.net/bundlephobia/minzip/vue-select-image) [![version](https://img.shields.io/npm/v/vue-select-image.svg)](https://www.npmjs.com/package/vue-select-image) 6 | [![downloads](https://img.shields.io/npm/dt/vue-select-image.svg)](https://www.npmjs.com/package/vue-select-image) [![Travis](https://img.shields.io/travis/mazipan/vue-select-image.svg)](https://travis-ci.org/mazipan/vue-select-image) 7 | [![codecov](https://codecov.io/gh/mazipan/vue-select-image/branch/master/graph/badge.svg)](https://codecov.io/gh/mazipan/vue-select-image) 8 | 9 | ## Demo 10 | 11 | [https://mazipan.github.io/vue-select-image/](https://mazipan.github.io/vue-select-image/) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | # Yarn 17 | yarn add vue-select-image 18 | 19 | # NPM 20 | npm i vue-select-image --save 21 | ``` 22 | 23 | ## How to use 24 | 25 | ### Import 26 | 27 | ```javascript 28 | import VueSelectImage from 'vue-select-image' 29 | // add stylesheet 30 | require('vue-select-image/dist/vue-select-image.css') 31 | ``` 32 | 33 | ### Register components 34 | 35 | ```javascript 36 | components: { VueSelectImage } 37 | ``` 38 | 39 | 40 | ### Register as global component 41 | 42 | ```javascript 43 | Vue.use(VueSelectImage) 44 | ``` 45 | 46 | ### Sample Array Image 47 | 48 | ```javascript 49 | [{ 50 | id: '1', 51 | src: 'https://unsplash.it/200?random', 52 | alt: 'Alt Image 1' 53 | }, { 54 | id: '2', 55 | src: 'https://unsplash.it/200?random', 56 | alt: 'Alt Image 2' 57 | }, { 58 | id: '2', 59 | src: 'https://unsplash.it/200?random', 60 | alt: 'Alt Image 2', 61 | disabled: true 62 | }] 63 | ``` 64 | 65 | | Field | Description | 66 | |------------------------|---------------------------------------------------| 67 | | id | Unique id for each image, will also set for id attribute on image DOM | 68 | | src | Src attribute for image | 69 | | alt | Alt attribute for image | 70 | | disabled | Image disabled, can not be select | 71 | 72 | ### Template 73 | 74 | #### Single Selection 75 | 76 | ```html 77 | 80 | 81 | ``` 82 | 83 | `onselectimage` will return emitted with parameter object image selected 84 | 85 | #### Multiple Selection 86 | 87 | ```html 88 | 93 | 94 | ``` 95 | 96 | `onselectmultipleimage` will return emitted with parameter list of object images selected 97 | 98 | ### Available Props 99 | 100 | | Attribute | Type | Default | Description | 101 | |-----------------|------------------|------------------|-------------------------------------- | 102 | | :dataImages | Array | [] | Array of images that will be shown | 103 | | :selectedImages | Array | [] | Array of initial selected images | 104 | | :isMultiple | Boolean | false | Flag to enable multiple selection | 105 | | :useLabel | Boolean | false | Flag to enable showing alt as label | 106 | | :rootClass | String | vue-select-image | Class for root element of this component | 107 | | :activeClass | String | --selected | Class for active state, will concat with :rootClass | 108 | | :h | String | auto | Height of images, ex: '50px' | 109 | | :w | String | auto | Width of images, ex: '50px' | 110 | | :limit | Number | 0 | To set maximum images can be select | 111 | 112 | ### Available Events 113 | 114 | | Events Attr | Return | 115 | |------------------------|---------------------------------------------------| 116 | | @onselectimage | Object image selected | 117 | | @onselectmultipleimage | Array of object image has been selected | 118 | | @onreachlimit | When the length of selected images reach the limit | 119 | 120 | ### Useful Methods (from v1.6.0) 121 | 122 | Sometimes you need to access our internal methods via `$refs`, you need to know this methods: 123 | 124 | | Methods Name | Use for | 125 | |----------------------------------|--------------------------------------------------- | 126 | | removeFromSingleSelected() | Reset selected image in single selection mode | 127 | | removeFromMultipleSelected() | Remove from selected list in multiple selection mode | 128 | | resetMultipleSelection(id) | Reset all selected list in multiple selection mode | 129 | | isExistInArray(id) | Will return object image if exist, undefined if not exist | 130 | 131 | ### Example 132 | 133 | Example can be found here : 134 | 135 | - [App.vue](https://github.com/mazipan/vue-select-image/blob/master/src/App.vue) 136 | - [app.js](https://github.com/mazipan/vue-select-image/blob/master/src/app.js) 137 | 138 | ### Credit 139 | 140 | Thanks for inspiration : [https://github.com/rvera/image-picker](https://github.com/rvera/image-picker) 141 | 142 | ## Support me 143 | 144 | - Via [trakteer](https://trakteer.id/mazipan) 145 | - Direct support, [send me an email](mailto:mazipanneh@gmail.com) 146 | 147 | ### Contributing 148 | 149 | If you'd like to contribute, head to the [contributing guidelines](/CONTRIBUTING.md). Inside you'll find directions for opening issues, coding standards, and notes on development. 150 | 151 | **Hope will usefull for you all.** 152 | 153 | Copyright © 2017 Built with ❤️ by Irfan Maulana 154 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 273 | 274 | 275 | 276 | 444 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | import VueSelectImage from 'src/VueSelectImage.vue' 2 | 3 | export default { 4 | name: 'app', 5 | components: { 6 | VueSelectImage, 7 | }, 8 | data () { 9 | return { 10 | title: '✅ Vue Select Image', 11 | subtitle: 'Vue 2.x component for selecting image from list', 12 | imageSelected: { 13 | id: '', 14 | src: '', 15 | alt: '' 16 | }, 17 | imageMultipleSelected: [], 18 | dataImages: [{ 19 | id: '1', 20 | src: 'http://placekitten.com/200/200', 21 | alt: 'jQuery' 22 | }, { 23 | id: '2', 24 | src: 'http://placekitten.com/200/200', 25 | alt: 'Angular' 26 | }, { 27 | id: '3', 28 | src: 'http://placekitten.com/200/200', 29 | alt: 'Vue.js' 30 | }, { 31 | id: '4', 32 | src: 'http://placekitten.com/200/200', 33 | alt: 'React' 34 | }, { 35 | id: '5', 36 | src: 'http://placekitten.com/200/200', 37 | alt: 'I am disabled', 38 | disabled: true 39 | }], 40 | initialSelected: [ 41 | { 42 | id: '2', 43 | src: 'http://placekitten.com/200/200', 44 | alt: 'Angular' 45 | }, 46 | { 47 | id: '3', 48 | src: 'http://placekitten.com/200/200', 49 | alt: 'Vue.js' 50 | } 51 | ], 52 | templateSingle: ` 53 | 54 | 56 | 57 | `, 58 | templateMultiple: ` 59 | 60 | 64 | 65 | ` 66 | } 67 | }, 68 | methods: { 69 | onSelectImage: function (data) { 70 | console.log('fire event onSelectImage: ', data) 71 | this.imageSelected = data 72 | }, 73 | onSelectMultipleImage: function (data) { 74 | console.log('fire event onSelectMultipleImage: ', data) 75 | this.imageMultipleSelected = data 76 | }, 77 | onReachLimit: function () { 78 | alert('Reach limit...') 79 | }, 80 | onUnselectSingleImage: function () { 81 | this.$refs['single-select-image'].removeFromSingleSelected() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /demo/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ✅ Vue Select Image By Irfan Maulana 6 | 7 | 8 | <% for (var chunk in htmlWebpackPlugin.files.css) { %> 9 | 10 | <% } %> 11 | 12 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> 13 | 14 | <% } %> 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | import Ads from 'vue-google-adsense' 5 | import VueHighlightJS from 'vue-highlightjs' 6 | 7 | /* global process */ 8 | if (process.env.NODE_ENV !== 'production') { 9 | Vue.config.devtools = true 10 | } 11 | 12 | Vue.use(require('vue-script2')) 13 | Vue.use(Ads.Adsense) 14 | Vue.use(Ads.InArticleAdsense) 15 | Vue.use(Ads.InFeedAdsense) 16 | 17 | Vue.use(VueHighlightJS) 18 | 19 | new Vue({ 20 | el: '#app', 21 | template: '', 22 | components: {App} 23 | }) 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | collectCoverageFrom: [ 4 | 'src/VueSelectImage.vue', 5 | '!**/node_modules/**' 6 | ], 7 | coverageDirectory: 'test/coverage', 8 | moduleNameMapper: { 9 | '^@/(.*)$': '/$1', 10 | '^vue$': 'vue/dist/vue.common.js' 11 | }, 12 | moduleFileExtensions: [ 13 | 'js', 14 | 'vue', 15 | 'json' 16 | ], 17 | transform: { 18 | '^.+\\.js$': '/node_modules/babel-jest', 19 | '.*\\.(vue)$': '/node_modules/vue-jest' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-select-image", 3 | "description": "Vue 2 Component for selecting image from list", 4 | "version": "1.9.0", 5 | "license": "MIT", 6 | "author": "Irfan Maulana (https://github.com/mazipan/)", 7 | "private": false, 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --open", 10 | "build": "cross-env NODE_ENV=production webpack --mode production --progress --hide-modules --config webpack.config.dist.js", 11 | "build:demo": "cross-env NODE_ENV=production webpack --mode production --progress --hide-modules", 12 | "publish:demo": "node ./publish-demo.js", 13 | "test": "./node_modules/.bin/jest --coverage", 14 | "dist": "rm -rf dist && rm -rf dist-demo && npm run build-demo && npm run build-lib && npm run publish-demo" 15 | }, 16 | "main": "dist/vue-select-image.js", 17 | "files": [ 18 | "dist/vue-select-image.js", 19 | "dist/vue-select-image.js.gz", 20 | "dist/vue-select-image.css", 21 | "dist/vue-select-image.css.gz" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mazipan/vue-select-image.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/mazipan/vue-select-image/issues" 29 | }, 30 | "keywords": [ 31 | "vue select image", 32 | "vue image select", 33 | "vue image picker", 34 | "vue2 image picker", 35 | "vue2 image select" 36 | ], 37 | "dependencies": { 38 | "vue": "^2.6.11" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.10.2", 42 | "@babel/preset-env": "^7.10.2", 43 | "avoriaz": "^6.3.0", 44 | "babel-jest": "^26.0.1", 45 | "babel-loader": "^8.1.0", 46 | "compression-webpack-plugin": "^4.0.0", 47 | "cross-env": "^7.0.2", 48 | "css-loader": "^3.5.3", 49 | "file-loader": "^6.0.0", 50 | "gh-pages": "3.0.0", 51 | "html-webpack-plugin": "^4.3.0", 52 | "jest": "^26.0.1", 53 | "mini-css-extract-plugin": "0.9.0", 54 | "node-sass": "^4.14.1", 55 | "optimize-css-assets-webpack-plugin": "5.0.3", 56 | "sass-loader": "^8.0.2", 57 | "terser-webpack-plugin": "^3.0.3", 58 | "vue-google-adsense": "^1.8.1", 59 | "vue-highlightjs": "1.3.3", 60 | "vue-jest": "^3.0.5", 61 | "vue-loader": "^15.9.2", 62 | "vue-script2": "^2.1.0", 63 | "vue-template-compiler": "^2.6.11", 64 | "webpack": "^4.43.0", 65 | "webpack-cli": "^3.3.11", 66 | "webpack-dev-server": "^3.11.0", 67 | "webpack-serve": "^3.2.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /publish-demo.js: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages'); 2 | const BRANCH = 'gh-pages'; 3 | const TODAY = new Date().toLocaleString(); 4 | const FOLDER_DIST = 'dist-demo' 5 | 6 | console.log(`start publishing folder ${FOLDER_DIST} to ${BRANCH}`); 7 | 8 | ghpages.publish(FOLDER_DIST, { 9 | branch: BRANCH, 10 | message: `Publishing folder ${FOLDER_DIST} to ${BRANCH} in ${TODAY}` 11 | }, () => { 12 | console.log(`done publishing folder ${FOLDER_DIST} to ${BRANCH}`); 13 | }); 14 | -------------------------------------------------------------------------------- /src/VueSelectImage.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import VueSelectImage from './VueSelectImage.vue' 2 | 3 | const plugin = { 4 | install: Vue => { 5 | Vue.component(VueSelectImage.name, VueSelectImage) 6 | } 7 | } 8 | 9 | VueSelectImage.install = plugin.install 10 | 11 | export default VueSelectImage -------------------------------------------------------------------------------- /src/vue-select-image.css: -------------------------------------------------------------------------------- 1 | 2 | .vue-select-image__wrapper { 3 | overflow: auto; 4 | list-style-image: none; 5 | list-style-position: outside; 6 | list-style-type: none; 7 | padding: 0px; 8 | margin: 0px; 9 | } 10 | 11 | .vue-select-image__item { 12 | margin: 0px 12px 12px 0px; 13 | float: left; 14 | } 15 | 16 | .vue-select-image__thumbnail{ 17 | cursor: pointer; 18 | padding: 6px; 19 | border: 1px solid #dddddd; 20 | 21 | display: block; 22 | padding: 4px; 23 | line-height: 20px; 24 | border: 1px solid #ddd; 25 | 26 | -webkit-border-radius: 4px; 27 | -moz-border-radius: 4px; 28 | border-radius: 4px; 29 | 30 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); 31 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); 32 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); 33 | 34 | -webkit-transition: all 0.2s ease-in-out; 35 | -moz-transition: all 0.2s ease-in-out; 36 | -o-transition: all 0.2s ease-in-out; 37 | transition: all 0.2s ease-in-out; 38 | } 39 | 40 | .vue-select-image__thumbnail--selected{ 41 | background: #0088cc; 42 | } 43 | 44 | .vue-select-image__thumbnail--disabled{ 45 | background: #b9b9b9; 46 | cursor: not-allowed; 47 | } 48 | 49 | .vue-select-image__thumbnail--disabled > .vue-select-image__img{ 50 | opacity: .5; 51 | } 52 | 53 | .vue-select-image__img{ 54 | -webkit-user-drag: none; 55 | display: block; 56 | max-width: 100%; 57 | margin-right: auto; 58 | margin-left: auto; 59 | } 60 | 61 | .vue-select-image__lbl{ 62 | line-height: 3; 63 | } 64 | 65 | @media only screen and (min-width: 1200px) { 66 | .vue-select-image__item { 67 | margin-left: 30px; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/vue-select-image-logic.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import Vue from 'vue' 3 | import VueSelectImage from '../src/VueSelectImage.vue' 4 | 5 | let vm = null 6 | const dataImages = [{ 7 | id: '1', 8 | src: 'https://unsplash.it/200?random', 9 | alt: 'Alt Image 1' 10 | }, { 11 | id: '2', 12 | src: 'https://unsplash.it/200?random', 13 | alt: 'Alt Image 2' 14 | }] 15 | 16 | beforeEach(() => { 17 | const Constructor = Vue.extend(VueSelectImage) 18 | vm = new Constructor({ 19 | propsData: { 20 | dataImages: dataImages, 21 | isMultiple: false, 22 | rootClass: 'vue-select-image', 23 | w: 'auto', 24 | h: 'auto' 25 | } 26 | }) 27 | vm.$mount() 28 | }); 29 | 30 | describe('test via extends Vue', () => { 31 | it('render props dataImages', () => { 32 | expect(vm.dataImages.length).toEqual(2) 33 | expect(vm.dataImagesLocal.length).toEqual(2) 34 | }) 35 | it('render props default', () => { 36 | expect(vm.isMultiple).toBe(false) 37 | expect(vm.rootClass).toEqual('vue-select-image') 38 | expect(vm.w).toEqual('auto') 39 | expect(vm.h).toEqual('auto') 40 | }) 41 | it('classThumbnailMultiple selected', () => { 42 | vm.multipleSelected = dataImages 43 | let result = vm.classThumbnailMultiple('1') 44 | expect(result).toEqual('vue-select-image__thumbnail is--multiple vue-select-image__thumbnail--selected') 45 | }) 46 | it('classThumbnailMultiple not selected', () => { 47 | vm.multipleSelected = [] 48 | let result = vm.classThumbnailMultiple('1') 49 | expect(result).toEqual('vue-select-image__thumbnail is--multiple') 50 | }) 51 | it('emit onselectimage event', () => { 52 | const spy = jest.spyOn(vm, '$emit') 53 | vm.onSelectImage(dataImages[0]) 54 | expect(spy).toHaveBeenCalled() 55 | expect(spy).toBeCalledWith('onselectimage', dataImages[0]) 56 | }) 57 | it('isExistInArray not exist', () => { 58 | vm.multipleSelected = [] 59 | let result = vm.isExistInArray(1) 60 | expect(result).toBeUndefined() 61 | }) 62 | it('isExistInArray return exist', () => { 63 | vm.multipleSelected = dataImages 64 | let result = vm.isExistInArray('1') 65 | expect(result).toEqual(dataImages[0]) 66 | }) 67 | it('emit onselectimage event', () => { 68 | const spy = jest.spyOn(vm, '$emit') 69 | vm.onSelectImage(dataImages[0]) 70 | expect(spy).toHaveBeenCalled() 71 | expect(spy).toBeCalledWith('onselectimage', dataImages[0]) 72 | }) 73 | it('emit onselectmultipleimage event', () => { 74 | const spy = jest.spyOn(vm, '$emit') 75 | vm.onSelectMultipleImage(dataImages[0]) 76 | expect(spy).toHaveBeenCalled() 77 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[0]]) 78 | }) 79 | it('emit onselectmultipleimage was exist', () => { 80 | vm.multipleSelected = dataImages 81 | const spy = jest.spyOn(vm, '$emit') 82 | vm.onSelectMultipleImage(dataImages[0]) 83 | expect(spy).toHaveBeenCalled() 84 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[1]]) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/vue-select-image.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { shallow } from 'avoriaz' 3 | import VueSelectImage from '../src/VueSelectImage.vue' 4 | 5 | describe('VueSelectImage.vue', () => { 6 | const dataImages = [{ 7 | id: '1', 8 | src: 'https://unsplash.it/200?random', 9 | alt: 'Alt Image 1' 10 | }, { 11 | id: '2', 12 | src: 'https://unsplash.it/200?random', 13 | alt: 'Alt Image 2' 14 | }] 15 | 16 | it('render correct amount of images', () => { 17 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages }}) 18 | expect(wrapper.find('.vue-select-image__item').length).toEqual(2) 19 | }) 20 | 21 | it('render with props multiple', () => { 22 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages: dataImages, isMultiple: true }}) 23 | expect(wrapper.find('.is--multiple').length).toEqual(2) 24 | }) 25 | 26 | it('render with props rootClass, h, and w', () => { 27 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages: dataImages, rootClass: 'a', w: '1em', h: '1em' }}) 28 | expect(wrapper.find('.a').length).toEqual(1) 29 | expect(wrapper.find('.a__img').length).toEqual(2) 30 | expect(wrapper.find('[width="1em"]').length).toEqual(2) 31 | expect(wrapper.find('[height="1em"]').length).toEqual(2) 32 | }) 33 | 34 | it.skip('render with props selectedImages', () => { 35 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages: dataImages, selectedImages: dataImages, isMultiple: true }}) 36 | expect(wrapper.find('.vue-select-image__thumbnail').length).toEqual(2) 37 | expect(wrapper.find('.vue-select-image__thumbnail--selected').length).toEqual(2) 38 | }) 39 | 40 | it('emit onselectimage event', () => { 41 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages }}) 42 | const spy = jest.spyOn(wrapper.vm, '$emit') 43 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 44 | expect(spy).toHaveBeenCalled() 45 | expect(spy).toBeCalledWith('onselectimage', dataImages[0]) 46 | }) 47 | 48 | it('assign singleSelected data', () => { 49 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages }}) 50 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 51 | expect(wrapper.data().singleSelected).toEqual(dataImages[0]) 52 | }) 53 | 54 | it('emit onselectmultipleimage event', () => { 55 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages: dataImages, isMultiple: true }}) 56 | const spy = jest.spyOn(wrapper.vm, '$emit') 57 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 58 | expect(spy).toHaveBeenCalled() 59 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[0]]) 60 | }) 61 | 62 | it('test repeat click with same index', () => { 63 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages: dataImages, isMultiple: true }}) 64 | const spy = jest.spyOn(wrapper.vm, '$emit') 65 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 66 | expect(spy).toHaveBeenCalled() 67 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[0]]) 68 | // repeat click but same index 69 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 70 | expect(spy).toHaveBeenCalled() 71 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[0]]) 72 | // repeat click but same index 73 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 74 | expect(spy).toHaveBeenCalled() 75 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[0]]) 76 | }) 77 | it('test repeat click different index', () => { 78 | const wrapper = shallow(VueSelectImage, { propsData: { dataImages: dataImages, isMultiple: true }}) 79 | const spy = jest.spyOn(wrapper.vm, '$emit') 80 | wrapper.find('.vue-select-image__thumbnail')[0].trigger('click') 81 | expect(spy).toHaveBeenCalled() 82 | expect(spy).toBeCalledWith('onselectmultipleimage', [dataImages[0]]) 83 | // repeat click different index 84 | wrapper.find('.vue-select-image__thumbnail')[1].trigger('click') 85 | expect(spy).toHaveBeenCalled() 86 | expect(spy).toBeCalledWith('onselectmultipleimage', dataImages) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const webpack = require('webpack'); 4 | const CompressionPlugin = require("compression-webpack-plugin") 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 6 | 7 | const TerserPlugin = require('terser-webpack-plugin'); 8 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") 9 | 10 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 11 | 12 | const NODE_ENV = process.env.NODE_ENV; 13 | 14 | const setPath = function(folderName) { 15 | return path.join(__dirname, folderName); 16 | } 17 | 18 | const config = { 19 | entry: { 20 | app: './src/plugin.js' 21 | }, 22 | output: { 23 | library: "VueSelectImage", 24 | libraryTarget: "umd", 25 | filename: "vue-select-image.js", 26 | umdNamedDefine: true, 27 | }, 28 | optimization:{ 29 | runtimeChunk: false, 30 | minimize: true, 31 | minimizer: [ 32 | new TerserPlugin(), 33 | new OptimizeCSSAssetsPlugin({}) 34 | ] 35 | }, 36 | resolveLoader: { 37 | modules: [setPath('node_modules')] 38 | }, 39 | resolve: { 40 | alias: { 41 | 'vue$': 'vue/dist/vue.esm.js', 42 | '@': path.resolve(__dirname, ''), 43 | 'demo': path.resolve(__dirname, 'demo'), 44 | 'src': path.resolve(__dirname, 'src') 45 | }, 46 | extensions: ['*', '.js', '.vue', '.json'] 47 | }, 48 | mode: 'production', 49 | plugins: [ 50 | new webpack.DefinePlugin({ 51 | 'process.env': { 52 | isStaging: (NODE_ENV === 'development'), 53 | NODE_ENV: '"'+NODE_ENV+'"' 54 | } 55 | }), 56 | new VueLoaderPlugin(), 57 | new MiniCssExtractPlugin({ 58 | filename: "vue-select-image.css" 59 | }), 60 | new CompressionPlugin({ 61 | algorithm: 'gzip' 62 | }) 63 | ], 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.vue$/, 68 | use: 'vue-loader' 69 | }, 70 | { 71 | test: /\.css$/, 72 | use: [ 73 | MiniCssExtractPlugin.loader, 74 | 'css-loader' 75 | ], 76 | }, 77 | { 78 | test: /\.scss$/, 79 | use: [ 80 | 'vue-style-loader', 81 | 'css-loader', 82 | 'sass-loader' 83 | ], 84 | }, 85 | { 86 | test: /\.sass$/, 87 | use: [ 88 | 'vue-style-loader', 89 | 'css-loader', 90 | 'sass-loader?indentedSyntax' 91 | ], 92 | }, 93 | { 94 | test: /\.m?js$/, 95 | exclude: /(node_modules|bower_components)/, 96 | use: { 97 | loader: 'babel-loader', 98 | options: { 99 | presets: ['@babel/preset-env'] 100 | } 101 | } 102 | }, 103 | { 104 | test: /\.(png|jpg|gif|svg)$/, 105 | loader: 'file-loader', 106 | options: { 107 | name: '[name].[ext]?[hash]' 108 | } 109 | } 110 | ] 111 | } 112 | }; 113 | module.exports = config; 114 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const CompressionPlugin = require("compression-webpack-plugin") 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 7 | 8 | const TerserPlugin = require('terser-webpack-plugin'); 9 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") 10 | 11 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 12 | 13 | const NODE_ENV = process.env.NODE_ENV; 14 | 15 | const setPath = function(folderName) { 16 | return path.join(__dirname, folderName); 17 | } 18 | 19 | const buildingForLocal = () => { 20 | return (NODE_ENV === 'development'); 21 | }; 22 | 23 | const setPublicPath = () => { 24 | return buildingForLocal() ? '/' : '/vue-select-image/'; 25 | }; 26 | 27 | const extractHTML = new HtmlWebpackPlugin({ 28 | title: 'History Search', 29 | filename: 'index.html', 30 | inject: true, 31 | template: path.join(__dirname, 'demo/index.ejs'), 32 | minify: { 33 | removeAttributeQuotes: true, 34 | collapseWhitespace: true, 35 | html5: true, 36 | minifyCSS: true, 37 | removeComments: true, 38 | removeEmptyAttributes: true 39 | }, 40 | environment: process.env.NODE_ENV 41 | }); 42 | 43 | 44 | const config = { 45 | entry: { 46 | app: path.join(setPath('demo'), 'main.js') 47 | }, 48 | output: { 49 | path: buildingForLocal() ? path.resolve(__dirname) : setPath('dist-demo'), 50 | publicPath: setPublicPath(), 51 | filename: buildingForLocal() ? 'js/[name].js' : 'js/[name].[hash].js' 52 | }, 53 | optimization:{ 54 | runtimeChunk: false, 55 | splitChunks: { 56 | chunks: "all", //Taken from https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693 57 | }, 58 | minimize: true, 59 | minimizer: [ 60 | new TerserPlugin(), 61 | new OptimizeCSSAssetsPlugin({}) 62 | ] 63 | }, 64 | resolveLoader: { 65 | modules: [ 'node_modules' ], 66 | }, 67 | resolve: { 68 | alias: { 69 | 'vue$': 'vue/dist/vue.esm.js', 70 | '@': path.resolve(__dirname, ''), 71 | 'demo': path.resolve(__dirname, 'demo'), 72 | 'src': path.resolve(__dirname, 'src') 73 | }, 74 | extensions: ['*', '.js', '.vue', '.json'] 75 | }, 76 | mode: buildingForLocal() ? 'development' : 'production', 77 | devServer: { 78 | historyApiFallback: true, 79 | noInfo: false 80 | }, 81 | plugins: [ 82 | new webpack.DefinePlugin({ 83 | 'process.env': { 84 | isStaging: (NODE_ENV === 'development' || NODE_ENV === 'staging'), 85 | NODE_ENV: '"'+NODE_ENV+'"' 86 | } 87 | }), 88 | extractHTML, 89 | new VueLoaderPlugin(), 90 | new MiniCssExtractPlugin({ 91 | filename: "css/[name].[hash].css" 92 | }), 93 | new CompressionPlugin({ 94 | algorithm: 'gzip' 95 | }) 96 | ], 97 | module: { 98 | rules: [ 99 | { 100 | test: /\.vue$/, 101 | use: 'vue-loader' 102 | }, 103 | { 104 | test: /\.css$/, 105 | use: [ 106 | MiniCssExtractPlugin.loader, 107 | 'css-loader' 108 | ], 109 | }, 110 | { 111 | test: /\.scss$/, 112 | use: [ 113 | 'vue-style-loader', 114 | 'css-loader', 115 | 'sass-loader' 116 | ], 117 | }, 118 | { 119 | test: /\.sass$/, 120 | use: [ 121 | 'vue-style-loader', 122 | 'css-loader', 123 | 'sass-loader?indentedSyntax' 124 | ], 125 | }, 126 | { 127 | test: /\.m?js$/, 128 | exclude: /(node_modules|bower_components)/, 129 | use: { 130 | loader: 'babel-loader', 131 | options: { 132 | presets: ['@babel/preset-env'] 133 | } 134 | } 135 | }, 136 | { 137 | test: /\.(png|jpg|gif|svg)$/, 138 | loader: 'file-loader', 139 | options: { 140 | name: '[name].[ext]?[hash]' 141 | } 142 | } 143 | ] 144 | } 145 | }; 146 | module.exports = config; 147 | --------------------------------------------------------------------------------