├── .browserslistrc ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── css │ └── main.css ├── index.html └── js │ ├── build.js │ ├── config.js │ ├── dialog-loader.js │ ├── file-util.js │ ├── index.js │ ├── lib │ ├── clean-css.js │ └── sass.worker.js │ ├── plugins.js │ ├── toggler.js │ └── util.js └── webpack.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | # https://github.com/browserslist/browserslist#readme 2 | 3 | >= 1% 4 | last 1 major version 5 | not dead 6 | Chrome >= 45 7 | Firefox >= 38 8 | Edge >= 12 9 | iOS >= 9 10 | Safari >= 9 11 | Android >= 4.4 12 | Opera >= 30 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: jservoire 2 | liberapay: Johann-S 3 | custom: https://www.paypal.me/jservoire 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | env: 4 | CI: true 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Clone repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: "10" 18 | 19 | - run: node --version 20 | - run: npm --version 21 | 22 | - name: Install npm dependencies 23 | run: npm ci 24 | 25 | - name: Run tests 26 | run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | /index.html 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bs-customizer 2 | 3 | [![Build Status](https://github.com/Johann-S/bs-customizer/workflows/Tests/badge.svg)](https://github.com/Johann-S/bs-customizer/actions?workflow=Tests) 4 | [![dependencies Status](https://img.shields.io/david/Johann-S/bs-customizer.svg)](https://david-dm.org/Johann-S/bs-customizer) 5 | [![devDependencies Status](https://img.shields.io/david/dev/Johann-S/bs-customizer.svg)](https://david-dm.org/Johann-S/bs-customizer?type=dev) 6 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com/) 7 | 8 | A simple customizer which allows you to build a custom version of [Bootstrap](https://getbootstrap.com/) v4 9 | 10 | Features: 11 | 12 | - Select which plugins you want to use 13 | - Build a custom Bootstrap CSS version 14 | - Choose to include Popper.js or not 15 | - Minify your custom version 16 | 17 | You can use it [here](https://johann-s.github.io/bs-customizer/). 18 | 19 | ## Contributing 20 | 21 | You're more than welcome to contribute to our project :heart: 22 | 23 | To contribute: 24 | 25 | - Edit files in `src/` 26 | - Test your changes by running `npm run dev` 27 | - Commit and open a pull request 28 | 29 | Thank you! 30 | 31 | ## Testing our latest changes 32 | 33 | If you want to try out our latest changes (pushed in our `master` branch) you can see them in . 34 | 35 | Our production branch is the `gh-pages` branch which can be seen on . 36 | 37 | ## Support me 38 | 39 | If you want to thank me, you can support me and become my [Patron](https://www.patreon.com/jservoire). 40 | 41 | ## License 42 | 43 | [MIT](https://github.com/Johann-S/bs-customizer/blob/master/LICENSE) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bs-customizer", 3 | "version": "1.4.0", 4 | "description": "Create custom Bootstrap 4.x builds", 5 | "private": true, 6 | "scripts": { 7 | "deploy": "gh-pages --dist ./ --src \"{index.html,dist/**}\"", 8 | "dev": "webpack-dev-server --open --mode=development --config webpack.config.js", 9 | "prod": "webpack --mode=production --config webpack.config.js", 10 | "standard": "standard --parser babel-eslint", 11 | "vnu": "html-validator --file=index.html", 12 | "test": "npm run standard && npm run prod && npm run vnu" 13 | }, 14 | "keywords": [ 15 | "customizer", 16 | "bootstrap", 17 | "bootstrap4", 18 | "javascript" 19 | ], 20 | "author": "Johann-S ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "autoprefixer": "^9.7.1", 24 | "axios": "^0.19.0", 25 | "bootstrap": "^4.3.1", 26 | "jquery": "^3.4.1", 27 | "jszip": "^3.2.2", 28 | "postcss": "^7.0.21", 29 | "sass.js": "^0.11.1", 30 | "uglifyjs-browser": "^3.0.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.7.2", 34 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 35 | "@babel/preset-env": "^7.7.1", 36 | "babel-eslint": "^10.0.3", 37 | "babel-loader": "^8.0.6", 38 | "clean-css": "^4.2.1", 39 | "clean-webpack-plugin": "^3.0.0", 40 | "copy-webpack-plugin": "^5.0.5", 41 | "css-loader": "^3.2.0", 42 | "gh-pages": "^2.1.1", 43 | "glob-all": "^3.1.0", 44 | "html-validator-cli": "^7.0.0", 45 | "html-webpack-plugin": "^3.2.0", 46 | "mini-css-extract-plugin": "^0.8.0", 47 | "optimize-css-assets-webpack-plugin": "^5.0.3", 48 | "postcss-loader": "^3.0.0", 49 | "purgecss-webpack-plugin": "^1.6.0", 50 | "script-ext-html-webpack-plugin": "^2.1.4", 51 | "standard": "^14.3.1", 52 | "uglifyjs-webpack-plugin": "^2.2.0", 53 | "webpack": "^4.41.2", 54 | "webpack-cli": "^3.3.10", 55 | "webpack-dev-server": "^3.9.0", 56 | "write-file-webpack-plugin": "^4.5.1" 57 | }, 58 | "standard": { 59 | "ignore": [ 60 | "src/js/lib/*.js" 61 | ] 62 | }, 63 | "engines": { 64 | "node": ">=10" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | .no-underline { 2 | text-decoration: none !important; 3 | } 4 | 5 | .icon { 6 | max-width: 25px; 7 | } 8 | 9 | .icon-build { 10 | max-width: 20px; 11 | } 12 | 13 | .loader { 14 | display: inline-block; 15 | font-size: initial; 16 | box-sizing: border-box; 17 | } 18 | 19 | @keyframes spin { 20 | 0% { 21 | transform: rotate(0); 22 | } 23 | 24 | 100% { 25 | transform: rotate(359deg); 26 | } 27 | } 28 | 29 | .spin { 30 | animation: spin 2s linear infinite; 31 | } 32 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bootstrap Customizer 8 | 9 | 10 |
11 |
12 |

Bootstrap Customizer

13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 |
Customize and download
24 |

Create a custom build of Bootstrap v<%= htmlWebpackPlugin.options.bsVersion %> by choosing the components and JavaScript plugins you need.

25 |
26 |
27 | 33 |
34 |
35 |

36 | JavaScript Plugins 37 | 40 |

41 |
42 |
43 |
44 |
45 | 46 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | 58 |
59 |
60 | 61 |
62 |
63 | 64 | 67 |
68 |
69 | 70 |
71 |
72 | 73 | 76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 | 86 |
87 |
88 | 89 |
90 |
91 | 92 | 95 |
96 |
97 | 98 |
99 |
100 | 101 | 104 |
105 |
106 | 107 |
108 |
109 | 110 | 113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 | 123 |
124 |
125 | 126 |
127 |
128 | 129 | 132 |
133 |
134 | 135 |
136 |
137 | 138 | 141 |
142 |
143 |
144 |
145 |
146 | 147 |
148 |

149 | Layout 150 | 153 |

