280d476be5d5e502922414a11df4e583ae6918f3
[project/luci.git] / libs / web / htdocs / luci-static / resources / cbi.js
1 /*
2 LuCI - Lua Configuration Interface
3
4 Copyright 2008 Steven Barth <steven@midlink.org>
5 Copyright 2008-2010 Jo-Philipp Wich <xm@subsignal.org>
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12 */
13
14 var cbi_d = [];
15 var cbi_t = [];
16 var cbi_c = [];
17
18 var cbi_validators = {
19
20 'integer': function(v)
21 {
22 return (v.match(/^-?[0-9]+$/) != null);
23 },
24
25 'uinteger': function(v)
26 {
27 return (cbi_validators.integer(v) && (v >= 0));
28 },
29
30 'ipaddr': function(v)
31 {
32 return cbi_validators.ip4addr(v) || cbi_validators.ip6addr(v);
33 },
34
35 'ip4addr': function(v)
36 {
37 if( v.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)(\/(\d+))?$/) )
38 {
39 return (RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
40 (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
41 (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
42 (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
43 (!RegExp.$5 || ((RegExp.$6 >= 0) && (RegExp.$6 <= 32)))
44 ;
45 }
46
47 return false;
48 },
49
50 'ip6addr': function(v)
51 {
52 if( v.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/) )
53 {
54 if( !RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)) )
55 {
56 var addr = RegExp.$1;
57
58 if( addr == '::' )
59 {
60 return true;
61 }
62
63 if( addr.indexOf('.') > 0 )
64 {
65 var off = addr.lastIndexOf(':');
66
67 if( !(off && cbi_validators.ip4addr(addr.substr(off+1))) )
68 return false;
69
70 addr = addr.substr(0, off) + ':0:0';
71 }
72
73 if( addr.indexOf('::') >= 0 )
74 {
75 var colons = 0;
76 var fill = '0';
77
78 for( var i = 1; i < (addr.length-1); i++ )
79 if( addr.charAt(i) == ':' )
80 colons++;
81
82 if( colons > 7 )
83 return false;
84
85 for( var i = 0; i < (7 - colons); i++ )
86 fill += ':0';
87
88 if (addr.match(/^(.*?)::(.*?)$/))
89 addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
90 (RegExp.$2 ? ':' + RegExp.$2 : '');
91 }
92
93 window.status = addr;
94 return (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null);
95 }
96 }
97
98 return false;
99 },
100
101 'port': function(v)
102 {
103 return cbi_validators.integer(v) && (v >= 0) && (v <= 65535);
104 },
105
106 'portrange': function(v)
107 {
108 if( v.match(/^(\d+)-(\d+)$/) )
109 {
110 var p1 = RegExp.$1;
111 var p2 = RegExp.$2;
112
113 return cbi_validators.port(p1) &&
114 cbi_validators.port(p2) &&
115 (parseInt(p1) <= parseInt(p2))
116 ;
117 }
118 else
119 {
120 return cbi_validators.port(v);
121 }
122 },
123
124 'macaddr': function(v)
125 {
126 return (v.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null);
127 },
128
129 'host': function(v)
130 {
131 return cbi_validators.hostname(v) || cbi_validators.ipaddr(v);
132 },
133
134 'hostname': function(v)
135 {
136 return (v.match(/^[a-zA-Z_][a-zA-Z0-9_\-.]*$/) != null);
137 },
138
139 'wpakey': function(v)
140 {
141 if( v.length == 64 )
142 return (v.match(/^[a-fA-F0-9]{64}$/) != null);
143 else
144 return (v.length >= 8) && (v.length <= 63);
145 },
146
147 'wepkey': function(v)
148 {
149 if( v.substr(0,2) == 's:' )
150 v = v.substr(2);
151
152 if( (v.length == 10) || (v.length == 26) )
153 return (v.match(/^[a-fA-F0-9]{10,26}$/) != null);
154 else
155 return (v.length == 5) || (v.length == 13);
156 },
157
158 'uciname': function(v)
159 {
160 return (v.match(/^[a-zA-Z0-9_]+$/) != null);
161 },
162
163 'range': function(v, args)
164 {
165 var min = parseInt(args[0]);
166 var max = parseInt(args[1]);
167 var val = parseInt(v);
168
169 if (!isNaN(min) && !isNaN(max) && !isNaN(val))
170 return ((val >= min) && (val <= max));
171
172 return false;
173 }
174 };
175
176
177 function cbi_d_add(field, dep, next) {
178 var obj = document.getElementById(field);
179 if (obj) {
180 var entry
181 for (var i=0; i<cbi_d.length; i++) {
182 if (cbi_d[i].id == field) {
183 entry = cbi_d[i];
184 break;
185 }
186 }
187 if (!entry) {
188 entry = {
189 "node": obj,
190 "id": field,
191 "parent": obj.parentNode.id,
192 "next": next,
193 "deps": []
194 };
195 cbi_d.unshift(entry);
196 }
197 entry.deps.push(dep)
198 }
199 }
200
201 function cbi_d_checkvalue(target, ref) {
202 var t = document.getElementById(target);
203 var value;
204
205 if (!t) {
206 var tl = document.getElementsByName(target);
207
208 if( tl.length > 0 && tl[0].type == 'radio' )
209 for( var i = 0; i < tl.length; i++ )
210 if( tl[i].checked ) {
211 value = tl[i].value;
212 break;
213 }
214
215 value = value ? value : "";
216 } else if (!t.value) {
217 value = "";
218 } else {
219 value = t.value;
220
221 if (t.type == "checkbox") {
222 value = t.checked ? value : "";
223 }
224 }
225
226 return (value == ref)
227 }
228
229 function cbi_d_check(deps) {
230 var reverse;
231 var def = false;
232 for (var i=0; i<deps.length; i++) {
233 var istat = true;
234 reverse = false;
235 for (var j in deps[i]) {
236 if (j == "!reverse") {
237 reverse = true;
238 } else if (j == "!default") {
239 def = true;
240 istat = false;
241 } else {
242 istat = (istat && cbi_d_checkvalue(j, deps[i][j]))
243 }
244 }
245 if (istat) {
246 return !reverse;
247 }
248 }
249 return def;
250 }
251
252 function cbi_d_update() {
253 var state = false;
254 for (var i=0; i<cbi_d.length; i++) {
255 var entry = cbi_d[i];
256 var next = document.getElementById(entry.next)
257 var node = document.getElementById(entry.id)
258 var parent = document.getElementById(entry.parent)
259
260 if (node && node.parentNode && !cbi_d_check(entry.deps)) {
261 node.parentNode.removeChild(node);
262 state = true;
263 if( entry.parent )
264 cbi_c[entry.parent]--;
265 } else if ((!node || !node.parentNode) && cbi_d_check(entry.deps)) {
266 if (!next) {
267 parent.appendChild(entry.node);
268 } else {
269 next.parentNode.insertBefore(entry.node, next);
270 }
271 state = true;
272 if( entry.parent )
273 cbi_c[entry.parent]++;
274 }
275 }
276
277 if (entry && entry.parent) {
278 cbi_t_update();
279 }
280
281 if (state) {
282 cbi_d_update();
283 }
284 }
285
286 function cbi_bind(obj, type, callback, mode) {
287 if (!obj.addEventListener) {
288 obj.attachEvent('on' + type,
289 function(){
290 var e = window.event;
291
292 if (!e.target && e.srcElement)
293 e.target = e.srcElement;
294
295 return !!callback(e);
296 }
297 );
298 } else {
299 obj.addEventListener(type, callback, !!mode);
300 }
301 return obj;
302 }
303
304 function cbi_combobox(id, values, def, man) {
305 var selid = "cbi.combobox." + id;
306 if (document.getElementById(selid)) {
307 return
308 }
309
310 var obj = document.getElementById(id)
311 var sel = document.createElement("select");
312 sel.id = selid;
313 sel.className = 'cbi-input-select';
314
315 if (obj.nextSibling) {
316 obj.parentNode.insertBefore(sel, obj.nextSibling);
317 } else {
318 obj.parentNode.appendChild(sel);
319 }
320
321 var dt = obj.getAttribute('cbi_datatype');
322 var op = obj.getAttribute('cbi_optional');
323
324 if (dt)
325 cbi_validate_field(sel, op == 'true', dt);
326
327 if (!values[obj.value]) {
328 if (obj.value == "") {
329 var optdef = document.createElement("option");
330 optdef.value = "";
331 optdef.appendChild(document.createTextNode(def));
332 sel.appendChild(optdef);
333 } else {
334 var opt = document.createElement("option");
335 opt.value = obj.value;
336 opt.selected = "selected";
337 opt.appendChild(document.createTextNode(obj.value));
338 sel.appendChild(opt);
339 }
340 }
341
342 for (var i in values) {
343 var opt = document.createElement("option");
344 opt.value = i;
345
346 if (obj.value == i) {
347 opt.selected = "selected";
348 }
349
350 opt.appendChild(document.createTextNode(values[i]));
351 sel.appendChild(opt);
352 }
353
354 var optman = document.createElement("option");
355 optman.value = "";
356 optman.appendChild(document.createTextNode(man));
357 sel.appendChild(optman);
358
359 obj.style.display = "none";
360
361 cbi_bind(sel, "change", function() {
362 if (sel.selectedIndex == sel.options.length - 1) {
363 obj.style.display = "inline";
364 sel.parentNode.removeChild(sel);
365 obj.focus();
366 } else {
367 obj.value = sel.options[sel.selectedIndex].value;
368 }
369
370 try {
371 cbi_d_update();
372 } catch (e) {
373 //Do nothing
374 }
375 })
376 }
377
378 function cbi_combobox_init(id, values, def, man) {
379 var obj = document.getElementById(id);
380 cbi_bind(obj, "blur", function() {
381 cbi_combobox(id, values, def, man)
382 });
383 cbi_combobox(id, values, def, man);
384 }
385
386 function cbi_filebrowser(id, url, defpath) {
387 var field = document.getElementById(id);
388 var browser = window.open(
389 url + ( field.value || defpath || '' ) + '?field=' + id,
390 "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes"
391 );
392
393 browser.focus();
394 }
395
396 function cbi_dynlist_init(name)
397 {
398 function cbi_dynlist_renumber(e)
399 {
400 var count = 1;
401 var childs = e.parentNode.childNodes;
402
403 for( var i = 0; i < childs.length; i++ )
404 if( childs[i].name == name )
405 childs[i].id = name + '.' + (count++);
406
407 e.focus();
408 }
409
410 function cbi_dynlist_keypress(ev)
411 {
412 ev = ev ? ev : window.event;
413
414 var se = ev.target ? ev.target : ev.srcElement;
415
416 if (se.nodeType == 3)
417 se = se.parentNode;
418
419 switch (ev.keyCode)
420 {
421 /* backspace, delete */
422 case 8:
423 case 46:
424 if (se.value.length == 0)
425 {
426 if (ev.preventDefault)
427 ev.preventDefault();
428
429 return false;
430 }
431
432 return true;
433
434 /* enter, arrow up, arrow down */
435 case 13:
436 case 38:
437 case 40:
438 if (ev.preventDefault)
439 ev.preventDefault();
440
441 return false;
442 }
443
444 return true;
445 }
446
447 function cbi_dynlist_keydown(ev)
448 {
449 ev = ev ? ev : window.event;
450
451 var se = ev.target ? ev.target : ev.srcElement;
452
453 if (se.nodeType == 3)
454 se = se.parentNode;
455
456 var prev = se.previousSibling;
457 while (prev && prev.name != name)
458 prev = prev.previousSibling;
459
460 var next = se.nextSibling;
461 while (next && next.name != name)
462 next = next.nextSibling;
463
464 switch (ev.keyCode)
465 {
466 /* backspace, delete */
467 case 8:
468 case 46:
469 var jump = (ev.keyCode == 8)
470 ? (prev || next) : (next || prev);
471
472 if (se.value.length == 0 && jump)
473 {
474 se.parentNode.removeChild(se.nextSibling);
475 se.parentNode.removeChild(se);
476
477 cbi_dynlist_renumber(jump);
478
479 if (ev.preventDefault)
480 ev.preventDefault();
481
482 return false;
483 }
484
485 break;
486
487 /* enter */
488 case 13:
489 var n = document.createElement('input');
490 n.name = se.name;
491 n.type = se.type;
492
493 cbi_bind(n, 'keydown', cbi_dynlist_keydown);
494 cbi_bind(n, 'keypress', cbi_dynlist_keypress);
495
496 if (next)
497 {
498 se.parentNode.insertBefore(n, next);
499 se.parentNode.insertBefore(document.createElement('br'), next);
500 }
501 else
502 {
503 se.parentNode.appendChild(n);
504 se.parentNode.appendChild(document.createElement('br'));
505 }
506
507 var dt = se.getAttribute('cbi_datatype');
508 var op = se.getAttribute('cbi_optional') == 'true';
509
510 if (dt)
511 cbi_validate_field(n, op, dt);
512
513 cbi_dynlist_renumber(n);
514 break;
515
516 /* arrow up */
517 case 38:
518 if (prev)
519 prev.focus();
520
521 break;
522
523 /* arrow down */
524 case 40:
525 if (next)
526 next.focus();
527
528 break;
529 }
530
531 return true;
532 }
533
534 var inputs = document.getElementsByName(name);
535 for( var i = 0; i < inputs.length; i++ )
536 {
537 cbi_bind(inputs[i], 'keydown', cbi_dynlist_keydown);
538 cbi_bind(inputs[i], 'keypress', cbi_dynlist_keypress);
539 }
540 }
541
542 //Hijacks the CBI form to send via XHR (requires Prototype)
543 function cbi_hijack_forms(layer, win, fail, load) {
544 var forms = layer.getElementsByTagName('form');
545 for (var i=0; i<forms.length; i++) {
546 $(forms[i]).observe('submit', function(event) {
547 // Prevent the form from also submitting the regular way
548 event.stop();
549
550 // Submit via XHR
551 event.element().request({
552 onSuccess: win,
553 onFailure: fail
554 });
555
556 if (load) {
557 load();
558 }
559 });
560 }
561 }
562
563
564 function cbi_t_add(section, tab) {
565 var t = document.getElementById('tab.' + section + '.' + tab);
566 var c = document.getElementById('container.' + section + '.' + tab);
567
568 if( t && c ) {
569 cbi_t[section] = (cbi_t[section] || [ ]);
570 cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
571 }
572 }
573
574 function cbi_t_switch(section, tab) {
575 if( cbi_t[section] && cbi_t[section][tab] ) {
576 var o = cbi_t[section][tab];
577 var h = document.getElementById('tab.' + section);
578 for( var tid in cbi_t[section] ) {
579 var o2 = cbi_t[section][tid];
580 if( o.tab.id != o2.tab.id ) {
581 o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
582 o2.container.style.display = 'none';
583 }
584 else {
585 if(h) h.value = tab;
586 o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
587 o2.container.style.display = 'block';
588 }
589 }
590 }
591 return false
592 }
593
594 function cbi_t_update() {
595 var hl_tabs = [ ];
596
597 for( var sid in cbi_t )
598 for( var tid in cbi_t[sid] )
599 if( cbi_c[cbi_t[sid][tid].cid] == 0 ) {
600 cbi_t[sid][tid].tab.style.display = 'none';
601 }
602 else if( cbi_t[sid][tid].tab && cbi_t[sid][tid].tab.style.display == 'none' ) {
603 cbi_t[sid][tid].tab.style.display = '';
604
605 var t = cbi_t[sid][tid].tab;
606 t.className += ' cbi-tab-highlighted';
607 hl_tabs.push(t);
608 }
609
610 if( hl_tabs.length > 0 )
611 window.setTimeout(function() {
612 for( var i = 0; i < hl_tabs.length; i++ )
613 hl_tabs[i].className = hl_tabs[i].className.replace(/ cbi-tab-highlighted/g, '');
614 }, 750);
615 }
616
617
618 function cbi_validate_form(form, errmsg)
619 {
620 /* if triggered by a section removal or addition, don't validate */
621 if( form.cbi_state == 'add-section' || form.cbi_state == 'del-section' )
622 return true;
623
624 if( form.cbi_validators )
625 {
626 for( var i = 0; i < form.cbi_validators.length; i++ )
627 {
628 var validator = form.cbi_validators[i];
629 if( !validator() && errmsg )
630 {
631 alert(errmsg);
632 return false;
633 }
634 }
635 }
636
637 return true;
638 }
639
640 function cbi_validate_reset(form)
641 {
642 window.setTimeout(
643 function() { cbi_validate_form(form, null) }, 100
644 );
645
646 return true;
647 }
648
649 function cbi_validate_field(cbid, optional, type)
650 {
651 var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid;
652 var vargs;
653
654 if( type.match(/^(\w+)\(([^\(\)]+)\)/) )
655 {
656 type = RegExp.$1;
657 vargs = RegExp.$2.split(/\s*,\s*/);
658 }
659
660 var vldcb = cbi_validators[type];
661
662 if( field && vldcb )
663 {
664 var validator = function()
665 {
666 // is not detached
667 if( field.form )
668 {
669 field.className = field.className.replace(/ cbi-input-invalid/g, '');
670
671 // validate value
672 var value = (field.options && field.options.selectedIndex > -1)
673 ? field.options[field.options.selectedIndex].value : field.value;
674
675 if( !(((value.length == 0) && optional) || vldcb(value, vargs)) )
676 {
677 // invalid
678 field.className += ' cbi-input-invalid';
679 return false;
680 }
681 }
682
683 return true;
684 };
685
686 if( ! field.form.cbi_validators )
687 field.form.cbi_validators = [ ];
688
689 field.form.cbi_validators.push(validator);
690
691 cbi_bind(field, "blur", validator);
692 cbi_bind(field, "keyup", validator);
693
694 if (field.nodeName == 'SELECT')
695 {
696 cbi_bind(field, "change", validator);
697 cbi_bind(field, "click", validator);
698 }
699
700 field.setAttribute("cbi_validate", validator);
701 field.setAttribute("cbi_datatype", type);
702 field.setAttribute("cbi_optional", (!!optional).toString());
703
704 validator();
705
706 var fcbox = document.getElementById('cbi.combobox.' + field.id);
707 if (fcbox)
708 cbi_validate_field(fcbox, optional, type);
709 }
710 }
711
712 if( ! String.serialize )
713 String.serialize = function(o)
714 {
715 switch(typeof(o))
716 {
717 case 'object':
718 // null
719 if( o == null )
720 {
721 return 'null';
722 }
723
724 // array
725 else if( o.length )
726 {
727 var i, s = '';
728
729 for( var i = 0; i < o.length; i++ )
730 s += (s ? ', ' : '') + String.serialize(o[i]);
731
732 return '[ ' + s + ' ]';
733 }
734
735 // object
736 else
737 {
738 var k, s = '';
739
740 for( k in o )
741 s += (s ? ', ' : '') + k + ': ' + String.serialize(o[k]);
742
743 return '{ ' + s + ' }';
744 }
745
746 break;
747
748 case 'string':
749 // complex string
750 if( o.match(/[^a-zA-Z0-9_,.: -]/) )
751 return 'decodeURIComponent("' + encodeURIComponent(o) + '")';
752
753 // simple string
754 else
755 return '"' + o + '"';
756
757 break;
758
759 default:
760 return o.toString();
761 }
762 }
763
764
765 if( ! String.format )
766 String.format = function()
767 {
768 if (!arguments || arguments.length < 1 || !RegExp)
769 return;
770
771 var html_esc = [/&/g, '&#38;', /"/g, '&#34;', /'/g, '&#39;', /</g, '&#60;', />/g, '&#62;'];
772 var quot_esc = [/"/g, '&#34;', /'/g, '&#39;'];
773
774 function esc(s, r) {
775 for( var i = 0; i < r.length; i += 2 )
776 s = s.replace(r[i], r[i+1]);
777 return s;
778 }
779
780 var str = arguments[0];
781 var out = '';
782 var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t))/;
783 var a = b = [], numSubstitutions = 0, numMatches = 0;
784
785 while( a = re.exec(str) )
786 {
787 var m = a[1];
788 var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5];
789 var pPrecision = a[6], pType = a[7];
790
791 numMatches++;
792
793 if (pType == '%')
794 {
795 subst = '%';
796 }
797 else
798 {
799 if (numSubstitutions++ < arguments.length)
800 {
801 var param = arguments[numSubstitutions];
802
803 var pad = '';
804 if (pPad && pPad.substr(0,1) == "'")
805 pad = leftpart.substr(1,1);
806 else if (pPad)
807 pad = pPad;
808
809 var justifyRight = true;
810 if (pJustify && pJustify === "-")
811 justifyRight = false;
812
813 var minLength = -1;
814 if (pMinLength)
815 minLength = parseInt(pMinLength);
816
817 var precision = -1;
818 if (pPrecision && pType == 'f')
819 precision = parseInt(pPrecision.substring(1));
820
821 var subst = param;
822
823 switch(pType)
824 {
825 case 'b':
826 subst = (parseInt(param) || 0).toString(2);
827 break;
828
829 case 'c':
830 subst = String.fromCharCode(parseInt(param) || 0);
831 break;
832
833 case 'd':
834 subst = (parseInt(param) || 0);
835 break;
836
837 case 'u':
838 subst = Math.abs(parseInt(param) || 0);
839 break;
840
841 case 'f':
842 subst = (precision > -1)
843 ? ((parseFloat(param) || 0.0)).toFixed(precision)
844 : (parseFloat(param) || 0.0);
845 break;
846
847 case 'o':
848 subst = (parseInt(param) || 0).toString(8);
849 break;
850
851 case 's':
852 subst = param;
853 break;
854
855 case 'x':
856 subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase();
857 break;
858
859 case 'X':
860 subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase();
861 break;
862
863 case 'h':
864 subst = esc(param, html_esc);
865 break;
866
867 case 'q':
868 subst = esc(param, quot_esc);
869 break;
870
871 case 'j':
872 subst = String.serialize(param);
873 break;
874
875 case 't':
876 var td = 0;
877 var th = 0;
878 var tm = 0;
879 var ts = (param || 0);
880
881 if (ts > 60) {
882 tm = Math.floor(ts / 60);
883 ts = (ts % 60);
884 }
885
886 if (tm > 60) {
887 th = Math.floor(tm / 60);
888 tm = (tm % 60);
889 }
890
891 if (th > 24) {
892 td = Math.floor(th / 24);
893 th = (th % 24);
894 }
895
896 subst = (td > 0)
897 ? String.format('%dd %dh %dm %ds', td, th, tm, ts)
898 : String.format('%dh %dm %ds', th, tm, ts);
899
900 break;
901 }
902 }
903 }
904
905 out += leftpart + subst;
906 str = str.substr(m.length);
907 }
908
909 return out + str;
910 }