Add a responsive file uploader

- Specify HTML5
- Allow drag and drop for loading configurations
This commit is contained in:
Scott Lahteine 2015-02-05 11:14:31 -08:00
parent 09a385d9b0
commit ac7a4358d6
5 changed files with 359 additions and 31 deletions

View File

@ -15,7 +15,7 @@ label, input, select, textarea { display: block; float: left; margin: 1px 0; }
label.newline, textarea { clear: both; }
label { width: 130px; height: 1em; padding: 10px 480px 10px 1em; margin-right: -470px; text-align: right; }
input[type="text"], select, .jstepper { margin: 0.75em 0 0; }
input[type="checkbox"], input[type="radio"] { margin: 1em 0 0; }
input[type="checkbox"], input[type="radio"], input[type="file"] { margin: 1em 0 0; }
#config_form { display: block; background: #DDD; padding: 20px; color: #000; }
/*#config_text, #config_adv_text { font-family: "Andale mono", monospace; clear: both; }*/
#config_text, #config_adv_text { height: 25em; overflow: auto; background-color: #FFF; color: #888; padding: 10px; }

View File

@ -1,15 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Marlin Configurator</title>
<meta charset="UTF-8">
<title>Marlin Configurator</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script src="js/binarystring.js"></script>
<script src="js/binaryfileuploader.js"></script>
<script src="js/configurator.js"></script>
<script src="js/jcanvas.js"></script>
<script src="js/jstepper.js"></script>
<link rel="stylesheet" href="css/configurator.css" type="text/css" media="all" />
</head>
<body>
<div id="main">
<section id="main">
<h1>Marlin Configurator 0.1a</h1>
<p>Enter values in the form, get a Marlin configuration.<br/>Will include a drop-down of known configurations.</p>
<ul id="help">
@ -18,9 +21,9 @@
<form id="config_form">
<!-- label>Serial Port:</label><input name="SERIAL_PORT" type="text" size="4" maxlength="2" value="99" / -->
<label>Drop Files Here:</label><input type="file" id="file-upload" />
<label>Serial Port:</label><select name="SERIAL_PORT"></select><div id="serial_stepper"></div>
<label class="newline">Serial Port:</label><select name="SERIAL_PORT"></select><div id="serial_stepper"></div>
<label>Baud Rate:</label><select name="BAUDRATE"></select>
@ -52,6 +55,6 @@
<br class="clear" />
</form>
</div>
<section>
</body>
</html>

View File

@ -0,0 +1,79 @@
function BinaryFileUploader(o) {
this.options = null;
this._defaultOptions = {
element: null, // HTML file element
onFileLoad: function(file) {
console.log(file.toString());
}
};
this._init = function(o) {
if (!this.hasFileUploaderSupport()) return;
this._verifyDependencies();
this.options = this._mergeObjects(this._defaultOptions, o);
this._verifyOptions();
this.addFileChangeListener();
}
this.hasFileUploaderSupport = function() {
return !!(window.File && window.FileReader && window.FileList && window.Blob);
}
this.addFileChangeListener = function() {
this.options.element.addEventListener(
'change',
this._bind(this, this.onFileChange)
);
}
this.onFileChange = function(e) {
// TODO accept multiple files
var file = e.target.files[0],
reader = new FileReader();
reader.onload = this._bind(this, this.onFileLoad);
reader.readAsBinaryString(file);
}
this.onFileLoad = function(e) {
var content = e.target.result,
string = new BinaryString(content);
this.options.onFileLoad(string);
}
this._mergeObjects = function(starting, override) {
var merged = starting;
for (key in override) merged[key] = override[key];
return merged;
}
this._verifyOptions = function() {
if (!(this.options.element && this.options.element.type && this.options.element.type === 'file')) {
throw 'Invalid element param in options. Must be a file upload DOM element';
}
if (typeof this.options.onFileLoad !== 'function') {
throw 'Invalid onFileLoad param in options. Must be a function';
}
}
this._verifyDependencies = function() {
if (!window.BinaryString) throw 'BinaryString is missing. Check that you\'ve correctly included it';
}
// helper function for binding methods to objects
this._bind = function(object, method) {
return function() {return method.apply(object, arguments);};
}
this._init(o);
}

View File

