├── .eslintignore ├── .gitattributes ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── bs-custom-file-input.d.ts ├── .editorconfig ├── src ├── selector.js ├── util.js ├── eventHandlers.js └── index.js ├── .babelrc ├── tests ├── .eslintrc.json ├── polyfill.js ├── mocha.html ├── browsers.js ├── karma.conf.js ├── index.html └── units │ ├── index.spec.js │ └── eventHandlers.spec.js ├── .eslintrc.json ├── LICENSE ├── rollup.config.js ├── dist ├── bs-custom-file-input.min.js ├── bs-custom-file-input.js ├── bs-custom-file-input.min.js.map └── bs-custom-file-input.js.map ├── package.json ├── CHANGELOG.md ├── demo └── index.html └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /demo/dist/ 3 | /tests/coverage/ 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tests/coverage/ 3 | /demo/dist/ 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: jservoire 2 | liberapay: Johann-S 3 | custom: https://www.paypal.me/jservoire 4 | -------------------------------------------------------------------------------- /bs-custom-file-input.d.ts: -------------------------------------------------------------------------------- 1 | declare const bsCustomFileInput: { 2 | init(inputSelector?: string, formSelector?: string): void; 3 | destroy(): void; 4 | }; 5 | 6 | export default bsCustomFileInput; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/selector.js: -------------------------------------------------------------------------------- 1 | const Selector = { 2 | CUSTOMFILE: '.custom-file input[type="file"]', 3 | CUSTOMFILELABEL: '.custom-file-label', 4 | FORM: 'form', 5 | INPUT: 'input', 6 | } 7 | 8 | export default Selector 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "loose": true, 7 | "modules": false 8 | } 9 | ] 10 | ], 11 | "env": { 12 | "test": { 13 | "plugins": ["istanbul"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "browser": true, 5 | "es6": false 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 5, 9 | "sourceType": "script" 10 | }, 11 | "extends": "../.eslintrc.json", 12 | "globals": { 13 | "bsCustomFileInput": false, 14 | "expect": false, 15 | "sinon": false 16 | }, 17 | "rules": { 18 | "no-var": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "babel-eslint", 9 | "rules": { 10 | "comma-dangle": [ 11 | "error", 12 | "always-multiline" 13 | ], 14 | "indent": [ 15 | "error", 16 | 2, { 17 | "SwitchCase": 1 18 | } 19 | ], 20 | "linebreak-style": [ 21 | "error", 22 | "unix" 23 | ], 24 | "no-tabs": "error", 25 | "no-trailing-spaces": "error", 26 | "no-var": "error", 27 | "semi": [ 28 | "error", 29 | "never" 30 | ], 31 | "sort-imports": "error" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var mochaFixtureDiv = document.createElement('div') 3 | mochaFixtureDiv.setAttribute('id', 'mocha-fixture') 4 | document.body.appendChild(mochaFixtureDiv) 5 | 6 | // Polyfill new File() 7 | try { 8 | new File([], 'test.txt') 9 | } 10 | catch(e) { 11 | // Fake polyfill for IE or Edge... 12 | window.File = function (part, name) { 13 | return { 14 | part: part, 15 | name: name, 16 | } 17 | } 18 | } 19 | 20 | // Event constructor shim 21 | if (!window.Event || typeof window.Event !== 'function') { 22 | var origEvent = window.Event 23 | window.Event = function(inType, params) { 24 | params = params || {} 25 | var e = document.createEvent('Event') 26 | e.initEvent(inType, Boolean(params.bubbles), Boolean(params.cancelable)) 27 | return e 28 | } 29 | window.Event.prototype = origEvent.prototype 30 | } 31 | })() 32 | -------------------------------------------------------------------------------- /tests/mocha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | env: 5 | CI: true 6 | FORCE_COLOR: 2 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: "10" 20 | 21 | - name: Install npm dependencies 22 | run: npm ci 23 | 24 | - name: Run tests 25 | run: npm test 26 | 27 | - name: Run BrowserStack tests 28 | run: npm run browserstack 29 | if: github.repository == 'Johann-S/bs-custom-file-input' && github.event_name == 'push' 30 | env: 31 | BROWSER_STACK_ACCESS_KEY: "${{ secrets.BROWSER_STACK_ACCESS_KEY }}" 32 | BROWSER_STACK_USERNAME: "${{ secrets.BROWSER_STACK_USERNAME }}" 33 | 34 | - name: Run Coveralls 35 | uses: coverallsapp/github-action@master 36 | with: 37 | github-token: "${{ secrets.GITHUB_TOKEN }}" 38 | path-to-lcov: "./tests/coverage/lcov.info" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Johann-S 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 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import Selector from './selector' 2 | 3 | const textNodeType = 3 4 | const getDefaultText = (input) => { 5 | let defaultText = '' 6 | 7 | const label = input.parentNode.querySelector(Selector.CUSTOMFILELABEL) 8 | 9 | if (label) { 10 | defaultText = label.textContent 11 | } 12 | 13 | return defaultText 14 | } 15 | 16 | const findFirstChildNode = (element) => { 17 | if (element.childNodes.length > 0) { 18 | const childNodes = [].slice.call(element.childNodes) 19 | 20 | for (let i = 0; i < childNodes.length; i++) { 21 | const node = childNodes[i] 22 | if (node.nodeType !== textNodeType) { 23 | return node 24 | } 25 | } 26 | } 27 | 28 | return element 29 | } 30 | 31 | const restoreDefaultText = (input) => { 32 | const defaultText = input.bsCustomFileInput.defaultText 33 | const label = input.parentNode.querySelector(Selector.CUSTOMFILELABEL) 34 | 35 | if (label) { 36 | const element = findFirstChildNode(label) 37 | 38 | element.textContent = defaultText 39 | } 40 | } 41 | 42 | export { 43 | getDefaultText, 44 | findFirstChildNode, 45 | restoreDefaultText, 46 | } 47 | -------------------------------------------------------------------------------- /src/eventHandlers.js: -------------------------------------------------------------------------------- 1 | import { findFirstChildNode, restoreDefaultText } from './util' 2 | import Selector from './selector' 3 | 4 | const fileApi = !!window.File 5 | const FAKE_PATH = 'fakepath' 6 | const FAKE_PATH_SEPARATOR = '\\' 7 | 8 | const getSelectedFiles = (input) => { 9 | if (input.hasAttribute('multiple') && fileApi) { 10 | return [].slice.call(input.files) 11 | .map((file) => file.name) 12 | .join(', ') 13 | } 14 | 15 | if (input.value.indexOf(FAKE_PATH) !== -1) { 16 | const splittedValue = input.value.split(FAKE_PATH_SEPARATOR) 17 | 18 | return splittedValue[splittedValue.length - 1] 19 | } 20 | 21 | return input.value 22 | } 23 | 24 | function handleInputChange() { 25 | const label = this.parentNode.querySelector(Selector.CUSTOMFILELABEL) 26 | 27 | if (label) { 28 | const element = findFirstChildNode(label) 29 | const inputValue = getSelectedFiles(this) 30 | 31 | if (inputValue.length) { 32 | element.textContent = inputValue 33 | } else { 34 | restoreDefaultText(this) 35 | } 36 | } 37 | } 38 | 39 | function handleFormReset() { 40 | const customFileList = [].slice.call(this.querySelectorAll(Selector.INPUT)) 41 | .filter((input) => !!input.bsCustomFileInput) 42 | 43 | for (let i = 0, len = customFileList.length; i < len; i++) { 44 | restoreDefaultText(customFileList[i]) 45 | } 46 | } 47 | 48 | export { 49 | handleInputChange, 50 | handleFormReset, 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require('path') 4 | const babel = require('rollup-plugin-babel') 5 | const { uglify } = require('rollup-plugin-uglify') 6 | 7 | const pkg = require(path.resolve(__dirname, 'package.json')) 8 | const year = new Date().getFullYear() 9 | 10 | const buildProd = process.env.PROD === 'true' 11 | const buildTest = process.env.TEST === 'true' 12 | const buildDev = process.env.DEV === 'true' 13 | 14 | const conf = { 15 | input: './src/index.js', 16 | output: { 17 | banner: 18 | `/*! 19 | * bsCustomFileInput v${pkg.version} (${pkg.homepage}) 20 | * Copyright 2018 - ${year} ${pkg.author} 21 | * Licensed under MIT (https://github.com/Johann-S/bs-custom-file-input/blob/master/LICENSE) 22 | */`, 23 | file: './dist/bs-custom-file-input.js', 24 | format: 'umd', 25 | name: 'bsCustomFileInput', 26 | sourcemap: true, 27 | }, 28 | plugins: [ 29 | babel({ 30 | exclude: 'node_modules/**', 31 | }), 32 | ], 33 | } 34 | 35 | if (buildTest) { 36 | conf.output.file = './tests/coverage/bs-custom-file-input.js' 37 | } 38 | 39 | if (buildDev) { 40 | conf.output.file = './tests/coverage/bs-custom-file-input.js' 41 | conf.watch = { 42 | include: 'src/**.js', 43 | } 44 | } 45 | 46 | if (buildProd) { 47 | conf.output.file = './dist/bs-custom-file-input.min.js' 48 | conf.plugins.push(uglify({ 49 | compress: { 50 | typeofs: false, 51 | }, 52 | mangle: true, 53 | output: { 54 | comments: /^!/, 55 | }, 56 | })) 57 | } 58 | 59 | module.exports = conf 60 | -------------------------------------------------------------------------------- /tests/browsers.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const browsers = { 4 | safariMac: { 5 | base: 'BrowserStack', 6 | os: 'OS X', 7 | os_version: 'High Sierra', 8 | browser: 'Safari', 9 | browser_version: 'latest', 10 | }, 11 | chromeMac: { 12 | base: 'BrowserStack', 13 | os: 'OS X', 14 | os_version: 'High Sierra', 15 | browser : 'Chrome', 16 | browser_version : 'latest', 17 | }, 18 | firefoxMac: { 19 | base: 'BrowserStack', 20 | os: 'OS X', 21 | os_version: 'High Sierra', 22 | browser: 'Firefox', 23 | browser_version: 'latest', 24 | }, 25 | edgeWin10: { 26 | base: 'BrowserStack', 27 | os: 'Windows', 28 | os_version: '10', 29 | browser: 'Edge', 30 | browser_version: 'latest', 31 | }, 32 | ie11Win10: { 33 | base: 'BrowserStack', 34 | os: 'Windows', 35 | os_version: '10', 36 | browser: 'IE', 37 | browser_version: '11.0', 38 | }, 39 | chromeWin10: { 40 | base: 'BrowserStack', 41 | os: 'Windows', 42 | os_version: '10', 43 | browser: 'Chrome', 44 | browser_version: 'latest', 45 | }, 46 | firefoxWin10: { 47 | base: 'BrowserStack', 48 | os: 'Windows', 49 | os_version: '10', 50 | browser: 'Firefox', 51 | browser_version: 'latest', 52 | }, 53 | ie10Win8: { 54 | base: 'BrowserStack', 55 | os: 'Windows', 56 | os_version: '8', 57 | browser: 'IE', 58 | browser_version: '10.0', 59 | }, 60 | iphoneX: { 61 | base: 'BrowserStack', 62 | os: 'ios', 63 | os_version: '11.0', 64 | device: 'iPhone X', 65 | real_mobile: true, 66 | }, 67 | pixel2: { 68 | base: 'BrowserStack', 69 | os: 'android', 70 | os_version: '8.0', 71 | device: 'Google Pixel 2', 72 | real_mobile: true, 73 | }, 74 | } 75 | 76 | const browsersKeys = Object.keys(browsers) 77 | 78 | module.exports = { 79 | browsers, 80 | browsersKeys, 81 | } 82 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const path = require('path') 3 | const ip = require('ip') 4 | const { 5 | browsers, 6 | browsersKeys, 7 | } = require('./browsers') 8 | 9 | const coveragePath = path.resolve(__dirname, 'coverage') 10 | const browserTest = process.env.browser === 'true' 11 | 12 | module.exports = function(config) { 13 | const conf = { 14 | basePath: '../', 15 | frameworks: ['chai', 'mocha', 'sinon'], 16 | files: [ 17 | 'tests/coverage/bs-custom-file-input.js', 18 | 'tests/polyfill.js', 19 | 'tests/units/index.spec.js', 20 | 'tests/units/eventHandlers.spec.js', 21 | ], 22 | exclude: [ 23 | 'tests/*.html', 24 | ], 25 | reporters: ['dots'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_ERROR || config.LOG_WARN, 29 | autoWatch: false, 30 | browsers: ['ChromeHeadless'], 31 | singleRun: true, 32 | concurrency: Infinity, 33 | } 34 | 35 | if (browserTest) { 36 | conf.files[0] = 'dist/bs-custom-file-input.min.js' 37 | conf.hostname = ip.address() 38 | conf.browserStack = { 39 | username: process.env.BROWSER_STACK_USERNAME, 40 | accessKey: process.env.BROWSER_STACK_ACCESS_KEY, 41 | build: `bsCustomFileInput-${new Date().toISOString()}`, 42 | project: 'bsCustomFileInput', 43 | retryLimit: 2, 44 | } 45 | 46 | conf.customLaunchers = browsers 47 | conf.browsers = browsersKeys 48 | conf.reporters.push('BrowserStack') 49 | } else { 50 | conf.reporters.push('coverage-istanbul') 51 | conf.coverageIstanbulReporter = { 52 | dir: coveragePath, 53 | reports: ['lcov', 'text-summary'], 54 | thresholds: { 55 | emitWarning: false, 56 | global: { 57 | statements: 95, 58 | branches: 95, 59 | functions: 95, 60 | lines: 95, 61 | }, 62 | }, 63 | } 64 | } 65 | 66 | config.set(conf) 67 | } 68 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { getDefaultText, restoreDefaultText } from './util' 2 | import { handleFormReset, handleInputChange } from './eventHandlers' 3 | import Selector from './selector' 4 | 5 | const customProperty = 'bsCustomFileInput' 6 | const Event = { 7 | FORMRESET : 'reset', 8 | INPUTCHANGE : 'change', 9 | } 10 | 11 | const bsCustomFileInput = { 12 | init(inputSelector = Selector.CUSTOMFILE, formSelector = Selector.FORM) { 13 | const customFileInputList = [].slice.call(document.querySelectorAll(inputSelector)) 14 | const formList = [].slice.call(document.querySelectorAll(formSelector)) 15 | 16 | for (let i = 0, len = customFileInputList.length; i < len; i++) { 17 | const input = customFileInputList[i] 18 | 19 | Object.defineProperty(input, customProperty, { 20 | value: { 21 | defaultText: getDefaultText(input), 22 | }, 23 | writable: true, 24 | }) 25 | 26 | handleInputChange.call(input) 27 | input.addEventListener(Event.INPUTCHANGE, handleInputChange) 28 | } 29 | 30 | for (let i = 0, len = formList.length; i < len; i++) { 31 | formList[i].addEventListener(Event.FORMRESET, handleFormReset) 32 | Object.defineProperty(formList[i], customProperty, { 33 | value: true, 34 | writable: true, 35 | }) 36 | } 37 | }, 38 | 39 | destroy() { 40 | const formList = [].slice.call(document.querySelectorAll(Selector.FORM)) 41 | .filter((form) => !!form.bsCustomFileInput) 42 | const customFileInputList = [].slice.call(document.querySelectorAll(Selector.INPUT)) 43 | .filter((input) => !!input.bsCustomFileInput) 44 | 45 | for (let i = 0, len = customFileInputList.length; i < len; i++) { 46 | const input = customFileInputList[i] 47 | 48 | restoreDefaultText(input) 49 | input[customProperty] = undefined 50 | 51 | input.removeEventListener(Event.INPUTCHANGE, handleInputChange) 52 | } 53 | 54 | for (let i = 0, len = formList.length; i < len; i++) { 55 | formList[i].removeEventListener(Event.FORMRESET, handleFormReset) 56 | formList[i][customProperty] = undefined 57 | } 58 | }, 59 | } 60 | 61 | export default bsCustomFileInput 62 | -------------------------------------------------------------------------------- /dist/bs-custom-file-input.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * bsCustomFileInput v1.3.4 (https://github.com/Johann-S/bs-custom-file-input) 3 | * Copyright 2018 - 2020 Johann-S 4 | * Licensed under MIT (https://github.com/Johann-S/bs-custom-file-input/blob/master/LICENSE) 5 | */ 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).bsCustomFileInput=t()}(this,function(){"use strict";var s={CUSTOMFILE:'.custom-file input[type="file"]',CUSTOMFILELABEL:".custom-file-label",FORM:"form",INPUT:"input"},l=function(e){if(0", 6 | "contributors": [ 7 | "XhmikosR " 8 | ], 9 | "main": "dist/bs-custom-file-input.js", 10 | "types": "bs-custom-file-input.d.ts", 11 | "bugs": { 12 | "url": "https://github.com/Johann-S/bs-custom-file-input/issues" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | }, 17 | "scripts": { 18 | "build": "rollup -c && rollup -c --environment PROD", 19 | "deploy:docs": "shx cp -r dist/ demo/dist/", 20 | "browserstack": "npm run build && cross-env browser=true karma start tests/karma.conf.js", 21 | "dev": "rollup --environment DEV -c -w", 22 | "js-lint": "eslint .", 23 | "prejs-test": "rollup -c --environment TEST,NODE_ENV:test", 24 | "js-test": "karma start tests/karma.conf.js", 25 | "release": "standard-version -a", 26 | "test": "npm run js-lint && npm run js-test" 27 | }, 28 | "files": [ 29 | "dist/*.{js,map}", 30 | "bs-custom-file-input.d.ts" 31 | ], 32 | "keywords": [ 33 | "bootstrap", 34 | "bootstrap 4", 35 | "custom file input", 36 | "vanillajs", 37 | "react", 38 | "angular" 39 | ], 40 | "homepage": "https://github.com/Johann-S/bs-custom-file-input", 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/Johann-S/bs-custom-file-input.git" 44 | }, 45 | "license": "MIT", 46 | "standard-version": { 47 | "scripts": { 48 | "postbump": "npm run build", 49 | "precommit": "git add dist" 50 | } 51 | }, 52 | "dependencies": {}, 53 | "devDependencies": { 54 | "@babel/core": "^7.9.0", 55 | "@babel/preset-env": "^7.9.0", 56 | "babel-eslint": "^10.1.0", 57 | "babel-plugin-istanbul": "^6.0.0", 58 | "bootstrap": "^4.4.1", 59 | "chai": "^4.2.0", 60 | "cross-env": "^7.0.2", 61 | "eslint": "^6.8.0", 62 | "ip": "^1.1.5", 63 | "karma": "^4.4.1", 64 | "karma-browserstack-launcher": "1.4.0", 65 | "karma-chai": "^0.1.0", 66 | "karma-chrome-launcher": "^3.1.0", 67 | "karma-coverage-istanbul-reporter": "^2.1.1", 68 | "karma-mocha": "^1.3.0", 69 | "karma-sinon": "^1.0.5", 70 | "mocha": "^7.1.1", 71 | "rollup": "^2.1.0", 72 | "rollup-plugin-babel": "^4.4.0", 73 | "rollup-plugin-uglify": "^6.0.4", 74 | "shx": "^0.3.2", 75 | "sinon": "^7.5.0", 76 | "standard-version": "^8.0.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.3.4](https://github.com/Johann-S/bs-custom-file-input/compare/v1.3.3...v1.3.4) (2020-02-03) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **core:** forgotten innerHTML ([dbffaa2](https://github.com/Johann-S/bs-custom-file-input/commit/dbffaa2eb060ab43cbec7739fe45a6446f68053a)) 11 | 12 | ### [1.3.3](https://github.com/Johann-S/bs-custom-file-input/compare/v1.3.2...v1.3.3) (2020-02-03) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **core:** use textContent instead of innerHTML ([d78897b](https://github.com/Johann-S/bs-custom-file-input/commit/d78897b8ef6afdd983e7629e168cba948adeed69)) 18 | 19 | ## [1.3.2](https://github.com/Johann-S/bs-custom-file-input/compare/v1.3.1...v1.3.2) (2019-03-28) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **display:** display files names on init ([0857b04](https://github.com/Johann-S/bs-custom-file-input/commit/0857b04)) 25 | 26 | 27 | 28 | 29 | ## [1.3.1](https://github.com/Johann-S/bs-custom-file-input/compare/v1.3.0...v1.3.1) (2018-12-10) 30 | 31 | 32 | 33 | 34 | # [1.3.0](https://github.com/Johann-S/bs-custom-file-input/compare/v1.1.1...v1.3.0) (2018-11-16) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * **display:** restore text on empty values ([21ba2c0](https://github.com/Johann-S/bs-custom-file-input/commit/21ba2c0)) 40 | 41 | 42 | ### Features 43 | 44 | * **display:** remove fakepath ([0cd194a](https://github.com/Johann-S/bs-custom-file-input/commit/0cd194a)) 45 | 46 | 47 | 48 | 49 | # [1.2.0](https://github.com/Johann-S/bs-custom-file-input/compare/v1.1.1...v1.2.0) (2018-09-18) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * **core:** remove unused events and better destroy method ([2845f4e](https://github.com/Johann-S/bs-custom-file-input/commit/2845f4e)) 55 | 56 | 57 | ### Features 58 | 59 | * **core:** allow to use children in label elements ([e92a581](https://github.com/Johann-S/bs-custom-file-input/commit/e92a581)) 60 | * **demo:** add a demo ([22f925b](https://github.com/Johann-S/bs-custom-file-input/commit/22f925b)) 61 | 62 | 63 | 64 | 65 | ## [1.1.1](https://github.com/Johann-S/bs-custom-file-input/compare/v1.1.0...v1.1.1) (2018-09-05) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **readme:** add angular example ([2bdd38a](https://github.com/Johann-S/bs-custom-file-input/commit/2bdd38a)) 71 | * **ts:** type definition ([1f7836a](https://github.com/Johann-S/bs-custom-file-input/commit/1f7836a)) 72 | 73 | 74 | 75 | 76 | # 1.1.0 (2018-09-04) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * **core:** make properties private ([b1a5dab](https://github.com/Johann-S/bs-custom-file-input/commit/b1a5dab)) 82 | 83 | 84 | ### Features 85 | 86 | * **core:** add typescript definition ([#29](https://github.com/Johann-S/bs-custom-file-input/pull/29)) ([c8e5a01](https://github.com/Johann-S/bs-custom-file-input/commit/c8e5a01)) 87 | * **core:** allow to pass custom selectors ([#25](https://github.com/Johann-S/bs-custom-file-input/issues/25)) ([85db226](https://github.com/Johann-S/bs-custom-file-input/commit/85db226)) 88 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | BS Custom File Input test 12 | 13 | 14 |
15 |
16 |

