cfc677ddd291d54056c5b7eebb0142c145ab408e
[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 var cols = this.tabs[0].fields.length;
2711
2712 if (this.options.addremove !== false || this.options.sortable)
2713 cols++;
2714
2715 tbody.append($('<tr />')
2716 .append($('<td />')
2717 .addClass('text-muted')
2718 .attr('colspan', cols)
2719 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
2720 }
2721
2722 for (var i = 0; i < s.length; i++)
2723 {
2724 var sid = s[i]['.name'];
2725 var inst = this.instance[sid] = { tabs: [ ] };
2726
2727 tbody.append(this.renderTableRow(sid, i));
2728 }
2729
2730 return tbody;
2731 },
2732
2733 renderBody: function(condensed, parent_sid)
2734 {
2735 return $('<table />')
2736 .addClass('table table-condensed table-hover')
2737 .append(this.renderTableHead())
2738 .append(this.renderTableBody(parent_sid));
2739 }
2740 });
2741
2742 cbi_class.NamedSection = cbi_class.TypedSection.extend({
2743 getUCISections: function(cb)
2744 {
2745 var sa = [ ];
2746 var sl = L.uci.sections(this.ownerMap.uci_package);
2747
2748 for (var i = 0; i < sl.length; i++)
2749 if (sl[i]['.name'] == this.uci_type)
2750 {
2751 sa.push(sl[i]);
2752 break;
2753 }
2754
2755 if (typeof(cb) == 'function' && sa.length > 0)
2756 cb.call(this, sa[0]);
2757
2758 return sa;
2759 }
2760 });
2761
2762 cbi_class.SingleSection = cbi_class.NamedSection.extend({
2763 render: function()
2764 {
2765 this.instance = { };
2766 this.instance[this.uci_type] = { tabs: [ ] };
2767
2768 return $('<div />')
2769 .addClass('luci2-section-item')
2770 .attr('id', this.id('sectionitem', this.uci_type))
2771 .attr('data-luci2-sid', this.uci_type)
2772 .append(this.renderPanelBody(this.uci_type, 0));
2773 }
2774 });
2775
2776 cbi_class.DummySection = cbi_class.TypedSection.extend({
2777 getUCISections: function(cb)
2778 {
2779 if (typeof(cb) == 'function')
2780 cb.apply(this, [ { '.name': this.uci_type } ]);
2781
2782 return [ { '.name': this.uci_type } ];
2783 }
2784 });
2785
2786 cbi_class.Map = L.ui.AbstractWidget.extend({
2787 init: function(uci_package, options)
2788 {
2789 var self = this;
2790
2791 this.uci_package = uci_package;
2792 this.sections = [ ];
2793 this.options = L.defaults(options, {
2794 save: function() { },
2795 prepare: function() { }
2796 });
2797 },
2798
2799 loadCallback: function()
2800 {
2801 var deferreds = [ L.deferrable(this.options.prepare.call(this)) ];
2802
2803 for (var i = 0; i < this.sections.length; i++)
2804 {
2805 var rv = this.sections[i].load();
2806 deferreds.push.apply(deferreds, rv);
2807 }
2808
2809 return $.when.apply($, deferreds);
2810 },
2811
2812 load: function()
2813 {
2814 var self = this;
2815 var packages = [ this.uci_package ];
2816
2817 for (var i = 0; i < this.sections.length; i++)
2818 packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
2819
2820 for (var i = 0; i < packages.length; i++)
2821 if (!L.uci.writable(packages[i]))
2822 {
2823 this.options.readonly = true;
2824 break;
2825 }
2826
2827 return L.uci.load(packages).then(function() {
2828 return self.loadCallback();
2829 });
2830 },
2831
2832 handleTab: function(ev)
2833 {
2834 ev.data.self.active_tab = $(ev.target).parent().index();
2835 },
2836
2837 handleApply: function(ev)
2838 {
2839 var self = ev.data.self;
2840
2841 self.trigger('apply', ev);
2842 },
2843
2844 handleSave: function(ev)
2845 {
2846 var self = ev.data.self;
2847
2848 self.send().then(function() {
2849 self.trigger('save', ev);
2850 });
2851 },
2852
2853 handleReset: function(ev)
2854 {
2855 var self = ev.data.self;
2856
2857 self.trigger('reset', ev);
2858 self.reset();
2859 },
2860
2861 renderTabHead: function(tab_index)
2862 {
2863 var section = this.sections[tab_index];
2864 var cur = this.active_tab || 0;
2865
2866 var tabh = $('<li />')
2867 .append($('<a />')
2868 .attr('id', section.id('sectiontab'))
2869 .attr('href', '#' + section.id('section'))
2870 .attr('data-toggle', 'tab')
2871 .text(section.label('caption') + ' ')
2872 .append($('<span />')
2873 .addClass('badge'))
2874 .on('shown.bs.tab', { self: this }, this.handleTab));
2875
2876 if (cur == tab_index)
2877 tabh.addClass('active');
2878
2879 return tabh;
2880 },
2881
2882 renderTabBody: function(tab_index)
2883 {
2884 var section = this.sections[tab_index];
2885 var desc = section.label('description');
2886 var cur = this.active_tab || 0;
2887
2888 var tabb = $('<div />')
2889 .addClass('tab-pane')
2890 .attr('id', section.id('section'));
2891
2892 if (cur == tab_index)
2893 tabb.addClass('active');
2894
2895 if (desc)
2896 tabb.append($('<p />')
2897 .text(desc));
2898
2899 var s = section.render(this.options.tabbed);
2900
2901 if (this.options.readonly || section.options.readonly)
2902 s.find('input, select, button, img.cbi-button').attr('disabled', true);
2903
2904 tabb.append(s);
2905
2906 return tabb;
2907 },
2908
2909 renderBody: function()
2910 {
2911 var tabs = $('<ul />')
2912 .addClass('nav nav-tabs');
2913
2914 var body = $('<div />')
2915 .append(tabs);
2916
2917 for (var i = 0; i < this.sections.length; i++)
2918 {
2919 tabs.append(this.renderTabHead(i));
2920 body.append(this.renderTabBody(i));
2921 }
2922
2923 if (this.options.tabbed)
2924 body.addClass('tab-content');
2925 else
2926 tabs.hide();
2927
2928 return body;
2929 },
2930
2931 renderFooter: function()
2932 {
2933 var evdata = {
2934 self: this
2935 };
2936
2937 return $('<div />')
2938 .addClass('panel panel-default panel-body text-right')
2939 .append($('<div />')
2940 .addClass('btn-group')
2941 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
2942 .click(evdata, this.handleApply))
2943 .append(L.ui.button(L.tr('Save'), 'default')
2944 .click(evdata, this.handleSave))
2945 .append(L.ui.button(L.tr('Reset'), 'default')
2946 .click(evdata, this.handleReset)));
2947 },
2948
2949 render: function()
2950 {
2951 var map = $('<form />');
2952
2953 if (typeof(this.options.caption) == 'string')
2954 map.append($('<h2 />')
2955 .text(this.options.caption));
2956
2957 if (typeof(this.options.description) == 'string')
2958 map.append($('<p />')
2959 .text(this.options.description));
2960
2961 map.append(this.renderBody());
2962
2963 if (this.options.pageaction !== false)
2964 map.append(this.renderFooter());
2965
2966 return map;
2967 },
2968
2969 finish: function()
2970 {
2971 for (var i = 0; i < this.sections.length; i++)
2972 this.sections[i].finish();
2973
2974 this.validate();
2975 },
2976
2977 redraw: function()
2978 {
2979 this.target.hide().empty().append(this.render());
2980 this.finish();
2981 this.target.show();
2982 },
2983
2984 section: function(widget, uci_type, options)
2985 {
2986 var w = widget ? new widget(uci_type, options) : null;
2987
2988 if (!(w instanceof L.cbi.AbstractSection))
2989 throw 'Widget must be an instance of AbstractSection';
2990
2991 w.ownerMap = this;
2992 w.index = this.sections.length;
2993
2994 this.sections.push(w);
2995 return w;
2996 },
2997
2998 add: function(conf, type, name)
2999 {
3000 return L.uci.add(conf, type, name);
3001 },
3002
3003 remove: function(conf, sid)
3004 {
3005 return L.uci.remove(conf, sid);
3006 },
3007
3008 get: function(conf, sid, opt)
3009 {
3010 return L.uci.get(conf, sid, opt);
3011 },
3012
3013 set: function(conf, sid, opt, val)
3014 {
3015 return L.uci.set(conf, sid, opt, val);
3016 },
3017
3018 validate: function()
3019 {
3020 var rv = true;
3021
3022 for (var i = 0; i < this.sections.length; i++)
3023 {
3024 if (!this.sections[i].validate())
3025 rv = false;
3026 }
3027
3028 return rv;
3029 },
3030
3031 save: function()
3032 {
3033 var self = this;
3034
3035 if (self.options.readonly)
3036 return L.deferrable();
3037
3038 var deferreds = [ ];
3039
3040 for (var i = 0; i < self.sections.length; i++)
3041 {
3042 var rv = self.sections[i].save();
3043 deferreds.push.apply(deferreds, rv);
3044 }
3045
3046 return $.when.apply($, deferreds).then(function() {
3047 return L.deferrable(self.options.save.call(self));
3048 });
3049 },
3050
3051 send: function()
3052 {
3053 if (!this.validate())
3054 return L.deferrable();
3055
3056 var self = this;
3057
3058 L.ui.saveScrollTop();
3059 L.ui.loading(true);
3060
3061 return this.save().then(function() {
3062 return L.uci.save();
3063 }).then(function() {
3064 return L.ui.updateChanges();
3065 }).then(function() {
3066 return self.load();
3067 }).then(function() {
3068 self.redraw();
3069 self = null;
3070
3071 L.ui.loading(false);
3072 L.ui.restoreScrollTop();
3073 });
3074 },
3075
3076 revert: function()
3077 {
3078 var packages = [ this.uci_package ];
3079
3080 for (var i = 0; i < this.sections.length; i++)
3081 packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
3082
3083 L.uci.unload(packages);
3084 },
3085
3086 reset: function()
3087 {
3088 var self = this;
3089
3090 self.revert();
3091
3092 return self.insertInto(self.target);
3093 },
3094
3095 insertInto: function(id)
3096 {
3097 var self = this;
3098 self.target = $(id);
3099
3100 L.ui.loading(true);
3101 self.target.hide();
3102
3103 return self.load().then(function() {
3104 self.target.empty().append(self.render());
3105 self.finish();
3106 self.target.show();
3107 self = null;
3108 L.ui.loading(false);
3109 });
3110 }
3111 });
3112
3113 cbi_class.Modal = cbi_class.Map.extend({
3114 handleApply: function(ev)
3115 {
3116 var self = ev.data.self;
3117
3118 self.trigger('apply', ev);
3119 },
3120
3121 handleSave: function(ev)
3122 {
3123 var self = ev.data.self;
3124
3125 self.send().then(function() {
3126 self.trigger('save', ev);
3127 self.close();
3128 });
3129 },
3130
3131 handleReset: function(ev)
3132 {
3133 var self = ev.data.self;
3134
3135 self.trigger('close', ev);
3136 self.revert();
3137 self.close();
3138 },
3139
3140 renderFooter: function()
3141 {
3142 var evdata = {
3143 self: this
3144 };
3145
3146 return $('<div />')
3147 .addClass('btn-group')
3148 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
3149 .click(evdata, this.handleApply))
3150 .append(L.ui.button(L.tr('Save'), 'default')
3151 .click(evdata, this.handleSave))
3152 .append(L.ui.button(L.tr('Cancel'), 'default')
3153 .click(evdata, this.handleReset));
3154 },
3155
3156 render: function()
3157 {
3158 var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
3159 var map = $('<form />');
3160
3161 var desc = this.label('description');
3162 if (desc)
3163 map.append($('<p />').text(desc));
3164
3165 map.append(this.renderBody());
3166
3167 modal.find('.modal-body').append(map);
3168 modal.find('.modal-footer').append(this.renderFooter());
3169
3170 return modal;
3171 },
3172
3173 redraw: function()
3174 {
3175 this.render();
3176 this.finish();
3177 },
3178
3179 show: function()
3180 {
3181 var self = this;
3182
3183 L.ui.loading(true);
3184
3185 return self.load().then(function() {
3186 self.render();
3187 self.finish();
3188
3189 L.ui.loading(false);
3190 });
3191 },
3192
3193 close: function()
3194 {
3195 L.ui.dialog(false);
3196 }
3197 });
3198
3199 return Class.extend(cbi_class);
3200 })();