Skip to content

Commit 9739271

Browse files
thomasweltoncvrebert
authored andcommitted
Add drag and drop config import; closes twbs#11004
Closes twbs#13790 by merging a rebased version of it.
1 parent f026cfb commit 9739271

File tree

4 files changed

+122
-18
lines changed

4 files changed

+122
-18
lines changed

docs/_jade/customizer-nav.jade

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// NOTE: DO NOT EDIT THE FOLLOWING SECTION DIRECTLY! It is autogenerated via the `build-customizer-html` Grunt task using the customizer-nav.jade template.
2+
li
3+
a(href='#import') Import
24
li
35
a(href='#less') Less components
46
li

docs/assets/css/src/docs.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,30 @@ h1[id] {
13781378
box-shadow: inset 0 2px 4px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);
13791379
}
13801380

1381+
.bs-dropzone {
1382+
position: relative;
1383+
padding: 20px;
1384+
margin-bottom: 20px;
1385+
color: #777;
1386+
text-align: center;
1387+
border: 2px dashed #eee;
1388+
border-radius: 4px;
1389+
}
1390+
.bs-dropzone h2 {
1391+
margin-top: 0;
1392+
margin-bottom: 5px;
1393+
}
1394+
.bs-dropzone .lead {
1395+
margin-bottom: 10px;
1396+
font-weight: normal;
1397+
color: #333;
1398+
}
1399+
.bs-dropzone hr {
1400+
width: 100px;
1401+
}
1402+
.bs-dropzone p:last-child {
1403+
margin-bottom: 0;
1404+
}
13811405

13821406
/*
13831407
* Brand guidelines

docs/assets/js/src/customizer.js

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ window.onload = function () { // wait for load in a dumb way because B-0
1616
' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' +
1717
' */\n\n'
1818

19+
var supportsFile = (window.File && window.FileReader && window.FileList && window.Blob)
20+
var importDropTarget = $('#import-drop-target')
21+
1922
function showError(msg, err) {
2023
$('<div id="bsCustomizerAlert" class="bs-customizer-alert">' +
2124
'<div class="container">' +
@@ -46,6 +49,11 @@ window.onload = function () { // wait for load in a dumb way because B-0
4649
}
4750
}
4851

52+
function showAlert(type, msg, insertAfter) {
53+
$('<div class="alert alert-' + type + '">' + msg + '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button></div>')
54+
.insertAfter(insertAfter)
55+
}
56+
4957
function getQueryParam(key) {
5058
key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, '\\$&') // escape RegEx meta chars
5159
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
106114
return data
107115
}
108116

