├── .browserslistrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── README.md ├── babel.config.js ├── config └── storybook │ ├── addons.js │ └── config.js ├── cypress.json ├── demo ├── App.vue ├── assets │ └── logo.png ├── main.js ├── router.js └── views │ ├── Dropzone.vue │ ├── FileListExample.vue │ ├── ImagePreview.vue │ └── SimpleFileDrop.vue ├── jest.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── components │ ├── FileBox.vue │ ├── FileDrop.vue │ ├── FileList.vue │ ├── ImageList.vue │ └── MyButton.vue ├── full.js ├── index.js ├── stories │ └── index.stories.js └── utils │ ├── filters.js │ ├── globalDragEventBus.js │ └── index.js ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── .eslintrc.js │ ├── FileDrop │ └── filedrop.spec.js │ └── resources │ ├── Simple.vue │ └── filedrop.utils.js ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/prettier'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | }, 11 | parserOptions: { 12 | parser: 'babel-eslint', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | TODO.md 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw* 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.circleci 3 | /demo 4 | /node_modules 5 | /public 6 | /tests 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "semi": false 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [WIP] vue-filedrop 2 | 3 | 4 | 5 | ## What this is 6 | 7 | > this lib has not been published and not ready to be used yet. 8 | 9 | Vue Filedrop is a UI component that provides a dropzone for files, like this: 10 | 11 | -> **TODO: enter image here** 12 | 13 | It *also* is a renderless component that wraps all of the business logic (drag&drop event handling etc) and provides an easy to consume API via a scoped slot. 14 | 15 | This allows developers to build their own UI representation of a Filedrop easily and withut worrying about edge cases and such. 16 | 17 | ## Usage 18 | 19 | ### UI component 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | ### Renderless component 26 | 27 | ```html 28 | 34 |
39 |

Drop files here or click to select

40 |

You can drop the file(s) here

41 |
    42 |
  • {{file.name}}
  • 43 |