154 |
155 |
156 |
157 |
158 | 159 | 162 |
163 |
164 |
165 |
166 | 167 | 170 |
171 |
172 |
173 |
174 |
175 | 176 |
177 |

178 | Content 179 | 182 |

183 |
184 |
185 |
186 |
187 | 188 | 191 |
192 |
193 |
194 |
195 | 196 | 199 |
200 |
201 |
202 |
203 | 204 | 207 |
208 |
209 |
210 |
211 | 212 | 215 |
216 |
217 |
218 |
219 |
220 |
221 | 222 | 225 |
226 |
227 |
228 |
229 |
230 | 231 |
232 |

233 | Components 234 | 237 |

238 |
239 |
240 |
241 |
242 | 243 | 246 |
247 |
248 |
249 |
250 | 251 | 254 |
255 |
256 |
257 |
258 | 259 | 262 |
263 |
264 |
265 |
266 | 267 | 270 |
271 |
272 |
273 |
274 |
275 |
276 | 277 | 280 |
281 |
282 |
283 |
284 | 285 | 288 |
289 |
290 |
291 |
292 | 293 | 296 |
297 |
298 |
299 |
300 | 301 | 304 |
305 |
306 |
307 |
308 |
309 |
310 | 311 | 314 |
315 |
316 |
317 |
318 | 319 | 322 |
323 |
324 |
325 |
326 | 327 | 330 |
331 |
332 |
333 |
334 | 335 | 338 |
339 |
340 |
341 |
342 |
343 |
344 | 345 | 348 |
349 |
350 |
351 |
352 |
353 | 354 |
355 |

356 | Utilities 357 | 360 |

361 | 362 |
363 |
364 |
365 |
366 | 367 | 370 |
371 |
372 |
373 |
374 | 375 | 378 |
379 |
380 |
381 |
382 | 383 | 386 |
387 |
388 |
389 |
390 | 391 | 394 |
395 |
396 |
397 |
398 |
399 |
400 | 401 | 404 |
405 |
406 |
407 |
408 | 409 | 412 |
413 |
414 |
415 |
416 | 417 | 420 |
421 |
422 |
423 |
424 | 425 | 428 |
429 |
430 |
431 |
432 |
433 |
434 | 435 | 438 |
439 |
440 |
441 |
442 | 443 | 446 |
447 |
448 |
449 |
450 | 451 | 454 |
455 |
456 |
457 |
458 | 459 | 462 |
463 |
464 |
465 |
466 |
467 |
468 | 469 | 472 |
473 |
474 |
475 |
476 | 477 | 480 |
481 |
482 |
483 |
484 | 485 | 488 |
489 |
490 |
491 |
492 | 493 | 496 |
497 |
498 |
499 |
500 |
501 |
502 | 503 | 506 |
507 |
508 |
509 |
510 | 511 | 514 |
515 |
516 |
517 |
518 |
519 | 520 |
521 |

Options

522 |
523 |
524 |
525 |
526 | 527 | 530 |
531 |
532 | 533 |
534 |
535 | 536 | 539 |
540 |
541 | 542 |
543 |
544 | 545 | 548 |
549 |
550 |
551 |
552 |
553 |
554 | 561 |
562 |
563 |
564 |
565 |
566 |

567 | Made with 568 | 569 | 570 | 571 | by Johann-S 572 |

