diff --git a/docs/_jade/customizer-nav.jade b/docs/_jade/customizer-nav.jade index c4f6ddf105979ed39586a30cffda1db2d9ff4651..eba71ef6bf124cddcbd4eadeb2c0d7eb77bc2115 100644 --- a/docs/_jade/customizer-nav.jade +++ b/docs/_jade/customizer-nav.jade @@ -1,4 +1,6 @@ // NOTE: DO NOT EDIT THE FOLLOWING SECTION DIRECTLY! It is autogenerated via the `build-customizer-html` Grunt task using the customizer-nav.jade template. +li + a(href='#import') Import li a(href='#less') Less components li diff --git a/docs/assets/css/src/docs.css b/docs/assets/css/src/docs.css index 142519bd5fc0704ad5f73483e47dfdecd1671806..ea0739893d5b35c849ec315e5ea90702ce38203a 100644 --- a/docs/assets/css/src/docs.css +++ b/docs/assets/css/src/docs.css @@ -1378,6 +1378,30 @@ h1[id] { box-shadow: inset 0 2px 4px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); } +.bs-dropzone { + position: relative; + padding: 20px; + margin-bottom: 20px; + color: #777; + text-align: center; + border: 2px dashed #eee; + border-radius: 4px; +} +.bs-dropzone h2 { + margin-top: 0; + margin-bottom: 5px; +} +.bs-dropzone .lead { + margin-bottom: 10px; + font-weight: normal; + color: #333; +} +.bs-dropzone hr { + width: 100px; +} +.bs-dropzone p:last-child { + margin-bottom: 0; +} /* * Brand guidelines diff --git a/docs/assets/js/src/customizer.js b/docs/assets/js/src/customizer.js index c3a212f2c7274db1df0e788509776982ecf2cd83..7340f1668fdc50ee5b41fab274de5a52c16a042d 100644 --- a/docs/assets/js/src/customizer.js +++ b/docs/assets/js/src/customizer.js @@ -16,6 +16,9 @@ window.onload = function () { // wait for load in a dumb way because B-0 ' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' + ' */\n\n' + var supportsFile = (window.File && window.FileReader && window.FileList && window.Blob) + var importDropTarget = $('#import-drop-target') + function showError(msg, err) { $('<div id="bsCustomizerAlert" class="bs-customizer-alert">' + '<div class="container">' + @@ -46,6 +49,11 @@ window.onload = function () { // wait for load in a dumb way because B-0 } } + function showAlert(type, msg, insertAfter) { + $('<div class="alert alert-' + type + '">' + msg + '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button></div>') + .insertAfter(insertAfter) + } + function getQueryParam(key) { key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, '\\$&') // escape RegEx meta chars var match = location.search.match(new RegExp('[?&]' + key + '=([^&]+)(&|$)')) @@ -106,6 +114,24 @@ window.onload = function () { // wait for load in a dumb way because B-0 return data } + function updateCustomizerFromJson(data) { + if (data.js) { + $('#plugin-section input').each(function () { + $(this).prop('checked', ~$.inArray(this.value, data.js)) + }) + } + if (data.css) { + $('#less-section input').each(function () { + $(this).prop('checked', ~$.inArray(this.value, data.css)) + }) + } + if (data.vars) { + for (var i in data.vars) { + $('input[data-var="' + i + '"]').val(data.vars[i]) + } + } + } + function parseUrl() { var id = getQueryParam('id') @@ -118,21 +144,7 @@ window.onload = function () { // wait for load in a dumb way because B-0 }) .success(function (result) { var data = JSON.parse(result.files['config.json'].content) - if (data.js) { - $('#plugin-section input').each(function () { - $(this).prop('checked', ~$.inArray(this.value, data.js)) - }) - } - if (data.css) { - $('#less-section input').each(function () { - $(this).prop('checked', ~$.inArray(this.value, data.css)) - }) - } - if (data.vars) { - for (var i in data.vars) { - $('input[data-var="' + i + '"]').val(data.vars[i]) - } - } + updateCustomizerFromJson(data) }) .error(function (err) { showError('Error fetching bootstrap config file', err) @@ -324,6 +336,61 @@ window.onload = function () { // wait for load in a dumb way because B-0 } } + function removeImportAlerts() { + importDropTarget.nextAll('.alert').remove() + } + + function handleConfigFileSelect(e) { + e.stopPropagation() + e.preventDefault() + + var file = (e.originalEvent.hasOwnProperty('dataTransfer')) ? e.originalEvent.dataTransfer.files[0] : e.originalEvent.target.files[0] + + if (!file.type.match('application/json')) { + return showAlert('danger', '<strong>Ruh roh.</strong> We can only read <code>.json</code> files. Please try again.', importDropTarget) + } + + var reader = new FileReader() + + reader.onload = (function () { + return function (e) { + var text = e.target.result + + try { + var json = JSON.parse(text) + + if (typeof json != 'object') { + throw new Error('JSON data from config file is not an object.') + } + + updateCustomizerFromJson(json) + showAlert('success', '<strong>Woohoo!</strong> Your configuration was successfully uploaded. Tweak your settings, then hit Download.', importDropTarget) + } catch (err) { + return showAlert('danger', '<strong>Shucks.</strong> We can only read valid <code>.json</code> files. Please try again.', importDropTarget) + } + } + })(file) + + reader.readAsText(file) + } + + function handleConfigDragOver(e) { + e.stopPropagation() + e.preventDefault() + e.originalEvent.dataTransfer.dropEffect = 'copy' + + removeImportAlerts() + } + + if (supportsFile) { + importDropTarget + .on('dragover', handleConfigDragOver) + .on('drop', handleConfigFileSelect) + } + + $('#import-file-select').on('select', handleConfigFileSelect) + $('#import-manual-trigger').on('click', removeImportAlerts) + var inputsComponent = $('#less-section input') var inputsPlugin = $('#plugin-section input') var inputsVariables = $('#less-variables-section input') @@ -410,7 +477,8 @@ window.onload = function () { // wait for load in a dumb way because B-0 { type: 'image/svg+xml;charset=utf-8' } ) var objectUrl = url.createObjectURL(svg); - if (/^blob:/.exec(objectUrl) === null) { + + if (/^blob:/.exec(objectUrl) === null || !supportsFile) { // `URL.createObjectURL` created a URL that started with something other // than "blob:", which means it has been polyfilled and is not supported by // this browser. diff --git a/docs/customize.html b/docs/customize.html index 2e067b8f4828c7a57ca5a402bb72fedaa611b51d..2e918bd6eec8840e3e46cf0187fa5e750c09584e 100644 --- a/docs/customize.html +++ b/docs/customize.html @@ -12,6 +12,7 @@ lead: Customize Bootstrap's components, Less variables, and jQuery plugins to ge <!--[if lt IE 9]> <style> .bs-customizer, + .bs-customizer-import, .bs-docs-sidebar { display: none; } @@ -23,6 +24,17 @@ lead: Customize Bootstrap's components, Less variables, and jQuery plugins to ge <![endif]--> <!-- Customizer form --> + +<div class="bs-docs-section bs-customizer-import"> + <div id="import-drop-target" class="bs-dropzone"> + <h2>âœ</h2> + <p class="lead">Have an existing configuration? Upload your <code>config.json</code> to import it.</p> + <p>Drag and drop here, or <label id="import-manual-trigger" class="btn-link">manually upload<input type="file" id="import-file-select" class="hidden"></label>.</p> + <hr> + <p><strong>Don't have one?</strong> That's okay—just start customizing the fields below.</p> + </div> +</div><!-- /import --> + <form class="bs-customizer" role="form"> <div class="bs-docs-section" id="less-section"> <button class="btn btn-default toggle" type="button">Toggle all</button> @@ -358,8 +370,6 @@ lead: Customize Bootstrap's components, Less variables, and jQuery plugins to ge {% include customizer-variables.html %} </div> - - <div class="bs-docs-section"> <h1 id="download" class="page-header">Download</h1>