luci2: split into submodules
[project/luci2/ui.git] / luci2 / htdocs / luci2 / cbi.js
1 (function() {
2         var type = function(f, l)
3         {
4                 f.message = l;
5                 return f;
6         };
7
8         var cbi_class = {
9                 validation: {
10                         i18n: function(msg)
11                         {
12                                 L.cbi.validation.message = L.tr(msg);
13                         },
14
15                         compile: function(code)
16                         {
17                                 var pos = 0;
18                                 var esc = false;
19                                 var depth = 0;
20                                 var types = L.cbi.validation.types;
21                                 var stack = [ ];
22
23                                 code += ',';
24
25                                 for (var i = 0; i < code.length; i++)
26                                 {
27                                         if (esc)
28                                         {
29                                                 esc = false;
30                                                 continue;
31                                         }
32
33                                         switch (code.charCodeAt(i))
34                                         {
35                                         case 92:
36                                                 esc = true;
37                                                 break;
38
39                                         case 40:
40                                         case 44:
41                                                 if (depth <= 0)
42                                                 {
43                                                         if (pos < i)
44                                                         {
45                                                                 var label = code.substring(pos, i);
46                                                                         label = label.replace(/\\(.)/g, '$1');
47                                                                         label = label.replace(/^[ \t]+/g, '');
48                                                                         label = label.replace(/[ \t]+$/g, '');
49
50                                                                 if (label && !isNaN(label))
51                                                                 {
52                                                                         stack.push(parseFloat(label));
53                                                                 }
54                                                                 else if (label.match(/^(['"]).*\1$/))
55                                                                 {
56                                                                         stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
57                                                                 }
58                                                                 else if (typeof types[label] == 'function')
59                                                                 {
60                                                                         stack.push(types[label]);
61                                                                         stack.push([ ]);
62                                                                 }
63                                                                 else
64                                                                 {
65                                                                         throw "Syntax error, unhandled token '"+label+"'";
66                                                                 }
67                                                         }
68                                                         pos = i+1;
69                                                 }
70                                                 depth += (code.charCodeAt(i) == 40);
71                                                 break;
72
73                                         case 41:
74                                                 if (--depth <= 0)
75                                                 {
76                                                         if (typeof stack[stack.length-2] != 'function')
77                                                                 throw "Syntax error, argument list follows non-function";
78
79                                                         stack[stack.length-1] =
80                                                                 L.cbi.validation.compile(code.substring(pos, i));
81
82                                                         pos = i+1;
83                                                 }
84                                                 break;
85                                         }
86                                 }
87
88                                 return stack;
89                         }
90                 }
91         };
92
93         var validation = cbi_class.validation;
94
95         validation.types = {
96                 'integer': function()
97                 {
98                         if (this.match(/^-?[0-9]+$/) != null)
99                                 return true;
100
101                         validation.i18n('Must be a valid integer');
102                         return false;
103                 },
104
105                 'uinteger': function()
106                 {
107                         if (validation.types['integer'].apply(this) && (this >= 0))
108                                 return true;
109
110                         validation.i18n('Must be a positive integer');
111                         return false;
112                 },
113
114                 'float': function()
115                 {
116                         if (!isNaN(parseFloat(this)))
117                                 return true;
118
119                         validation.i18n('Must be a valid number');
120                         return false;
121                 },
122
123                 'ufloat': function()
124                 {
125                         if (validation.types['float'].apply(this) && (this >= 0))
126                                 return true;
127
128                         validation.i18n('Must be a positive number');
129                         return false;
130                 },
131
132                 'ipaddr': function()
133                 {
134                         if (L.parseIPv4(this) || L.parseIPv6(this))
135                                 return true;
136
137                         validation.i18n('Must be a valid IP address');
138                         return false;
139                 },
140
141                 'ip4addr': function()
142                 {
143                         if (L.parseIPv4(this))
144                                 return true;
145
146                         validation.i18n('Must be a valid IPv4 address');
147                         return false;
148                 },
149
150                 'ip6addr': function()
151                 {
152                         if (L.parseIPv6(this))
153                                 return true;
154
155                         validation.i18n('Must be a valid IPv6 address');
156                         return false;
157                 },
158
159                 'netmask4': function()
160                 {
161                         if (L.isNetmask(L.parseIPv4(this)))
162                                 return true;
163
164                         validation.i18n('Must be a valid IPv4 netmask');
165                         return false;
166                 },
167
168                 'netmask6': function()
169                 {
170                         if (L.isNetmask(L.parseIPv6(this)))
171                                 return true;
172
173                         validation.i18n('Must be a valid IPv6 netmask6');
174                         return false;
175                 },
176
177                 'cidr4': function()
178                 {
179                         if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
180                                 if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
181                                         return true;
182
183                         validation.i18n('Must be a valid IPv4 prefix');
184                         return false;
185                 },
186
187                 'cidr6': function()
188                 {
189                         if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
190                                 if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
191                                         return true;
192
193                         validation.i18n('Must be a valid IPv6 prefix');
194                         return false;
195                 },
196
197                 'ipmask4': function()
198                 {
199                         if (this.match(/^([0-9.]+)\/([0-9.]+)$/))
200                         {
201                                 var addr = RegExp.$1, mask = RegExp.$2;
202                                 if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask)))
203                                         return true;
204                         }
205
206                         validation.i18n('Must be a valid IPv4 address/netmask pair');
207                         return false;
208                 },
209
210                 'ipmask6': function()
211                 {
212                         if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/))
213                         {
214                                 var addr = RegExp.$1, mask = RegExp.$2;
215                                 if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask)))
216                                         return true;
217                         }
218
219                         validation.i18n('Must be a valid IPv6 address/netmask pair');
220                         return false;
221                 },
222
223                 'port': function()
224                 {
225                         if (validation.types['integer'].apply(this) &&
226                                 (this >= 0) && (this <= 65535))
227                                 return true;
228
229                         validation.i18n('Must be a valid port number');
230                         return false;
231                 },
232
233                 'portrange': function()
234                 {
235                         if (this.match(/^(\d+)-(\d+)$/))
236                         {
237                                 var p1 = RegExp.$1;
238                                 var p2 = RegExp.$2;
239
240                                 if (validation.types['port'].apply(p1) &&
241                                     validation.types['port'].apply(p2) &&
242                                     (parseInt(p1) <= parseInt(p2)))
243                                         return true;
244                         }
245                         else if (validation.types['port'].apply(this))
246                         {
247                                 return true;
248                         }
249
250                         validation.i18n('Must be a valid port range');
251                         return false;
252                 },
253
254                 'macaddr': function()
255                 {
256                         if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
257                                 return true;
258
259                         validation.i18n('Must be a valid MAC address');
260                         return false;
261                 },
262
263                 'host': function()
264                 {
265                         if (validation.types['hostname'].apply(this) ||
266                             validation.types['ipaddr'].apply(this))
267                                 return true;
268
269                         validation.i18n('Must be a valid hostname or IP address');
270                         return false;
271                 },
272
273                 'hostname': function()
274                 {
275                         if ((this.length <= 253) &&
276                             ((this.match(/^[a-zA-Z0-9]+$/) != null ||
277                              (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
278                               this.match(/[^0-9.]/)))))
279                                 return true;
280
281                         validation.i18n('Must be a valid host name');
282                         return false;
283                 },
284
285                 'network': function()
286                 {
287                         if (validation.types['uciname'].apply(this) ||
288                             validation.types['host'].apply(this))
289                                 return true;
290
291                         validation.i18n('Must be a valid network name');
292                         return false;
293                 },
294
295                 'wpakey': function()
296                 {
297                         var v = this;
298
299                         if ((v.length == 64)
300                               ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
301                                   : ((v.length >= 8) && (v.length <= 63)))
302                                 return true;
303
304                         validation.i18n('Must be a valid WPA key');
305                         return false;
306                 },
307
308                 'wepkey': function()
309                 {
310                         var v = this;
311
312                         if (v.substr(0,2) == 's:')
313                                 v = v.substr(2);
314
315                         if (((v.length == 10) || (v.length == 26))
316                               ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
317                               : ((v.length == 5) || (v.length == 13)))
318                                 return true;
319
320                         validation.i18n('Must be a valid WEP key');
321                         return false;
322                 },
323
324                 'uciname': function()
325                 {
326                         if (this.match(/^[a-zA-Z0-9_]+$/) != null)
327                                 return true;
328
329                         validation.i18n('Must be a valid UCI identifier');
330                         return false;
331                 },
332
333                 'range': function(min, max)
334                 {
335                         var val = parseFloat(this);
336
337                         if (validation.types['integer'].apply(this) &&
338                             !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
339                                 return true;
340
341                         validation.i18n('Must be a number between %d and %d');
342                         return false;
343                 },
344
345                 'min': function(min)
346                 {
347                         var val = parseFloat(this);
348
349                         if (validation.types['integer'].apply(this) &&
350                             !isNaN(min) && !isNaN(val) && (val >= min))
351                                 return true;
352
353                         validation.i18n('Must be a number greater or equal to %d');
354                         return false;
355                 },
356
357                 'max': function(max)
358                 {
359                         var val = parseFloat(this);
360
361                         if (validation.types['integer'].apply(this) &&
362                             !isNaN(max) && !isNaN(val) && (val <= max))
363                                 return true;
364
365                         validation.i18n('Must be a number lower or equal to %d');
366                         return false;
367                 },
368
369                 'rangelength': function(min, max)
370                 {
371                         var val = '' + this;
372
373                         if (!isNaN(min) && !isNaN(max) &&
374                             (val.length >= min) && (val.length <= max))
375                                 return true;
376
377                         if (min != max)
378                                 validation.i18n('Must be between %d and %d characters');
379                         else
380                                 validation.i18n('Must be %d characters');
381                         return false;
382                 },
383
384                 'minlength': function(min)
385                 {
386                         var val = '' + this;
387
388                         if (!isNaN(min) && (val.length >= min))
389                                 return true;
390
391                         validation.i18n('Must be at least %d characters');
392                         return false;
393                 },
394
395                 'maxlength': function(max)
396                 {
397                         var val = '' + this;
398
399                         if (!isNaN(max) && (val.length <= max))
400                                 return true;
401
402                         validation.i18n('Must be at most %d characters');
403                         return false;
404                 },
405
406                 'or': function()
407                 {
408                         var msgs = [ ];
409
410                         for (var i = 0; i < arguments.length; i += 2)
411                         {
412                                 delete validation.message;
413
414                                 if (typeof(arguments[i]) != 'function')
415                                 {
416                                         if (arguments[i] == this)
417                                                 return true;
418                                         i--;
419                                 }
420                                 else if (arguments[i].apply(this, arguments[i+1]))
421                                 {
422                                         return true;
423                                 }
424
425                                 if (validation.message)
426                                         msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
427                         }
428
429                         validation.message = msgs.join( L.tr(' - or - '));
430                         return false;
431                 },
432
433                 'and': function()
434                 {
435                         var msgs = [ ];
436
437                         for (var i = 0; i < arguments.length; i += 2)
438                         {
439                                 delete validation.message;
440
441                                 if (typeof arguments[i] != 'function')
442                                 {
443                                         if (arguments[i] != this)
444                                                 return false;
445                                         i--;
446                                 }
447                                 else if (!arguments[i].apply(this, arguments[i+1]))
448                                 {
449                                         return false;
450                                 }
451
452                                 if (validation.message)
453                                         msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
454                         }
455
456                         validation.message = msgs.join(', ');
457                         return true;
458                 },
459
460                 'neg': function()
461                 {
462                         return validation.types['or'].apply(
463                                 this.replace(/^[ \t]*![ \t]*/, ''), arguments);
464                 },
465
466                 'list': function(subvalidator, subargs)
467                 {
468                         if (typeof subvalidator != 'function')
469                                 return false;
470
471                         var tokens = this.match(/[^ \t]+/g);
472                         for (var i = 0; i < tokens.length; i++)
473                                 if (!subvalidator.apply(tokens[i], subargs))
474                                         return false;
475
476                         return true;
477                 },
478
479                 'phonedigit': function()
480                 {
481                         if (this.match(/^[0-9\*#!\.]+$/) != null)
482                                 return true;
483
484                         validation.i18n('Must be a valid phone number digit');
485                         return false;
486                 },
487
488                 'string': function()
489                 {
490                         return true;
491                 }
492         };
493
494         cbi_class.AbstractValue = L.ui.AbstractWidget.extend({
495                 init: function(name, options)
496                 {
497                         this.name = name;
498                         this.instance = { };
499                         this.dependencies = [ ];
500                         this.rdependency = { };
501
502                         this.options = L.defaults(options, {
503                                 placeholder: '',
504                                 datatype: 'string',
505                                 optional: false,
506                                 keep: true
507                         });
508                 },
509
510                 id: function(sid)
511                 {
512                         return this.ownerSection.id('field', sid || '__unknown__', this.name);
513                 },
514
515                 render: function(sid, condensed)
516                 {
517                         var i = this.instance[sid] = { };
518
519                         i.top = $('<div />')
520                                 .addClass('luci2-field');
521
522                         if (!condensed)
523                         {
524                                 i.top.addClass('form-group');
525
526                                 if (typeof(this.options.caption) == 'string')
527                                         $('<label />')
528                                                 .addClass('col-lg-2 control-label')
529                                                 .attr('for', this.id(sid))
530                                                 .text(this.options.caption)
531                                                 .appendTo(i.top);
532                         }
533
534                         i.error = $('<div />')
535                                 .hide()
536                                 .addClass('luci2-field-error label label-danger');
537
538                         i.widget = $('<div />')
539                                 .addClass('luci2-field-widget')
540                                 .append(this.widget(sid))
541                                 .append(i.error)
542                                 .appendTo(i.top);
543
544                         if (!condensed)
545                         {
546                                 i.widget.addClass('col-lg-5');
547
548                                 $('<div />')
549                                         .addClass('col-lg-5')
550                                         .text((typeof(this.options.description) == 'string') ? this.options.description : '')
551                                         .appendTo(i.top);
552                         }
553
554                         return i.top;
555                 },
556
557                 active: function(sid)
558                 {
559                         return (this.instance[sid] && !this.instance[sid].disabled);
560                 },
561
562                 ucipath: function(sid)
563                 {
564                         return {
565                                 config:  (this.options.uci_package || this.ownerMap.uci_package),
566                                 section: (this.options.uci_section || sid),
567                                 option:  (this.options.uci_option  || this.name)
568                         };
569                 },
570
571                 ucivalue: function(sid)
572                 {
573                         var uci = this.ucipath(sid);
574                         var val = this.ownerMap.get(uci.config, uci.section, uci.option);
575
576                         if (typeof(val) == 'undefined')
577                                 return this.options.initial;
578
579                         return val;
580                 },
581
582                 formvalue: function(sid)
583                 {
584                         var v = $('#' + this.id(sid)).val();
585                         return (v === '') ? undefined : v;
586                 },
587
588                 textvalue: function(sid)
589                 {
590                         var v = this.formvalue(sid);
591
592                         if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
593                                 v = this.ucivalue(sid);
594
595                         if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
596                                 v = this.options.placeholder;
597
598                         if (typeof(v) == 'undefined' || v === '')
599                                 return undefined;
600
601                         if (typeof(v) == 'string' && $.isArray(this.choices))
602                         {
603                                 for (var i = 0; i < this.choices.length; i++)
604                                         if (v === this.choices[i][0])
605                                                 return this.choices[i][1];
606                         }
607                         else if (v === true)
608                                 return L.tr('yes');
609                         else if (v === false)
610                                 return L.tr('no');
611                         else if ($.isArray(v))
612                                 return v.join(', ');
613
614                         return v;
615                 },
616
617                 changed: function(sid)
618                 {
619                         var a = this.ucivalue(sid);
620                         var b = this.formvalue(sid);
621
622                         if (typeof(a) != typeof(b))
623                                 return true;
624
625                         if ($.isArray(a))
626                         {
627                                 if (a.length != b.length)
628                                         return true;
629
630                                 for (var i = 0; i < a.length; i++)
631                                         if (a[i] != b[i])
632                                                 return true;
633
634                                 return false;
635                         }
636                         else if ($.isPlainObject(a))
637                         {
638                                 for (var k in a)
639                                         if (!(k in b))
640                                                 return true;
641
642                                 for (var k in b)
643                                         if (!(k in a) || a[k] !== b[k])
644                                                 return true;
645
646                                 return false;
647                         }
648
649                         return (a != b);
650                 },
651
652                 save: function(sid)
653                 {
654                         var uci = this.ucipath(sid);
655
656                         if (this.instance[sid].disabled)
657                         {
658                                 if (!this.options.keep)
659                                         return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
660
661                                 return false;
662                         }
663
664                         var chg = this.changed(sid);
665                         var val = this.formvalue(sid);
666
667                         if (chg)
668                                 this.ownerMap.set(uci.config, uci.section, uci.option, val);
669
670                         return chg;
671                 },
672
673                 findSectionID: function($elem)
674                 {
675                         return this.ownerSection.findParentSectionIDs($elem)[0];
676                 },
677
678                 setError: function($elem, msg, msgargs)
679                 {
680                         var $field = $elem.parents('.luci2-field:first');
681                         var $error = $field.find('.luci2-field-error:first');
682
683                         if (typeof(msg) == 'string' && msg.length > 0)
684                         {
685                                 $field.addClass('luci2-form-error');
686                                 $elem.parent().addClass('has-error');
687
688                                 $error.text(msg.format.apply(msg, msgargs)).show();
689                                 $field.trigger('validate');
690
691                                 return false;
692                         }
693                         else
694                         {
695                                 $elem.parent().removeClass('has-error');
696
697                                 var $other_errors = $field.find('.has-error');
698                                 if ($other_errors.length == 0)
699                                 {
700                                         $field.removeClass('luci2-form-error');
701                                         $error.text('').hide();
702                                         $field.trigger('validate');
703
704                                         return true;
705                                 }
706
707                                 return false;
708                         }
709                 },
710
711                 handleValidate: function(ev)
712                 {
713                         var $elem = $(this);
714
715                         var d = ev.data;
716                         var rv = true;
717                         var val = $elem.val();
718                         var vstack = d.vstack;
719
720                         if (vstack && typeof(vstack[0]) == 'function')
721                         {
722                                 delete validation.message;
723
724                                 if ((val.length == 0 && !d.opt))
725                                 {
726                                         rv = d.self.setError($elem, L.tr('Field must not be empty'));
727                                 }
728                                 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
729                                 {
730                                         rv = d.self.setError($elem, validation.message, vstack[1]);
731                                 }
732                                 else
733                                 {
734                                         rv = d.self.setError($elem);
735                                 }
736                         }
737
738                         if (rv)
739                         {
740                                 var sid = d.self.findSectionID($elem);
741
742                                 for (var field in d.self.rdependency)
743                                 {
744                                         d.self.rdependency[field].toggle(sid);
745                                         d.self.rdependency[field].validate(sid);
746                                 }
747
748                                 d.self.ownerSection.tabtoggle(sid);
749                         }
750
751                         return rv;
752                 },
753
754                 attachEvents: function(sid, elem)
755                 {
756                         var evdata = {
757                                 self:   this,
758                                 opt:    this.options.optional
759                         };
760
761                         if (this.events)
762                                 for (var evname in this.events)
763                                         elem.on(evname, evdata, this.events[evname]);
764
765                         if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
766                                 return elem;
767
768                         var vstack;
769                         if (typeof(this.options.datatype) == 'string')
770                         {
771                                 try {
772                                         evdata.vstack = L.cbi.validation.compile(this.options.datatype);
773                                 } catch(e) { };
774                         }
775                         else if (typeof(this.options.datatype) == 'function')
776                         {
777                                 var vfunc = this.options.datatype;
778                                 evdata.vstack = [ function(elem) {
779                                         var rv = vfunc(this, elem);
780                                         if (rv !== true)
781                                                 validation.message = rv;
782                                         return (rv === true);
783                                 }, [ elem ] ];
784                         }
785
786                         if (elem.prop('tagName') == 'SELECT')
787                         {
788                                 elem.change(evdata, this.handleValidate);
789                         }
790                         else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
791                         {
792                                 elem.click(evdata, this.handleValidate);
793                                 elem.blur(evdata, this.handleValidate);
794                         }
795                         else
796                         {
797                                 elem.keyup(evdata, this.handleValidate);
798                                 elem.blur(evdata, this.handleValidate);
799                         }
800
801                         elem.addClass('luci2-field-validate')
802                                 .on('validate', evdata, this.handleValidate);
803
804                         return elem;
805                 },
806
807                 validate: function(sid)
808                 {
809                         var i = this.instance[sid];
810
811                         i.widget.find('.luci2-field-validate').trigger('validate');
812
813                         return (i.disabled || i.error.text() == '');
814                 },
815
816                 depends: function(d, v, add)
817                 {
818                         var dep;
819
820                         if ($.isArray(d))
821                         {
822                                 dep = { };
823                                 for (var i = 0; i < d.length; i++)
824                                 {
825                                         if (typeof(d[i]) == 'string')
826                                                 dep[d[i]] = true;
827                                         else if (d[i] instanceof L.cbi.AbstractValue)
828                                                 dep[d[i].name] = true;
829                                 }
830                         }
831                         else if (d instanceof L.cbi.AbstractValue)
832                         {
833                                 dep = { };
834                                 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
835                         }
836                         else if (typeof(d) == 'object')
837                         {
838                                 dep = d;
839                         }
840                         else if (typeof(d) == 'string')
841                         {
842                                 dep = { };
843                                 dep[d] = (typeof(v) == 'undefined') ? true : v;
844                         }
845
846                         if (!dep || $.isEmptyObject(dep))
847                                 return this;
848
849                         for (var field in dep)
850                         {
851                                 var f = this.ownerSection.fields[field];
852                                 if (f)
853                                         f.rdependency[this.name] = this;
854                                 else
855                                         delete dep[field];
856                         }
857
858                         if ($.isEmptyObject(dep))
859                                 return this;
860
861                         if (!add || !this.dependencies.length)
862                                 this.dependencies.push(dep);
863                         else
864                                 for (var i = 0; i < this.dependencies.length; i++)
865                                         $.extend(this.dependencies[i], dep);
866
867                         return this;
868                 },
869
870                 toggle: function(sid)
871                 {
872                         var d = this.dependencies;
873                         var i = this.instance[sid];
874
875                         if (!d.length)
876                                 return true;
877
878                         for (var n = 0; n < d.length; n++)
879                         {
880                                 var rv = true;
881
882                                 for (var field in d[n])
883                                 {
884                                         var val = this.ownerSection.fields[field].formvalue(sid);
885                                         var cmp = d[n][field];
886
887                                         if (typeof(cmp) == 'boolean')
888                                         {
889                                                 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
890                                                 {
891                                                         rv = false;
892                                                         break;
893                                                 }
894                                         }
895                                         else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
896                                         {
897                                                 if (val != cmp)
898                                                 {
899                                                         rv = false;
900                                                         break;
901                                                 }
902                                         }
903                                         else if (typeof(cmp) == 'function')
904                                         {
905                                                 if (!cmp(val))
906                                                 {
907                                                         rv = false;
908                                                         break;
909                                                 }
910                                         }
911                                         else if (cmp instanceof RegExp)
912                                         {
913                                                 if (!cmp.test(val))
914                                                 {
915                                                         rv = false;
916                                                         break;
917                                                 }
918                                         }
919                                 }
920
921                                 if (rv)
922                                 {
923                                         if (i.disabled)
924                                         {
925                                                 i.disabled = false;
926                                                 i.top.removeClass('luci2-field-disabled');
927                                                 i.top.fadeIn();
928                                         }
929
930                                         return true;
931                                 }
932                         }
933
934                         if (!i.disabled)
935                         {
936                                 i.disabled = true;
937                                 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
938                                 i.top.addClass('luci2-field-disabled');
939                         }
940
941                         return false;
942                 }
943         });
944
945         cbi_class.CheckboxValue = cbi_class.AbstractValue.extend({
946                 widget: function(sid)
947                 {
948                         var o = this.options;
949
950                         if (typeof(o.enabled)  == 'undefined') o.enabled  = '1';
951                         if (typeof(o.disabled) == 'undefined') o.disabled = '0';
952
953                         var i = $('<input />')
954                                 .attr('id', this.id(sid))
955                                 .attr('type', 'checkbox')
956                                 .prop('checked', this.ucivalue(sid));
957
958                         return $('<div />')
959                                 .addClass('checkbox')
960                                 .append(this.attachEvents(sid, i));
961                 },
962
963                 ucivalue: function(sid)
964                 {
965                         var v = this.callSuper('ucivalue', sid);
966
967                         if (typeof(v) == 'boolean')
968                                 return v;
969
970                         return (v == this.options.enabled);
971                 },
972
973                 formvalue: function(sid)
974                 {
975                         var v = $('#' + this.id(sid)).prop('checked');
976
977                         if (typeof(v) == 'undefined')
978                                 return !!this.options.initial;
979
980                         return v;
981                 },
982
983                 save: function(sid)
984                 {
985                         var uci = this.ucipath(sid);
986
987                         if (this.instance[sid].disabled)
988                         {
989                                 if (!this.options.keep)
990                                         return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
991
992                                 return false;
993                         }
994
995                         var chg = this.changed(sid);
996                         var val = this.formvalue(sid);
997
998                         if (chg)
999                         {
1000                                 if (this.options.optional && val == this.options.initial)
1001                                         this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
1002                                 else
1003                                         this.ownerMap.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
1004                         }
1005
1006                         return chg;
1007                 }
1008         });
1009
1010         cbi_class.InputValue = cbi_class.AbstractValue.extend({
1011                 widget: function(sid)
1012                 {
1013                         var i = $('<input />')
1014                                 .addClass('form-control')
1015                                 .attr('id', this.id(sid))
1016                                 .attr('type', 'text')
1017                                 .attr('placeholder', this.options.placeholder)
1018                                 .val(this.ucivalue(sid));
1019
1020                         return this.attachEvents(sid, i);
1021                 }
1022         });
1023
1024         cbi_class.PasswordValue = cbi_class.AbstractValue.extend({
1025                 widget: function(sid)
1026                 {
1027                         var i = $('<input />')
1028                                 .addClass('form-control')
1029                                 .attr('id', this.id(sid))
1030                                 .attr('type', 'password')
1031                                 .attr('placeholder', this.options.placeholder)
1032                                 .val(this.ucivalue(sid));
1033
1034                         var t = $('<span />')
1035                                 .addClass('input-group-btn')
1036                                 .append(L.ui.button(L.tr('Reveal'), 'default')
1037                                         .click(function(ev) {
1038                                                 var b = $(this);
1039                                                 var i = b.parent().prev();
1040                                                 var t = i.attr('type');
1041                                                 b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
1042                                                 i.attr('type', (t == 'password') ? 'text' : 'password');
1043                                                 b = i = t = null;
1044                                         }));
1045
1046                         this.attachEvents(sid, i);
1047
1048                         return $('<div />')
1049                                 .addClass('input-group')
1050                                 .append(i)
1051                                 .append(t);
1052                 }
1053         });
1054
1055         cbi_class.ListValue = cbi_class.AbstractValue.extend({
1056                 widget: function(sid)
1057                 {
1058                         var s = $('<select />')
1059                                 .addClass('form-control');
1060
1061                         if (this.options.optional && !this.has_empty)
1062                                 $('<option />')
1063                                         .attr('value', '')
1064                                         .text(L.tr('-- Please choose --'))
1065                                         .appendTo(s);
1066
1067                         if (this.choices)
1068                                 for (var i = 0; i < this.choices.length; i++)
1069                                         $('<option />')
1070                                                 .attr('value', this.choices[i][0])
1071                                                 .text(this.choices[i][1])
1072                                                 .appendTo(s);
1073
1074                         s.attr('id', this.id(sid)).val(this.ucivalue(sid));
1075
1076                         return this.attachEvents(sid, s);
1077                 },
1078
1079                 value: function(k, v)
1080                 {
1081                         if (!this.choices)
1082                                 this.choices = [ ];
1083
1084                         if (k == '')
1085                                 this.has_empty = true;
1086
1087                         this.choices.push([k, v || k]);
1088                         return this;
1089                 }
1090         });
1091
1092         cbi_class.MultiValue = cbi_class.ListValue.extend({
1093                 widget: function(sid)
1094                 {
1095                         var v = this.ucivalue(sid);
1096                         var t = $('<div />').attr('id', this.id(sid));
1097
1098                         if (!$.isArray(v))
1099                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
1100
1101                         var s = { };
1102                         for (var i = 0; i < v.length; i++)
1103                                 s[v[i]] = true;
1104
1105                         if (this.choices)
1106                                 for (var i = 0; i < this.choices.length; i++)
1107                                 {
1108                                         $('<label />')
1109                                                 .addClass('checkbox')
1110                                                 .append($('<input />')
1111                                                         .attr('type', 'checkbox')
1112                                                         .attr('value', this.choices[i][0])
1113                                                         .prop('checked', s[this.choices[i][0]]))
1114                                                 .append(this.choices[i][1])
1115                                                 .appendTo(t);
1116                                 }
1117
1118                         return t;
1119                 },
1120
1121                 formvalue: function(sid)
1122                 {
1123                         var rv = [ ];
1124                         var fields = $('#' + this.id(sid) + ' > label > input');
1125
1126                         for (var i = 0; i < fields.length; i++)
1127                                 if (fields[i].checked)
1128                                         rv.push(fields[i].getAttribute('value'));
1129
1130                         return rv;
1131                 },
1132
1133                 textvalue: function(sid)
1134                 {
1135                         var v = this.formvalue(sid);
1136                         var c = { };
1137
1138                         if (this.choices)
1139                                 for (var i = 0; i < this.choices.length; i++)
1140                                         c[this.choices[i][0]] = this.choices[i][1];
1141
1142                         var t = [ ];
1143
1144                         for (var i = 0; i < v.length; i++)
1145                                 t.push(c[v[i]] || v[i]);
1146
1147                         return t.join(', ');
1148                 }
1149         });
1150
1151         cbi_class.ComboBox = cbi_class.AbstractValue.extend({
1152                 _change: function(ev)
1153                 {
1154                         var s = ev.target;
1155                         var self = ev.data.self;
1156
1157                         if (s.selectedIndex == (s.options.length - 1))
1158                         {
1159                                 ev.data.select.hide();
1160                                 ev.data.input.show().focus();
1161                                 ev.data.input.val('');
1162                         }
1163                         else if (self.options.optional && s.selectedIndex == 0)
1164                         {
1165                                 ev.data.input.val('');
1166                         }
1167                         else
1168                         {
1169                                 ev.data.input.val(ev.data.select.val());
1170                         }
1171
1172                         ev.stopPropagation();
1173                 },
1174
1175                 _blur: function(ev)
1176                 {
1177                         var seen = false;
1178                         var val = this.value;
1179                         var self = ev.data.self;
1180
1181                         ev.data.select.empty();
1182
1183                         if (self.options.optional && !self.has_empty)
1184                                 $('<option />')
1185                                         .attr('value', '')
1186                                         .text(L.tr('-- please choose --'))
1187                                         .appendTo(ev.data.select);
1188
1189                         if (self.choices)
1190                                 for (var i = 0; i < self.choices.length; i++)
1191                                 {
1192                                         if (self.choices[i][0] == val)
1193                                                 seen = true;
1194
1195                                         $('<option />')
1196                                                 .attr('value', self.choices[i][0])
1197                                                 .text(self.choices[i][1])
1198                                                 .appendTo(ev.data.select);
1199                                 }
1200
1201                         if (!seen && val != '')
1202                                 $('<option />')
1203                                         .attr('value', val)
1204                                         .text(val)
1205                                         .appendTo(ev.data.select);
1206
1207                         $('<option />')
1208                                 .attr('value', ' ')
1209                                 .text(L.tr('-- custom --'))
1210                                 .appendTo(ev.data.select);
1211
1212                         ev.data.input.hide();
1213                         ev.data.select.val(val).show().blur();
1214                 },
1215
1216                 _enter: function(ev)
1217                 {
1218                         if (ev.which != 13)
1219                                 return true;
1220
1221                         ev.preventDefault();
1222                         ev.data.self._blur(ev);
1223                         return false;
1224                 },
1225
1226                 widget: function(sid)
1227                 {
1228                         var d = $('<div />')
1229                                 .attr('id', this.id(sid));
1230
1231                         var t = $('<input />')
1232                                 .addClass('form-control')
1233                                 .attr('type', 'text')
1234                                 .hide()
1235                                 .appendTo(d);
1236
1237                         var s = $('<select />')
1238                                 .addClass('form-control')
1239                                 .appendTo(d);
1240
1241                         var evdata = {
1242                                 self: this,
1243                                 input: t,
1244                                 select: s
1245                         };
1246
1247                         s.change(evdata, this._change);
1248                         t.blur(evdata, this._blur);
1249                         t.keydown(evdata, this._enter);
1250
1251                         t.val(this.ucivalue(sid));
1252                         t.blur();
1253
1254                         this.attachEvents(sid, t);
1255                         this.attachEvents(sid, s);
1256
1257                         return d;
1258                 },
1259
1260                 value: function(k, v)
1261                 {
1262                         if (!this.choices)
1263                                 this.choices = [ ];
1264
1265                         if (k == '')
1266                                 this.has_empty = true;
1267
1268                         this.choices.push([k, v || k]);
1269                         return this;
1270                 },
1271
1272                 formvalue: function(sid)
1273                 {
1274                         var v = $('#' + this.id(sid)).children('input').val();
1275                         return (v == '') ? undefined : v;
1276                 }
1277         });
1278
1279         cbi_class.DynamicList = cbi_class.ComboBox.extend({
1280                 _redraw: function(focus, add, del, s)
1281                 {
1282                         var v = s.values || [ ];
1283                         delete s.values;
1284
1285                         $(s.parent).children('div.input-group').children('input').each(function(i) {
1286                                 if (i != del)
1287                                         v.push(this.value || '');
1288                         });
1289
1290                         $(s.parent).empty();
1291
1292                         if (add >= 0)
1293                         {
1294                                 focus = add + 1;
1295                                 v.splice(focus, 0, '');
1296                         }
1297                         else if (v.length == 0)
1298                         {
1299                                 focus = 0;
1300                                 v.push('');
1301                         }
1302
1303                         for (var i = 0; i < v.length; i++)
1304                         {
1305                                 var evdata = {
1306                                         sid: s.sid,
1307                                         self: s.self,
1308                                         parent: s.parent,
1309                                         index: i,
1310                                         remove: ((i+1) < v.length)
1311                                 };
1312
1313                                 var btn;
1314                                 if (evdata.remove)
1315                                         btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
1316                                 else
1317                                         btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
1318
1319                                 if (this.choices)
1320                                 {
1321                                         var txt = $('<input />')
1322                                                 .addClass('form-control')
1323                                                 .attr('type', 'text')
1324                                                 .hide();
1325
1326                                         var sel = $('<select />')
1327                                                 .addClass('form-control');
1328
1329                                         $('<div />')
1330                                                 .addClass('input-group')
1331                                                 .append(txt)
1332                                                 .append(sel)
1333                                                 .append($('<span />')
1334                                                         .addClass('input-group-btn')
1335                                                         .append(btn))
1336                                                 .appendTo(s.parent);
1337
1338                                         evdata.input = this.attachEvents(s.sid, txt);
1339                                         evdata.select = this.attachEvents(s.sid, sel);
1340
1341                                         sel.change(evdata, this._change);
1342                                         txt.blur(evdata, this._blur);
1343                                         txt.keydown(evdata, this._keydown);
1344
1345                                         txt.val(v[i]);
1346                                         txt.blur();
1347
1348                                         if (i == focus || -(i+1) == focus)
1349                                                 sel.focus();
1350
1351                                         sel = txt = null;
1352                                 }
1353                                 else
1354                                 {
1355                                         var f = $('<input />')
1356                                                 .attr('type', 'text')
1357                                                 .attr('index', i)
1358                                                 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
1359                                                 .addClass('form-control')
1360                                                 .keydown(evdata, this._keydown)
1361                                                 .keypress(evdata, this._keypress)
1362                                                 .val(v[i]);
1363
1364                                         $('<div />')
1365                                                 .addClass('input-group')
1366                                                 .append(f)
1367                                                 .append($('<span />')
1368                                                         .addClass('input-group-btn')
1369                                                         .append(btn))
1370                                                 .appendTo(s.parent);
1371
1372                                         if (i == focus)
1373                                         {
1374                                                 f.focus();
1375                                         }
1376                                         else if (-(i+1) == focus)
1377                                         {
1378                                                 f.focus();
1379
1380                                                 /* force cursor to end */
1381                                                 var val = f.val();
1382                                                 f.val(' ');
1383                                                 f.val(val);
1384                                         }
1385
1386                                         evdata.input = this.attachEvents(s.sid, f);
1387
1388                                         f = null;
1389                                 }
1390
1391                                 evdata = null;
1392                         }
1393
1394                         s = null;
1395                 },
1396
1397                 _keypress: function(ev)
1398                 {
1399                         switch (ev.which)
1400                         {
1401                                 /* backspace, delete */
1402                                 case 8:
1403                                 case 46:
1404                                         if (ev.data.input.val() == '')
1405                                         {
1406                                                 ev.preventDefault();
1407                                                 return false;
1408                                         }
1409
1410                                         return true;
1411
1412                                 /* enter, arrow up, arrow down */
1413                                 case 13:
1414                                 case 38:
1415                                 case 40:
1416                                         ev.preventDefault();
1417                                         return false;
1418                         }
1419
1420                         return true;
1421                 },
1422
1423                 _keydown: function(ev)
1424                 {
1425                         var input = ev.data.input;
1426
1427                         switch (ev.which)
1428                         {
1429                                 /* backspace, delete */
1430                                 case 8:
1431                                 case 46:
1432                                         if (input.val().length == 0)
1433                                         {
1434                                                 ev.preventDefault();
1435
1436                                                 var index = ev.data.index;
1437                                                 var focus = index;
1438
1439                                                 if (ev.which == 8)
1440                                                         focus = -focus;
1441
1442                                                 ev.data.self._redraw(focus, -1, index, ev.data);
1443                                                 return false;
1444                                         }
1445
1446                                         break;
1447
1448                                 /* enter */
1449                                 case 13:
1450                                         ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
1451                                         break;
1452
1453                                 /* arrow up */
1454                                 case 38:
1455                                         var prev = input.parent().prevAll('div.input-group:first').children('input');
1456                                         if (prev.is(':visible'))
1457                                                 prev.focus();
1458                                         else
1459                                                 prev.next('select').focus();
1460                                         break;
1461
1462                                 /* arrow down */
1463                                 case 40:
1464                                         var next = input.parent().nextAll('div.input-group:first').children('input');
1465                                         if (next.is(':visible'))
1466                                                 next.focus();
1467                                         else
1468                                                 next.next('select').focus();
1469                                         break;
1470                         }
1471
1472                         return true;
1473                 },
1474
1475                 _btnclick: function(ev)
1476                 {
1477                         if (!this.getAttribute('disabled'))
1478                         {
1479                                 if (ev.data.remove)
1480                                 {
1481                                         var index = ev.data.index;
1482                                         ev.data.self._redraw(-index, -1, index, ev.data);
1483                                 }
1484                                 else
1485                                 {
1486                                         ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
1487                                 }
1488                         }
1489
1490                         return false;
1491                 },
1492
1493                 widget: function(sid)
1494                 {
1495                         this.options.optional = true;
1496
1497                         var v = this.ucivalue(sid);
1498
1499                         if (!$.isArray(v))
1500                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
1501
1502                         var d = $('<div />')
1503                                 .attr('id', this.id(sid))
1504                                 .addClass('cbi-input-dynlist');
1505
1506                         this._redraw(NaN, -1, -1, {
1507                                 self:      this,
1508                                 parent:    d[0],
1509                                 values:    v,
1510                                 sid:       sid
1511                         });
1512
1513                         return d;
1514                 },
1515
1516                 ucivalue: function(sid)
1517                 {
1518                         var v = this.callSuper('ucivalue', sid);
1519
1520                         if (!$.isArray(v))
1521                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
1522
1523                         return v;
1524                 },
1525
1526                 formvalue: function(sid)
1527                 {
1528                         var rv = [ ];
1529                         var fields = $('#' + this.id(sid) + ' input');
1530
1531                         for (var i = 0; i < fields.length; i++)
1532                                 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
1533                                         rv.push(fields[i].value);
1534
1535                         return rv;
1536                 }
1537         });
1538
1539         cbi_class.DummyValue = cbi_class.AbstractValue.extend({
1540                 widget: function(sid)
1541                 {
1542                         return $('<div />')
1543                                 .addClass('form-control-static')
1544                                 .attr('id', this.id(sid))
1545                                 .html(this.ucivalue(sid) || this.label('placeholder'));
1546                 },
1547
1548                 formvalue: function(sid)
1549                 {
1550                         return this.ucivalue(sid);
1551                 }
1552         });
1553
1554         cbi_class.ButtonValue = cbi_class.AbstractValue.extend({
1555                 widget: function(sid)
1556                 {
1557                         this.options.optional = true;
1558
1559                         var btn = $('<button />')
1560                                 .addClass('btn btn-default')
1561                                 .attr('id', this.id(sid))
1562                                 .attr('type', 'button')
1563                                 .text(this.label('text'));
1564
1565                         return this.attachEvents(sid, btn);
1566                 }
1567         });
1568
1569         cbi_class.NetworkList = cbi_class.AbstractValue.extend({
1570                 load: function(sid)
1571                 {
1572                         return L.network.load();
1573                 },
1574
1575                 _device_icon: function(dev)
1576                 {
1577                         return $('<img />')
1578                                 .attr('src', dev.icon())
1579                                 .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
1580                 },
1581
1582                 widget: function(sid)
1583                 {
1584                         var id = this.id(sid);
1585                         var ul = $('<ul />')
1586                                 .attr('id', id)
1587                                 .addClass('list-unstyled');
1588
1589                         var itype = this.options.multiple ? 'checkbox' : 'radio';
1590                         var value = this.ucivalue(sid);
1591                         var check = { };
1592
1593                         if (!this.options.multiple)
1594                                 check[value] = true;
1595                         else
1596                                 for (var i = 0; i < value.length; i++)
1597                                         check[value[i]] = true;
1598
1599                         var interfaces = L.network.getInterfaces();
1600
1601                         for (var i = 0; i < interfaces.length; i++)
1602                         {
1603                                 var iface = interfaces[i];
1604
1605                                 $('<li />')
1606                                         .append($('<label />')
1607                                                 .addClass(itype + ' inline')
1608                                                 .append(this.attachEvents(sid, $('<input />')
1609                                                         .attr('name', itype + id)
1610                                                         .attr('type', itype)
1611                                                         .attr('value', iface.name())
1612                                                         .prop('checked', !!check[iface.name()])))
1613                                                 .append(iface.renderBadge()))
1614                                         .appendTo(ul);
1615                         }
1616
1617                         if (!this.options.multiple)
1618                         {
1619                                 $('<li />')
1620                                         .append($('<label />')
1621                                                 .addClass(itype + ' inline text-muted')
1622                                                 .append(this.attachEvents(sid, $('<input />')
1623                                                         .attr('name', itype + id)
1624                                                         .attr('type', itype)
1625                                                         .attr('value', '')
1626                                                         .prop('checked', $.isEmptyObject(check))))
1627                                                 .append(L.tr('unspecified')))
1628                                         .appendTo(ul);
1629                         }
1630
1631                         return ul;
1632                 },
1633
1634                 ucivalue: function(sid)
1635                 {
1636                         var v = this.callSuper('ucivalue', sid);
1637
1638                         if (!this.options.multiple)
1639                         {
1640                                 if ($.isArray(v))
1641                                 {
1642                                         return v[0];
1643                                 }
1644                                 else if (typeof(v) == 'string')
1645                                 {
1646                                         v = v.match(/\S+/);
1647                                         return v ? v[0] : undefined;
1648                                 }
1649
1650                                 return v;
1651                         }
1652                         else
1653                         {
1654                                 if (typeof(v) == 'string')
1655                                         v = v.match(/\S+/g);
1656
1657                                 return v || [ ];
1658                         }
1659                 },
1660
1661                 formvalue: function(sid)
1662                 {
1663                         var inputs = $('#' + this.id(sid) + ' input');
1664
1665                         if (!this.options.multiple)
1666                         {
1667                                 for (var i = 0; i < inputs.length; i++)
1668                                         if (inputs[i].checked && inputs[i].value !== '')
1669                                                 return inputs[i].value;
1670
1671                                 return undefined;
1672                         }
1673
1674                         var rv = [ ];
1675
1676                         for (var i = 0; i < inputs.length; i++)
1677                                 if (inputs[i].checked)
1678                                         rv.push(inputs[i].value);
1679
1680                         return rv.length ? rv : undefined;
1681                 }
1682         });
1683
1684         cbi_class.DeviceList = cbi_class.NetworkList.extend({
1685                 handleFocus: function(ev)
1686                 {
1687                         var self = ev.data.self;
1688                         var input = $(this);
1689
1690                         input.parent().prev().prop('checked', true);
1691                 },
1692
1693                 handleBlur: function(ev)
1694                 {
1695                         ev.which = 10;
1696                         ev.data.self.handleKeydown.call(this, ev);
1697                 },
1698
1699                 handleKeydown: function(ev)
1700                 {
1701                         if (ev.which != 10 && ev.which != 13)
1702                                 return;
1703
1704                         var sid = ev.data.sid;
1705                         var self = ev.data.self;
1706                         var input = $(this);
1707                         var ifnames = L.toArray(input.val());
1708
1709                         if (!ifnames.length)
1710                                 return;
1711
1712                         L.network.createDevice(ifnames[0]);
1713
1714                         self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
1715                 },
1716
1717                 load: function(sid)
1718                 {
1719                         return L.network.load();
1720                 },
1721
1722                 _redraw: function(sid, ul, sel)
1723                 {
1724                         var id = ul.attr('id');
1725                         var devs = L.network.getDevices();
1726                         var iface = L.network.getInterface(sid);
1727                         var itype = this.options.multiple ? 'checkbox' : 'radio';
1728                         var check = { };
1729
1730                         if (!sel)
1731                         {
1732                                 for (var i = 0; i < devs.length; i++)
1733                                         if (devs[i].isInNetwork(iface))
1734                                                 check[devs[i].name()] = true;
1735                         }
1736                         else
1737                         {
1738                                 if (this.options.multiple)
1739                                         check = L.toObject(this.formvalue(sid));
1740
1741                                 check[sel] = true;
1742                         }
1743
1744                         ul.empty();
1745
1746                         for (var i = 0; i < devs.length; i++)
1747                         {
1748                                 var dev = devs[i];
1749
1750                                 if (dev.isBridge() && this.options.bridges === false)
1751                                         continue;
1752
1753                                 if (!dev.isBridgeable() && this.options.multiple)
1754                                         continue;
1755
1756                                 var badge = $('<span />')
1757                                         .addClass('badge')
1758                                         .append($('<img />').attr('src', dev.icon()))
1759                                         .append(' %s: %s'.format(dev.name(), dev.description()));
1760
1761                                 //var ifcs = dev.getInterfaces();
1762                                 //if (ifcs.length)
1763                                 //{
1764                                 //      for (var j = 0; j < ifcs.length; j++)
1765                                 //              badge.append((j ? ', ' : ' (') + ifcs[j].name());
1766                                 //
1767                                 //      badge.append(')');
1768                                 //}
1769
1770                                 $('<li />')
1771                                         .append($('<label />')
1772                                                 .addClass(itype + ' inline')
1773                                                 .append($('<input />')
1774                                                         .attr('name', itype + id)
1775                                                         .attr('type', itype)
1776                                                         .attr('value', dev.name())
1777                                                         .prop('checked', !!check[dev.name()]))
1778                                                 .append(badge))
1779                                         .appendTo(ul);
1780                         }
1781
1782
1783                         $('<li />')
1784                                 .append($('<label />')
1785                                         .attr('for', 'custom' + id)
1786                                         .addClass(itype + ' inline')
1787                                         .append($('<input />')
1788                                                 .attr('name', itype + id)
1789                                                 .attr('type', itype)
1790                                                 .attr('value', ''))
1791                                         .append($('<span />')
1792                                                 .addClass('badge')
1793                                                 .append($('<input />')
1794                                                         .attr('id', 'custom' + id)
1795                                                         .attr('type', 'text')
1796                                                         .attr('placeholder', L.tr('Custom device …'))
1797                                                         .on('focus', { self: this, sid: sid }, this.handleFocus)
1798                                                         .on('blur', { self: this, sid: sid }, this.handleBlur)
1799                                                         .on('keydown', { self: this, sid: sid }, this.handleKeydown))))
1800                                 .appendTo(ul);
1801
1802                         if (!this.options.multiple)
1803                         {
1804                                 $('<li />')
1805                                         .append($('<label />')
1806                                                 .addClass(itype + ' inline text-muted')
1807                                                 .append($('<input />')
1808                                                         .attr('name', itype + id)
1809                                                         .attr('type', itype)
1810                                                         .attr('value', '')
1811                                                         .prop('checked', $.isEmptyObject(check)))
1812                                                 .append(L.tr('unspecified')))
1813                                         .appendTo(ul);
1814                         }
1815                 },
1816
1817                 widget: function(sid)
1818                 {
1819                         var id = this.id(sid);
1820                         var ul = $('<ul />')
1821                                 .attr('id', id)
1822                                 .addClass('list-unstyled');
1823
1824                         this._redraw(sid, ul);
1825
1826                         return ul;
1827                 },
1828
1829                 save: function(sid)
1830                 {
1831                         if (this.instance[sid].disabled)
1832                                 return;
1833
1834                         var ifnames = this.formvalue(sid);
1835                         //if (!ifnames)
1836                         //      return;
1837
1838                         var iface = L.network.getInterface(sid);
1839                         if (!iface)
1840                                 return;
1841
1842                         iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
1843                 }
1844         });
1845
1846
1847         cbi_class.AbstractSection = L.ui.AbstractWidget.extend({
1848                 id: function()
1849                 {
1850                         var s = [ arguments[0], this.ownerMap.uci_package, this.uci_type ];
1851
1852                         for (var i = 1; i < arguments.length && typeof(arguments[i]) == 'string'; i++)
1853                                 s.push(arguments[i].replace(/\./g, '_'));
1854
1855                         return s.join('_');
1856                 },
1857
1858                 option: function(widget, name, options)
1859                 {
1860                         if (this.tabs.length == 0)
1861                                 this.tab({ id: '__default__', selected: true });
1862
1863                         return this.taboption('__default__', widget, name, options);
1864                 },
1865
1866                 tab: function(options)
1867                 {
1868                         if (options.selected)
1869                                 this.tabs.selected = this.tabs.length;
1870
1871                         this.tabs.push({
1872                                 id:          options.id,
1873                                 caption:     options.caption,
1874                                 description: options.description,
1875                                 fields:      [ ],
1876                                 li:          { }
1877                         });
1878                 },
1879
1880                 taboption: function(tabid, widget, name, options)
1881                 {
1882                         var tab;
1883                         for (var i = 0; i < this.tabs.length; i++)
1884                         {
1885                                 if (this.tabs[i].id == tabid)
1886                                 {
1887                                         tab = this.tabs[i];
1888                                         break;
1889                                 }
1890                         }
1891
1892                         if (!tab)
1893                                 throw 'Cannot append to unknown tab ' + tabid;
1894
1895                         var w = widget ? new widget(name, options) : null;
1896
1897                         if (!(w instanceof L.cbi.AbstractValue))
1898                                 throw 'Widget must be an instance of AbstractValue';
1899
1900                         w.ownerSection = this;
1901                         w.ownerMap     = this.ownerMap;
1902
1903                         this.fields[name] = w;
1904                         tab.fields.push(w);
1905
1906                         return w;
1907                 },
1908
1909                 tabtoggle: function(sid)
1910                 {
1911                         for (var i = 0; i < this.tabs.length; i++)
1912                         {
1913                                 var tab = this.tabs[i];
1914                                 var elem = $('#' + this.id('nodetab', sid, tab.id));
1915                                 var empty = true;
1916
1917                                 for (var j = 0; j < tab.fields.length; j++)
1918                                 {
1919                                         if (tab.fields[j].active(sid))
1920                                         {
1921                                                 empty = false;
1922                                                 break;
1923                                         }
1924                                 }
1925
1926                                 if (empty && elem.is(':visible'))
1927                                         elem.fadeOut();
1928                                 else if (!empty)
1929                                         elem.fadeIn();
1930                         }
1931                 },
1932
1933                 validate: function(parent_sid)
1934                 {
1935                         var s = this.getUCISections(parent_sid);
1936                         var n = 0;
1937
1938                         for (var i = 0; i < s.length; i++)
1939                         {
1940                                 var $item = $('#' + this.id('sectionitem', s[i]['.name']));
1941
1942                                 $item.find('.luci2-field-validate').trigger('validate');
1943                                 n += $item.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
1944                         }
1945
1946                         return (n == 0);
1947                 },
1948
1949                 load: function(parent_sid)
1950                 {
1951                         var deferreds = [ ];
1952
1953                         var s = this.getUCISections(parent_sid);
1954                         for (var i = 0; i < s.length; i++)
1955                         {
1956                                 for (var f in this.fields)
1957                                 {
1958                                         if (typeof(this.fields[f].load) != 'function')
1959                                                 continue;
1960
1961                                         var rv = this.fields[f].load(s[i]['.name']);
1962                                         if (L.isDeferred(rv))
1963                                                 deferreds.push(rv);
1964                                 }
1965
1966                                 for (var j = 0; j < this.subsections.length; j++)
1967                                 {
1968                                         var rv = this.subsections[j].load(s[i]['.name']);
1969                                         deferreds.push.apply(deferreds, rv);
1970                                 }
1971                         }
1972
1973                         return deferreds;
1974                 },
1975
1976                 save: function(parent_sid)
1977                 {
1978                         var deferreds = [ ];
1979                         var s = this.getUCISections(parent_sid);
1980
1981                         for (i = 0; i < s.length; i++)
1982                         {
1983                                 if (!this.options.readonly)
1984                                 {
1985                                         for (var f in this.fields)
1986                                         {
1987                                                 if (typeof(this.fields[f].save) != 'function')
1988                                                         continue;
1989
1990                                                 var rv = this.fields[f].save(s[i]['.name']);
1991                                                 if (L.isDeferred(rv))
1992                                                         deferreds.push(rv);
1993                                         }
1994                                 }
1995
1996                                 for (var j = 0; j < this.subsections.length; j++)
1997                                 {
1998                                         var rv = this.subsections[j].save(s[i]['.name']);
1999                                         deferreds.push.apply(deferreds, rv);
2000                                 }
2001                         }
2002
2003                         return deferreds;
2004                 },
2005
2006                 teaser: function(sid)
2007                 {
2008                         var tf = this.teaser_fields;
2009
2010                         if (!tf)
2011                         {
2012                                 tf = this.teaser_fields = [ ];
2013
2014                                 if ($.isArray(this.options.teasers))
2015                                 {
2016                                         for (var i = 0; i < this.options.teasers.length; i++)
2017                                         {
2018                                                 var f = this.options.teasers[i];
2019                                                 if (f instanceof L.cbi.AbstractValue)
2020                                                         tf.push(f);
2021                                                 else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
2022                                                         tf.push(this.fields[f]);
2023                                         }
2024                                 }
2025                                 else
2026                                 {
2027                                         for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
2028                                                 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
2029                                                         tf.push(this.tabs[i].fields[j]);
2030                                 }
2031                         }
2032
2033                         var t = '';
2034
2035                         for (var i = 0; i < tf.length; i++)
2036                         {
2037                                 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
2038                                         continue;
2039
2040                                 var n = tf[i].options.caption || tf[i].name;
2041                                 var v = tf[i].textvalue(sid);
2042
2043                                 if (typeof(v) == 'undefined')
2044                                         continue;
2045
2046                                 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
2047                         }
2048
2049                         return t;
2050                 },
2051
2052                 findAdditionalUCIPackages: function()
2053                 {
2054                         var packages = [ ];
2055
2056                         for (var i = 0; i < this.tabs.length; i++)
2057                                 for (var j = 0; j < this.tabs[i].fields.length; j++)
2058                                         if (this.tabs[i].fields[j].options.uci_package)
2059                                                 packages.push(this.tabs[i].fields[j].options.uci_package);
2060
2061                         return packages;
2062                 },
2063
2064                 findParentSectionIDs: function($elem)
2065                 {
2066                         var rv = [ ];
2067                         var $parents = $elem.parents('.luci2-section-item');
2068
2069                         for (var i = 0; i < $parents.length; i++)
2070                                 rv.push($parents[i].getAttribute('data-luci2-sid'));
2071
2072                         return rv;
2073                 }
2074         });
2075
2076         cbi_class.TypedSection = cbi_class.AbstractSection.extend({
2077                 init: function(uci_type, options)
2078                 {
2079                         this.uci_type = uci_type;
2080                         this.options  = options;
2081                         this.tabs     = [ ];
2082                         this.fields   = { };
2083                         this.subsections  = [ ];
2084                         this.active_panel = { };
2085                         this.active_tab   = { };
2086
2087                         this.instance = { };
2088                 },
2089
2090                 filter: function(section, parent_sid)
2091                 {
2092                         return true;
2093                 },
2094
2095                 sort: function(section1, section2)
2096                 {
2097                         return 0;
2098                 },
2099
2100                 subsection: function(widget, uci_type, options)
2101                 {
2102                         var w = widget ? new widget(uci_type, options) : null;
2103
2104                         if (!(w instanceof L.cbi.AbstractSection))
2105                                 throw 'Widget must be an instance of AbstractSection';
2106
2107                         w.ownerSection = this;
2108                         w.ownerMap     = this.ownerMap;
2109                         w.index        = this.subsections.length;
2110
2111                         this.subsections.push(w);
2112                         return w;
2113                 },
2114
2115                 getUCISections: function(parent_sid)
2116                 {
2117                         var s1 = L.uci.sections(this.ownerMap.uci_package);
2118                         var s2 = [ ];
2119
2120                         for (var i = 0; i < s1.length; i++)
2121                                 if (s1[i]['.type'] == this.uci_type)
2122                                         if (this.filter(s1[i], parent_sid))
2123                                                 s2.push(s1[i]);
2124
2125                         s2.sort(this.sort);
2126
2127                         return s2;
2128                 },
2129
2130                 add: function(name, parent_sid)
2131                 {
2132                         return this.ownerMap.add(this.ownerMap.uci_package, this.uci_type, name);
2133                 },
2134
2135                 remove: function(sid, parent_sid)
2136                 {
2137                         return this.ownerMap.remove(this.ownerMap.uci_package, sid);
2138                 },
2139
2140                 handleAdd: function(ev)
2141                 {
2142                         var addb = $(this);
2143                         var name = undefined;
2144                         var self = ev.data.self;
2145                         var sid  = self.findParentSectionIDs(addb)[0];
2146
2147                         if (addb.prev().prop('nodeName') == 'INPUT')
2148                                 name = addb.prev().val();
2149
2150                         if (addb.prop('disabled') || name === '')
2151                                 return;
2152
2153                         L.ui.saveScrollTop();
2154
2155                         self.setPanelIndex(sid, -1);
2156                         self.ownerMap.save();
2157
2158                         ev.data.sid  = self.add(name, sid);
2159                         ev.data.type = self.uci_type;
2160                         ev.data.name = name;
2161
2162                         self.trigger('add', ev);
2163
2164                         self.ownerMap.redraw();
2165
2166                         L.ui.restoreScrollTop();
2167                 },
2168
2169                 handleRemove: function(ev)
2170                 {
2171                         var self = ev.data.self;
2172                         var sids = self.findParentSectionIDs($(this));
2173
2174                         if (sids.length)
2175                         {
2176                                 L.ui.saveScrollTop();
2177
2178                                 ev.sid = sids[0];
2179                                 ev.parent_sid = sids[1];
2180
2181                                 self.trigger('remove', ev);
2182
2183                                 self.ownerMap.save();
2184                                 self.remove(ev.sid, ev.parent_sid);
2185                                 self.ownerMap.redraw();
2186
2187                                 L.ui.restoreScrollTop();
2188                         }
2189
2190                         ev.stopPropagation();
2191                 },
2192
2193                 handleSID: function(ev)
2194                 {
2195                         var self = ev.data.self;
2196                         var text = $(this);
2197                         var addb = text.next();
2198                         var errt = addb.next();
2199                         var name = text.val();
2200
2201                         if (!/^[a-zA-Z0-9_]*$/.test(name))
2202                         {
2203                                 errt.text(L.tr('Invalid section name')).show();
2204                                 text.addClass('error');
2205                                 addb.prop('disabled', true);
2206                                 return false;
2207                         }
2208
2209                         if (L.uci.get(self.ownerMap.uci_package, name))
2210                         {
2211                                 errt.text(L.tr('Name already used')).show();
2212                                 text.addClass('error');
2213                                 addb.prop('disabled', true);
2214                                 return false;
2215                         }
2216
2217                         errt.text('').hide();
2218                         text.removeClass('error');
2219                         addb.prop('disabled', false);
2220                         return true;
2221                 },
2222
2223                 handleTab: function(ev)
2224                 {
2225                         var self = ev.data.self;
2226                         var $tab = $(this);
2227                         var sid  = self.findParentSectionIDs($tab)[0];
2228
2229                         self.active_tab[sid] = $tab.parent().index();
2230                 },
2231
2232                 handleTabValidate: function(ev)
2233                 {
2234                         var $pane = $(ev.delegateTarget);
2235                         var $badge = $pane.parent()
2236                                 .children('.nav-tabs')
2237                                 .children('li')
2238                                 .eq($pane.index() - 1) // item #1 is the <ul>
2239                                 .find('.badge:first');
2240
2241                         var err_count = $pane.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
2242                         if (err_count > 0)
2243                                 $badge
2244                                         .text(err_count)
2245                                         .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
2246                                         .show();
2247                         else
2248                                 $badge.hide();
2249                 },
2250
2251                 handlePanelValidate: function(ev)
2252                 {
2253                         var $elem = $(this);
2254                         var $badge = $elem
2255                                 .prevAll('.luci2-section-header:first')
2256                                 .children('.luci2-section-teaser')
2257                                 .find('.badge:first');
2258
2259                         var err_count = $elem.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
2260                         if (err_count > 0)
2261                                 $badge
2262                                         .text(err_count)
2263                                         .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
2264                                         .show();
2265                         else
2266                                 $badge.hide();
2267                 },
2268
2269                 handlePanelCollapse: function(ev)
2270                 {
2271                         var self = ev.data.self;
2272
2273                         var $items = $(ev.delegateTarget).children('.luci2-section-item');
2274
2275                         var $this_panel  = $(ev.target);
2276                         var $this_teaser = $this_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
2277
2278                         var $prev_panel  = $items.children('.luci2-section-panel.in');
2279                         var $prev_teaser = $prev_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
2280
2281                         var sids = self.findParentSectionIDs($prev_panel);
2282
2283                         self.setPanelIndex(sids[1], $this_panel.parent().index());
2284
2285                         $prev_panel
2286                                 .removeClass('in')
2287                                 .addClass('collapse');
2288
2289                         $prev_teaser
2290                                 .show()
2291                                 .children('span:last')
2292                                 .empty()
2293                                 .append(self.teaser(sids[0]));
2294
2295                         $this_teaser
2296                                 .hide();
2297
2298                         ev.stopPropagation();
2299                 },
2300
2301                 handleSort: function(ev)
2302                 {
2303                         var self = ev.data.self;
2304
2305                         var $item = $(this).parents('.luci2-section-item:first');
2306                         var $next = ev.data.up ? $item.prev() : $item.next();
2307
2308                         if ($item.length && $next.length)
2309                         {
2310                                 var cur_sid = $item.attr('data-luci2-sid');
2311                                 var new_sid = $next.attr('data-luci2-sid');
2312
2313                                 L.uci.swap(self.ownerMap.uci_package, cur_sid, new_sid);
2314
2315                                 self.ownerMap.save();
2316                                 self.ownerMap.redraw();
2317                         }
2318
2319                         ev.stopPropagation();
2320                 },
2321
2322                 getPanelIndex: function(parent_sid)
2323                 {
2324                         return (this.active_panel[parent_sid || '__top__'] || 0);
2325                 },
2326
2327                 setPanelIndex: function(parent_sid, new_index)
2328                 {
2329                         if (typeof(new_index) == 'number')
2330                                 this.active_panel[parent_sid || '__top__'] = new_index;
2331                 },
2332
2333                 renderAdd: function()
2334                 {
2335                         if (!this.options.addremove)
2336                                 return null;
2337
2338                         var text = L.tr('Add section');
2339                         var ttip = L.tr('Create new section...');
2340
2341                         if ($.isArray(this.options.add_caption))
2342                                 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
2343                         else if (typeof(this.options.add_caption) == 'string')
2344                                 text = this.options.add_caption, ttip = '';
2345
2346                         var add = $('<div />');
2347
2348                         if (this.options.anonymous === false)
2349                         {
2350                                 $('<input />')
2351                                         .addClass('cbi-input-text')
2352                                         .attr('type', 'text')
2353                                         .attr('placeholder', ttip)
2354                                         .blur({ self: this }, this.handleSID)
2355                                         .keyup({ self: this }, this.handleSID)
2356                                         .appendTo(add);
2357
2358                                 $('<img />')
2359                                         .attr('src', L.globals.resource + '/icons/cbi/add.gif')
2360                                         .attr('title', text)
2361                                         .addClass('cbi-button')
2362                                         .click({ self: this }, this.handleAdd)
2363                                         .appendTo(add);
2364
2365                                 $('<div />')
2366                                         .addClass('cbi-value-error')
2367                                         .hide()
2368                                         .appendTo(add);
2369                         }
2370                         else
2371                         {
2372                                 L.ui.button(text, 'success', ttip)
2373                                         .click({ self: this }, this.handleAdd)
2374                                         .appendTo(add);
2375                         }
2376
2377                         return add;
2378                 },
2379
2380                 renderRemove: function(index)
2381                 {
2382                         if (!this.options.addremove)
2383                                 return null;
2384
2385                         var text = L.tr('Remove');
2386                         var ttip = L.tr('Remove this section');
2387
2388                         if ($.isArray(this.options.remove_caption))
2389                                 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
2390                         else if (typeof(this.options.remove_caption) == 'string')
2391                                 text = this.options.remove_caption, ttip = '';
2392
2393                         return L.ui.button(text, 'danger', ttip)
2394                                 .click({ self: this, index: index }, this.handleRemove);
2395                 },
2396
2397                 renderSort: function(index)
2398                 {
2399                         if (!this.options.sortable)
2400                                 return null;
2401
2402                         var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
2403                                 .click({ self: this, index: index, up: true }, this.handleSort);
2404
2405                         var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
2406                                 .click({ self: this, index: index, up: false }, this.handleSort);
2407
2408                         return b1.add(b2);
2409                 },
2410
2411                 renderCaption: function()
2412                 {
2413                         return $('<h3 />')
2414                                 .addClass('panel-title')
2415                                 .append(this.label('caption') || this.uci_type);
2416                 },
2417
2418                 renderDescription: function()
2419                 {
2420                         var text = this.label('description');
2421
2422                         if (text)
2423                                 return $('<div />')
2424                                         .addClass('luci2-section-description')
2425                                         .text(text);
2426
2427                         return null;
2428                 },
2429
2430                 renderTeaser: function(sid, index)
2431                 {
2432                         if (this.options.collabsible || this.ownerMap.options.collabsible)
2433                         {
2434                                 return $('<div />')
2435                                         .attr('id', this.id('teaser', sid))
2436                                         .addClass('luci2-section-teaser well well-sm')
2437                                         .append($('<span />')
2438                                                 .addClass('badge'))
2439                                         .append($('<span />'));
2440                         }
2441
2442                         return null;
2443                 },
2444
2445                 renderHead: function(condensed)
2446                 {
2447                         if (condensed)
2448                                 return null;
2449
2450                         return $('<div />')
2451                                 .addClass('panel-heading')
2452                                 .append(this.renderCaption())
2453                                 .append(this.renderDescription());
2454                 },
2455
2456                 renderTabDescription: function(sid, index, tab_index)
2457                 {
2458                         var tab = this.tabs[tab_index];
2459
2460                         if (typeof(tab.description) == 'string')
2461                         {
2462                                 return $('<div />')
2463                                         .addClass('cbi-tab-descr')
2464                                         .text(tab.description);
2465                         }
2466
2467                         return null;
2468                 },
2469
2470                 renderTabHead: function(sid, index, tab_index)
2471                 {
2472                         var tab = this.tabs[tab_index];
2473                         var cur = this.active_tab[sid] || 0;
2474
2475                         var tabh = $('<li />')
2476                                 .append($('<a />')
2477                                         .attr('id', this.id('nodetab', sid, tab.id))
2478                                         .attr('href', '#' + this.id('node', sid, tab.id))
2479                                         .attr('data-toggle', 'tab')
2480                                         .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
2481                                         .append($('<span />')
2482                                                 .addClass('badge'))
2483                                         .on('shown.bs.tab', { self: this, sid: sid }, this.handleTab));
2484
2485                         if (cur == tab_index)
2486                                 tabh.addClass('active');
2487
2488                         if (!tab.fields.length)
2489                                 tabh.hide();
2490
2491                         return tabh;
2492                 },
2493
2494                 renderTabBody: function(sid, index, tab_index)
2495                 {
2496                         var tab = this.tabs[tab_index];
2497                         var cur = this.active_tab[sid] || 0;
2498
2499                         var tabb = $('<div />')
2500                                 .addClass('tab-pane')
2501                                 .attr('id', this.id('node', sid, tab.id))
2502                                 .append(this.renderTabDescription(sid, index, tab_index))
2503                                 .on('validate', this.handleTabValidate);
2504
2505                         if (cur == tab_index)
2506                                 tabb.addClass('active');
2507
2508                         for (var i = 0; i < tab.fields.length; i++)
2509                                 tabb.append(tab.fields[i].render(sid));
2510
2511                         return tabb;
2512                 },
2513
2514                 renderPanelHead: function(sid, index, parent_sid)
2515                 {
2516                         var head = $('<div />')
2517                                 .addClass('luci2-section-header')
2518                                 .append(this.renderTeaser(sid, index))
2519                                 .append($('<div />')
2520                                         .addClass('btn-group')
2521                                         .append(this.renderSort(index))
2522                                         .append(this.renderRemove(index)));
2523
2524                         if (this.options.collabsible)
2525                         {
2526                                 head.attr('data-toggle', 'collapse')
2527                                         .attr('data-parent', this.id('sectiongroup', parent_sid))
2528                                         .attr('data-target', '#' + this.id('panel', sid));
2529                         }
2530
2531                         return head;
2532                 },
2533
2534                 renderPanelBody: function(sid, index, parent_sid)
2535                 {
2536                         var body = $('<div />')
2537                                 .attr('id', this.id('panel', sid))
2538                                 .addClass('luci2-section-panel')
2539                                 .on('validate', this.handlePanelValidate);
2540
2541                         if (this.options.collabsible || this.ownerMap.options.collabsible)
2542                         {
2543                                 body.addClass('panel-collapse collapse');
2544
2545                                 if (index == this.getPanelIndex(parent_sid))
2546                                         body.addClass('in');
2547                         }
2548
2549                         var tab_heads = $('<ul />')
2550                                 .addClass('nav nav-tabs');
2551
2552                         var tab_bodies = $('<div />')
2553                                 .addClass('form-horizontal tab-content')
2554                                 .append(tab_heads);
2555
2556                         for (var j = 0; j < this.tabs.length; j++)
2557                         {
2558                                 tab_heads.append(this.renderTabHead(sid, index, j));
2559                                 tab_bodies.append(this.renderTabBody(sid, index, j));
2560                         }
2561
2562                         body.append(tab_bodies);
2563
2564                         if (this.tabs.length <= 1)
2565                                 tab_heads.hide();
2566
2567                         for (var i = 0; i < this.subsections.length; i++)
2568                                 body.append(this.subsections[i].render(false, sid));
2569
2570                         return body;
2571                 },
2572
2573                 renderBody: function(condensed, parent_sid)
2574                 {
2575                         var s = this.getUCISections(parent_sid);
2576                         var n = this.getPanelIndex(parent_sid);
2577
2578                         if (n < 0)
2579                                 this.setPanelIndex(parent_sid, n + s.length);
2580                         else if (n >= s.length)
2581                                 this.setPanelIndex(parent_sid, s.length - 1);
2582
2583                         var body = $('<ul />')
2584                                 .addClass('luci2-section-group list-group');
2585
2586                         if (this.options.collabsible)
2587                         {
2588                                 body.attr('id', this.id('sectiongroup', parent_sid))
2589                                         .on('show.bs.collapse', { self: this }, this.handlePanelCollapse);
2590                         }
2591
2592                         if (s.length == 0)
2593                         {
2594                                 body.append($('<li />')
2595                                         .addClass('list-group-item text-muted')
2596                                         .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
2597                         }
2598
2599                         for (var i = 0; i < s.length; i++)
2600                         {
2601                                 var sid = s[i]['.name'];
2602                                 var inst = this.instance[sid] = { tabs: [ ] };
2603
2604                                 body.append($('<li />')
2605                                         .addClass('luci2-section-item list-group-item')
2606                                         .attr('id', this.id('sectionitem', sid))
2607                                         .attr('data-luci2-sid', sid)
2608                                         .append(this.renderPanelHead(sid, i, parent_sid))
2609                                         .append(this.renderPanelBody(sid, i, parent_sid)));
2610                         }
2611
2612                         return body;
2613                 },
2614
2615                 render: function(condensed, parent_sid)
2616                 {
2617                         this.instance = { };
2618
2619                         var panel = $('<div />')
2620                                 .addClass('panel panel-default')
2621                                 .append(this.renderHead(condensed))
2622                                 .append(this.renderBody(condensed, parent_sid));
2623
2624                         if (this.options.addremove)
2625                                 panel.append($('<div />')
2626                                         .addClass('panel-footer')
2627                                         .append(this.renderAdd()));
2628
2629                         return panel;
2630                 },
2631
2632                 finish: function(parent_sid)
2633                 {
2634                         var s = this.getUCISections(parent_sid);
2635
2636                         for (var i = 0; i < s.length; i++)
2637                         {
2638                                 var sid = s[i]['.name'];
2639
2640                                 if (i != this.getPanelIndex(parent_sid))
2641                                         $('#' + this.id('teaser', sid)).children('span:last')
2642                                                 .append(this.teaser(sid));
2643                                 else
2644                                         $('#' + this.id('teaser', sid))
2645                                                 .hide();
2646
2647                                 for (var j = 0; j < this.subsections.length; j++)
2648                                         this.subsections[j].finish(sid);
2649                         }
2650                 }
2651         });
2652
2653         cbi_class.TableSection = cbi_class.TypedSection.extend({
2654                 renderTableHead: function()
2655                 {
2656                         var thead = $('<thead />')
2657                                 .append($('<tr />')
2658                                         .addClass('cbi-section-table-titles'));
2659
2660                         for (var j = 0; j < this.tabs[0].fields.length; j++)
2661                                 thead.children().append($('<th />')
2662                                         .addClass('cbi-section-table-cell')
2663                                         .css('width', this.tabs[0].fields[j].options.width || '')
2664                                         .append(this.tabs[0].fields[j].label('caption')));
2665
2666                         if (this.options.addremove !== false || this.options.sortable)
2667                                 thead.children().append($('<th />')
2668                                         .addClass('cbi-section-table-cell')
2669                                         .text(' '));
2670
2671                         return thead;
2672                 },
2673
2674                 renderTableRow: function(sid, index)
2675                 {
2676                         var row = $('<tr />')
2677                                 .addClass('luci2-section-item')
2678                                 .attr('id', this.id('sectionitem', sid))
2679                                 .attr('data-luci2-sid', sid);
2680
2681                         for (var j = 0; j < this.tabs[0].fields.length; j++)
2682                         {
2683                                 row.append($('<td />')
2684                                         .css('width', this.tabs[0].fields[j].options.width || '')
2685                                         .append(this.tabs[0].fields[j].render(sid, true)));
2686                         }
2687
2688                         if (this.options.addremove !== false || this.options.sortable)
2689                         {
2690                                 row.append($('<td />')
2691                                         .css('width', '1%')
2692                                         .addClass('text-right')
2693                                         .append($('<div />')
2694                                                 .addClass('btn-group')
2695                                                 .append(this.renderSort(index))
2696                                                 .append(this.renderRemove(index))));
2697                         }
2698
2699                         return row;
2700                 },
2701
2702                 renderTableBody: function(parent_sid)
2703                 {
2704                         var s = this.getUCISections(parent_sid);
2705
2706                         var tbody = $('<tbody />');
2707
2708                         if (s.length == 0)
2709                         {
2710