luci2: add CSS for grid views + mobile improvements
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
1 /*
2 LuCI2 - OpenWrt Web Interface
3
4 Copyright 2013-2014 Jo-Philipp Wich <jow@openwrt.org>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11 */
12
13 String.prototype.format = function()
14 {
15 var html_esc = [/&/g, '&#38;', /"/g, '&#34;', /'/g, '&#39;', /</g, '&#60;', />/g, '&#62;'];
16 var quot_esc = [/"/g, '&#34;', /'/g, '&#39;'];
17
18 function esc(s, r) {
19 for( var i = 0; i < r.length; i += 2 )
20 s = s.replace(r[i], r[i+1]);
21 return s;
22 }
23
24 var str = this;
25 var out = '';
26 var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/;
27 var a = b = [], numSubstitutions = 0, numMatches = 0;
28
29 while ((a = re.exec(str)) != null)
30 {
31 var m = a[1];
32 var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5];
33 var pPrecision = a[6], pType = a[7];
34
35 numMatches++;
36
37 if (pType == '%')
38 {
39 subst = '%';
40 }
41 else
42 {
43 if (numSubstitutions < arguments.length)
44 {
45 var param = arguments[numSubstitutions++];
46
47 var pad = '';
48 if (pPad && pPad.substr(0,1) == "'")
49 pad = leftpart.substr(1,1);
50 else if (pPad)
51 pad = pPad;
52
53 var justifyRight = true;
54 if (pJustify && pJustify === "-")
55 justifyRight = false;
56
57 var minLength = -1;
58 if (pMinLength)
59 minLength = parseInt(pMinLength);
60
61 var precision = -1;
62 if (pPrecision && pType == 'f')
63 precision = parseInt(pPrecision.substring(1));
64
65 var subst = param;
66
67 switch(pType)
68 {
69 case 'b':
70 subst = (parseInt(param) || 0).toString(2);
71 break;
72
73 case 'c':
74 subst = String.fromCharCode(parseInt(param) || 0);
75 break;
76
77 case 'd':
78 subst = (parseInt(param) || 0);
79 break;
80
81 case 'u':
82 subst = Math.abs(parseInt(param) || 0);
83 break;
84
85 case 'f':
86 subst = (precision > -1)
87 ? ((parseFloat(param) || 0.0)).toFixed(precision)
88 : (parseFloat(param) || 0.0);
89 break;
90
91 case 'o':
92 subst = (parseInt(param) || 0).toString(8);
93 break;
94
95 case 's':
96 subst = param;
97 break;
98
99 case 'x':
100 subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase();
101 break;
102
103 case 'X':
104 subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase();
105 break;
106
107 case 'h':
108 subst = esc(param, html_esc);
109 break;
110
111 case 'q':
112 subst = esc(param, quot_esc);
113 break;
114
115 case 'j':
116 subst = String.serialize(param);
117 break;
118
119 case 't':
120 var td = 0;
121 var th = 0;
122 var tm = 0;
123 var ts = (param || 0);
124
125 if (ts > 60) {
126 tm = Math.floor(ts / 60);
127 ts = (ts % 60);
128 }
129
130 if (tm > 60) {
131 th = Math.floor(tm / 60);
132 tm = (tm % 60);
133 }
134
135 if (th > 24) {
136 td = Math.floor(th / 24);
137 th = (th % 24);
138 }
139
140 subst = (td > 0)
141 ? '%dd %dh %dm %ds'.format(td, th, tm, ts)
142 : '%dh %dm %ds'.format(th, tm, ts);
143
144 break;
145
146 case 'm':
147 var mf = pMinLength ? parseInt(pMinLength) : 1000;
148 var pr = pPrecision ? Math.floor(10*parseFloat('0'+pPrecision)) : 2;
149
150 var i = 0;
151 var val = parseFloat(param || 0);
152 var units = [ '', 'K', 'M', 'G', 'T', 'P', 'E' ];
153
154 for (i = 0; (i < units.length) && (val > mf); i++)
155 val /= mf;
156
157 subst = val.toFixed(pr) + ' ' + units[i];
158 break;
159 }
160
161 subst = (typeof(subst) == 'undefined') ? '' : subst.toString();
162
163 if (minLength > 0 && pad.length > 0)
164 for (var i = 0; i < (minLength - subst.length); i++)
165 subst = justifyRight ? (pad + subst) : (subst + pad);
166 }
167 }
168
169 out += leftpart + subst;
170 str = str.substr(m.length);
171 }
172
173 return out + str;
174 }
175
176 if (!window.location.origin)
177 window.location.origin = '%s//%s%s'.format(
178 window.location.protocol,
179 window.location.hostname,
180 (window.location.port ? ':' + window.location.port : '')
181 );
182
183 function LuCI2()
184 {
185 var L = this;
186
187 var Class = function() { };
188
189 Class.extend = function(properties)
190 {
191 Class.initializing = true;
192
193 var prototype = new this();
194 var superprot = this.prototype;
195
196 Class.initializing = false;
197
198 $.extend(prototype, properties, {
199 callSuper: function() {
200 var args = [ ];
201 var meth = arguments[0];
202
203 if (typeof(superprot[meth]) != 'function')
204 return undefined;
205
206 for (var i = 1; i < arguments.length; i++)
207 args.push(arguments[i]);
208
209 return superprot[meth].apply(this, args);
210 }
211 });
212
213 function _class()
214 {
215 this.options = arguments[0] || { };
216
217 if (!Class.initializing && typeof(this.init) == 'function')
218 this.init.apply(this, arguments);
219 }
220
221 _class.prototype = prototype;
222 _class.prototype.constructor = _class;
223
224 _class.extend = Class.extend;
225
226 return _class;
227 };
228
229 Class.require = function(name)
230 {
231 var path = '/' + name.replace(/\./g, '/') + '.js';
232
233 return $.ajax(path, {
234 method: 'GET',
235 async: false,
236 cache: true,
237 dataType: 'text'
238 }).then(function(text) {
239 var code = '%s\n\n//@ sourceURL=%s/%s'.format(text, window.location.origin, path);
240 var construct = eval(code);
241
242 var parts = name.split(/\./);
243 var cparent = L.Class || (L.Class = { });
244
245 for (var i = 1; i < parts.length - 1; i++)
246 {
247 cparent = cparent[parts[i]];
248
249 if (!cparent)
250 throw "Missing parent class";
251 }
252
253 cparent[parts[i]] = construct;
254 });
255 };
256
257 Class.instantiate = function(name)
258 {
259 Class.require(name).then(function() {
260 var parts = name.split(/\./);
261 var iparent = L;
262 var construct = L.Class;
263
264 for (var i = 1; i < parts.length - 1; i++)
265 {
266 iparent = iparent[parts[i]];
267 construct = construct[parts[i]];
268
269 if (!iparent)
270 throw "Missing parent class";
271 }
272
273 if (construct[parts[i]])
274 iparent[parts[i]] = new construct[parts[i]]();
275 });
276 };
277
278 this.defaults = function(obj, def)
279 {
280 for (var key in def)
281 if (typeof(obj[key]) == 'undefined')
282 obj[key] = def[key];
283
284 return obj;
285 };
286
287 this.isDeferred = function(x)
288 {
289 return (typeof(x) == 'object' &&
290 typeof(x.then) == 'function' &&
291 typeof(x.promise) == 'function');
292 };
293
294 this.deferrable = function()
295 {
296 if (this.isDeferred(arguments[0]))
297 return arguments[0];
298
299 var d = $.Deferred();
300 d.resolve.apply(d, arguments);
301
302 return d.promise();
303 };
304
305 this.i18n = {
306
307 loaded: false,
308 catalog: { },
309 plural: function(n) { return 0 + (n != 1) },
310
311 init: function() {
312 if (L.i18n.loaded)
313 return;
314
315 var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase();
316 var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ];
317
318 for (var i = 0; i < langs.length; i++)
319 $.ajax('%s/i18n/base.%s.json'.format(L.globals.resource, langs[i]), {
320 async: false,
321 cache: true,
322 dataType: 'json',
323 success: function(data) {
324 $.extend(L.i18n.catalog, data);
325
326 var pe = L.i18n.catalog[''];
327 if (pe)
328 {
329 delete L.i18n.catalog[''];
330 try {
331 var pf = new Function('n', 'return 0 + (' + pe + ')');
332 L.i18n.plural = pf;
333 } catch (e) { };
334 }
335 }
336 });
337
338 L.i18n.loaded = true;
339 }
340
341 };
342
343 this.tr = function(msgid)
344 {
345 L.i18n.init();
346
347 var msgstr = L.i18n.catalog[msgid];
348
349 if (typeof(msgstr) == 'undefined')
350 return msgid;
351 else if (typeof(msgstr) == 'string')
352 return msgstr;
353 else
354 return msgstr[0];
355 };
356
357 this.trp = function(msgid, msgid_plural, count)
358 {
359 L.i18n.init();
360
361 var msgstr = L.i18n.catalog[msgid];
362
363 if (typeof(msgstr) == 'undefined')
364 return (count == 1) ? msgid : msgid_plural;
365 else if (typeof(msgstr) == 'string')
366 return msgstr;
367 else
368 return msgstr[L.i18n.plural(count)];
369 };
370
371 this.trc = function(msgctx, msgid)
372 {
373 L.i18n.init();
374
375 var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
376
377 if (typeof(msgstr) == 'undefined')
378 return msgid;
379 else if (typeof(msgstr) == 'string')
380 return msgstr;
381 else
382 return msgstr[0];
383 };
384
385 this.trcp = function(msgctx, msgid, msgid_plural, count)
386 {
387 L.i18n.init();
388
389 var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
390
391 if (typeof(msgstr) == 'undefined')
392 return (count == 1) ? msgid : msgid_plural;
393 else if (typeof(msgstr) == 'string')
394 return msgstr;
395 else
396 return msgstr[L.i18n.plural(count)];
397 };
398
399 this.setHash = function(key, value)
400 {
401 var h = '';
402 var data = this.getHash(undefined);
403
404 if (typeof(value) == 'undefined')
405 delete data[key];
406 else
407 data[key] = value;
408
409 var keys = [ ];
410 for (var k in data)
411 keys.push(k);
412
413 keys.sort();
414
415 for (var i = 0; i < keys.length; i++)
416 {
417 if (i > 0)
418 h += ',';
419
420 h += keys[i] + ':' + data[keys[i]];
421 }
422
423 if (h.length)
424 location.hash = '#' + h;
425 else
426 location.hash = '';
427 };
428
429 this.getHash = function(key)
430 {
431 var data = { };
432 var tuples = (location.hash || '#').substring(1).split(/,/);
433
434 for (var i = 0; i < tuples.length; i++)
435 {
436 var tuple = tuples[i].split(/:/);
437 if (tuple.length == 2)
438 data[tuple[0]] = tuple[1];
439 }
440
441 if (typeof(key) != 'undefined')
442 return data[key];
443
444 return data;
445 };
446
447 this.toArray = function(x)
448 {
449 switch (typeof(x))
450 {
451 case 'number':
452 case 'boolean':
453 return [ x ];
454
455 case 'string':
456 var r = [ ];
457 var l = x.split(/\s+/);
458 for (var i = 0; i < l.length; i++)
459 if (l[i].length > 0)
460 r.push(l[i]);
461 return r;
462
463 case 'object':
464 if ($.isArray(x))
465 {
466 var r = [ ];
467 for (var i = 0; i < x.length; i++)
468 r.push(x[i]);
469 return r;
470 }
471 else if ($.isPlainObject(x))
472 {
473 var r = [ ];
474 for (var k in x)
475 if (x.hasOwnProperty(k))
476 r.push(k);
477 return r.sort();
478 }
479 }
480
481 return [ ];
482 };
483
484 this.toObject = function(x)
485 {
486 switch (typeof(x))
487 {
488 case 'number':
489 case 'boolean':
490 return { x: true };
491
492 case 'string':
493 var r = { };
494 var l = x.split(/\x+/);
495 for (var i = 0; i < l.length; i++)
496 if (l[i].length > 0)
497 r[l[i]] = true;
498 return r;
499
500 case 'object':
501 if ($.isArray(x))
502 {
503 var r = { };
504 for (var i = 0; i < x.length; i++)
505 r[x[i]] = true;
506 return r;
507 }
508 else if ($.isPlainObject(x))
509 {
510 return x;
511 }
512 }
513
514 return { };
515 };
516
517 this.filterArray = function(array, item)
518 {
519 if (!$.isArray(array))
520 return [ ];
521
522 for (var i = 0; i < array.length; i++)
523 if (array[i] === item)
524 array.splice(i--, 1);
525
526 return array;
527 };
528
529 this.toClassName = function(str, suffix)
530 {
531 var n = '';
532 var l = str.split(/[\/.]/);
533
534 for (var i = 0; i < l.length; i++)
535 if (l[i].length > 0)
536 n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase();
537
538 if (typeof(suffix) == 'string')
539 n += suffix;
540
541 return n;
542 };
543
544 this.toColor = function(str)
545 {
546 if (typeof(str) != 'string' || str.length == 0)
547 return '#CCCCCC';
548
549 if (str == 'wan')
550 return '#F09090';
551 else if (str == 'lan')
552 return '#90F090';
553
554 var i = 0, hash = 0;
555
556 while (i < str.length)
557 hash = str.charCodeAt(i++) + ((hash << 5) - hash);
558
559 var r = (hash & 0xFF) % 128;
560 var g = ((hash >> 8) & 0xFF) % 128;
561
562 var min = 0;
563 var max = 128;
564
565 if ((r + g) < 128)
566 min = 128 - r - g;
567 else
568 max = 255 - r - g;
569
570 var b = min + (((hash >> 16) & 0xFF) % (max - min));
571
572 return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
573 };
574
575 this.parseIPv4 = function(str)
576 {
577 if ((typeof(str) != 'string' && !(str instanceof String)) ||
578 !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
579 return undefined;
580
581 var num = [ ];
582 var parts = str.split(/\./);
583
584 for (var i = 0; i < parts.length; i++)
585 {
586 var n = parseInt(parts[i], 10);
587 if (isNaN(n) || n > 255)
588 return undefined;
589
590 num.push(n);
591 }
592
593 return num;
594 };
595
596 this.parseIPv6 = function(str)
597 {
598 if ((typeof(str) != 'string' && !(str instanceof String)) ||
599 !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
600 return undefined;
601
602 var parts = str.split(/::/);
603 if (parts.length == 0 || parts.length > 2)
604 return undefined;
605
606 var lnum = [ ];
607 if (parts[0].length > 0)
608 {
609 var left = parts[0].split(/:/);
610 for (var i = 0; i < left.length; i++)
611 {
612 var n = parseInt(left[i], 16);
613 if (isNaN(n))
614 return undefined;
615
616 lnum.push((n / 256) >> 0);
617 lnum.push(n % 256);
618 }
619 }
620
621 var rnum = [ ];
622 if (parts.length > 1 && parts[1].length > 0)
623 {
624 var right = parts[1].split(/:/);
625
626 for (var i = 0; i < right.length; i++)
627 {
628 if (right[i].indexOf('.') > 0)
629 {
630 var addr = L.parseIPv4(right[i]);
631 if (!addr)
632 return undefined;
633
634 rnum.push.apply(rnum, addr);
635 continue;
636 }
637
638 var n = parseInt(right[i], 16);
639 if (isNaN(n))
640 return undefined;
641
642 rnum.push((n / 256) >> 0);
643 rnum.push(n % 256);
644 }
645 }
646
647 if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
648 return undefined;
649
650 var num = [ ];
651
652 num.push.apply(num, lnum);
653
654 for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
655 num.push(0);
656
657 num.push.apply(num, rnum);
658
659 if (num.length > 16)
660 return undefined;
661
662 return num;
663 };
664
665 this.isNetmask = function(addr)
666 {
667 if (!$.isArray(addr))
668 return false;
669
670 var c;
671
672 for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
673
674 if (c == addr.length)
675 return true;
676
677 if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
678 (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
679 (addr[c] == 128) || (addr[c] == 0))
680 {
681 for (c++; (c < addr.length) && (addr[c] == 0); c++);
682
683 if (c == addr.length)
684 return true;
685 }
686
687 return false;
688 };
689
690 this.globals = {
691 timeout: 15000,
692 resource: '/luci2',
693 sid: '00000000000000000000000000000000'
694 };
695
696 Class.instantiate('luci2.rpc');
697 Class.instantiate('luci2.uci');
698 Class.instantiate('luci2.network');
699 Class.instantiate('luci2.wireless');
700 Class.instantiate('luci2.firewall');
701 Class.instantiate('luci2.system');
702 Class.instantiate('luci2.session');
703 Class.instantiate('luci2.ui');
704 Class.instantiate('luci2.cbi');
705 };