BS Custom File Input tests

17 | 18 |
19 | 22 |
23 | 24 |
25 |

Examples

26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 |

Example with form

43 |
44 |
45 |
46 | 49 |
50 |
51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |

Example with label containing a child

66 |
67 | 68 | 71 |
72 |
73 |
74 | 75 | 76 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /tests/units/index.spec.js: -------------------------------------------------------------------------------- 1 | var customInputFile = [ 2 | '
', 3 | ' ', 4 | ' ', 5 | '
', 6 | ].join('') 7 | 8 | describe('index.js', function () { 9 | var input 10 | var mochaFixtureDiv 11 | 12 | before(function () { 13 | mochaFixtureDiv = document.getElementById('mocha-fixture') 14 | }) 15 | 16 | beforeEach(function() { 17 | mochaFixtureDiv.innerHTML = customInputFile 18 | input = document.querySelector('input') 19 | }) 20 | 21 | afterEach(function () { 22 | mochaFixtureDiv.innerHTML = '' 23 | }) 24 | 25 | describe('init', function () { 26 | it('should add bsCustomFileInput property', function () { 27 | bsCustomFileInput.init() 28 | 29 | expect(input.bsCustomFileInput).not.undefined 30 | }) 31 | 32 | it('should store default text', function () { 33 | bsCustomFileInput.init() 34 | 35 | var label = document.querySelector('.custom-file-label') 36 | 37 | expect(input.bsCustomFileInput.defaultText).equal(label.innerHTML) 38 | }) 39 | 40 | it('should add listener to the given input', function () { 41 | var spy = sinon.spy(input, 'addEventListener') 42 | 43 | bsCustomFileInput.init() 44 | 45 | expect(spy.called).equal(true) 46 | }) 47 | 48 | it('should add an event listener on forms', function () { 49 | var form = document.createElement('form') 50 | form.innerHTML = customInputFile 51 | 52 | mochaFixtureDiv.appendChild(form) 53 | 54 | var spy = sinon.spy(form, 'addEventListener') 55 | 56 | bsCustomFileInput.init() 57 | 58 | expect(spy.called).to.be.true 59 | }) 60 | 61 | it('should select only my custom input selector', function () { 62 | mochaFixtureDiv.innerHTML = [ 63 | '', 64 | customInputFile, 65 | ].join('') 66 | 67 | bsCustomFileInput.init('#test') 68 | 69 | var testInput = document.getElementById('test') 70 | var otherInput = document.querySelector('.custom-file input[type="file"]') 71 | 72 | expect(testInput.bsCustomFileInput).not.undefined 73 | expect(otherInput.bsCustomFileInput).undefined 74 | }) 75 | 76 | it('should display already selected files on init', function () { 77 | input.setAttribute('multiple', '') 78 | Object.defineProperty(input, 'files', { 79 | value: [ 80 | new File([], 'myFakeFile.exe'), 81 | new File([], 'fakeImage.png'), 82 | ], 83 | }) 84 | 85 | bsCustomFileInput.init() 86 | 87 | var label = document.querySelector('.custom-file-label') 88 | 89 | expect(label.innerHTML).equal('myFakeFile.exe, fakeImage.png') 90 | }) 91 | }) 92 | 93 | describe('destroy', function () { 94 | it('should remove bsCustomFileInput property', function () { 95 | bsCustomFileInput.init() 96 | bsCustomFileInput.destroy() 97 | 98 | expect(input.bsCustomFileInput).to.undefined 99 | }) 100 | 101 | it('should remove event listener on forms', function () { 102 | var form = document.createElement('form') 103 | form.innerHTML = [ 104 | '
', 105 | ' ', 106 | '
', 107 | ].join('') 108 | 109 | mochaFixtureDiv.appendChild(form) 110 | 111 | var spy = sinon.spy(form, 'removeEventListener') 112 | 113 | bsCustomFileInput.init() 114 | bsCustomFileInput.destroy() 115 | 116 | expect(spy.called).to.be.true 117 | }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bootstrap custom file input 9 | 14 | 15 | 16 | 36 |
37 |
38 |
39 |

Examples

40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 |
54 | 55 |
56 |

Example with form

57 |
58 |
59 |
60 | 63 |
64 |
65 |
66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 |
79 |

Example with label containing a child

80 |
81 | 82 | 85 |
86 |
87 |
88 |
89 |
90 |
91 |

92 | Made with 93 | 94 | 95 | 96 | by Johann-S 97 |

98 |
99 |
100 | 101 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bs-custom-file-input 2 | 3 | [![npm version](https://img.shields.io/npm/v/bs-custom-file-input.svg)](https://www.npmjs.com/package/bs-custom-file-input) 4 | [![dependencies Status](https://img.shields.io/david/Johann-S/bs-custom-file-input.svg)](https://david-dm.org/Johann-S/bs-custom-file-input) 5 | [![devDependencies Status](https://img.shields.io/david/dev/Johann-S/bs-custom-file-input.svg)](https://david-dm.org/Johann-S/bs-custom-file-input?type=dev) 6 | [![Build Status](https://github.com/Johann-S/bs-custom-file-input/workflows/Tests/badge.svg)](https://github.com/Johann-S/bs-custom-file-input/actions?workflow=Tests) 7 | [![Coverage Status](https://img.shields.io/coveralls/github/Johann-S/bs-custom-file-input/master.svg)](https://coveralls.io/github/Johann-S/bs-custom-file-input?branch=master) 8 | [![JS gzip size](https://img.badgesize.io/Johann-S/bs-custom-file-input/master/dist/bs-custom-file-input.min.js?compression=gzip&label=JS+gzip+size)](https://github.com/Johann-S/bs-custom-file-input/tree/master/dist/bs-custom-file-input.min.js) 9 | [![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=L1Z6cllmR0pVVUZBRmxTaGtEcm1QamUxdTZoQmRLeUFvWVlOcW5iODNVWT0tLUZTVWRKUzc4T05xSmhlZlJObVRKNEE9PQ==--177788f5ac0c50dcd3dd3eed31e39662d5612e7f)](https://www.browserstack.com/automate/public-build/L1Z6cllmR0pVVUZBRmxTaGtEcm1QamUxdTZoQmRLeUFvWVlOcW5iODNVWT0tLUZTVWRKUzc4T05xSmhlZlJObVRKNEE9PQ==--177788f5ac0c50dcd3dd3eed31e39662d5612e7f) 10 | 11 | A little plugin which makes Bootstrap 4 custom file input dynamic with no dependencies. 12 | 13 | You can use it on [React](https://stackblitz.com/edit/bs-custom-file-input-react) and [Angular](https://stackblitz.com/edit/bs-custom-file-input-angular) too because this plugin is written with the most used JavaScript framework: [VanillaJS](http://vanilla-js.com/). 14 | 15 | [Demo](https://bs-custom-file-input.netlify.com/) 16 | 17 | Features: 18 | 19 | - Works with Bootstrap 4 20 | - Works without *dependencies* and jQuery 21 | - Display file name 22 | - Display file names for `multiple` input 23 | - Reset your custom file input to its initial state 24 | - Handle form reset 25 | - Allow custom selectors for input, and form 26 | - Allow to drag and drop files 27 | - Allow you to change the default display with a child in the `