44 | 45 | ``` 46 | 47 | ## Installation 48 | 49 | > WARNING 50 | > This package hasnt been published yet, so install instructions don't work. 51 | 52 | ### Webpack or other bundlers 53 | 54 | ```bash 55 | npm i vue-filedrop 56 | # or 57 | yarn add filedrop 58 | ``` 59 | 60 | ```javascript 61 | import Vue from 'vue' 62 | import Filedrop from 'vue-filedrop' 63 | 64 | Vue.use(Filedrop, { 65 | // Default component names, customizable 66 | // setting one to `false` skips their global registration 67 | filedropName: 'Filedrop', 68 | filedropUiName: 'FiledropUi' 69 | }) 70 | ``` 71 | 72 | Alternatively, you can import components indivudally and register them (globally or locally): 73 | 74 | ```html 75 | <-- e.g. in a .vue file: --> 76 | 88 | ``` 89 | 90 | ### Importing from `/src` / Tree-shaking 91 | 92 |
93 | If you want proper tree-shaking support, we recommend to import froms src (click for more) 94 | 95 | ```javascript 96 | import Vue from 'vue' 97 | import Filedrop from 'vue-filedrop/src/index' 98 | Vue.use(Filedrop) 99 | ``` 100 | 101 | However, in most setups this requires some adjustment of the (webpack) build setup as content of `/node_modules` is usually ignored by babel-loader configs. 102 | 103 | Vue CLI projects offer the [transpileDependencies](https://cli.vuejs.org/config/#transpiledependencies) to do this quick&easy: 104 | 105 | ```javascript 106 | // vue.config.js 107 | module.exports = { 108 | transpileDependencies: ['vue-filedrop'] 109 | } 110 | ``` 111 | 112 |
113 | 114 | ## Browser 115 | 116 |
117 | Click here to see in-Browser install instructions 118 | 119 | ```html 120 | 121 | 122 | ``` 123 | 124 | In the browser, the plugin will automatically register the components globally 125 | 126 |
127 | 128 | ## Features 129 | 130 | - [ ] todo 131 | 132 | ## Development 133 | 134 |
135 | Click here to see information for constributors 136 | 137 | ### Project setup 138 | 139 | ```bash 140 | yarn install 141 | ``` 142 | 143 | ### Compiles and hot-reloads for development 144 | 145 | ```bash 146 | yarn run serve 147 | ``` 148 | 149 | ### Compiles and minifies for production 150 | 151 | ```bash 152 | yarn run build 153 | ``` 154 | 155 | ### Lints and fixes files 156 | 157 | ```bash 158 | yarn run lint 159 | ``` 160 | 161 | ### Run all tests 162 | 163 | ```bash 164 | yarn run test 165 | ``` 166 | 167 | ### Run your unit tests 168 | 169 | ```bash 170 | yarn run test:unit 171 | ``` 172 | 173 | ### Customize configuration 174 | 175 | This project is based on Vue CLI, see its [Configuration Reference](https://cli.vuejs.org/config/) for further info. 176 |
-------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@vue/app', 5 | { 6 | useBuiltIns: false, 7 | polyfills: false, 8 | }, 9 | ], 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /config/storybook/addons.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import '@storybook/addon-actions/register' 3 | import '@storybook/addon-knobs/register' 4 | import '@storybook/addon-links/register' 5 | import '@storybook/addon-notes/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('../../src/stories', true, /.stories.js$/) 5 | 6 | function loadStories() { 7 | req.keys().forEach(filename => req(filename)) 8 | } 9 | 10 | configure(loadStories, module) 11 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 56 | -------------------------------------------------------------------------------- /demo/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinusBorg/vue-filedrop/1e828dab478f7a8bc302d83fd9542d3cea6ed739/demo/assets/logo.png -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import VueFiledrop from '../src/full' 5 | 6 | Vue.use(VueFiledrop) 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | router, 12 | render: h => h(App), 13 | }).$mount('#app') 14 | -------------------------------------------------------------------------------- /demo/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | mode: 'history', 8 | base: process.env.BASE_URL, 9 | routes: [ 10 | { 11 | path: '/', 12 | component: require('./views/SimpleFileDrop').default, 13 | meta: { 14 | title: 'The simplest FileDrop', 15 | }, 16 | }, 17 | { 18 | path: '/file-list', 19 | component: require('./views/FileListExample').default, 20 | meta: { 21 | title: 'A File listing in a FileDrop', 22 | }, 23 | }, 24 | { 25 | path: '/image-preview', 26 | component: require('./views/ImagePreview').default, 27 | meta: { 28 | title: 'Previewing Images with thumbnails', 29 | }, 30 | }, 31 | { 32 | path: '/dropzone', 33 | component: require('./views/Dropzone').default, 34 | meta: { 35 | title: 'Imitating Dropzone style', 36 | }, 37 | }, 38 | ], 39 | }) 40 | -------------------------------------------------------------------------------- /demo/views/Dropzone.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /demo/views/FileListExample.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /demo/views/ImagePreview.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | 69 | -------------------------------------------------------------------------------- /demo/views/SimpleFileDrop.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest', 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1', 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)', 15 | ], 16 | testURL: 'http://localhost/', 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-filedrop", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve demo/main", 7 | "build": "rimraf node_modules/.cache && yarn build:lib:full && yarn build:lib:core --no-clean", 8 | "lint": "vue-cli-service lint", 9 | "build:demo": "vue-cli-service build demo/main", 10 | "build:lib:core": "vue-cli-service build --target lib --name VueFiledrop --dest dist/core src/index.js", 11 | "build:lib:full": "vue-cli-service build --target lib --name VueFiledrop src/full.js", 12 | "build:lib:wc": "rimraf node_modules/.cache && vue-cli-service build --target wc --no-clean --dest dist/wc src/full.js", 13 | "sb:build": "vue-cli-service storybook:build -c config/storybook", 14 | "sb:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook", 15 | "test:e2e": "vue-cli-service test:e2e", 16 | "test:unit": "vue-cli-service test:unit" 17 | }, 18 | "dependencies": { 19 | "vue-reactive-provide": "^0.2.2" 20 | }, 21 | "devDependencies": { 22 | "@storybook/addon-actions": "^5.0.0", 23 | "@storybook/addon-knobs": "^5.0.0", 24 | "@storybook/addon-links": "^5.0.0", 25 | "@storybook/addon-notes": "^5.0.0", 26 | "@vue/cli-plugin-babel": "^3.2.0", 27 | "@vue/cli-plugin-e2e-cypress": "^3.4.0", 28 | "@vue/cli-plugin-eslint": "^3.2.0", 29 | "@vue/cli-plugin-unit-jest": "^3.2.0", 30 | "@vue/cli-service": "^3.2.0", 31 | "@vue/eslint-config-prettier": "^4.0.0", 32 | "@vue/test-utils": "^1.0.0-beta.28", 33 | "babel-core": "7.0.0-bridge.0", 34 | "babel-eslint": "^10.0.1", 35 | "babel-jest": "^23.6.0", 36 | "eslint": "^5.8.0", 37 | "eslint-plugin-vue": "^5.0.0-0", 38 | "lint-staged": "^7.2.2", 39 | "node-sass": "^4.11.0", 40 | "prettier-eslint": "^8.8.2", 41 | "sass-loader": "^7.1.0", 42 | "vue": "^2.5.17", 43 | "vue-cli-plugin-storybook": "^0.6.1", 44 | "vue-router": "^3.0.1", 45 | "vue-template-compiler": "^2.5.17" 46 | }, 47 | "files": [ 48 | "dist", 49 | "src" 50 | ], 51 | "gitHooks": { 52 | "pre-commit": "lint-staged" 53 | }, 54 | "lint-staged": { 55 | "*.js": [ 56 | "vue-cli-service lint", 57 | "git add" 58 | ], 59 | "*.vue": [ 60 | "vue-cli-service lint", 61 | "git add" 62 | ] 63 | }, 64 | "main": "dist/VueFiledrop.common.js", 65 | "sideEffects": [ 66 | "*.css" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinusBorg/vue-filedrop/1e828dab478f7a8bc302d83fd9542d3cea6ed739/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-filedrop 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/FileBox.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 48 | 49 | 84 | -------------------------------------------------------------------------------- /src/components/FileDrop.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 204 | 205 | 211 | -------------------------------------------------------------------------------- /src/components/FileList.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 47 | 48 | 114 | -------------------------------------------------------------------------------- /src/components/ImageList.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 78 | 79 | 124 | -------------------------------------------------------------------------------- /src/components/MyButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/full.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | /** 4 | * This is not yet part of the lib. 5 | * 6 | * The goal ist to first release the core version and then add 7 | * additional components as we identify the use cases. 8 | */ 9 | 10 | import FileDrop from './components/FileDrop' 11 | 12 | import FileBox from './components/FileBox' 13 | import ImageList from './components/ImageList' 14 | import FileList from './components/FileList' 15 | 16 | import * as filters from './utils' 17 | 18 | const notFalse = v => v !== false 19 | /* eslint-disable-next-line no-unused-vars */ 20 | function install(_Vue, options = {}) { 21 | notFalse(options.FileDrop) && 22 | Vue.component(options.FileDrop || 'FileDrop', FileDrop) 23 | notFalse(options.FileBox) && 24 | Vue.component(options.FileBox || 'FileBox', FileBox) 25 | notFalse(options.ImageList) && 26 | Vue.component(options.ImageList || 'ImageList', ImageList) 27 | notFalse(options.FileList) && 28 | Vue.component(options.FileList || 'FileList', FileList) 29 | 30 | if (options.filters !== false) { 31 | Object.keys(filters).forEach(filterName => { 32 | Vue.filter(`filedrop:${filterName}`, filters[filterName]) 33 | }) 34 | } 35 | } 36 | 37 | // autoinstall for script-tag includes 38 | const w = window 39 | const hasWindow = typeof w !== 'undefined' 40 | if (hasWindow & w.Vue && w.Vue === Vue) { 41 | Vue.use({ install }) 42 | } 43 | 44 | export default { 45 | install, 46 | } 47 | 48 | export { FileBox, FileDrop } 49 | export * from './utils/filters' 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import FileDrop from './components/FileDrop' 3 | 4 | /* eslint-disable-next-line no-unused-vars */ 5 | function install(_Vue, { filedropName } = {}) { 6 | if (filedropName !== false) { 7 | Vue.component(filedropName || FileDrop.name, FileDrop) 8 | } 9 | } 10 | 11 | // autoinstall for script-tag includes 12 | const hasWindow = typeof window !== 'undefined' 13 | if (hasWindow & window.Vue && window.Vue === Vue) { 14 | Vue.use({ install }) 15 | } 16 | 17 | export default { 18 | install, 19 | } 20 | 21 | export { FileDrop } 22 | -------------------------------------------------------------------------------- /src/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { storiesOf } from '@storybook/vue' 3 | import { action } from '@storybook/addon-actions' 4 | import { linkTo } from '@storybook/addon-links' 5 | 6 | import MyButton from '../components/MyButton.vue' 7 | 8 | storiesOf('Button', module) 9 | .add('with text', () => ({ 10 | components: { MyButton }, 11 | template: 'Hello Button', 12 | methods: { action: action('clicked') }, 13 | })) 14 | .add('with JSX', () => ({ 15 | components: { MyButton }, 16 | render() { 17 | return With JSX 18 | }, 19 | methods: { action: linkTo('Button', 'with some emoji') }, 20 | })) 21 | .add('with some emoji', () => ({ 22 | components: { MyButton }, 23 | template: '😀 😎 👍 💯', 24 | methods: { action: action('clicked') }, 25 | })) 26 | -------------------------------------------------------------------------------- /src/utils/filters.js: -------------------------------------------------------------------------------- 1 | const sizes = { 2 | kiB: 1000, 3 | MB: 1000000, 4 | GB: 1000000000, 5 | } 6 | function getSizeFactor(size) { 7 | if (size >= sizes.GB) { 8 | return [sizes.GB, 'GB'] 9 | } else if (size >= sizes.MB) { 10 | return [sizes.MB, 'MB'] 11 | } else if (size >= sizes.kiB) { 12 | return [sizes.kiB, 'kiB'] 13 | } else { 14 | return [1, 'B'] 15 | } 16 | } 17 | const defaultSize = [1000, 'kiB'] 18 | 19 | export function size(v, unit = 'auto') { 20 | const [factor, text] = unit === 'auto' ? getSizeFactor(v) : defaultSize 21 | return `${Math.round((v / factor) * 100, 2) / 100} ${text}` 22 | } 23 | 24 | export function shorten(_text = '', maxlen = 50, points = true) { 25 | if (typeof _text !== 'string') return '' 26 | 27 | const text = _text.slice(0, maxlen) 28 | 29 | return points && text !== _text ? `${text}...` : text 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/globalDragEventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const vm = new Vue({ 4 | created() { 5 | const html = document.querySelector('html') 6 | html.addEventListener('dragenter', this.dragenter) 7 | html.addEventListener('dragleave', this.dragleave) 8 | html.addEventListener('dragend', this.dragend) 9 | html.addEventListener('drop', this.dragend) 10 | }, 11 | beforeDestroy() { 12 | const html = document.querySelector('html') 13 | html.addEventListener('dragenter', this.dragenter) 14 | html.removeEventListener('dragleave', this.dragleave) 15 | html.removeEventListener('dragend', this.dragend) 16 | html.removeEventListener('drop', this.dragend) 17 | }, 18 | methods: { 19 | dragenter() { 20 | if (this.$_firstE) { 21 | this.$_secondE = true 22 | } else { 23 | this.$_firstE = true 24 | this.$emit('dragging', true) 25 | } 26 | }, 27 | dragleave() { 28 | if (this.$_secondE) { 29 | this.$_secondE = false 30 | } else if (this.$_firstE) { 31 | this.$_firstE = false 32 | } 33 | 34 | if (!this.$_firstE && !this.$_secondE) { 35 | this.$emit('dragging', false) 36 | } 37 | }, 38 | dragend() { 39 | this.$emit('dragging', false) 40 | }, 41 | }, 42 | }) 43 | 44 | export default vm 45 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export const PROVIDE_KEY = '__vueFiledrop__' 2 | 3 | const READ_AS_VALUES = ['ArrayBuffer', 'BinaryString', 'DataURL', 'Text'] 4 | 5 | export function validateReadAs(v) { 6 | const isValid = READ_AS_VALUES.includes(v) 7 | if (!isValid) 8 | console.warn( 9 | `[vue-droparea: ${v} is not a valid value for the 'readAs' prop.` 10 | ) 11 | return isValid 12 | } 13 | 14 | export function processFiles(files, readAs = 'BinaryString') { 15 | if (!validateReadAs(readAs)) { 16 | throw new Error(`[vue-filedrop]: ${readAs} is not a valid FileReader mode. 17 | Expected one of ['ArrayBuffer', 'BinaryString', 'DataURL', 'Text'] 18 | `) 19 | } 20 | 21 | const promises = [...files].map(file => { 22 | return readFile(file, readAs) 23 | }) 24 | return Promise.all(promises) 25 | } 26 | 27 | function readFile(file, readAs) { 28 | return new Promise((resolve, reject) => { 29 | const reader = new FileReader() 30 | 31 | reader.onerror = reject 32 | reader.onload = () => { 33 | resolve({ 34 | name: file.name, 35 | size: file.size, 36 | type: file.type, 37 | data: reader.result, 38 | }) 39 | } 40 | 41 | reader[`readAs${readAs}`](file) 42 | }) 43 | } 44 | 45 | export function pick(object, keys = []) { 46 | return keys.reduce((acc, key) => { 47 | acc[key] = object[key] 48 | return acc 49 | }, {}) 50 | } 51 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cypress'], 3 | env: { 4 | mocha: true, 5 | 'cypress/globals': true, 6 | }, 7 | rules: { 8 | strict: 'off', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/guides/guides/plugins-guide.html 2 | 3 | // if you need a custom webpack configuration you can uncomment the following import 4 | // and then use the `file:preprocessor` event 5 | // as explained in the cypress docs 6 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 7 | 8 | /* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */ 9 | // const webpack = require('@cypress/webpack-preprocessor') 10 | 11 | module.exports = (on, config) => { 12 | // on('file:preprocessor', webpack({ 13 | // webpackOptions: require('@vue/cli-service/webpack.config'), 14 | // watchOptions: {} 15 | // })) 16 | 17 | return Object.assign({}, config, { 18 | fixturesFolder: 'tests/e2e/fixtures', 19 | integrationFolder: 'tests/e2e/specs', 20 | screenshotsFolder: 'tests/e2e/screenshots', 21 | videosFolder: 'tests/e2e/videos', 22 | supportFile: 'tests/e2e/support/index.js', 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h2', 'The simplest FileDrop') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/FileDrop/filedrop.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import FileDrop from '@/components/FileDrop' 4 | import { 5 | createFile, 6 | createWrapper, 7 | InjectCatcherStub, 8 | mockConsole, 9 | } from '../resources/filedrop.utils' 10 | const tick = () => Vue.nextTick() 11 | 12 | const lastCallArgs = fn => { 13 | return fn.mock.calls[fn.mock.calls.length - 1][0] 14 | } 15 | 16 | describe('The FileDrop component', () => { 17 | test('mounts with normal slot', async () => { 18 | const wrapper = mount(FileDrop, { 19 | slots: { 20 | default: `

