2 * Copyright 2013 Twitter, Inc.
4 * Licensed under the Creative Commons Attribution 3.0 Unported License. For
5 * details, see http://creativecommons.org/licenses/by/3.0/.
9 window.onload = function () { // wait for load in a dumb way because B-0
10 var cw = '/*!\n * Bootstrap v3.0.0\n *\n * Copyright 2013 Twitter, Inc\n * Licensed under the Apache License v2.0\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Designed and built with all the love in the world @twitter by @mdo and @fat.\n */\n\n'
12 function showError(msg, err) {
13 $('<div id="bsCustomizerAlert" class="bs-customizer-alert">\
14 <div class="container">\
15 <a href="#bsCustomizerAlert" data-dismiss="alert" class="close pull-right">×</a>\
16 <p class="bs-customizer-alert-text"><span class="glyphicon glyphicon-warning-sign"></span>' + msg + '</p>' +
17 (err.extract ? '<pre class="bs-customizer-alert-extract">' + err.extract.join('\n') + '</pre>' : '') + '\
19 </div>').appendTo('body').alert()
23 function showCallout(msg, showUpTop) {
24 var callout = $('<div class="bs-callout bs-callout-danger">\
30 callout.appendTo('.bs-docs-container')
32 callout.insertAfter('.bs-customize-download')
36 function getQueryParam(key) {
37 key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, "\\$&"); // escape RegEx meta chars
38 var match = location.search.match(new RegExp("[?&]"+key+"=([^&]+)(&|$)"));
39 return match && decodeURIComponent(match[1].replace(/\+/g, " "));
42 function createGist(configJson) {
44 "description": "Bootstrap Customizer Config",
53 url: 'https://api.github.com/gists',
56 data: JSON.stringify(data)
58 .success(function(result) {
59 history.replaceState(false, document.title, window.location.origin + window.location.pathname + '?id=' + result.id)
61 .error(function(err) {
62 showError('<strong>Ruh roh!</strong> Could not save gist file, configuration not saved.', err)
66 function getCustomizerData() {
69 $('#less-variables-section input')
71 $(this).val() && (vars[ $(this).prev().text() ] = $(this).val())
76 css: $('#less-section input:checked') .map(function () { return this.value }).toArray(),
77 js: $('#plugin-section input:checked').map(function () { return this.value }).toArray()
80 if ($.isEmptyObject(data.vars) && !data.css.length && !data.js.length) return
86 var id = getQueryParam('id')
91 url: 'https://api.github.com/gists/' + id,
95 .success(function(result) {
96 var data = JSON.parse(result.files['config.json'].content)
98 $('#plugin-section input').each(function () {
99 $(this).prop('checked', ~$.inArray(this.value, data.js))
103 $('#less-section input').each(function () {
104 $(this).prop('checked', ~$.inArray(this.value, data.css))
108 for (var i in data.vars) {
109 $('input[data-var="' + i + '"]').val(data.vars[i])
113 .error(function(err) {
114 showError('Error fetching bootstrap config file', err)
118 function generateZip(css, js, fonts, config, complete) {
119 if (!css && !js) return showError('<strong>Ruh roh!</strong> No Bootstrap files selected.', new Error('no Bootstrap'))
121 var zip = new JSZip()
124 var cssFolder = zip.folder('css')
125 for (var fileName in css) {
126 cssFolder.file(fileName, css[fileName])
131 var jsFolder = zip.folder('js')
132 for (var fileName in js) {
133 jsFolder.file(fileName, js[fileName])
138 var fontsFolder = zip.folder('fonts')
139 for (var fileName in fonts) {
140 fontsFolder.file(fileName, fonts[fileName], {base64: true})
145 zip.file('config.json', config)
148 var content = zip.generate({type:"blob"})
153 function generateCustomCSS(vars) {
156 for (var key in vars) {
157 result += key + ': ' + vars[key] + ';\n'
160 return result + '\n\n'
163 function generateFonts() {
164 var glyphicons = $('#less-section [value="glyphicons.less"]:checked')
165 if (glyphicons.length) {
170 // Returns an Array of @import'd filenames from 'bootstrap.less' in the order
171 // in which they appear in the file.
172 function bootstrapLessFilenames() {
173 var IMPORT_REGEX = /^@import \"(.*?)\";$/
174 var bootstrapLessLines = __less['bootstrap.less'].split('\n')
176 for (var i = 0, imports = []; i < bootstrapLessLines.length; i++) {
177 var match = IMPORT_REGEX.exec(bootstrapLessLines[i])
178 if (match) imports.push(match[1])
184 function generateCSS() {
185 var oneChecked = false
186 var lessFileIncludes = {}
187 $('#less-section input').each(function() {
189 var checked = $this.is(':checked')
190 lessFileIncludes[$this.val()] = checked
192 oneChecked = oneChecked || checked
195 if (!oneChecked) return false
201 $('#less-variables-section input')
203 $(this).val() && (vars[ $(this).prev().text() ] = $(this).val())
206 $.each(bootstrapLessFilenames(), function(index, filename) {
207 var fileInclude = lessFileIncludes[filename]
209 // Files not explicitly unchecked are compiled into the final stylesheet.
210 // Core stylesheets like 'normalize.less' are not included in the form
211 // since disabling them would wreck everything, and so their 'fileInclude'
212 // will be 'undefined'.
213 if (fileInclude || (fileInclude == null)) css += __less[filename]
215 // Custom variables are added after Bootstrap variables so the custom
216 // ones take precedence.
217 if (('variables.less' === filename) && vars) css += generateCustomCSS(vars)
220 css = css.replace(/@import[^\n]*/gi, '') //strip any imports
223 var parser = new less.Parser({
224 paths: ['variables.less', 'mixins.less']
226 , filename: 'bootstrap.css'
227 }).parse(css, function (err, tree) {
229 return showError('<strong>Ruh roh!</strong> Could not parse less files.', err)
232 'bootstrap.css' : cw + tree.toCSS(),
233 'bootstrap.min.css' : cw + tree.toCSS({ compress: true })
237 return showError('<strong>Ruh roh!</strong> Could not parse less files.', err)
243 function generateJavascript() {
244 var $checked = $('#plugin-section input:checked')
245 if (!$checked.length) return false
248 .map(function () { return __js[this.value] })
254 'bootstrap.min.js': cw + uglify(js)
258 var inputsComponent = $('#less-section input')
259 var inputsPlugin = $('#plugin-section input')
260 var inputsVariables = $('#less-variables-section input')
262 $('#less-section .toggle').on('click', function (e) {
264 inputsComponent.prop('checked', !inputsComponent.is(':checked'))
267 $('#plugin-section .toggle').on('click', function (e) {
269 inputsPlugin.prop('checked', !inputsPlugin.is(':checked'))
272 $('#less-variables-section .toggle').on('click', function (e) {
274 inputsVariables.val('')
277 $('[data-dependencies]').on('click', function () {
278 if (!$(this).is(':checked')) return
279 var dependencies = this.getAttribute('data-dependencies')
280 if (!dependencies) return
281 dependencies = dependencies.split(',')
282 for (var i = 0; i < dependencies.length; i++) {
283 var dependency = $('[value="' + dependencies[i] + '"]')
284 dependency && dependency.prop('checked', true)
288 $('[data-dependents]').on('click', function () {
289 if ($(this).is(':checked')) return
290 var dependents = this.getAttribute('data-dependents')
291 if (!dependents) return
292 dependents = dependents.split(',')
293 for (var i = 0; i < dependents.length; i++) {
294 var dependent = $('[value="' + dependents[i] + '"]')
295 dependent && dependent.prop('checked', false)
299 var $compileBtn = $('#btn-compile')
300 var $downloadBtn = $('#btn-download')
302 $compileBtn.on('click', function (e) {
303 var configData = getCustomizerData()
304 var configJson = JSON.stringify(configData, null, 2)
308 $compileBtn.attr('disabled', 'disabled')
310 generateZip(generateCSS(), generateJavascript(), generateFonts(), configJson, function (blob) {
311 $compileBtn.removeAttr('disabled')
312 saveAs(blob, "bootstrap.zip")
313 createGist(configJson)
317 // browser support alerts
318 if (!window.URL && navigator.userAgent.toLowerCase().indexOf('safari') != -1) {
319 showCallout("Looks like you're using safari, which sadly doesn't have the best support\
320 for HTML5 blobs. Because of this your file will be downloaded with the name <code>\"untitled\"</code>.\
321 However, if you check your downloads folder, just rename this <code>\"untitled\"</code> file\
322 to <code>\"bootstrap.zip\"</code> and you should be good to go!")
323 } else if (!window.URL && !window.webkitURL) {
324 $('.bs-docs-section, .bs-sidebar').css('display', 'none')
326 showCallout("Looks like your current browser doesn't support the Bootstrap Customizer. Please take a second\
327 to <a href=\"https://www.google.com/intl/en/chrome/browser/\"> upgrade to a more modern browser</a>.", true)