@ -0,0 +1,168 @@
function BinaryString(source) {
this._source = null;
this._bytes = [];
this._pos = 0;
this._length = 0;
this._init = function(source) {
this._source = source;
this._bytes = this._stringToBytes(this._source);
this._length = this._bytes.length;
}
this.current = function() {return this._pos;}
this.rewind = function() {return this.jump(0);}
this.end = function() {return this.jump(this.length() - 1);}
this.next = function() {return this.jump(this.current() + 1);}
this.prev = function() {return this.jump(this.current() - 1);}
this.jump = function(pos) {
if (pos < 0 || pos >= this.length()) return false;
this._pos = pos;
return true;
}
this.readByte = function(pos) {
pos = (typeof pos == 'number') ? pos : this.current();
return this.readBytes(1, pos)[0];
}
this.readBytes = function(length, pos) {
length = length || 1;
pos = (typeof pos == 'number') ? pos : this.current();
if (pos > this.length() ||
pos < 0 ||
length <= 0 ||
pos + length > this.length() ||
pos + length < 0
) {
return false;
}
var bytes = [];
for (var i = pos; i < pos + length; i++) {
bytes.push(this._bytes[i]);
}
return bytes;
}
this.length = function() {return this._length;}
this.toString = function() {
var string = '',
length = this.length();
for (var i = 0; i < length; i++) {
string += String.fromCharCode(this.readByte(i));
}
return string;
}
this.toUtf8 = function() {
var inc = 0,
string = '',
length = this.length();
// determine if first 3 characters are the BOM
// then skip them in output if so
if (length >= 3 &&
this.readByte(0) === 0xEF &&
this.readByte(1) === 0xBB &&
this.readByte(2) === 0xBF
) {
inc = 3;
}
for (; inc < length; inc++) {
var byte1 = this.readByte(inc),
byte2 = 0,
byte3 = 0,
byte4 = 0,
code1 = 0,
code2 = 0,
point = 0;
switch (true) {
// single byte character; same as ascii
case (byte1 < 0x80):
code1 = byte1;
break;
// 2 byte character
case (byte1 >= 0xC2 && byte1 < 0xE0):
byte2 = this.readByte(++inc);
code1 = ((byte1 & 0x1F) << 6) +
(byte2 & 0x3F);
break;
// 3 byte character
case (byte1 >= 0xE0 && byte1 < 0xF0):
byte2 = this.readByte(++inc);
byte3 = this.readByte(++inc);
code1 = ((byte1 & 0xFF) << 12) +
((byte2 & 0x3F) << 6) +
(byte3 & 0x3F);
break;
// 4 byte character
case (byte1 >= 0xF0 && byte1 < 0xF5):
byte2 = this.readByte(++inc);
byte3 = this.readByte(++inc);
byte4 = this.readByte(++inc);
point = ((byte1 & 0x07) << 18) +
((byte2 & 0x3F) << 12) +
((byte3 & 0x3F) << 6) +
(byte4 & 0x3F)
point -= 0x10000;
code1 = (point >> 10) + 0xD800;
code2 = (point & 0x3FF) + 0xDC00;
break;
default:
throw 'Invalid byte ' + this._byteToString(byte1) + ' whilst converting to UTF-8';
break;
}
string += (code2) ? String.fromCharCode(code1, code2)
: String.fromCharCode(code1);
}
return string;
}
this.toArray = function() {return this.readBytes(this.length() - 1, 0);}
this._stringToBytes = function(str) {
var bytes = [],
chr = 0;
for (var i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
bytes.push(chr & 0xFF);
}
return bytes;
}
this._byteToString = function(byte) {
var asString = byte.toString(16).toUpperCase();
while (asString.length < 2) {
asString = '0' + asString;
}
return '0x' + asString;
}
this._init(source);
}

View File