Test

`, 21 | }, 22 | }) 23 | await Vue.nextTick() 24 | const el = wrapper.find('.test') 25 | 26 | expect(el.text()).toBe('Test') 27 | }) 28 | 29 | test('scoped slot passes slotProps', async () => { 30 | const { fn } = createWrapper() 31 | await Vue.nextTick() 32 | expect(fn).toHaveBeenCalledWith(expect.any(Object)) 33 | const props = lastCallArgs(fn) 34 | 35 | expect(props.open).toEqual(expect.any(Function)) 36 | }) 37 | 38 | test('renders input', async () => { 39 | const { wrapper } = createWrapper() 40 | 41 | const input = wrapper.find('input') 42 | 43 | await Vue.nextTick() 44 | expect(input.element).toBeDefined() 45 | expect(input.element.type).toBe('file') 46 | }) 47 | 48 | test('input can add a file', async () => { 49 | const { wrapper, fn } = createWrapper() 50 | 51 | const file = createFile() 52 | 53 | // hacky because we can't trigger a proper change event on the input 54 | wrapper.vm.onFileInputChange({ 55 | target: { 56 | files: [file], 57 | }, 58 | }) 59 | 60 | await tick() 61 | 62 | expect(lastCallArgs(fn).files).toEqual(expect.any(Array)) 63 | expect(lastCallArgs(fn).files.length).toBe(1) 64 | }) 65 | 66 | test('drop can add a file', async () => { 67 | const { wrapper, fn } = createWrapper() 68 | 69 | const file = createFile() 70 | 71 | // hacky because we can't trigger a proper change event on the input 72 | wrapper.vm.onFileDrop({ 73 | dataTransfer: { 74 | files: [file], 75 | }, 76 | }) 77 | 78 | await tick() 79 | 80 | expect(lastCallArgs(fn).files).toEqual(expect.any(Array)) 81 | expect(lastCallArgs(fn).files.length).toBe(1) 82 | }) 83 | 84 | test('remove does remove a file', async () => { 85 | const { wrapper, fn } = createWrapper() 86 | 87 | const file = createFile() 88 | 89 | // hacky because we can't trigger a proper change event on the input 90 | wrapper.vm.onFileDrop({ 91 | dataTransfer: { 92 | files: [file], 93 | }, 94 | }) 95 | 96 | await tick() 97 | 98 | const props = lastCallArgs(fn) 99 | 100 | expect(lastCallArgs(fn).files.length).toBe(1) 101 | props.remove(0) 102 | 103 | await tick() 104 | expect(lastCallArgs(fn).files.length).toBe(0) 105 | }) 106 | 107 | test('clear does remove all files', async () => { 108 | const { wrapper, fn } = createWrapper({ 109 | propsData: { 110 | multiple: true, 111 | }, 112 | }) 113 | 114 | const file = createFile() 115 | const file2 = createFile() 116 | // hacky because we can't trigger a proper change event on the input 117 | wrapper.vm.onFileDrop({ 118 | dataTransfer: { 119 | files: [file, file2], 120 | }, 121 | }) 122 | 123 | await tick() 124 | 125 | const props = lastCallArgs(fn) 126 | 127 | expect(lastCallArgs(fn).files.length).toBe(2) 128 | props.clear() 129 | 130 | await tick() 131 | expect(lastCallArgs(fn).files.length).toBe(0) 132 | }) 133 | 134 | test('emits event for added files', async () => { 135 | const { wrapper } = createWrapper() 136 | 137 | const file = createFile() 138 | const cb = jest.fn() 139 | // hacky because we can't trigger a proper change event on the input 140 | wrapper.vm.$on('change', cb) 141 | wrapper.vm.onFileDrop({ 142 | dataTransfer: { 143 | files: [file], 144 | }, 145 | }) 146 | 147 | await tick() 148 | 149 | expect(wrapper.vm.files.length).toBe(1) 150 | expect(cb).toHaveBeenCalledWith(expect.arrayContaining([file])) 151 | 152 | cb.mockClear() 153 | wrapper.vm.emit() 154 | expect(cb).toHaveBeenCalledWith(expect.arrayContaining([file])) 155 | }) 156 | 157 | test('can switch root element', async () => { 158 | const { wrapper } = createWrapper({ 159 | propsData: { 160 | tag: 'span', 161 | }, 162 | }) 163 | 164 | expect(wrapper.vm.$el.tagName).toBe('SPAN') 165 | }) 166 | 167 | test('respects the max prop', async () => { 168 | const spys = mockConsole() 169 | const { wrapper, fn } = createWrapper({ 170 | propsData: { 171 | max: 2, 172 | }, 173 | }) 174 | 175 | const input = wrapper.find('input') 176 | 177 | expect(input.element.max).toBe('2') 178 | 179 | const file = createFile() 180 | const file2 = createFile() 181 | const file3 = createFile() 182 | wrapper.vm.onFileDrop({ 183 | dataTransfer: { 184 | files: [file, file2, file3], 185 | }, 186 | }) 187 | 188 | await tick() 189 | 190 | const props = lastCallArgs(fn) 191 | expect(props.maxExceeded).toBe(1) 192 | expect(props.files.length).toBe(0) 193 | 194 | spys.unwatch() 195 | }) 196 | 197 | test('allows only one file to be dropped when max is set to 1', async () => { 198 | const spys = mockConsole() 199 | const { wrapper } = createWrapper({ 200 | propsData: { 201 | max: 1, 202 | }, 203 | }) 204 | 205 | const file = createFile() 206 | const file2 = createFile() 207 | 208 | wrapper.vm.onFileDrop({ 209 | dataTransfer: { 210 | files: [file, file2], 211 | }, 212 | }) 213 | 214 | expect(wrapper.vm.files.length).toBe(0) 215 | expect(wrapper.vm.maxExceeded).toBe(1) 216 | 217 | wrapper.vm.onFileDrop({ 218 | dataTransfer: { 219 | files: [file], 220 | }, 221 | }) 222 | 223 | expect(wrapper.vm.files.length).toBe(1) 224 | expect(wrapper.vm.maxExceeded).toBe(false) 225 | 226 | spys.unwatch() 227 | }) 228 | 229 | test('provides the same API through provide and scopedSlot', async () => { 230 | const slotSpy = jest.fn() 231 | const injectSpy = jest.fn() 232 | 233 | createWrapper({ 234 | scopedSlots: { 235 | default: function(props) { 236 | slotSpy(props) 237 | return this.$createElement(InjectCatcherStub(injectSpy)) 238 | }, 239 | }, 240 | }) 241 | 242 | await tick() 243 | 244 | const slotProps = lastCallArgs(slotSpy) 245 | const injectProps = lastCallArgs(injectSpy) 246 | 247 | expect(slotProps).toMatchObject(injectProps) 248 | }) 249 | }) 250 | -------------------------------------------------------------------------------- /tests/unit/resources/Simple.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/unit/resources/filedrop.utils.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | 3 | import FileDrop from '@/components/FileDrop' 4 | 5 | export const createFile = (content = 'text') => { 6 | return new File([content], 'test.txt', { type: 'text/pain' }) 7 | } 8 | 9 | export const mockConsole = () => { 10 | const spys = { 11 | error: jest.spyOn(global.console, 'error').mockImplementation(() => ({})), 12 | log: jest.spyOn(global.console, 'log').mockImplementation(() => ({})), 13 | warn: jest.spyOn(global.console, 'warn').mockImplementation(() => ({})), 14 | unwatch: () => { 15 | spys.error.mockRestore() 16 | spys.log.mockRestore() 17 | spys.warn.mockRestore() 18 | }, 19 | } 20 | 21 | return spys 22 | } 23 | 24 | export const createWrapper = (options = {}) => { 25 | const fn = jest.fn() 26 | const instance = mount(FileDrop, { 27 | ref: 'filedrop', 28 | scopedSlots: { 29 | default: props => { 30 | fn(props) 31 | return null 32 | }, 33 | }, 34 | ...options, 35 | }) 36 | 37 | return { 38 | fn, 39 | wrapper: instance, 40 | } 41 | } 42 | 43 | export const InjectCatcherStub = fn => ({ 44 | inject: { 45 | filedrop: require('../../../src/utils/index').PROVIDE_KEY, 46 | }, 47 | created() { 48 | fn(this.filedrop) 49 | }, 50 | render() { 51 | return null 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | css: { 4 | extract: process.env.NODE_ENV === 'production', 5 | }, 6 | chainWebpack: config => { 7 | config.resolve.symlinks = false 8 | }, 9 | configureWebpack: () => { 10 | if (process.env.NODE_ENV !== 'production') return 11 | return { 12 | externals: { 13 | 'vue-reactive-provide': { 14 | commonjs: 'vue-reactive-provide', 15 | commonjs2: 'vue-reactive-provide', 16 | root: 'VueReactiveProvide', 17 | }, 18 | }, 19 | } 20 | }, 21 | } 22 | --------------------------------------------------------------------------------