117+
function updateCustomizerFromJson(data) {
118+
if (data.js) {
119+
$('#plugin-section input').each(function () {
120+
$(this).prop('checked', ~$.inArray(this.value, data.js))
121+
})
122+
}
123+
if (data.css) {
124+
$('#less-section input').each(function () {
125+
$(this).prop('checked', ~$.inArray(this.value, data.css))
126+
})
127+
}
128+
if (data.vars) {
129+
for (var i in data.vars) {
130+
$('input[data-var="' + i + '"]').val(data.vars[i])
131+
}
132+
}
133+
}
134+
109135
function parseUrl() {
110136
var id = getQueryParam('id')
111137

@@ -118,21 +144,7 @@ window.onload = function () { // wait for load in a dumb way because B-0
118144
})
119145
.success(function (result) {
120146
var data = JSON.parse(result.files['config.json'].content)
121-
if (data.js) {
122-
$('#plugin-section input').each(function () {
123-
$(this).prop('checked', ~$.inArray(this.value, data.js))
124-
})
125-
}
126-
if (data.css) {
127-
$('#less-section input').each(function () {
128-
$(this).prop('checked', ~$.inArray(this.value, data.css))
129-
})
130-
}
131-
if (data.vars) {
132-
for (var i in data.vars) {
133-
$('input[data-var="' + i + '"]').val(data.vars[i])
134-
}
135-
}
147+
updateCustomizerFromJson(data)
136148
})
137149
.error(function (err) {
138150
showError('Error fetching bootstrap config file', err)
@@ -324,6 +336,61 @@ window.onload = function () { // wait for load in a dumb way because B-0
324336
}
325337
}
326338

339+
function removeImportAlerts() {
340+
importDropTarget.nextAll('.alert').remove()
341+
}
342+
343+
function handleConfigFileSelect(e) {
344+
e.stopPropagation()
345+
e.preventDefault()
346+
347+
var file = (e.originalEvent.hasOwnProperty('dataTransfer')) ? e.originalEvent.dataTransfer.files[0] : e.originalEvent.target.files[0]
348+
349+
if (!file.type.match('application/json')) {
350+
return showAlert('danger', '<strong>Ruh roh.</strong> We can only read <code>.json</code> files. Please try again.', importDropTarget)
351+
}
352+
353+
var reader = new FileReader()
354+
355+
reader.onload = (function () {
356+
return function (e) {
357+
var text = e.target.result
358+
359+
try {
360+
var json = JSON.parse(text)
361+
362+
if (typeof json != 'object') {
363+
throw new Error('JSON data from config file is not an object.')
364+
}
365+
366+
updateCustomizerFromJson(json)
367+
showAlert('success', '<strong>Woohoo!</strong> Your configuration was successfully uploaded. Tweak your settings, then hit Download.', importDropTarget)
368+
} catch (err) {
369+
return showAlert('danger', '<strong>Shucks.</strong> We can only read valid <code>.json</code> files. Please try again.', importDropTarget)
370+
}
371+
}
372+
})(file)
373+
374+
reader.readAsText(file)
375+
}
376+
377+
function handleConfigDragOver(e) {
378+
e.stopPropagation()
379+
e.preventDefault()
380+
e.originalEvent.dataTransfer.dropEffect = 'copy'
381+
382+
removeImportAlerts()
383+
}
384+
385+
if (supportsFile) {
386+
importDropTarget
387+
.on('dragover', handleConfigDragOver)
388+
.on('drop', handleConfigFileSelect)
389+
}
390+
391+
$('#import-file-select').on('select', handleConfigFileSelect)
392+
$('#import-manual-trigger').on('click', removeImportAlerts)
393+
327394
var inputsComponent = $('#less-section input')
328395
var inputsPlugin = $('#plugin-section input')
329396
var inputsVariables = $('#less-variables-section input')
@@ -410,7 +477,8 @@ window.onload = function () { // wait for load in a dumb way because B-0
410477
{ type: 'image/svg+xml;charset=utf-8' }
411478
)
412479
var objectUrl = url.createObjectURL(svg);
413-
if (/^blob:/.exec(objectUrl) === null) {
480+
481+
if (/^blob:/.exec(objectUrl) === null || !supportsFile) {
414482
// `URL.createObjectURL` created a URL that started with something other
415483
// than "blob:", which means it has been polyfilled and is not supported by
416484
// this browser.

docs/customize.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<!--[if lt IE 9]>
1313
<style>
1414
.bs-customizer,
15+
.bs-customizer-import,
1516
.bs-docs-sidebar {
1617
display: none;
1718
}
@@ -23,6 +24,17 @@
2324
<![endif]-->
2425

2526
<!-- Customizer form -->
27+
28+
<div class="bs-docs-section bs-customizer-import">
29+
<div id="import-drop-target" class="bs-dropzone">
30+
<h2></h2>
31+
<p class="lead">Have an existing configuration? Upload your <code>config.json</code> to import it.</p>
32+
<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>
33+
<hr>
34+
<p><strong>Don't have one?</strong> That's okay—just start customizing the fields below.</p>
35+
</div>
36+
</div><!-- /import -->
37+
2638
<form class="bs-customizer" role="form">
2739
<div class="bs-docs-section" id="less-section">
2840
<button class="btn btn-default toggle" type="button">Toggle all</button>
@@ -358,8 +370,6 @@ <h1 id="less-variables" class="page-header">Less variables</h1>
358370
{% include customizer-variables.html %}
359371
</div>
360372

361-
362-
363373
<div class="bs-docs-section">
364374
<h1 id="download" class="page-header">Download</h1>
365375

0 commit comments

Comments
 (0)