@ -55,6 +55,9 @@ var configuratorApp = (function(){
// private variables and functions go here
var self,
pi2 = Math.PI * 2,
boards_file = 'boards.h',
config_file = 'Configuration.h',
config_adv_file = 'Configuration_adv.h',
$config = $('#config_text'),
$config_adv = $('#config_adv_text'),
boards_list = {},
@ -67,37 +70,106 @@ var configuratorApp = (function(){
init: function() {
self = this; // a 'this' for use when 'this' is something else
// Make a droppable file uploader
var $uploader = $('#file-upload');
var fileUploader = new BinaryFileUploader({
element: $uploader[0],
onFileLoad: function(file) { console.log(this); self.handleFileLoad(file, $uploader); }
});
if (!fileUploader.hasFileUploaderSupport()) alert('Your browser doesn\'t support the file reading API');
// Read boards.h
boards_list = {};
$.get(marlin_config + "/boards.h", function(txt) {
// Get all the boards and save them into an object
var r, findDef = new RegExp('[ \\t]*#define[ \\t]+(BOARD_[^ \\t]+)[ \\t]+(\\d+)[ \\t]*(//[ \\t]*)?(.+)?', 'gm');
while((r = findDef.exec(txt)) !== null) {
boards_list[r[1]] = r[2].prePad(3, '  ') + " — " + r[4].replace(/\).*/, ')');
}
var errFunc = function(jqXHR, textStatus, errorThrown) {
alert('Failed to load '+this.url+'. Try the file field.');
};
$.ajax({
url: marlin_config+'/'+boards_file,
type: 'GET',
async: false,
cache: false,
success: function(txt) {
// Get all the boards and save them into an object
self.initBoardsFromText(txt);
},
error: errFunc
});
// Read Configuration.h
$.get(marlin_config+"/Configuration.h", function(txt) {
// File contents into the textarea
$config.text(txt);
// Get all the thermistors and save them into an object
var r, s, findDef = new RegExp('(//.*\n)+\\s+(#define[ \\t]+TEMP_SENSOR_0)', 'g');
r = findDef.exec(txt);
findDef = new RegExp('^//[ \\t]*([-\\d]+)[ \\t]+is[ \\t]+(.*)[ \\t]*$', 'gm');
while((s = findDef.exec(r[0])) !== null) {
therms_list[s[1]] = s[1].prePad(4, '  ') + " — " + s[2];
}
$.ajax({
url: marlin_config+'/'+config_file,
type: 'GET',
async: false,
cache: false,
success: function(txt) {
// File contents into the textarea
$config.text(txt);
self.initThermistorsFromText(txt);
},
error: errFunc
});
// Read Configuration.h
$.get(marlin_config+"/Configuration_adv.h", function(txt) {
$config_adv.text(txt);
self.setupConfigForm();
$.ajax({
url: marlin_config+'/'+config_adv_file,
type: 'GET',
async: false,
cache: false,
success: function(txt) {
// File contents into the textarea
$config_adv.text(txt);
self.setupConfigForm();
},
error: errFunc
});
},
initBoardsFromText: function(txt) {
boards_list = {};
var r, findDef = new RegExp('[ \\t]*#define[ \\t]+(BOARD_[^ \\t]+)[ \\t]+(\\d+)[ \\t]*(//[ \\t]*)?(.+)?', 'gm');
while((r = findDef.exec(txt)) !== null) {
boards_list[r[1]] = r[2].prePad(3, '  ') + " — " + r[4].replace(/\).*/, ')');
}
},
initThermistorsFromText: function(txt) {
// Get all the thermistors and save them into an object
var r, s, findDef = new RegExp('(//.*\n)+\\s+(#define[ \\t]+TEMP_SENSOR_0)', 'g');
r = findDef.exec(txt);
findDef = new RegExp('^//[ \\t]*([-\\d]+)[ \\t]+is[ \\t]+(.*)[ \\t]*$', 'gm');
while((s = findDef.exec(r[0])) !== null) {
therms_list[s[1]] = s[1].prePad(4, '  ') + " — " + s[2];
}
},
handleFileLoad: function(file, $uploader) {
file += '';
var filename = $uploader.val().replace(/.*[\/\\](.*)$/, '$1');
switch(filename) {
case config_file:
$config.text(file);
this.initThermistorsFromText(file);
this.refreshConfigForm();
break;
case config_adv_file:
$config_adv.text(file);
this.refreshConfigForm();
break;
case boards_file:
this.initBoardsFromText(file);
$('#MOTHERBOARD').html('').addOptions(boards_list);
this.initField('MOTHERBOARD');
break;
default:
console.log("Can't parse "+filename);
break;
}
},
setupConfigForm: function() {
// Modify form fields and make the form responsive.
// As values change on the form, we could update the
@ -128,6 +200,17 @@ var configuratorApp = (function(){
);
});
$('#SERIAL_PORT').addOptions([0,1,2,3,4,5,6,7]);
$('#BAUDRATE').addOptions([2400,9600,19200,38400,57600,115200,250000]);
$('#MOTHERBOARD').addOptions(boards_list);
$('#EXTRUDERS').addOptions([1,2,3,4]);
$('#POWER_SUPPLY').addOptions({'1':'ATX','2':'Xbox 360'});
this.refreshConfigForm();
},
refreshConfigForm: function() {
/**
* For now I'm manually creating these references
* but I should be able to parse Configuration.h
@ -140,30 +223,25 @@ var configuratorApp = (function(){
* Then we only need to specify exceptions to
* standard behavior, (which is to add a text field)
*/
$('#SERIAL_PORT').addOptions([0,1,2,3,4,5,6,7]);
this.initField('SERIAL_PORT');
$('#BAUDRATE').addOptions([2400,9600,19200,38400,57600,115200,250000]);
this.initField('BAUDRATE');
this.initField('BTENABLED');
$('#MOTHERBOARD').addOptions(boards_list);
this.initField('MOTHERBOARD');
this.initField('CUSTOM_MENDEL_NAME');
this.initField('MACHINE_UUID');
$('#EXTRUDERS').addOptions([1,2,3,4]);
this.initField('EXTRUDERS');
$('#POWER_SUPPLY').addOptions({'1':'ATX','2':'Xbox 360'});
this.initField('POWER_SUPPLY');
this.initField('PS_DEFAULT_OFF');
$('#TEMP_SENSOR_0, #TEMP_SENSOR_1, #TEMP_SENSOR_2, #TEMP_SENSOR_BED').addOptions(therms_list);
$('#TEMP_SENSOR_0, #TEMP_SENSOR_1, #TEMP_SENSOR_2, #TEMP_SENSOR_BED').html('').addOptions(therms_list);
this.initField('TEMP_SENSOR_0');
this.initField('TEMP_SENSOR_1');
this.initField('TEMP_SENSOR_2');