luci-base: Add time and data datatypes for use with firewall app
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / cbi.js
1 /*
2         LuCI - Lua Configuration Interface
3
4         Copyright 2008 Steven Barth <steven@midlink.org>
5         Copyright 2008-2012 Jo-Philipp Wich <jow@openwrt.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()
21         {
22                 return (this.match(/^-?[0-9]+$/) != null);
23         },
24
25         'uinteger': function()
26         {
27                 return (cbi_validators.integer.apply(this) && (this >= 0));
28         },
29
30         'float': function()
31         {
32                 return !isNaN(parseFloat(this));
33         },
34
35         'ufloat': function()
36         {
37                 return (cbi_validators['float'].apply(this) && (this >= 0));
38         },
39
40         'ipaddr': function()
41         {
42                 return cbi_validators.ip4addr.apply(this) ||
43                         cbi_validators.ip6addr.apply(this);
44         },
45
46         'ip4addr': function()
47         {
48                 if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
49                 {
50                         return (RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
51                                (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
52                                (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
53                                (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
54                                ((RegExp.$6.indexOf('.') < 0)
55                                   ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
56                                   : (cbi_validators.ip4addr.apply(RegExp.$6)))
57                         ;
58                 }
59
60                 return false;
61         },
62
63         'ip6addr': function()
64         {
65                 if( this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/) )
66                 {
67                         if( !RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)) )
68                         {
69                                 var addr = RegExp.$1;
70
71                                 if( addr == '::' )
72                                 {
73                                         return true;
74                                 }
75
76                                 if( addr.indexOf('.') > 0 )
77                                 {
78                                         var off = addr.lastIndexOf(':');
79
80                                         if( !(off && cbi_validators.ip4addr.apply(addr.substr(off+1))) )
81                                                 return false;
82
83                                         addr = addr.substr(0, off) + ':0:0';
84                                 }
85
86                                 if( addr.indexOf('::') >= 0 )
87                                 {
88                                         var colons = 0;
89                                         var fill = '0';
90
91                                         for( var i = 1; i < (addr.length-1); i++ )
92                                                 if( addr.charAt(i) == ':' )
93                                                         colons++;
94
95                                         if( colons > 7 )
96                                                 return false;
97
98                                         for( var i = 0; i < (7 - colons); i++ )
99                                                 fill += ':0';
100
101                                         if (addr.match(/^(.*?)::(.*?)$/))
102                                                 addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
103                                                        (RegExp.$2 ? ':' + RegExp.$2 : '');
104                                 }
105
106                                 return (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null);
107                         }
108                 }
109
110                 return false;
111         },
112
113         'port': function()
114         {
115                 return cbi_validators.integer.apply(this) &&
116                         (this >= 0) && (this <= 65535);
117         },
118
119         'portrange': function()
120         {
121                 if (this.match(/^(\d+)-(\d+)$/))
122                 {
123                         var p1 = RegExp.$1;
124                         var p2 = RegExp.$2;
125
126                         return cbi_validators.port.apply(p1) &&
127                                cbi_validators.port.apply(p2) &&
128                                (parseInt(p1) <= parseInt(p2))
129                         ;
130                 }
131                 else
132                 {
133                         return cbi_validators.port.apply(this);
134                 }
135         },
136
137         'macaddr': function()
138         {
139                 return (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null);
140         },
141
142         'host': function()
143         {
144                 return cbi_validators.hostname.apply(this) ||
145                         cbi_validators.ipaddr.apply(this);
146         },
147
148         'hostname': function()
149         {
150                 if (this.length <= 253)
151                         return (this.match(/^[a-zA-Z0-9]+$/) != null ||
152                                 (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
153                                  this.match(/[^0-9.]/)));
154
155                 return false;
156         },
157
158         'network': function()
159         {
160                 return cbi_validators.uciname.apply(this) ||
161                         cbi_validators.host.apply(this);
162         },
163
164         'hostport': function()
165         {
166                 var hp = this.split(/:/);
167
168                 if (hp.length == 2)
169                         return (cbi_validators.host.apply(hp[0]) &&
170                                 cbi_validators.port.apply(hp[1]));
171
172                 return false;
173         },
174
175         'ipaddrport': function()
176         {
177                 var hp = this.split(/:/);
178
179                 if (hp.length == 2)
180                         return (cbi_validators.ipaddr.apply(hp[0]) &&
181                                 cbi_validators.port.apply(hp[1]));
182
183                 return false;
184         },
185
186         'wpakey': function()
187         {
188                 var v = this;
189
190                 if( v.length == 64 )
191                         return (v.match(/^[a-fA-F0-9]{64}$/) != null);
192                 else
193                         return (v.length >= 8) && (v.length <= 63);
194         },
195
196         'wepkey': function()
197         {
198                 var v = this;
199
200                 if ( v.substr(0,2) == 's:' )
201                         v = v.substr(2);
202
203                 if( (v.length == 10) || (v.length == 26) )
204                         return (v.match(/^[a-fA-F0-9]{10,26}$/) != null);
205                 else
206                         return (v.length == 5) || (v.length == 13);
207         },
208
209         'uciname': function()
210         {
211                 return (this.match(/^[a-zA-Z0-9_]+$/) != null);
212         },
213
214         'range': function(min, max)
215         {
216                 var val = parseFloat(this);
217                 if (!isNaN(min) && !isNaN(max) && !isNaN(val))
218                         return ((val >= min) && (val <= max));
219
220                 return false;
221         },
222
223         'min': function(min)
224         {
225                 var val = parseFloat(this);
226                 if (!isNaN(min) && !isNaN(val))
227                         return (val >= min);
228
229                 return false;
230         },
231
232         'max': function(max)
233         {
234                 var val = parseFloat(this);
235                 if (!isNaN(max) && !isNaN(val))
236                         return (val <= max);
237
238                 return false;
239         },
240
241         'rangelength': function(min, max)
242         {
243                 var val = '' + this;
244                 if (!isNaN(min) && !isNaN(max))
245                         return ((val.length >= min) && (val.length <= max));
246
247                 return false;
248         },
249
250         'minlength': function(min)
251         {
252                 var val = '' + this;
253                 if (!isNaN(min))
254                         return (val.length >= min);
255
256                 return false;
257         },
258
259         'maxlength': function(max)
260         {
261                 var val = '' + this;
262                 if (!isNaN(max))
263                         return (val.length <= max);
264
265                 return false;
266         },
267
268         'or': function()
269         {
270                 for (var i = 0; i < arguments.length; i += 2)
271                 {
272                         if (typeof arguments[i] != 'function')
273                         {
274                                 if (arguments[i] == this)
275                                         return true;
276                                 i--;
277                         }
278                         else if (arguments[i].apply(this, arguments[i+1]))
279                         {
280                                 return true;
281                         }
282                 }
283                 return false;
284         },
285
286         'and': function()
287         {
288                 for (var i = 0; i < arguments.length; i += 2)
289                 {
290                         if (typeof arguments[i] != 'function')
291                         {
292                                 if (arguments[i] != this)
293                                         return false;
294                                 i--;
295                         }
296                         else if (!arguments[i].apply(this, arguments[i+1]))
297                         {
298                                 return false;
299                         }
300                 }
301                 return true;
302         },
303
304         'neg': function()
305         {
306                 return cbi_validators.or.apply(
307                         this.replace(/^[ \t]*![ \t]*/, ''), arguments);
308         },
309
310         'list': function(subvalidator, subargs)
311         {
312                 if (typeof subvalidator != 'function')
313                         return false;
314
315                 var tokens = this.match(/[^ \t]+/g);
316                 for (var i = 0; i < tokens.length; i++)
317                         if (!subvalidator.apply(tokens[i], subargs))
318                                 return false;
319
320                 return true;
321         },
322         'phonedigit': function()
323         {
324                 return (this.match(/^[0-9\*#!\.]+$/) != null);
325         },
326         'timehhmmss': function()
327         {
328                 return (this.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/) != null);
329         },
330         'dateyyyymmdd': function()
331         {
332                 if (this == null) {
333                         return false;
334                 }
335                 if (this.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
336                         var year = RegExp.$1;
337                         var month = RegExp.$2;
338                         var day = RegExp.$2
339
340                         var days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30 , 31, 30, 31 ];
341                         function is_leap_year(year) {
342                                 return ((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0);
343                         }
344                         function get_days_in_month(month, year) {
345                                 if ((month == 2) && is_leap_year(year)) {
346                                         return 29;
347                                 } else {
348                                         return days_in_month[month];
349                                 }
350                         }
351                         /* Firewall rules in the past don't make sense */
352                         if (year < 2015) {
353                                 return false;
354                         }
355                         if ((month <= 0) || (month > 12)) {
356                                 return false;
357                         }
358                         if ((day <= 0) || (day > get_days_in_month(month, year))) {
359                                 return false;
360                         }
361                         return true;
362
363                 } else {
364                         return false;
365                 }
366         }
367 };
368
369
370 function cbi_d_add(field, dep, next) {
371         var obj = document.getElementById(field);
372         if (obj) {
373                 var entry
374                 for (var i=0; i<cbi_d.length; i++) {
375                         if (cbi_d[i].id == field) {
376                                 entry = cbi_d[i];
377                                 break;
378                         }
379                 }
380                 if (!entry) {
381                         entry = {
382                                 "node": obj,
383                                 "id": field,
384                                 "parent": obj.parentNode.id,
385                                 "next": next,
386                                 "deps": []
387                         };
388                         cbi_d.unshift(entry);
389                 }
390                 entry.deps.push(dep)
391         }
392 }
393
394 function cbi_d_checkvalue(target, ref) {
395         var t = document.getElementById(target);
396         var value;
397
398         if (!t) {
399                 var tl = document.getElementsByName(target);
400
401                 if( tl.length > 0 && (tl[0].type == 'radio' || tl[0].type == 'checkbox'))
402                         for( var i = 0; i < tl.length; i++ )
403                                 if( tl[i].checked ) {
404                                         value = tl[i].value;
405                                         break;
406                                 }
407
408                 value = value ? value : "";
409         } else if (!t.value) {
410                 value = "";
411         } else {
412                 value = t.value;
413
414                 if (t.type == "checkbox") {
415                         value = t.checked ? value : "";
416                 }
417         }
418
419         return (value == ref)
420 }
421
422 function cbi_d_check(deps) {
423         var reverse;
424         var def = false;
425         for (var i=0; i<deps.length; i++) {
426                 var istat = true;
427                 reverse = false;
428                 for (var j in deps[i]) {
429                         if (j == "!reverse") {
430                                 reverse = true;
431                         } else if (j == "!default") {
432                                 def = true;
433                                 istat = false;
434                         } else {
435                                 istat = (istat && cbi_d_checkvalue(j, deps[i][j]))
436                         }
437                 }
438                 if (istat) {
439                         return !reverse;
440                 }
441         }
442         return def;
443 }
444
445 function cbi_d_update() {
446         var state = false;
447         for (var i=0; i<cbi_d.length; i++) {
448                 var entry = cbi_d[i];
449                 var next  = document.getElementById(entry.next)
450                 var node  = document.getElementById(entry.id)
451                 var parent = document.getElementById(entry.parent)
452
453                 if (node && node.parentNode && !cbi_d_check(entry.deps)) {
454                         node.parentNode.removeChild(node);
455                         state = true;
456                         if( entry.parent )
457                                 cbi_c[entry.parent]--;
458                 } else if ((!node || !node.parentNode) && cbi_d_check(entry.deps)) {
459                         if (!next) {
460                                 parent.appendChild(entry.node);
461                         } else {
462                                 next.parentNode.insertBefore(entry.node, next);
463                         }
464                         state = true;
465                         if( entry.parent )
466                                 cbi_c[entry.parent]++;
467                 }
468         }
469
470         if (entry && entry.parent) {
471                 if (!cbi_t_update())
472                         cbi_tag_last(parent);
473         }
474
475         if (state) {
476                 cbi_d_update();
477         }
478 }
479
480 function cbi_bind(obj, type, callback, mode) {
481         if (!obj.addEventListener) {
482                 obj.attachEvent('on' + type,
483                         function(){
484                                 var e = window.event;
485
486                                 if (!e.target && e.srcElement)
487                                         e.target = e.srcElement;
488
489                                 return !!callback(e);
490                         }
491                 );
492         } else {
493                 obj.addEventListener(type, callback, !!mode);
494         }
495         return obj;
496 }
497
498 function cbi_combobox(id, values, def, man) {
499         var selid = "cbi.combobox." + id;
500         if (document.getElementById(selid)) {
501                 return
502         }
503
504         var obj = document.getElementById(id)
505         var sel = document.createElement("select");
506                 sel.id = selid;
507                 sel.className = obj.className.replace(/cbi-input-text/, 'cbi-input-select');
508
509         if (obj.nextSibling) {
510                 obj.parentNode.insertBefore(sel, obj.nextSibling);
511         } else {
512                 obj.parentNode.appendChild(sel);
513         }
514
515         var dt = obj.getAttribute('cbi_datatype');
516         var op = obj.getAttribute('cbi_optional');
517
518         if (dt)
519                 cbi_validate_field(sel, op == 'true', dt);
520
521         if (!values[obj.value]) {
522                 if (obj.value == "") {
523                         var optdef = document.createElement("option");
524                         optdef.value = "";
525                         optdef.appendChild(document.createTextNode(def));
526                         sel.appendChild(optdef);
527                 } else {
528                         var opt = document.createElement("option");
529                         opt.value = obj.value;
530                         opt.selected = "selected";
531                         opt.appendChild(document.createTextNode(obj.value));
532                         sel.appendChild(opt);
533                 }
534         }
535
536         for (var i in values) {
537                 var opt = document.createElement("option");
538                 opt.value = i;
539
540                 if (obj.value == i) {
541                         opt.selected = "selected";
542                 }
543
544                 opt.appendChild(document.createTextNode(values[i]));
545                 sel.appendChild(opt);
546         }
547
548         var optman = document.createElement("option");
549         optman.value = "";
550         optman.appendChild(document.createTextNode(man));
551         sel.appendChild(optman);
552
553         obj.style.display = "none";
554
555         cbi_bind(sel, "change", function() {
556                 if (sel.selectedIndex == sel.options.length - 1) {
557                         obj.style.display = "inline";
558                         sel.parentNode.removeChild(sel);
559                         obj.focus();
560                 } else {
561                         obj.value = sel.options[sel.selectedIndex].value;
562                 }
563
564                 try {
565                         cbi_d_update();
566                 } catch (e) {
567                         //Do nothing
568                 }
569         })
570
571         // Retrigger validation in select
572         sel.focus();
573         sel.blur();
574 }
575
576 function cbi_combobox_init(id, values, def, man) {
577         var obj = document.getElementById(id);
578         cbi_bind(obj, "blur", function() {
579                 cbi_combobox(id, values, def, man)
580         });
581         cbi_combobox(id, values, def, man);
582 }
583
584 function cbi_filebrowser(id, url, defpath) {
585         var field   = document.getElementById(id);
586         var browser = window.open(
587                 url + ( field.value || defpath || '' ) + '?field=' + id,
588                 "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes"
589         );
590
591         browser.focus();
592 }
593
594 function cbi_browser_init(id, respath, url, defpath)
595 {
596         function cbi_browser_btnclick(e) {
597                 cbi_filebrowser(id, url, defpath);
598                 return false;
599         }
600
601         var field = document.getElementById(id);
602
603         var btn = document.createElement('img');
604         btn.className = 'cbi-image-button';
605         btn.src = respath + '/cbi/folder.gif';
606         field.parentNode.insertBefore(btn, field.nextSibling);
607
608         cbi_bind(btn, 'click', cbi_browser_btnclick);
609 }
610
611 function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choices)
612 {
613         var input0 = document.getElementsByName(name)[0];
614         var prefix = input0.name;
615         var parent = input0.parentNode;
616         var holder = input0.placeholder;
617
618         var values;
619
620         function cbi_dynlist_redraw(focus, add, del)
621         {
622                 values = [ ];
623
624                 while (parent.firstChild)
625                 {
626                         var n = parent.firstChild;
627                         var i = parseInt(n.index);
628
629                         if (i != del)
630                         {
631                                 if (n.nodeName.toLowerCase() == 'input')
632                                         values.push(n.value || '');
633                                 else if (n.nodeName.toLowerCase() == 'select')
634                                         values[values.length-1] = n.options[n.selectedIndex].value;
635                         }
636
637                         parent.removeChild(n);
638                 }
639
640                 if (add >= 0)
641                 {
642                         focus = add+1;
643                         values.splice(focus, 0, '');
644                 }
645                 else if (values.length == 0)
646                 {
647                         focus = 0;
648                         values.push('');
649                 }
650
651                 for (var i = 0; i < values.length; i++)
652                 {
653                         var t = document.createElement('input');
654                                 t.id = prefix + '.' + (i+1);
655                                 t.name = prefix;
656                                 t.value = values[i];
657                                 t.type = 'text';
658                                 t.index = i;
659                                 t.className = 'cbi-input-text';
660
661                         if (i == 0 && holder)
662                         {
663                                 t.placeholder = holder;
664                         }
665
666                         var b = document.createElement('img');
667                                 b.src = respath + ((i+1) < values.length ? '/cbi/remove.gif' : '/cbi/add.gif');
668                                 b.className = 'cbi-image-button';
669
670                         parent.appendChild(t);
671                         parent.appendChild(b);
672                         if (datatype == 'file')
673                         {
674                                 cbi_browser_init(t.id, respath, url, defpath);
675                         }
676
677                         parent.appendChild(document.createElement('br'));
678
679                         if (datatype)
680                         {
681                                 cbi_validate_field(t.id, ((i+1) == values.length) || optional, datatype);
682                         }
683
684                         if (choices)
685                         {
686                                 cbi_combobox_init(t.id, choices[0], '', choices[1]);
687                                 b.index = i;
688
689                                 cbi_bind(b, 'keydown',  cbi_dynlist_keydown);
690                                 cbi_bind(b, 'keypress', cbi_dynlist_keypress);
691
692                                 if (i == focus || -i == focus)
693                                         b.focus();
694                         }
695                         else
696                         {
697                                 cbi_bind(t, 'keydown',  cbi_dynlist_keydown);
698                                 cbi_bind(t, 'keypress', cbi_dynlist_keypress);
699
700                                 if (i == focus)
701                                 {
702                                         t.focus();
703                                 }
704                                 else if (-i == focus)
705                                 {
706                                         t.focus();
707
708                                         /* force cursor to end */
709                                         var v = t.value;
710                                         t.value = ' '
711                                         t.value = v;
712                                 }
713                         }
714
715                         cbi_bind(b, 'click', cbi_dynlist_btnclick);
716                 }
717         }
718
719         function cbi_dynlist_keypress(ev)
720         {
721                 ev = ev ? ev : window.event;
722
723                 var se = ev.target ? ev.target : ev.srcElement;
724
725                 if (se.nodeType == 3)
726                         se = se.parentNode;
727
728                 switch (ev.keyCode)
729                 {
730                         /* backspace, delete */
731                         case 8:
732                         case 46:
733                                 if (se.value.length == 0)
734                                 {
735                                         if (ev.preventDefault)
736                                                 ev.preventDefault();
737
738                                         return false;
739                                 }
740
741                                 return true;
742
743                         /* enter, arrow up, arrow down */
744                         case 13:
745                         case 38:
746                         case 40:
747                                 if (ev.preventDefault)
748                                         ev.preventDefault();
749
750                                 return false;
751                 }
752
753                 return true;
754         }
755
756         function cbi_dynlist_keydown(ev)
757         {
758                 ev = ev ? ev : window.event;
759
760                 var se = ev.target ? ev.target : ev.srcElement;
761
762                 if (se.nodeType == 3)
763                         se = se.parentNode;
764
765                 var prev = se.previousSibling;
766                 while (prev && prev.name != name)
767                         prev = prev.previousSibling;
768
769                 var next = se.nextSibling;
770                 while (next && next.name != name)
771                         next = next.nextSibling;
772
773                 /* advance one further in combobox case */
774                 if (next && next.nextSibling.name == name)
775                         next = next.nextSibling;
776
777                 switch (ev.keyCode)
778                 {
779                         /* backspace, delete */
780                         case 8:
781                         case 46:
782                                 var del = (se.nodeName.toLowerCase() == 'select')
783                                         ? true : (se.value.length == 0);
784
785                                 if (del)
786                                 {
787                                         if (ev.preventDefault)
788                                                 ev.preventDefault();
789
790                                         var focus = se.index;
791                                         if (ev.keyCode == 8)
792                                                 focus = -focus+1;
793
794                                         cbi_dynlist_redraw(focus, -1, se.index);
795
796                                         return false;
797                                 }
798
799                                 break;
800
801                         /* enter */
802                         case 13:
803                                 cbi_dynlist_redraw(-1, se.index, -1);
804                                 break;
805
806                         /* arrow up */
807                         case 38:
808                                 if (prev)
809                                         prev.focus();
810
811                                 break;
812
813                         /* arrow down */
814                         case 40:
815                                 if (next)
816                                         next.focus();
817
818                                 break;
819                 }
820
821                 return true;
822         }
823
824         function cbi_dynlist_btnclick(ev)
825         {
826                 ev = ev ? ev : window.event;
827
828                 var se = ev.target ? ev.target : ev.srcElement;
829                 var input = se.previousSibling;
830                 while (input && input.name != name) {
831                         input = input.previousSibling;
832                 }
833
834                 if (se.src.indexOf('remove') > -1)
835                 {
836                         input.value = '';
837
838                         cbi_dynlist_keydown({
839                                 target:  input,
840                                 keyCode: 8
841                         });
842                 }
843                 else
844                 {
845                         cbi_dynlist_keydown({
846                                 target:  input,
847                                 keyCode: 13
848                         });
849                 }
850
851                 return false;
852         }
853
854         cbi_dynlist_redraw(NaN, -1, -1);
855 }
856
857 //Hijacks the CBI form to send via XHR (requires Prototype)
858 function cbi_hijack_forms(layer, win, fail, load) {
859         var forms = layer.getElementsByTagName('form');
860         for (var i=0; i<forms.length; i++) {
861                 $(forms[i]).observe('submit', function(event) {
862                         // Prevent the form from also submitting the regular way
863                         event.stop();
864
865                         // Submit via XHR
866                         event.element().request({
867                                 onSuccess: win,
868                                 onFailure: fail
869                         });
870
871                         if (load) {
872                                 load();
873                         }
874                 });
875         }
876 }
877
878
879 function cbi_t_add(section, tab) {
880         var t = document.getElementById('tab.' + section + '.' + tab);
881         var c = document.getElementById('container.' + section + '.' + tab);
882
883         if( t && c ) {
884                 cbi_t[section] = (cbi_t[section] || [ ]);
885                 cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
886         }
887 }
888
889 function cbi_t_switch(section, tab) {
890         if( cbi_t[section] && cbi_t[section][tab] ) {
891                 var o = cbi_t[section][tab];
892                 var h = document.getElementById('tab.' + section);
893                 for( var tid in cbi_t[section] ) {
894                         var o2 = cbi_t[section][tid];
895                         if( o.tab.id != o2.tab.id ) {
896                                 o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
897                                 o2.container.style.display = 'none';
898                         }
899                         else {
900                                 if(h) h.value = tab;
901                                 o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
902                                 o2.container.style.display = 'block';
903                         }
904                 }
905         }
906         return false
907 }
908
909 function cbi_t_update() {
910         var hl_tabs = [ ];
911         var updated = false;
912
913         for( var sid in cbi_t )
914                 for( var tid in cbi_t[sid] )
915                 {
916                         if( cbi_c[cbi_t[sid][tid].cid] == 0 ) {
917                                 cbi_t[sid][tid].tab.style.display = 'none';
918                         }
919                         else if( cbi_t[sid][tid].tab && cbi_t[sid][tid].tab.style.display == 'none' ) {
920                                 cbi_t[sid][tid].tab.style.display = '';
921
922                                 var t = cbi_t[sid][tid].tab;
923                                 t.className += ' cbi-tab-highlighted';
924                                 hl_tabs.push(t);
925                         }
926
927                         cbi_tag_last(cbi_t[sid][tid].container);
928                         updated = true;
929                 }
930
931         if( hl_tabs.length > 0 )
932                 window.setTimeout(function() {
933                         for( var i = 0; i < hl_tabs.length; i++ )
934                                 hl_tabs[i].className = hl_tabs[i].className.replace(/ cbi-tab-highlighted/g, '');
935                 }, 750);
936
937         return updated;
938 }
939
940
941 function cbi_validate_form(form, errmsg)
942 {
943         /* if triggered by a section removal or addition, don't validate */
944         if( form.cbi_state == 'add-section' || form.cbi_state == 'del-section' )
945                 return true;
946
947         if( form.cbi_validators )
948         {
949                 for( var i = 0; i < form.cbi_validators.length; i++ )
950                 {
951                         var validator = form.cbi_validators[i];
952                         if( !validator() && errmsg )
953                         {
954                                 alert(errmsg);
955                                 return false;
956                         }
957                 }
958         }
959
960         return true;
961 }
962
963 function cbi_validate_reset(form)
964 {
965         window.setTimeout(
966                 function() { cbi_validate_form(form, null) }, 100
967         );
968
969         return true;
970 }
971
972 function cbi_validate_compile(code)
973 {
974         var pos = 0;
975         var esc = false;
976         var depth = 0;
977         var stack = [ ];
978
979         code += ',';
980
981         for (var i = 0; i < code.length; i++)
982         {
983                 if (esc)
984                 {
985                         esc = false;
986                         continue;
987                 }
988
989                 switch (code.charCodeAt(i))
990                 {
991                 case 92:
992                         esc = true;
993                         break;
994
995                 case 40:
996                 case 44:
997                         if (depth <= 0)
998                         {
999                                 if (pos < i)
1000                                 {
1001                                         var label = code.substring(pos, i);
1002                                                 label = label.replace(/\\(.)/g, '$1');
1003                                                 label = label.replace(/^[ \t]+/g, '');
1004                                                 label = label.replace(/[ \t]+$/g, '');
1005
1006                                         if (label && !isNaN(label))
1007                                         {
1008                                                 stack.push(parseFloat(label));
1009                                         }
1010                                         else if (label.match(/^(['"]).*\1$/))
1011                                         {
1012                                                 stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
1013                                         }
1014                                         else if (typeof cbi_validators[label] == 'function')
1015                                         {
1016                                                 stack.push(cbi_validators[label]);
1017                                                 stack.push(null);
1018                                         }
1019                                         else
1020                                         {
1021                                                 throw "Syntax error, unhandled token '"+label+"'";
1022                                         }
1023                                 }
1024                                 pos = i+1;
1025                         }
1026                         depth += (code.charCodeAt(i) == 40);
1027                         break;
1028
1029                 case 41:
1030                         if (--depth <= 0)
1031                         {
1032                                 if (typeof stack[stack.length-2] != 'function')
1033                                         throw "Syntax error, argument list follows non-function";
1034
1035                                 stack[stack.length-1] =
1036                                         arguments.callee(code.substring(pos, i));
1037
1038                                 pos = i+1;
1039                         }
1040                         break;
1041                 }
1042         }
1043
1044         return stack;
1045 }
1046
1047 function cbi_validate_field(cbid, optional, type)
1048 {
1049         var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid;
1050         var vstack; try { vstack = cbi_validate_compile(type); } catch(e) { };
1051
1052         if (field && vstack && typeof vstack[0] == "function")
1053         {
1054                 var validator = function()
1055                 {
1056                         // is not detached
1057                         if( field.form )
1058                         {
1059                                 field.className = field.className.replace(/ cbi-input-invalid/g, '');
1060
1061                                 // validate value
1062                                 var value = (field.options && field.options.selectedIndex > -1)
1063                                         ? field.options[field.options.selectedIndex].value : field.value;
1064
1065                                 if (!(((value.length == 0) && optional) || vstack[0].apply(value, vstack[1])))
1066                                 {
1067                                         // invalid
1068                                         field.className += ' cbi-input-invalid';
1069                                         return false;
1070                                 }
1071                         }
1072
1073                         return true;
1074                 };
1075
1076                 if( ! field.form.cbi_validators )
1077                         field.form.cbi_validators = [ ];
1078
1079                 field.form.cbi_validators.push(validator);
1080
1081                 cbi_bind(field, "blur",  validator);
1082                 cbi_bind(field, "keyup", validator);
1083
1084                 if (field.nodeName == 'SELECT')
1085                 {
1086                         cbi_bind(field, "change", validator);
1087                         cbi_bind(field, "click",  validator);
1088                 }
1089
1090                 field.setAttribute("cbi_validate", validator);
1091                 field.setAttribute("cbi_datatype", type);
1092                 field.setAttribute("cbi_optional", (!!optional).toString());
1093
1094                 validator();
1095
1096                 var fcbox = document.getElementById('cbi.combobox.' + field.id);
1097                 if (fcbox)
1098                         cbi_validate_field(fcbox, optional, type);
1099         }
1100 }
1101
1102 function cbi_row_swap(elem, up, store)
1103 {
1104         var tr = elem.parentNode;
1105         while (tr && tr.nodeName.toLowerCase() != 'tr')
1106                 tr = tr.parentNode;
1107
1108         if (!tr)
1109                 return false;
1110
1111         var table = tr.parentNode;
1112         while (table && table.nodeName.toLowerCase() != 'table')
1113                 table = table.parentNode;
1114
1115         if (!table)
1116                 return false;
1117
1118         var s = up ? 3 : 2;
1119         var e = up ? table.rows.length : table.rows.length - 1;
1120
1121         for (var idx = s; idx < e; idx++)
1122         {
1123                 if (table.rows[idx] == tr)
1124                 {
1125                         if (up)
1126                                 tr.parentNode.insertBefore(table.rows[idx], table.rows[idx-1]);
1127                         else
1128                                 tr.parentNode.insertBefore(table.rows[idx+1], table.rows[idx]);
1129
1130                         break;
1131                 }
1132         }
1133
1134         var ids = [ ];
1135         for (idx = 2; idx < table.rows.length; idx++)
1136         {
1137                 table.rows[idx].className = table.rows[idx].className.replace(
1138                         /cbi-rowstyle-[12]/, 'cbi-rowstyle-' + (1 + (idx % 2))
1139                 );
1140
1141                 if (table.rows[idx].id && table.rows[idx].id.match(/-([^\-]+)$/) )
1142                         ids.push(RegExp.$1);
1143         }
1144
1145         var input = document.getElementById(store);
1146         if (input)
1147                 input.value = ids.join(' ');
1148
1149         return false;
1150 }
1151
1152 function cbi_tag_last(container)
1153 {
1154         var last;
1155
1156         for (var i = 0; i < container.childNodes.length; i++)
1157         {
1158                 var c = container.childNodes[i];
1159                 if (c.nodeType == 1 && c.nodeName.toLowerCase() == 'div')
1160                 {
1161                         c.className = c.className.replace(/ cbi-value-last$/, '');
1162                         last = c;
1163                 }
1164         }
1165
1166         if (last)
1167         {
1168                 last.className += ' cbi-value-last';
1169         }
1170 }
1171
1172 String.prototype.serialize = function()
1173 {
1174         var o = this;
1175         switch(typeof(o))
1176         {
1177                 case 'object':
1178                         // null
1179                         if( o == null )
1180                         {
1181                                 return 'null';
1182                         }
1183
1184                         // array
1185                         else if( o.length )
1186                         {
1187                                 var i, s = '';
1188
1189                                 for( var i = 0; i < o.length; i++ )
1190                                         s += (s ? ', ' : '') + String.serialize(o[i]);
1191
1192                                 return '[ ' + s + ' ]';
1193                         }
1194
1195                         // object
1196                         else
1197                         {
1198                                 var k, s = '';
1199
1200                                 for( k in o )
1201                                         s += (s ? ', ' : '') + k + ': ' + String.serialize(o[k]);
1202
1203                                 return '{ ' + s + ' }';
1204                         }
1205
1206                         break;
1207
1208                 case 'string':
1209                         // complex string
1210                         if( o.match(/[^a-zA-Z0-9_,.: -]/) )
1211                                 return 'decodeURIComponent("' + encodeURIComponent(o) + '")';
1212
1213                         // simple string
1214                         else
1215                                 return '"' + o + '"';
1216
1217                         break;
1218
1219                 default:
1220                         return o.toString();
1221         }
1222 }
1223
1224 String.prototype.format = function()
1225 {
1226         if (!RegExp)
1227                 return;
1228
1229         var html_esc = [/&/g, '&#38;', /"/g, '&#34;', /'/g, '&#39;', /</g, '&#60;', />/g, '&#62;'];
1230         var quot_esc = [/"/g, '&#34;', /'/g, '&#39;'];
1231
1232         function esc(s, r) {
1233                 for( var i = 0; i < r.length; i += 2 )
1234                         s = s.replace(r[i], r[i+1]);
1235                 return s;
1236         }
1237
1238         var str = this;
1239         var out = '';
1240         var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/;
1241         var a = b = [], numSubstitutions = 0, numMatches = 0;
1242
1243         while( a = re.exec(str) )
1244         {
1245                 var m = a[1];
1246                 var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5];
1247                 var pPrecision = a[6], pType = a[7];
1248
1249                 numMatches++;
1250
1251                 if (pType == '%')
1252                 {
1253                         subst = '%';
1254                 }
1255                 else
1256                 {
1257                         if (numSubstitutions < arguments.length)
1258                         {
1259                                 var param = arguments[numSubstitutions++];
1260
1261                                 var pad = '';
1262                                 if (pPad && pPad.substr(0,1) == "'")
1263                                         pad = leftpart.substr(1,1);
1264                                 else if (pPad)
1265                                         pad = pPad;
1266
1267                                 var justifyRight = true;
1268                                 if (pJustify && pJustify === "-")
1269                                         justifyRight = false;
1270
1271                                 var minLength = -1;
1272                                 if (pMinLength)
1273                                         minLength = parseInt(pMinLength);
1274
1275                                 var precision = -1;
1276                                 if (pPrecision && pType == 'f')
1277                                         precision = parseInt(pPrecision.substring(1));
1278
1279                                 var subst = param;
1280
1281                                 switch(pType)
1282                                 {
1283                                         case 'b':
1284                                                 subst = (parseInt(param) || 0).toString(2);
1285                                                 break;
1286
1287                                         case 'c':
1288                                                 subst = String.fromCharCode(parseInt(param) || 0);
1289                                                 break;
1290
1291                                         case 'd':
1292                                                 subst = (parseInt(param) || 0);
1293                                                 break;
1294
1295                                         case 'u':
1296                                                 subst = Math.abs(parseInt(param) || 0);
1297                                                 break;
1298
1299                                         case 'f':
1300                                                 subst = (precision > -1)
1301                                                         ? ((parseFloat(param) || 0.0)).toFixed(precision)
1302                                                         : (parseFloat(param) || 0.0);
1303                                                 break;
1304
1305                                         case 'o':
1306                                                 subst = (parseInt(param) || 0).toString(8);
1307                                                 break;
1308
1309                                         case 's':
1310                                                 subst = param;
1311                                                 break;
1312
1313                                         case 'x':
1314                                                 subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase();
1315                                                 break;
1316
1317                                         case 'X':
1318                                                 subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase();
1319                                                 break;
1320
1321                                         case 'h':
1322                                                 subst = esc(param, html_esc);
1323                                                 break;
1324
1325                                         case 'q':
1326                                                 subst = esc(param, quot_esc);
1327                                                 break;
1328
1329                                         case 'j':
1330                                                 subst = String.serialize(param);
1331                                                 break;
1332
1333                                         case 't':
1334                                                 var td = 0;
1335                                                 var th = 0;
1336                                                 var tm = 0;
1337                                                 var ts = (param || 0);
1338
1339                                                 if (ts > 60) {
1340                                                         tm = Math.floor(ts / 60);
1341                                                         ts = (ts % 60);
1342                                                 }
1343
1344                                                 if (tm > 60) {
1345                                                         th = Math.floor(tm / 60);
1346                                                         tm = (tm % 60);
1347                                                 }
1348
1349                                                 if (th > 24) {
1350                                                         td = Math.floor(th / 24);
1351                                                         th = (th % 24);
1352                                                 }
1353
1354                                                 subst = (td > 0)
1355                                                         ? String.format('%dd %dh %dm %ds', td, th, tm, ts)
1356                                                         : String.format('%dh %dm %ds', th, tm, ts);
1357
1358                                                 break;
1359
1360                                         case 'm':
1361                                                 var mf = pMinLength ? parseInt(pMinLength) : 1000;
1362                                                 var pr = pPrecision ? Math.floor(10*parseFloat('0'+pPrecision)) : 2;
1363
1364                                                 var i = 0;
1365                                                 var val = parseFloat(param || 0);
1366                                                 var units = [ '', 'K', 'M', 'G', 'T', 'P', 'E' ];
1367
1368                                                 for (i = 0; (i < units.length) && (val > mf); i++)
1369                                                         val /= mf;
1370
1371                                                 subst = val.toFixed(pr) + ' ' + units[i];
1372                                                 break;
1373                                 }
1374                         }
1375                 }
1376
1377                 out += leftpart + subst;
1378                 str = str.substr(m.length);
1379         }
1380
1381         return out + str;
1382 }
1383
1384 String.prototype.nobr = function()
1385 {
1386         return this.replace(/[\s\n]+/g, '&#160;');
1387 }
1388
1389 String.serialize = function()
1390 {
1391         var a = [ ];
1392         for (var i = 1; i < arguments.length; i++)
1393                 a.push(arguments[i]);
1394         return ''.serialize.apply(arguments[0], a);
1395 }
1396
1397 String.format = function()
1398 {
1399         var a = [ ];
1400         for (var i = 1; i < arguments.length; i++)
1401                 a.push(arguments[i]);
1402         return ''.format.apply(arguments[0], a);
1403 }
1404
1405 String.nobr = function()
1406 {
1407         var a = [ ];
1408         for (var i = 1; i < arguments.length; i++)
1409                 a.push(arguments[i]);
1410         return ''.nobr.apply(arguments[0], a);
1411 }