573 |
574 |
575 | 576 | 577 | -------------------------------------------------------------------------------- /src/js/build.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import JSZip from 'jszip' 3 | import Sass from 'sass.js/dist/sass' 4 | import CleanCSS from './lib/clean-css' 5 | import postcss from 'postcss' 6 | import autoprefixer from 'autoprefixer' 7 | 8 | import { bootstrapVersion } from './config' 9 | import { jsPlugins, scssPlugins } from './plugins' 10 | import { createJsFileContent, generateLink } from './file-util' 11 | import { formatScssList, uniqArray } from './util' 12 | 13 | const year = new Date().getFullYear() 14 | const header = `/*! 15 | * Bootstrap v${bootstrapVersion} custom (https://getbootstrap.com/) 16 | * Built with https://johann-s.github.io/bs-customizer/ 17 | * Copyright 2011-${year} The Bootstrap Authors 18 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 19 | */ 20 | ` 21 | 22 | const popperCDN = 'https://unpkg.com/popper.js/dist/umd/popper.js' 23 | const autoPrefixerConfig = { 24 | cascade: false, 25 | overrideBrowserslist: [ 26 | '>= 1%', 27 | 'last 1 major version', 28 | 'not dead', 29 | 'Chrome >= 45', 30 | 'Firefox >= 38', 31 | 'Edge >= 12', 32 | 'Explorer >= 10', 33 | 'iOS >= 9', 34 | 'Safari >= 9', 35 | 'Android >= 4.4', 36 | 'Opera >= 30' 37 | ] 38 | } 39 | 40 | const configCleanCSS = { 41 | level: 1, 42 | format: { 43 | breakWith: 'lf' 44 | } 45 | } 46 | const configSass = { 47 | precision: 6, 48 | style: Sass.style.expanded 49 | } 50 | 51 | const buildJavaScript = (files, minify) => { 52 | return new Promise((resolve, reject) => { 53 | axios.all(files) 54 | .then(filesData => { 55 | resolve(createJsFileContent(filesData, minify)) 56 | }) 57 | .catch(() => { 58 | reject(new Error('An error occured during building JS files')) 59 | }) 60 | }) 61 | } 62 | 63 | const buildScss = (files, minify) => { 64 | return new Promise((resolve, reject) => { 65 | axios.all(files) 66 | .then(scssFiles => { 67 | const sass = new Sass() 68 | sass.options(configSass) 69 | 70 | const resultFileOrder = [] 71 | scssFiles.forEach(result => { 72 | const splittedString = result.config.url.split('/') 73 | const fileName = splittedString[splittedString.length - 1] 74 | 75 | if (splittedString.indexOf('mixins') !== -1) { 76 | const path = `mixins/${fileName}` 77 | resultFileOrder.push(path) 78 | sass.writeFile(path, result.data) 79 | } else if (splittedString.indexOf('utilities') !== -1) { 80 | const path = `utilities/${fileName}` 81 | resultFileOrder.push(path) 82 | sass.writeFile(path, result.data) 83 | } else if (splittedString.indexOf('vendor') !== -1) { 84 | const path = `vendor/${fileName}` 85 | resultFileOrder.push(path) 86 | sass.writeFile(path, result.data) 87 | } else { 88 | resultFileOrder.push(fileName) 89 | sass.writeFile(fileName, result.data) 90 | } 91 | }) 92 | 93 | const result = formatScssList(resultFileOrder) 94 | .map(file => { 95 | if (file.charAt(0) === '_') { 96 | file = file.substr(1) 97 | } 98 | 99 | const splitFile = file.split('.scss') 100 | return `@import "${splitFile[0]}";` 101 | }) 102 | 103 | sass.compile(result.join(' '), result => { 104 | if (result.status === 0) { 105 | let cssContent = result.text 106 | 107 | postcss([ 108 | autoprefixer(autoPrefixerConfig) 109 | ]) 110 | .process(cssContent, { from: undefined }) 111 | .then(compiled => { 112 | cssContent = compiled.css 113 | 114 | if (minify) { 115 | cssContent = new CleanCSS(configCleanCSS).minify(cssContent).styles 116 | } 117 | 118 | resolve(cssContent) 119 | }) 120 | } else { 121 | reject(result.message) 122 | } 123 | }) 124 | }) 125 | }) 126 | } 127 | 128 | const build = (pluginList, addPopper, minify, includeCSS) => { 129 | const fileName = `bootstrap.custom${minify ? '.min' : ''}` 130 | const zip = new JSZip() 131 | 132 | let listJsRequest = [] 133 | let listScssRequest = [] 134 | 135 | pluginList.forEach(plugin => { 136 | if (jsPlugins[plugin]) { 137 | listJsRequest = listJsRequest.concat(jsPlugins[plugin].js) 138 | 139 | if (includeCSS) { 140 | listScssRequest = listScssRequest.concat(jsPlugins[plugin].scss) 141 | } 142 | } else { 143 | listScssRequest = listScssRequest.concat(scssPlugins[plugin]) 144 | } 145 | }) 146 | 147 | listJsRequest = uniqArray(listJsRequest) 148 | .map(url => axios.get(url)) 149 | 150 | if (addPopper) { 151 | listJsRequest.unshift(axios.get(popperCDN)) 152 | } 153 | 154 | if (listScssRequest.length > 0) { 155 | // Add reboot by default 156 | listScssRequest = listScssRequest.concat(scssPlugins.Reboot) 157 | listScssRequest = uniqArray(listScssRequest).map(url => axios.get(url)) 158 | } 159 | 160 | return new Promise(resolve => { 161 | buildJavaScript(listJsRequest, minify) 162 | .then(jsFileContent => { 163 | if (jsFileContent.length > 0) { 164 | zip.file(`${fileName}.js`, `${header}${jsFileContent}`) 165 | } 166 | 167 | if (listScssRequest.length > 0) { 168 | return buildScss(listScssRequest, minify) 169 | } 170 | 171 | return Promise.resolve('') 172 | }) 173 | .then(cssContent => { 174 | if (cssContent.length > 0) { 175 | zip.file(`${fileName}.css`, `${header}${cssContent}`) 176 | } 177 | 178 | zip.generateAsync({ type: 'blob' }) 179 | .then(content => { 180 | resolve(generateLink(content)) 181 | }) 182 | }) 183 | }) 184 | } 185 | 186 | export { 187 | build 188 | } 189 | -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | const bootstrapVersion = '4.3.1' 2 | const bsJsCDN = `https://unpkg.com/bootstrap@${bootstrapVersion}/js/dist/` 3 | const bsScssCDN = `https://unpkg.com/bootstrap@${bootstrapVersion}/scss/` 4 | 5 | module.exports = Object.freeze({ 6 | bootstrapVersion, 7 | bsJsCDN, 8 | bsScssCDN 9 | }) 10 | -------------------------------------------------------------------------------- /src/js/dialog-loader.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery' 2 | import 'bootstrap/js/dist/modal' 3 | 4 | let $modal 5 | let $loader 6 | let $generatedLink 7 | let $loadingStatus 8 | let $modalHeader 9 | 10 | const dialogClass = 'js-dialog-loader' 11 | const template = ` 12 | 39 | ` 40 | 41 | const createModal = () => { 42 | $(document.body).append(template) 43 | $modal = $(`.${dialogClass}`) 44 | $loader = $('.js-loader') 45 | $generatedLink = $('.js-generated-link') 46 | $loadingStatus = $('.js-loading-status') 47 | $modalHeader = $modal.find('.js-modal-header') 48 | 49 | $modal.on('hidden.bs.modal', () => { 50 | $loadingStatus.text('') 51 | 52 | $modalHeader.addClass('d-none') 53 | URL.revokeObjectURL($generatedLink.attr('href')) 54 | $generatedLink.addClass('d-none') 55 | .attr('href', '') 56 | 57 | $loader.removeClass('d-none') 58 | $loadingStatus.removeClass('d-none') 59 | }) 60 | } 61 | 62 | const showModal = (callback) => { 63 | $loadingStatus.text('Building your custom Bootstrap...') 64 | $modal 65 | .one('shown.bs.modal', () => callback()) 66 | .modal('show') 67 | } 68 | 69 | const updateLink = (fileName, link) => { 70 | $generatedLink.attr('href', link) 71 | .attr('download', fileName) 72 | .removeClass('d-none') 73 | 74 | $modalHeader.removeClass('d-none') 75 | $loader.addClass('d-none') 76 | $loadingStatus.addClass('d-none') 77 | } 78 | 79 | const hideModal = () => { 80 | $modal.modal('hide') 81 | } 82 | 83 | export { 84 | createModal, 85 | showModal, 86 | hideModal, 87 | updateLink 88 | } 89 | -------------------------------------------------------------------------------- /src/js/file-util.js: -------------------------------------------------------------------------------- 1 | import uglify from 'uglifyjs-browser' 2 | 3 | const uglifyConfig = { 4 | compress: { 5 | typeofs: false 6 | }, 7 | mangle: true, 8 | output: { 9 | comments: /^!/ 10 | } 11 | } 12 | 13 | const concatFileData = (files) => { 14 | let content = '' 15 | 16 | files.forEach(file => { 17 | content += file.data 18 | }) 19 | 20 | return content 21 | } 22 | 23 | const createJsFileContent = (jsFiles, minify) => { 24 | let content = concatFileData(jsFiles) 25 | 26 | if (minify) { 27 | const minifyResult = uglify.minify(content, uglifyConfig) 28 | 29 | if (minifyResult.error) { 30 | throw new Error('Unable to minify') 31 | } 32 | 33 | content = minifyResult.code 34 | } 35 | 36 | return content 37 | } 38 | 39 | const createCssFileContent = scssFiles => { 40 | return concatFileData(scssFiles) 41 | } 42 | 43 | const generateLink = fileContent => { 44 | const downloadUrl = URL.createObjectURL(fileContent, { 45 | type: 'application/zip' 46 | }) 47 | 48 | return downloadUrl 49 | } 50 | 51 | export { 52 | createJsFileContent, 53 | createCssFileContent, 54 | generateLink 55 | } 56 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery' 2 | import Sass from 'sass.js/dist/sass' 3 | 4 | import { formatList, ucfirst, getSassWorkerPath, supportedBrowser } from './util' 5 | import { createModal, showModal, updateLink } from './dialog-loader' 6 | import { initToggler } from './toggler' 7 | import { bootstrapVersion } from './config' 8 | 9 | import 'bootstrap/dist/css/bootstrap.css' 10 | import '../css/main.css' 11 | 12 | $(() => { 13 | if (!supportedBrowser()) { 14 | document.querySelector('.js-alert-browser').classList.remove('d-none') 15 | return 16 | } 17 | 18 | const { checkboxPopper, chkCSS, chkMinify } = initToggler() 19 | Sass.setWorkerUrl(getSassWorkerPath()) 20 | createModal() 21 | 22 | const formEl = document.querySelector('.js-form-customize') 23 | 24 | formEl.addEventListener('submit', (event) => { 25 | event.preventDefault() 26 | 27 | const formData = $(formEl).serializeArray() 28 | if (formData.length === 0) { 29 | return 30 | } 31 | 32 | showModal(() => { 33 | import(/* webpackChunkName: "build" */ './build') 34 | .then(({ build }) => { 35 | const fileName = `bootstrap.${bootstrapVersion}.custom.zip` 36 | const pluginList = formatList(formData.map((value) => ucfirst(value.name))) 37 | 38 | build(pluginList, checkboxPopper.checked, chkMinify.checked, chkCSS.checked) 39 | .then(url => { 40 | updateLink(fileName, url) 41 | }) 42 | }) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/js/plugins.js: -------------------------------------------------------------------------------- 1 | import { bsJsCDN, bsScssCDN } from './config' 2 | 3 | const jsPlugins = { 4 | Alert: { 5 | js: [ 6 | `${bsJsCDN}util.js`, 7 | `${bsJsCDN}alert.js` 8 | ], 9 | scss: [ 10 | `${bsScssCDN}_functions.scss`, 11 | `${bsScssCDN}_variables.scss`, 12 | `${bsScssCDN}mixins/_border-radius.scss`, 13 | `${bsScssCDN}mixins/_gradients.scss`, 14 | `${bsScssCDN}mixins/_alert.scss`, 15 | `${bsScssCDN}mixins/_transition.scss`, 16 | `${bsScssCDN}_root.scss`, 17 | `${bsScssCDN}_transitions.scss`, 18 | `${bsScssCDN}_alert.scss` 19 | ] 20 | }, 21 | Button: { 22 | js: [ 23 | `${bsJsCDN}button.js` 24 | ], 25 | scss: [ 26 | `${bsScssCDN}_functions.scss`, 27 | `${bsScssCDN}_variables.scss`, 28 | `${bsScssCDN}mixins/_border-radius.scss`, 29 | `${bsScssCDN}mixins/_gradients.scss`, 30 | `${bsScssCDN}mixins/_box-shadow.scss`, 31 | `${bsScssCDN}mixins/_hover.scss`, 32 | `${bsScssCDN}mixins/_transition.scss`, 33 | `${bsScssCDN}mixins/_buttons.scss`, 34 | `${bsScssCDN}_root.scss`, 35 | `${bsScssCDN}_buttons.scss`, 36 | `${bsScssCDN}_button-group.scss`, 37 | `${bsScssCDN}_close.scss` 38 | ] 39 | }, 40 | Carousel: { 41 | js: [ 42 | `${bsJsCDN}util.js`, 43 | `${bsJsCDN}carousel.js` 44 | ], 45 | scss: [ 46 | `${bsScssCDN}_functions.scss`, 47 | `${bsScssCDN}_variables.scss`, 48 | `${bsScssCDN}mixins/_border-radius.scss`, 49 | `${bsScssCDN}mixins/_breakpoints.scss`, 50 | `${bsScssCDN}mixins/_hover.scss`, 51 | `${bsScssCDN}mixins/_transition.scss`, 52 | `${bsScssCDN}_root.scss`, 53 | `${bsScssCDN}_card.scss`, 54 | `${bsScssCDN}_carousel.scss` 55 | ] 56 | }, 57 | Collapse: { 58 | js: [ 59 | `${bsJsCDN}util.js`, 60 | `${bsJsCDN}collapse.js` 61 | ], 62 | scss: [ 63 | `${bsScssCDN}_functions.scss`, 64 | `${bsScssCDN}_variables.scss`, 65 | `${bsScssCDN}mixins/_transition.scss`, 66 | `${bsScssCDN}_root.scss`, 67 | `${bsScssCDN}_transitions.scss` 68 | ] 69 | }, 70 | Dropdown: { 71 | js: [ 72 | `${bsJsCDN}util.js`, 73 | `${bsJsCDN}dropdown.js` 74 | ], 75 | scss: [ 76 | `${bsScssCDN}_functions.scss`, 77 | `${bsScssCDN}_variables.scss`, 78 | `${bsScssCDN}mixins/_border-radius.scss`, 79 | `${bsScssCDN}mixins/_box-shadow.scss`, 80 | `${bsScssCDN}mixins/_caret.scss`, 81 | `${bsScssCDN}mixins/_nav-divider.scss`, 82 | `${bsScssCDN}mixins/_hover.scss`, 83 | `${bsScssCDN}mixins/_gradients.scss`, 84 | `${bsScssCDN}_root.scss`, 85 | `${bsScssCDN}_dropdown.scss` 86 | ] 87 | }, 88 | Modal: { 89 | js: [ 90 | `${bsJsCDN}util.js`, 91 | `${bsJsCDN}modal.js` 92 | ], 93 | scss: [ 94 | `${bsScssCDN}_functions.scss`, 95 | `${bsScssCDN}_variables.scss`, 96 | `${bsScssCDN}mixins/_border-radius.scss`, 97 | `${bsScssCDN}mixins/_box-shadow.scss`, 98 | `${bsScssCDN}mixins/_breakpoints.scss`, 99 | `${bsScssCDN}mixins/_transition.scss`, 100 | `${bsScssCDN}_root.scss`, 101 | `${bsScssCDN}_transitions.scss`, 102 | `${bsScssCDN}_modal.scss` 103 | ] 104 | }, 105 | Popover: { 106 | js: [ 107 | `${bsJsCDN}tooltip.js`, 108 | `${bsJsCDN}popover.js` 109 | ], 110 | scss: [ 111 | `${bsScssCDN}_functions.scss`, 112 | `${bsScssCDN}_variables.scss`, 113 | `${bsScssCDN}mixins/_border-radius.scss`, 114 | `${bsScssCDN}mixins/_box-shadow.scss`, 115 | `${bsScssCDN}mixins/_reset-text.scss`, 116 | `${bsScssCDN}_root.scss`, 117 | `${bsScssCDN}_popover.scss` 118 | ] 119 | }, 120 | Scrollspy: { 121 | js: [ 122 | `${bsJsCDN}util.js`, 123 | `${bsJsCDN}scrollspy.js` 124 | ], 125 | scss: [ 126 | `${bsScssCDN}_functions.scss`, 127 | `${bsScssCDN}_variables.scss`, 128 | `${bsScssCDN}mixins/_border-radius.scss`, 129 | `${bsScssCDN}mixins/_breakpoints.scss`, 130 | `${bsScssCDN}mixins/_hover.scss`, 131 | `${bsScssCDN}_root.scss`, 132 | `${bsScssCDN}_nav.scss`, 133 | `${bsScssCDN}_navbar.scss` 134 | ] 135 | }, 136 | Tab: { 137 | js: [ 138 | `${bsJsCDN}util.js`, 139 | `${bsJsCDN}tab.js` 140 | ], 141 | scss: [ 142 | `${bsScssCDN}_functions.scss`, 143 | `${bsScssCDN}_variables.scss`, 144 | `${bsScssCDN}mixins/_border-radius.scss`, 145 | `${bsScssCDN}mixins/_breakpoints.scss`, 146 | `${bsScssCDN}mixins/_hover.scss`, 147 | `${bsScssCDN}mixins/_list-group.scss`, 148 | `${bsScssCDN}mixins/_transition.scss`, 149 | `${bsScssCDN}_root.scss`, 150 | `${bsScssCDN}_nav.scss`, 151 | `${bsScssCDN}_navbar.scss`, 152 | `${bsScssCDN}_list-group.scss`, 153 | `${bsScssCDN}_transitions.scss` 154 | ] 155 | }, 156 | Toast: { 157 | js: [ 158 | `${bsJsCDN}util.js`, 159 | `${bsJsCDN}toast.js` 160 | ], 161 | scss: [ 162 | `${bsScssCDN}_toasts.scss` 163 | ] 164 | }, 165 | Tooltip: { 166 | js: [ 167 | `${bsJsCDN}util.js`, 168 | `${bsJsCDN}tooltip.js` 169 | ], 170 | scss: [ 171 | `${bsScssCDN}_functions.scss`, 172 | `${bsScssCDN}_variables.scss`, 173 | `${bsScssCDN}mixins/_border-radius.scss`, 174 | `${bsScssCDN}mixins/_reset-text.scss`, 175 | `${bsScssCDN}mixins/_transition.scss`, 176 | `${bsScssCDN}_root.scss`, 177 | `${bsScssCDN}_transitions.scss`, 178 | `${bsScssCDN}_tooltip.scss` 179 | ] 180 | } 181 | } 182 | 183 | const scssPlugins = { 184 | Badge: [ 185 | `${bsScssCDN}_functions.scss`, 186 | `${bsScssCDN}_variables.scss`, 187 | `${bsScssCDN}mixins/_badge.scss`, 188 | `${bsScssCDN}mixins/_border-radius.scss`, 189 | `${bsScssCDN}mixins/_hover.scss`, 190 | `${bsScssCDN}mixins/_transition.scss`, 191 | `${bsScssCDN}_root.scss`, 192 | `${bsScssCDN}_badge.scss` 193 | ], 194 | Borders: [ 195 | `${bsScssCDN}_functions.scss`, 196 | `${bsScssCDN}_variables.scss`, 197 | `${bsScssCDN}_root.scss`, 198 | `${bsScssCDN}utilities/_borders.scss` 199 | ], 200 | Breadcrumb: [ 201 | `${bsScssCDN}_functions.scss`, 202 | `${bsScssCDN}_variables.scss`, 203 | `${bsScssCDN}mixins/_border-radius.scss`, 204 | `${bsScssCDN}_root.scss`, 205 | `${bsScssCDN}_breadcrumb.scss` 206 | ], 207 | ButtonGroup: jsPlugins.Button.scss, 208 | Card: [ 209 | `${bsScssCDN}_functions.scss`, 210 | `${bsScssCDN}_variables.scss`, 211 | `${bsScssCDN}mixins/_border-radius.scss`, 212 | `${bsScssCDN}mixins/_breakpoints.scss`, 213 | `${bsScssCDN}mixins/_hover.scss`, 214 | `${bsScssCDN}_root.scss`, 215 | `${bsScssCDN}_card.scss` 216 | ], 217 | Clearfix: [ 218 | `${bsScssCDN}_functions.scss`, 219 | `${bsScssCDN}_variables.scss`, 220 | `${bsScssCDN}mixins/_clearfix.scss`, 221 | `${bsScssCDN}_root.scss`, 222 | `${bsScssCDN}utilities/_clearfix.scss` 223 | ], 224 | CloseIcon: [ 225 | `${bsScssCDN}_functions.scss`, 226 | `${bsScssCDN}_variables.scss`, 227 | `${bsScssCDN}mixins/_hover.scss`, 228 | `${bsScssCDN}_root.scss`, 229 | `${bsScssCDN}_close.scss` 230 | ], 231 | Code: [ 232 | `${bsScssCDN}_functions.scss`, 233 | `${bsScssCDN}_variables.scss`, 234 | `${bsScssCDN}mixins/_border-radius.scss`, 235 | `${bsScssCDN}mixins/_box-shadow.scss`, 236 | `${bsScssCDN}_root.scss`, 237 | `${bsScssCDN}_code.scss` 238 | ], 239 | Colors: [ 240 | `${bsScssCDN}_functions.scss`, 241 | `${bsScssCDN}_variables.scss`, 242 | `${bsScssCDN}mixins/_background-variant.scss`, 243 | `${bsScssCDN}mixins/_breakpoints.scss`, 244 | `${bsScssCDN}mixins/_deprecate.scss`, 245 | `${bsScssCDN}mixins/_hover.scss`, 246 | `${bsScssCDN}mixins/_text-emphasis.scss`, 247 | `${bsScssCDN}mixins/_text-hide.scss`, 248 | `${bsScssCDN}mixins/_text-truncate.scss`, 249 | `${bsScssCDN}_root.scss`, 250 | `${bsScssCDN}utilities/_text.scss`, 251 | `${bsScssCDN}utilities/_background.scss` 252 | ], 253 | Display: [ 254 | `${bsScssCDN}_functions.scss`, 255 | `${bsScssCDN}_variables.scss`, 256 | `${bsScssCDN}mixins/_breakpoints.scss`, 257 | `${bsScssCDN}_root.scss`, 258 | `${bsScssCDN}_print.scss`, 259 | `${bsScssCDN}utilities/_display.scss` 260 | ], 261 | Embed: [ 262 | `${bsScssCDN}utilities/_embed.scss` 263 | ], 264 | Flex: [ 265 | `${bsScssCDN}_functions.scss`, 266 | `${bsScssCDN}_variables.scss`, 267 | `${bsScssCDN}mixins/_breakpoints.scss`, 268 | `${bsScssCDN}utilities/_flex.scss` 269 | ], 270 | Float: [ 271 | `${bsScssCDN}_functions.scss`, 272 | `${bsScssCDN}_variables.scss`, 273 | `${bsScssCDN}mixins/_breakpoints.scss`, 274 | `${bsScssCDN}mixins/_float.scss`, 275 | `${bsScssCDN}utilities/_float.scss` 276 | ], 277 | Forms: [ 278 | `${bsScssCDN}_functions.scss`, 279 | `${bsScssCDN}_variables.scss`, 280 | `${bsScssCDN}mixins/_border-radius.scss`, 281 | `${bsScssCDN}mixins/_box-shadow.scss`, 282 | `${bsScssCDN}mixins/_breakpoints.scss`, 283 | `${bsScssCDN}mixins/_forms.scss`, 284 | `${bsScssCDN}mixins/_gradients.scss`, 285 | `${bsScssCDN}mixins/_transition.scss`, 286 | `${bsScssCDN}_root.scss`, 287 | `${bsScssCDN}_custom-forms.scss`, 288 | `${bsScssCDN}_forms.scss` 289 | ], 290 | Grid: [ 291 | `${bsScssCDN}_functions.scss`, 292 | `${bsScssCDN}_variables.scss`, 293 | `${bsScssCDN}mixins/_breakpoints.scss`, 294 | `${bsScssCDN}mixins/_grid-framework.scss`, 295 | `${bsScssCDN}mixins/_grid.scss`, 296 | `${bsScssCDN}_grid.scss` 297 | ], 298 | Images: [ 299 | `${bsScssCDN}_functions.scss`, 300 | `${bsScssCDN}_variables.scss`, 301 | `${bsScssCDN}mixins/_border-radius.scss`, 302 | `${bsScssCDN}mixins/_box-shadow.scss`, 303 | `${bsScssCDN}mixins/_image.scss`, 304 | `${bsScssCDN}_root.scss`, 305 | `${bsScssCDN}_images.scss` 306 | ], 307 | InputGroup: [ 308 | `${bsScssCDN}_functions.scss`, 309 | `${bsScssCDN}_variables.scss`, 310 | `${bsScssCDN}mixins/_border-radius.scss`, 311 | `${bsScssCDN}_root.scss`, 312 | `${bsScssCDN}_input-group.scss` 313 | ], 314 | Jumbotron: [ 315 | `${bsScssCDN}_functions.scss`, 316 | `${bsScssCDN}_variables.scss`, 317 | `${bsScssCDN}mixins/_border-radius.scss`, 318 | `${bsScssCDN}mixins/_breakpoints.scss`, 319 | `${bsScssCDN}_root.scss`, 320 | `${bsScssCDN}_jumbotron.scss` 321 | ], 322 | ListGroup: [ 323 | `${bsScssCDN}_functions.scss`, 324 | `${bsScssCDN}_variables.scss`, 325 | `${bsScssCDN}mixins/_border-radius.scss`, 326 | `${bsScssCDN}mixins/_hover.scss`, 327 | `${bsScssCDN}mixins/_list-group.scss`, 328 | `${bsScssCDN}mixins/_breakpoints.scss`, 329 | `${bsScssCDN}_root.scss`, 330 | `${bsScssCDN}_list-group.scss` 331 | ], 332 | Media: [ 333 | `${bsScssCDN}_media.scss` 334 | ], 335 | Navs: [ 336 | `${bsScssCDN}_functions.scss`, 337 | `${bsScssCDN}_variables.scss`, 338 | `${bsScssCDN}mixins/_border-radius.scss`, 339 | `${bsScssCDN}mixins/_hover.scss`, 340 | `${bsScssCDN}_root.scss`, 341 | `${bsScssCDN}_nav.scss` 342 | ], 343 | Navbar: [ 344 | `${bsScssCDN}_functions.scss`, 345 | `${bsScssCDN}_variables.scss`, 346 | `${bsScssCDN}mixins/_border-radius.scss`, 347 | `${bsScssCDN}mixins/_breakpoints.scss`, 348 | `${bsScssCDN}mixins/_hover.scss`, 349 | `${bsScssCDN}_root.scss`, 350 | `${bsScssCDN}_navbar.scss` 351 | ], 352 | Overflow: [ 353 | `${bsScssCDN}utilities/_overflow.scss` 354 | ], 355 | Pagination: [ 356 | `${bsScssCDN}_functions.scss`, 357 | `${bsScssCDN}_variables.scss`, 358 | `${bsScssCDN}mixins/_border-radius.scss`, 359 | `${bsScssCDN}mixins/_lists.scss`, 360 | `${bsScssCDN}mixins/_pagination.scss`, 361 | `${bsScssCDN}_root.scss`, 362 | `${bsScssCDN}_pagination.scss` 363 | ], 364 | Position: [ 365 | `${bsScssCDN}_functions.scss`, 366 | `${bsScssCDN}_variables.scss`, 367 | `${bsScssCDN}utilities/_position.scss` 368 | ], 369 | Progress: [ 370 | `${bsScssCDN}_functions.scss`, 371 | `${bsScssCDN}_variables.scss`, 372 | `${bsScssCDN}mixins/_border-radius.scss`, 373 | `${bsScssCDN}mixins/_box-shadow.scss`, 374 | `${bsScssCDN}mixins/_gradients.scss`, 375 | `${bsScssCDN}mixins/_transition.scss`, 376 | `${bsScssCDN}_root.scss`, 377 | `${bsScssCDN}_progress.scss` 378 | ], 379 | Reboot: [ 380 | `${bsScssCDN}_functions.scss`, 381 | `${bsScssCDN}_variables.scss`, 382 | `${bsScssCDN}mixins/_hover.scss`, 383 | `${bsScssCDN}vendor/_rfs.scss`, 384 | `${bsScssCDN}_root.scss`, 385 | `${bsScssCDN}_reboot.scss` 386 | ], 387 | Screenreaders: [ 388 | `${bsScssCDN}mixins/_screen-reader.scss`, 389 | `${bsScssCDN}utilities/_screenreaders.scss` 390 | ], 391 | Shadows: [ 392 | `${bsScssCDN}_functions.scss`, 393 | `${bsScssCDN}_variables.scss`, 394 | `${bsScssCDN}utilities/_shadows.scss` 395 | ], 396 | Sizing: [ 397 | `${bsScssCDN}_functions.scss`, 398 | `${bsScssCDN}_variables.scss`, 399 | `${bsScssCDN}utilities/_sizing.scss` 400 | ], 401 | Spacing: [ 402 | `${bsScssCDN}_functions.scss`, 403 | `${bsScssCDN}_variables.scss`, 404 | `${bsScssCDN}mixins/_breakpoints.scss`, 405 | `${bsScssCDN}utilities/_spacing.scss` 406 | ], 407 | Spinners: [ 408 | `${bsScssCDN}_spinners.scss` 409 | ], 410 | StretchedLink: [ 411 | `${bsScssCDN}utilities/_stretched-link.scss` 412 | ], 413 | Tables: [ 414 | `${bsScssCDN}_functions.scss`, 415 | `${bsScssCDN}_variables.scss`, 416 | `${bsScssCDN}mixins/_breakpoints.scss`, 417 | `${bsScssCDN}mixins/_hover.scss`, 418 | `${bsScssCDN}mixins/_table-row.scss`, 419 | `${bsScssCDN}_root.scss`, 420 | `${bsScssCDN}_tables.scss` 421 | ], 422 | Text: [ 423 | `${bsScssCDN}_functions.scss`, 424 | `${bsScssCDN}_variables.scss`, 425 | `${bsScssCDN}mixins/_breakpoints.scss`, 426 | `${bsScssCDN}mixins/_deprecate.scss`, 427 | `${bsScssCDN}mixins/_hover.scss`, 428 | `${bsScssCDN}mixins/_text-emphasis.scss`, 429 | `${bsScssCDN}mixins/_text-hide.scss`, 430 | `${bsScssCDN}mixins/_text-truncate.scss`, 431 | `${bsScssCDN}_root.scss`, 432 | `${bsScssCDN}utilities/_text.scss` 433 | ], 434 | Type: [ 435 | `${bsScssCDN}_functions.scss`, 436 | `${bsScssCDN}_variables.scss`, 437 | `${bsScssCDN}mixins/_lists.scss`, 438 | `${bsScssCDN}_root.scss`, 439 | `${bsScssCDN}_type.scss` 440 | ], 441 | VerticalAlign: [ 442 | `${bsScssCDN}utilities/_align.scss` 443 | ], 444 | Visibility: [ 445 | `${bsScssCDN}mixins/_visibility.scss`, 446 | `${bsScssCDN}utilities/_visibility.scss` 447 | ] 448 | } 449 | 450 | export { 451 | jsPlugins, 452 | scssPlugins 453 | } 454 | -------------------------------------------------------------------------------- /src/js/toggler.js: -------------------------------------------------------------------------------- 1 | let chooseToImportPopper = true 2 | 3 | const initToggler = () => { 4 | const jsGroupList = [].slice.call(document.querySelectorAll('.js-group')) 5 | const popperCheckboxList = [].slice.call(document.querySelectorAll('.js-require-popper')) 6 | const checkboxPopper = document.querySelector('.js-check-popper') 7 | const checkboxPopover = popperCheckboxList.find(chk => chk.classList.contains('js-popover')) 8 | const checkboxTooltip = popperCheckboxList.find(chk => chk.classList.contains('js-tooltip')) 9 | 10 | jsGroupList.forEach(group => { 11 | const checkboxes = [].slice.call(group.querySelectorAll('.js-checkbox')) 12 | 13 | group.querySelector('.js-btn-toggle') 14 | .addEventListener('click', () => { 15 | const checkedList = checkboxes.filter(chkBox => chkBox.checked) 16 | 17 | if (checkedList.length > 0) { 18 | // Uncheck all checked checkboxes 19 | checkedList.forEach(chkBox => { chkBox.checked = false }) 20 | } else { 21 | // Check all checkboxes 22 | checkboxes.forEach(chkBox => { chkBox.checked = true }) 23 | } 24 | 25 | // Check if Popper.js is needed or not 26 | checkboxPopper.checked = Boolean(popperCheckboxList.filter(chk => chk.checked).length) 27 | }) 28 | }) 29 | 30 | checkboxPopper.addEventListener('click', function () { 31 | chooseToImportPopper = this.checked 32 | }) 33 | 34 | popperCheckboxList.forEach(chkboxNeedPopper => { 35 | chkboxNeedPopper.addEventListener('click', () => { 36 | if (!chkboxNeedPopper.checked) { 37 | const stillCheckedList = popperCheckboxList 38 | .filter(chk => chk.checked) 39 | 40 | if (stillCheckedList.length === 0) { 41 | checkboxPopper.checked = false 42 | } 43 | } else if (chooseToImportPopper) { 44 | checkboxPopper.checked = true 45 | } 46 | }) 47 | }) 48 | 49 | checkboxPopover.addEventListener('change', () => { 50 | if (checkboxPopover.checked && !checkboxTooltip.checked) { 51 | checkboxTooltip.checked = checkboxPopover.checked 52 | } 53 | }) 54 | 55 | return { 56 | checkboxPopper, 57 | chkMinify: document.querySelector('.js-check-minify'), 58 | chkCSS: document.querySelector('.js-check-css') 59 | } 60 | } 61 | 62 | export { 63 | initToggler 64 | } 65 | -------------------------------------------------------------------------------- /src/js/util.js: -------------------------------------------------------------------------------- 1 | const formatList = list => { 2 | const listPlugin = [].concat(list) 3 | 4 | // Insert Tooltip if not present and Popover is needed 5 | if (listPlugin.indexOf('popover') !== -1 && listPlugin.indexOf('tooltip') === -1) { 6 | const popoverIndex = listPlugin.indexOf('popover') 7 | listPlugin.splice(popoverIndex, 0, 'tooltip') 8 | } 9 | 10 | // Tooltip present and Popover present, Tooltip comes first 11 | const popoverIndex = listPlugin.indexOf('popover') 12 | const tooltipIndex = listPlugin.indexOf('tooltip') 13 | if ( 14 | (popoverIndex !== -1 && tooltipIndex !== -1) && 15 | popoverIndex < tooltipIndex 16 | ) { 17 | listPlugin.splice(tooltipIndex, 1) 18 | listPlugin.splice(popoverIndex, 0, 'tooltip') 19 | } 20 | 21 | return listPlugin 22 | } 23 | 24 | const formatScssList = list => { 25 | let result = [] 26 | const mixinList = list.filter(str => str.indexOf('mixins') !== -1) 27 | const utilitiesList = list.filter(str => str.indexOf('utilities') !== -1) 28 | 29 | const functionsIndex = list.findIndex(file => file.indexOf('functions') !== -1) 30 | if (functionsIndex !== -1) { 31 | result.push(list[functionsIndex]) 32 | } 33 | 34 | const varIndex = list.findIndex(file => file.indexOf('variables') !== -1) 35 | if (varIndex !== -1) { 36 | result.push(list[varIndex]) 37 | } 38 | 39 | const rfsIndex = list.findIndex(file => file.indexOf('vendor') !== -1) 40 | if (rfsIndex !== -1) { 41 | result.push(list[rfsIndex]) 42 | } 43 | 44 | // Add mixins 45 | result = result.concat(mixinList) 46 | // Remove mixins from the list 47 | mixinList.forEach(mixin => { 48 | const mixinIndex = list.indexOf(mixin) 49 | list.splice(mixinIndex, 1) 50 | }) 51 | 52 | const plugins = [ 53 | 'root', 54 | 'reboot', 55 | 'type', 56 | 'images', 57 | 'code', 58 | 'grid', 59 | 'tables', 60 | 'forms', 61 | 'buttons', 62 | 'transitions', 63 | 'dropdown', 64 | 'button-group', 65 | 'input-group', 66 | 'custom-forms', 67 | 'nav', 68 | 'navbar', 69 | 'card', 70 | 'breadcrumb', 71 | 'pagination', 72 | 'badge', 73 | 'jumbotron', 74 | 'alert', 75 | 'progress', 76 | 'media', 77 | 'list-group', 78 | 'close', 79 | 'modal', 80 | 'toasts', 81 | 'tooltip', 82 | 'popover', 83 | 'carousel', 84 | 'spinners' 85 | ] 86 | 87 | plugins.forEach(item => { 88 | let index 89 | if (item === 'forms') { 90 | index = list.findIndex(file => file.indexOf(item) !== -1 && file.indexOf('custom') === -1) 91 | } else if (item === 'nav') { 92 | index = list.findIndex(file => file.indexOf(item) !== -1 && file.indexOf('navbar') === -1) 93 | } else { 94 | index = list.findIndex(file => file.indexOf(item) !== -1) 95 | } 96 | 97 | if (index !== -1) { 98 | result.push(list[index]) 99 | } 100 | }) 101 | 102 | // Add utilities 103 | result = result.concat(utilitiesList) 104 | // Remove utilities from the list 105 | utilitiesList.forEach(utility => { 106 | const utilityIndex = list.indexOf(utility) 107 | list.splice(utilityIndex, 1) 108 | }) 109 | 110 | const printIndex = list.findIndex(file => file.indexOf('print') !== -1) 111 | if (printIndex !== -1) { 112 | result.push(list[printIndex]) 113 | } 114 | 115 | return result 116 | } 117 | 118 | const ucfirst = str => `${str.charAt(0).toUpperCase()}${str.slice(1)}` 119 | 120 | const uniqArray = array => array.filter((elem, pos, arr) => arr.indexOf(elem) === pos) 121 | 122 | const getSassWorkerPath = () => { 123 | const bsCustomizer = 'bs-customizer' 124 | const location = window.location 125 | let origin = location.origin 126 | 127 | if (!origin) { 128 | const port = location.port ? `:${location.port}` : '' 129 | 130 | origin = `${location.protocol}//${location.hostname}${port}` 131 | } 132 | 133 | const indexPath = location.pathname 134 | .split('/') 135 | .indexOf(bsCustomizer) 136 | 137 | const bsCustomizerPath = indexPath !== -1 ? `/${bsCustomizer}/` : '/' 138 | 139 | return `${origin}${bsCustomizerPath}dist/sass.worker.js` 140 | } 141 | 142 | const supportedBrowser = () => { 143 | const supportBlob = 'Blob' in window 144 | const supportPromise = 'Promise' in window 145 | const supportArrayFindIndex = Boolean(Array.prototype.findIndex) 146 | 147 | return supportBlob && supportArrayFindIndex && supportPromise 148 | } 149 | 150 | export { 151 | formatList, 152 | formatScssList, 153 | ucfirst, 154 | uniqArray, 155 | getSassWorkerPath, 156 | supportedBrowser 157 | } 158 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob-all') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | const CopyWebpackPlugin = require('copy-webpack-plugin') 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 7 | const PurgecssPlugin = require('purgecss-webpack-plugin') 8 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const WriteFilePlugin = require('write-file-webpack-plugin') 11 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') 12 | const CleanCSS = require('clean-css') 13 | const { bootstrapVersion } = require('./src/js/config') 14 | 15 | const fileName = 'bs-customizer.min' 16 | const paths = { 17 | src: `${path.join(__dirname, 'src/js/')}*.js`, 18 | index: `${path.join(__dirname, 'src/')}*.html` 19 | } 20 | 21 | const cleanCssConfig = { 22 | level: { 23 | 1: { 24 | specialComments: 'all' 25 | } 26 | }, 27 | format: { 28 | breakWith: 'lf' 29 | }, 30 | returnPromise: true 31 | } 32 | 33 | const uglifyJsConfig = { 34 | compress: { 35 | typeofs: false 36 | }, 37 | mangle: true, 38 | output: { 39 | comments: /^!|@preserve|@license|@cc_on/i 40 | } 41 | } 42 | 43 | const htmlminifierOpts = { 44 | collapseBooleanAttributes: true, 45 | collapseWhitespace: true, 46 | conservativeCollapse: false, 47 | decodeEntities: true, 48 | minifyCSS: { 49 | level: { 50 | 1: { 51 | specialComments: 0 52 | } 53 | } 54 | }, 55 | minifyJS: uglifyJsConfig, 56 | minifyURLs: false, 57 | processConditionalComments: true, 58 | removeAttributeQuotes: true, 59 | removeComments: true, 60 | removeOptionalAttributes: true, 61 | removeOptionalTags: true, 62 | removeRedundantAttributes: true, 63 | removeScriptTypeAttributes: true, 64 | removeStyleLinkTypeAttributes: true, 65 | removeTagWhitespace: false, // this leads to invalid HTML 66 | sortAttributes: true, 67 | sortClassName: true 68 | } 69 | 70 | module.exports = (env, args) => { 71 | const isProd = args.mode === 'production' 72 | 73 | const conf = { 74 | entry: './src/js/index.js', 75 | output: { 76 | filename: '[name].bundle.js', 77 | chunkFilename: '[name].bundle.js', 78 | path: path.resolve(__dirname, 'dist'), 79 | publicPath: '/dist/' 80 | }, 81 | plugins: [ 82 | new CleanWebpackPlugin({ 83 | cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'index.html')] 84 | }), 85 | new CopyWebpackPlugin([ 86 | 'src/js/lib/sass.worker.js' 87 | ]), 88 | new MiniCssExtractPlugin({ 89 | chunkFilename: `${fileName}.css` 90 | }), 91 | new HtmlWebpackPlugin({ 92 | template: path.resolve(__dirname, 'src/index.html'), 93 | filename: path.resolve(__dirname, 'index.html'), 94 | minify: isProd ? htmlminifierOpts : false, 95 | bsVersion: bootstrapVersion 96 | }), 97 | new ScriptExtHtmlWebpackPlugin({ 98 | defaultAttribute: 'async' 99 | }), 100 | new WriteFilePlugin() 101 | ], 102 | module: { 103 | rules: [ 104 | { 105 | test: /\.js$/, 106 | exclude: /(node_modules)/, 107 | use: { 108 | loader: 'babel-loader', 109 | options: { 110 | presets: ['@babel/preset-env'], 111 | plugins: ['@babel/plugin-syntax-dynamic-import'], 112 | ignore: [ 113 | './src/js/lib/*.js' 114 | ] 115 | } 116 | } 117 | }, 118 | { 119 | test: /\.css$/, 120 | use: [ 121 | { 122 | loader: MiniCssExtractPlugin.loader, 123 | options: {} 124 | }, 125 | 'css-loader', 126 | 'postcss-loader' 127 | ] 128 | } 129 | ] 130 | }, 131 | resolve: { 132 | alias: { 133 | jquery$: path.resolve(__dirname, 'node_modules/jquery/dist/jquery.slim.js') 134 | } 135 | } 136 | } 137 | 138 | if (isProd) { 139 | conf.output.publicPath = 'dist/' 140 | conf.optimization = { 141 | minimizer: [ 142 | new UglifyJsPlugin({ 143 | cache: true, 144 | parallel: true, 145 | uglifyOptions: uglifyJsConfig 146 | }), 147 | new OptimizeCSSAssetsPlugin({ 148 | cssProcessor: { 149 | process: (input, opts) => { 150 | delete opts.to 151 | 152 | const cleanCss = new CleanCSS(Object.assign(cleanCssConfig, opts)) 153 | return cleanCss.minify(input) 154 | .then(output => ({ css: output.styles })) 155 | } 156 | } 157 | }) 158 | ] 159 | } 160 | 161 | conf.plugins.push( 162 | new PurgecssPlugin({ 163 | paths: glob.sync([ 164 | paths.src, 165 | paths.index 166 | ]), 167 | whitelistPatterns: [ 168 | /^modal/ 169 | ] 170 | }) 171 | ) 172 | } else { 173 | conf.devtool = 'source-map' 174 | conf.devServer = { 175 | publicPath: '/dist/' 176 | } 177 | } 178 | 179 | return conf 180 | } 181 | --------------------------------------------------------------------------------