Browse Source

More robust fetch code, download buttons

- Fetch code now handles github, local, or http: access
- Download button creates a time-stamped inline download
- More complete error messages
- Show warnings when approaching the hourly fetch quota (limit
reloading!)
- Added my test domain (where the _htaccess is deployed)
- `_htaccess` file added to set Access-Control-Allow-Origin “*”
- Marlin logo with css
- Limit selectable areas to avoid annoying selections
- Messages now persist until dismissed
- Default repo for files set to thinkyhead, ‘marlin_configurator’ branch
- Cosmetic changes
Scott Lahteine 9 years ago
parent
commit
e74138b2be

+ 1
- 0
Marlin/configurator/config/Configuration.h View File

@@ -51,6 +51,7 @@ Here are some standard links for getting your machine calibrated:
51 51
 #define SERIAL_PORT 0
52 52
 
53 53
 // This determines the communication speed of the printer
54
+// :[2400,9600,19200,38400,57600,115200,250000]
54 55
 #define BAUDRATE 250000
55 56
 
56 57
 // This enables the serial port associated to the Bluetooth interface

+ 1
- 0
Marlin/configurator/config/_htaccess View File

@@ -0,0 +1 @@
1
+Header set Access-Control-Allow-Origin "*"

+ 137
- 37
Marlin/configurator/css/configurator.css View File

@@ -1,22 +1,82 @@
1 1
 /* configurator.css */
2 2
 /* Styles for Marlin Configurator */
3 3
 
4
-body { margin: 0; padding: 0; background: #56A; color: #FFC; font-family: sans-serif; }
5
-fieldset { height: 16.1em; overflow: auto; margin-top: 10px; }
6
-#main { max-width: 1000px; margin: 0 auto; }
7
-#main { padding: 0 4%; width: 92%; }
8
-#main { font-family: monospace; }
9
-h1, #message { text-align: center; }
4
+.clear { clear: both; }
5
+
6
+/* Prevent selection except PRE tags */
7
+* {
8
+    -webkit-touch-callout: none;
9
+    -webkit-user-select: none;
10
+    -khtml-user-select: none;
11
+    -moz-user-select: none;
12
+    -ms-user-select: none;
13
+    user-select: none;
14
+	}
15
+pre {
16
+    -webkit-touch-callout: text;
17
+    -webkit-user-select: text;
18
+    -khtml-user-select: text;
19
+    -moz-user-select: text;
20
+    -ms-user-select: text;
21
+    user-select: text;
22
+	}
23
+
24
+body { margin: 0; padding: 0; background: #56A; color: #000; font-family: monospace; }
25
+#main {
26
+	max-width: 1000px;
27
+	margin: 0 auto;
28
+ 	padding: 0 4%; width: 92%;
29
+	}
30
+
31
+h1, h2, h3, h4, h5, h6 { clear: both; }
32
+
33
+h1, p.info { font-family: sans-serif; }
34
+h1 {
35
+	background: transparent url(logo.png) right top no-repeat;
36
+	height: 38px;
37
+	margin-bottom: -30px;
38
+	}
39
+p.info { padding: 0; color: #000; }
40
+p.info span { color: #800; }
41
+
42
+#message { text-align: center; }
10 43
 #message { width: 80%; margin: 0 auto 0.25em; color: #FF0; }
11 44
 #message p { padding: 2px 0; }
12 45
 #message p.error, #message p.message { color: #F00; background: #FF4; font-weight: bold; border-radius: 0.8em; }
13 46
 #message p.message { color: #080; background: #CFC; }
47
+#message p.message span {
48
+	color: #A00;
49
+	background: rgba(255, 255, 255, 1);
50
+	border: 1px solid rgba(0,0,0,0.5);
51
+	border-radius: 1em;
52
+	float: right;
53
+	margin-right: 0.5em;
54
+	padding: 0 3px;
55
+	font-family: sans-serif;
56
+	font-size: small;
57
+	position: relative;
58
+	top: -1px;
59
+	}
14 60
 
15
-.info { color: #AAF; }
16
-.info span { color: #FFF; }
17
-.info span span { color: #000; font-weight: bold; }
18 61
 #help strong { color: #0DD; }
19 62
 img { display: none; }
63
+
64
+/* Forms */
65
+
66
+#config_form {
67
+	display: block;
68
+	background: #EEE;
69
+	padding: 6px 20px 20px;
70
+	color: #000;
71
+	position: relative;
72
+	border-top-right-radius: 1.5em;
73
+	}
74
+fieldset {
75
+	height: 16.1em;
76
+	overflow-y: scroll;
77
+	overflow-x: hidden;
78
+	margin-top: 10px;
79
+	}
20 80
 label, input, select, textarea { display: block; float: left; margin: 1px 0; }
21 81
 label.newline, textarea, fieldset { clear: both; }
22 82
 label {
@@ -28,31 +88,9 @@ label {
28 88
 	}
29 89
 input[type="text"], select { margin: 0.75em 0 0; }
30 90
 input[type="checkbox"], input[type="radio"], input[type="file"] { margin: 1em 0 0; }
31
-#config_form {
32
-	display: block;
33
-	background: #EEE;
34
-	padding: 6px 20px 20px;
35
-	color: #000;
36
-	position: relative;
37
-	}
38
-
39
-/*#config_text, #config_adv_text { font-family: "Andale mono", monospace; clear: both; }*/
40
-#config_text, #config_adv_text {
41
-	height: 25em;
42
-	padding: 10px;
43
-	border: 2px solid #888;
44
-	border-radius: 5px;
45
-	overflow: auto;
46
-	background-color: #FFF;
47
-	color: #000;
48
-	font-family: "Fira Mono";
49
-	font-size: small;
50
-	}
51 91
 input[type="checkbox"], input[type="radio"].enabler { margin-left: 1em; }
92
+
52 93
 input:disabled { color: #BBB; }
53
-.clear { clear: both; }
54
-h1, h2, h3, h4, h5, h6 { clear: both; }
55
-h2 { margin: 0; padding: 1em 0 0; }
56 94
 
57 95
 ul.tabs { padding: 0; list-style: none; }
58 96
 ul.tabs li { display: inline; }
@@ -100,6 +138,8 @@ fieldset legend { display: none; }
100 138
 #serial_stepper { padding-top: 0.75em; display: block; float: left; }
101 139
 #SERIAL_PORT { display: none; }
102 140
 
141
+/* Tooltips */
142
+
103 143
 #tooltip {
104 144
 	display: none;
105 145
 	max-width: 30em;
@@ -138,18 +178,61 @@ fieldset legend { display: none; }
138 178
 	}
139 179
 #tooltip>strong { color: #00B; }
140 180
 
141
-span.disclose {
181
+/* Tooltips Checkbox */
182
+
183
+#tipson { float: right; font-weight: bold; font-size: 100%; font-family: helvetica; }
184
+#tipson input { float: none; display: inline; }
185
+
186
+/* Config Text */
187
+
188
+pre.config {
189
+	height: 25em;
190
+	padding: 10px;
191
+	border: 2px solid #888;
192
+	border-radius: 5px;
193
+	overflow: auto;
194
+	clear: both;
195
+	background-color: #FFF;
196
+	color: #000;
197
+	font-family: "Fira Mono";
198
+	font-size: small;
199
+	}
200
+
201
+/* Pre Headers */
202
+
203
+h2 {
204
+	width: 100%;
205
+	margin: 12px -300px 4px 0;
206
+	padding: 0;
207
+	float: left;
208
+	}
209
+
210
+/* Disclosure Widget */
211
+
212
+span.disclose, a.download {︎
213
+	display: block;
142 214
 	float: right;
143
-	margin-top: -10px;
215
+	margin-top: 12px;
216
+	}
217
+
218
+span.disclose {
219
+	margin-right: -10px; /* total width */
220
+	margin-left: 14px;
144 221
 	width: 0;
145 222
 	height: 0;
223
+	position: relative;
224
+	left: 3px;
225
+	top: 3px;
146 226
 	cursor: pointer;
147 227
 	border-left: 8px solid transparent;
148 228
 	border-right: 8px solid transparent;
149 229
 	border-top: 10px solid #000;
150 230
 	}
151 231
 span.disclose.closed {
152
-	margin: -14px 4px 0 0;
232
+	margin-right: -8px; /* total width */
233
+	margin-left: 10px;
234
+	left: 0;
235
+	top: 0;
153 236
 	border-top: 8px solid transparent;
154 237
 	border-bottom: 8px solid transparent;
155 238
 	border-right: 10px solid #000;
@@ -160,9 +243,26 @@ span.disclose.almost {
160 243
     transform: rotate(45deg);
161 244
 	}
162 245
 span.disclose.closed.almost {
246
+	left: 1px;
247
+	top: 3px;
163 248
     -ms-transform: rotate(315deg); /* IE 9 */
164 249
     -webkit-transform: rotate(315deg); /* Chrome, Safari, Opera */
165 250
     transform: rotate(315deg);
166 251
 	}
167
-#tipson { float: right; font-weight: bold; font-size: 100%; font-family: helvetica; }
168
-#tipson input { float: none; display: inline; }
252
+
253
+/* Download Button */
254
+
255
+a.download {
256
+	visibility: hidden;
257
+	padding: 2px;
258
+	border: 1px solid #494;
259
+	border-radius: 4px;
260
+	margin: 12px 0 0;
261
+	background: #FFF;
262
+	color: #494;
263
+	font-family: sans-serif;
264
+	font-size: small;
265
+	font-weight: bold;
266
+	text-decoration: none;
267
+	}
268
+

BIN
Marlin/configurator/css/logo.png View File


+ 16
- 9
Marlin/configurator/index.html View File

@@ -2,7 +2,7 @@
2 2
 <html>
3 3
   <head>
4 4
     <meta charset="UTF-8">
5
-    <title>Marlin Configurator</title>
5
+    <title>Marlin Firmware Configurator</title>
6 6
     <link href='http://fonts.googleapis.com/css?family=Fira+Mono&amp;subset=latin,latin-ext' rel='stylesheet' type='text/css' />
7 7
     <script src="js/jquery-2.1.3.min.js"></script>
8 8
     <script src="js/binarystring.js"></script>
@@ -15,11 +15,9 @@
15 15
   <body>
16 16
     <section id="main">
17 17
       <h1>Marlin Configurator</h1>
18
+      <p class="info">Select presets (coming soon), modify, and download.</p>
18 19
 
19
-      <div id="message">
20
-        <p class="info">Enter values in the form, get a Marlin configuration.<br/>Will include a drop-down of known configurations.</p>
21
-      </div>
22
-
20
+      <div id="message"></div>
23 21
       <div id="tabs"></div>
24 22
 
25 23
       <form id="config_form">
@@ -83,10 +81,19 @@
83 81
           <legend>More…</legend>
84 82
         </fieldset>
85 83
 
86
-        <h2>Marlin/Configuration.h</h2><span class="disclose"></span>
87
-        <pre id="config_text" class="hilightable"></pre>
88
-        <h2>Marlin/Configuration_adv.h</h2><span class="disclose"></span>
89
-        <pre id="config_adv_text" class="hilightable"></pre>
84
+        <section id="config_text">
85
+          <h2>Configuration.h</h2>
86
+          <span class="disclose"></span>
87
+          <a href="" class="download">Download</a>
88
+          <pre class="hilightable config"></pre>
89
+        </section>
90
+
91
+        <section id="config_adv_text">
92
+          <h2>Configuration_adv.h</h2>
93
+          <span class="disclose"></span>
94
+          <a href="" class="download">Download</a>
95
+          <pre class="hilightable config"></pre>
96
+        </section>
90 97
 
91 98
         <br class="clear" />
92 99
       </form>

+ 166
- 25
Marlin/configurator/js/configurator.js View File

@@ -16,7 +16,62 @@
16 16
 
17 17
 $(function(){
18 18
 
19
-var marlin_config = 'https://api.github.com/repos/MarlinFirmware/Marlin/contents/Marlin';
19
+/**
20
+ * Github API useful GET paths. (Start with "https://api.github.com/repos/:owner/:repo/")
21
+ *
22
+ *   contributors                               Get a list of contributors
23
+ *   tags                                       Get a list of tags
24
+ *   contents/[path]?ref=branch/tag/commit      Get the contents of a file
25
+ */
26
+
27
+ // GitHub
28
+ // Warning! Limited to 60 requests per hour!
29
+var config = {
30
+  type:  'github',
31
+  host:  'https://api.github.com',
32
+  owner: 'thinkyhead',
33
+  repo:  'Marlin',
34
+  ref:   'marlin_configurator',
35
+  path:  'Marlin/configurator/config'
36
+};
37
+/**/
38
+
39
+/* // Remote
40
+var config = {
41
+  type:  'remote',
42
+  host:  'http://www.thinkyhead.com',
43
+  path:  '_marlin/config'
44
+};
45
+/**/
46
+
47
+/* // Local
48
+var config = {
49
+  type:  'local',
50
+  path:  'config'
51
+};
52
+/**/
53
+
54
+function github_command(conf, command, path) {
55
+  var req = conf.host+'/repos/'+conf.owner+'/'+conf.repo+'/'+command;
56
+  if (path) req += '/' + path;
57
+  return req;
58
+}
59
+function config_path(item) {
60
+  var path = '', ref = '';
61
+  switch(config.type) {
62
+    case 'github':
63
+      path = github_command(config, 'contents', config.path);
64
+      if (config.ref !== undefined) ref = '?ref=' + config.ref;
65
+      break;
66
+    case 'remote':
67
+      path = config.host + '/' + config.path + '/';
68
+      break;
69
+    case 'local':
70
+      path = config.path + '/';
71
+      break;
72
+  }
73
+  return path + '/' + item + ref;
74
+}
20 75
 
21 76
 // Extend builtins
22 77
 String.prototype.lpad = function(len, chr) {
@@ -25,6 +80,7 @@ String.prototype.lpad = function(len, chr) {
25 80
   if (need > 0) { s = new Array(need+1).join(chr) + s; }
26 81
   return s;
27 82
 };
83
+
28 84
 String.prototype.prePad = function(len, chr) { return len ? this.lpad(len, chr) : this; };
29 85
 String.prototype.zeroPad = function(len)     { return this.prePad(len, '0'); };
30 86
 String.prototype.toHTML = function()         { return jQuery('<div>').text(this).html(); };
@@ -36,6 +92,19 @@ Number.prototype.limit = function(m1, m2)  {
36 92
   if (m2 == null) return this > m1 ? m1 : this;
37 93
   return this < m1 ? m1 : this > m2 ? m2 : this;
38 94
 };
95
+Date.prototype.fileStamp = function(filename) {
96
+  var fs = this.getFullYear()
97
+    + ((this.getMonth()+1)+'').zeroPad(2)
98
+    + (this.getDate()+'').zeroPad(2)
99
+    + (this.getHours()+'').zeroPad(2)
100
+    + (this.getMinutes()+'').zeroPad(2)
101
+    + (this.getSeconds()+'').zeroPad(2);
102
+
103
+  if (filename !== undefined)
104
+    return filename.replace(/^(.+)(\.\w+)$/g, '$1-['+fs+']$2');
105
+
106
+  return fs;
107
+}
39 108
 
40 109
 /**
41 110
  * selectField.addOptions takes an array or keyed object
@@ -49,6 +118,12 @@ $.fn.extend({
49 118
         sel.append( $('<option>',{value:isArr?v:k}).text(v) );
50 119
       });
51 120
     });
121
+  },
122
+  noSelect: function() {
123
+    return this
124
+            .attr('unselectable', 'on')
125
+            .css('user-select', 'none')
126
+            .on('selectstart', false);
52 127
   }
53 128
 });
54 129
 
@@ -62,10 +137,11 @@ var configuratorApp = (function(){
62 137
       boards_file = 'boards.h',
63 138
       config_file = 'Configuration.h',
64 139
       config_adv_file = 'Configuration_adv.h',
140
+      $msgbox = $('#message'),
65 141
       $form = $('#config_form'),
66 142
       $tooltip = $('#tooltip'),
67
-      $config = $('#config_text'),
68
-      $config_adv = $('#config_adv_text'),
143
+      $config = $('#config_text pre'),
144
+      $config_adv = $('#config_adv_text pre'),
69 145
       define_list = [[],[]],
70 146
       boards_list = {},
71 147
       therms_list = {},
@@ -88,6 +164,9 @@ var configuratorApp = (function(){
88 164
       // Make tabs for all the fieldsets
89 165
       this.makeTabsForFieldsets();
90 166
 
167
+      // No selection on errors
168
+      $msgbox.noSelect();
169
+
91 170
       // Make a droppable file uploader, if possible
92 171
       var $uploader = $('#file-upload');
93 172
       var fileUploader = new BinaryFileUploader({
@@ -99,41 +178,98 @@ var configuratorApp = (function(){
99 178
 
100 179
       // Make the disclosure items work
101 180
       $('.disclose').click(function(){
102
-        var $dis = $(this), $pre = $dis.next('pre');
181
+        var $dis = $(this), $pre = $dis.nextAll('pre:first');
103 182
         var didAnim = function() {$dis.toggleClass('closed almost');};
104 183
         $dis.addClass('almost').hasClass('closed')
105
-          ? $pre.slideDown(500, didAnim)
106
-          : $pre.slideUp(500, didAnim);
184
+          ? $pre.slideDown(200, didAnim)
185
+          : $pre.slideUp(200, didAnim);
107 186
       });
108 187
 
109 188
       // Read boards.h, Configuration.h, Configuration_adv.h
110 189
       var ajax_count = 0, success_count = 0;
111 190
       var loaded_items = {};
112 191
       var config_files = [boards_file, config_file, config_adv_file];
113
-      var isGithub = marlin_config.match('api.github');
192
+      var isGithub = config.type == 'github';
193
+      var rateLimit = 0;
114 194
       $.each(config_files, function(i,fname){
195
+        var url = config_path(fname);
115 196
         $.ajax({
116
-          url: marlin_config+'/'+fname,
197
+          url: url,
117 198
           type: 'GET',
118
-          dataType: isGithub ? 'jsonp' : 'script',
199
+          dataType: isGithub ? 'jsonp' : undefined,
119 200
           async: true,
120 201
           cache: false,
202
+          error: function(req, stat, err) {
203
+            self.log(req, 1);
204
+            if (req.status == 200) {
205
+              if (typeof req.responseText === 'string') {
206
+                var txt = req.responseText;
207
+                loaded_items[fname] = function(){ self.fileLoaded(fname, txt); };
208
+                success_count++;
209
+                // self.setMessage('The request for "'+fname+'" may be malformed.', 'error');
210
+              }
211
+            }
212
+            else {
213
+              self.setRequestError(req.status ? req.status : '(Access-Control-Allow-Origin?)', url);
214
+            }
215
+          },
121 216
           success: function(txt) {
122
-            loaded_items[fname] = function(){ self.fileLoaded(fname, isGithub ? atob(txt.data.content) : txt); };
123
-            success_count++;
217
+            if (isGithub && typeof txt.meta.status !== undefined && txt.meta.status != 200) {
218
+              self.setRequestError(txt.meta.status, url);
219
+            }
220
+            else {
221
+              // self.log(txt, 1);
222
+              if (isGithub) {
223
+                rateLimit = {
224
+                  quota: 1 * txt.meta['X-RateLimit-Remaining'],
225
+                  timeLeft: Math.floor(txt.meta['X-RateLimit-Reset'] - Date.now()/1000),
226
+                };
227
+              }
228
+              loaded_items[fname] = function(){ self.fileLoaded(fname, isGithub ? atob(txt.data.content) : txt); };
229
+              success_count++;
230
+            }
124 231
           },
125 232
           complete: function() {
126 233
             ajax_count++;
127 234
             if (ajax_count >= 3) {
128 235
               $.each(config_files, function(){ if (loaded_items[this]) loaded_items[this](); });
129 236
               if (success_count < ajax_count)
130
-                self.setMessage('Unable to load configurations. Use the upload field instead.', 'error');
237
+                self.setMessage('Unable to load configurations. Try the upload field.', 'error');
238
+              var r;
239
+              if (r = rateLimit) {
240
+                if (r.quota < 20) {
241
+                  self.setMessage(
242
+                    'Approaching request limit (' +
243
+                    r.quota + ' remaining.' +
244
+                    ' Reset in ' + Math.floor(r.timeLeft/60) + ':' + (r.timeLeft%60+'').zeroPad(2) + ')'
245
+                  );
246
+                }
247
+              }
131 248
             }
132 249
           }
133 250
         });
134 251
       });
135 252
     },
136 253
 
254
+    createDownloadLink: function(adv) {
255
+      var $c = adv ? $config_adv : $config, txt = $c.text();
256
+      var filename = (adv ? config_adv_file : config_file);
257
+      $c.prevAll('.download:first')
258
+        .mouseover(function() {
259
+          var d = new Date(), fn = d.fileStamp(filename);
260
+          $(this).attr({ download:fn, href:'download:'+fn, title:'download:'+fn });
261
+        })
262
+        .click(function(){
263
+          var $button = $(this);
264
+          $(this).attr({ href:'data:text/plain;charset=utf-8,' + encodeURIComponent($c.text()) });
265
+          setTimeout(function(){
266
+            $button.attr({ href:$button.attr('title') });
267
+          }, 100);
268
+          return true;
269
+        })
270
+        .css({visibility:'visible'});
271
+    },
272
+
137 273
     /**
138 274
      * Init the boards array from a boards.h file
139 275
      */
@@ -239,6 +375,7 @@ var configuratorApp = (function(){
239 375
             this.log(define_list[0], 2);
240 376
             this.createFieldsForDefines(0);
241 377
             this.refreshConfigForm();
378
+            this.createDownloadLink(false);
242 379
             has_config = true;
243 380
           }
244 381
           else {
@@ -253,6 +390,7 @@ var configuratorApp = (function(){
253 390
             define_list[1] = this.getDefinesFromText(txt);
254 391
             this.log(define_list[1], 2);
255 392
             this.refreshConfigForm();
393
+            this.createDownloadLink(true);
256 394
             has_config_adv = true;
257 395
           }
258 396
           else {
@@ -289,7 +427,7 @@ var configuratorApp = (function(){
289 427
       });
290 428
 
291 429
       // Get all 'switchable' class items and add a checkbox
292
-      $('#config_form .switchable').each(function(){
430
+      $form.find('.switchable').each(function(){
293 431
         $(this).after(
294 432
           $('<input>',{type:'checkbox',value:'1',class:'enabler'}).prop('checked',true)
295 433
             .attr('id',this.id + '-switch')
@@ -651,6 +789,7 @@ var configuratorApp = (function(){
651 789
         }
652 790
       }
653 791
 
792
+      // Success?
654 793
       if (info.type) {
655 794
         // Get the end-of-line comment, if there is one
656 795
         var tooltip = '';
@@ -668,11 +807,11 @@ var configuratorApp = (function(){
668 807
               tooltip = '';
669 808
               break;
670 809
             }
671
-            tooltip += ' ' + s[1] + "\n";
810
+            tooltip += ' ' + s[1] + '\n';
672 811
           }
673 812
         }
674 813
 
675
-        findDef = new RegExp('^[ \\t]*'+name+'[ \\t]*', 'm');
814
+        findDef = new RegExp('^[ \\t]*'+name); // To strip the name from the start
676 815
         $.extend(info, {
677 816
           tooltip: '<strong>'+name+'</strong> '+tooltip.replace(findDef,'').trim().toHTML(),
678 817
           lineNum: this.getLineNumberOfText(info.line, txt)
@@ -700,24 +839,22 @@ var configuratorApp = (function(){
700 839
     setMessage: function(msg,type) {
701 840
       if (msg) {
702 841
         if (type === undefined) type = 'message';
703
-        var $err = $('<p class="'+type+'">'+msg+'</p>'), err = $err[0];
704
-        $('#message').prepend($err);
842
+        var $err = $('<p class="'+type+'">'+msg+'<span>x</span></p>').appendTo($msgbox), err = $err[0];
705 843
         var baseColor = $err.css('color').replace(/rgba?\(([^),]+,[^),]+,[^),]+).*/, 'rgba($1,');
706
-        var d = new Date();
707 844
         err.pulse_offset = (pulse_offset += 200);
708
-        err.startTime = d.getTime() + pulse_offset;
845
+        err.startTime = Date.now() + pulse_offset;
709 846
         err.pulser = setInterval(function(){
710
-            d = new Date();
711
-            var pulse_time = d.getTime() + err.pulse_offset;
712
-            $err.css({color:baseColor+(0.5+Math.sin(pulse_time/200)*0.4)+')'});
713
-            if (pulse_time - err.startTime > 5000) {
847
+            var pulse_time = Date.now() + err.pulse_offset;
848
+            var opac = 0.5+Math.sin(pulse_time/200)*0.4;
849
+            $err.css({color:baseColor+(opac)+')'});
850
+            if (pulse_time - err.startTime > 2500 && opac > 0.899) {
714 851
               clearInterval(err.pulser);
715
-              $err.remove();
716 852
             }
717 853
           }, 50);
854
+        $err.click(function(e) { $(this).remove(); return false; }).css({cursor:'pointer'});
718 855
       }
719 856
       else {
720
-        $('#message p.error, #message p.warning').each(function() {
857
+        $msgbox.find('p.error, p.warning').each(function() {
721 858
           if (this.pulser !== undefined && this.pulser)
722 859
             clearInterval(this.pulser);
723 860
           $(this).remove();
@@ -725,6 +862,10 @@ var configuratorApp = (function(){
725 862
       }
726 863
     },
727 864
 
865
+    setRequestError: function(stat, path) {
866
+      self.setMessage('Error '+stat+' – ' + path.replace(/^(https:\/\/[^\/]+\/)?.+(\/[^\/]+)$/, '$1...$2'), 'error');
867
+    },
868
+
728 869
     log: function(o,l) {
729 870
       if (l === undefined) l = 0;
730 871
       if (this.logging>=l*1) console.log(o);

Loading…
Cancel
Save