luci-base: cbi.js: recognize invalid input in dropdown create field
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / cbi.js
1 /*
2         LuCI - Lua Configuration Interface
3
4         Copyright 2008 Steven Barth <steven@midlink.org>
5         Copyright 2008-2012 Jo-Philipp Wich <jow@openwrt.org>
6
7         Licensed under the Apache License, Version 2.0 (the "License");
8         you may not use this file except in compliance with the License.
9         You may obtain a copy of the License at
10
11         http://www.apache.org/licenses/LICENSE-2.0
12 */
13
14 var cbi_d = [];
15 var cbi_t = [];
16 var cbi_strings = { path: {}, label: {} };
17
18 function Int(x) {
19         return (/^-?\d+$/.test(x) ? +x : NaN);
20 }
21
22 function Dec(x) {
23         return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
24 }
25
26 function IPv4(x) {
27         if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
28                 return null;
29
30         if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255)
31                 return null;
32
33         return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
34 }
35
36 function IPv6(x) {
37         if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
38                 var v6 = RegExp.$1, v4 = IPv4(RegExp.$2);
39
40                 if (!v4)
41                         return null;
42
43                 x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
44                        + ':' + (v4[2] * 256 + v4[3]).toString(16);
45         }
46
47         if (!x.match(/^[a-fA-F0-9:]+$/))
48                 return null;
49
50         var prefix_suffix = x.split(/::/);
51
52         if (prefix_suffix.length > 2)
53                 return null;
54
55         var prefix = (prefix_suffix[0] || '0').split(/:/);
56         var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
57
58         if (suffix.length ? (prefix.length + suffix.length > 7) : (prefix.length > 8))
59                 return null;
60
61         var i, word;
62         var words = [];
63
64         for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
65                 if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
66                         words.push(word);
67                 else
68                         return null;
69
70         for (i = 0; i < (8 - prefix.length - suffix.length); i++)
71                 words.push(0);
72
73         for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16))
74                 if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
75                         words.push(word);
76                 else
77                         return null;
78
79         return words;
80 }
81
82 var cbi_validators = {
83
84         'integer': function()
85         {
86                 return !!Int(this);
87         },
88
89         'uinteger': function()
90         {
91                 return (Int(this) >= 0);
92         },
93
94         'float': function()
95         {
96                 return !!Dec(this);
97         },
98
99         'ufloat': function()
100         {
101                 return (Dec(this) >= 0);
102         },
103
104         'ipaddr': function()
105         {
106                 return cbi_validators.ip4addr.apply(this) ||
107                         cbi_validators.ip6addr.apply(this);
108         },
109
110         'ip4addr': function()
111         {
112                 var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|\/(\d{1,2}))?$/);
113                 return !!(m && IPv4(m[1]) && (m[2] ? IPv4(m[2]) : (m[3] ? cbi_validators.ip4prefix.apply(m[3]) : true)));
114         },
115
116         'ip6addr': function()
117         {
118                 var m = this.match(/^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/);
119                 return !!(m && IPv6(m[1]) && (m[2] ? cbi_validators.ip6prefix.apply(m[2]) : true));
120         },
121
122         'ip4prefix': function()
123         {
124                 return !isNaN(this) && this >= 0 && this <= 32;
125         },
126
127         'ip6prefix': function()
128         {
129                 return !isNaN(this) && this >= 0 && this <= 128;
130         },
131
132         'cidr': function()
133         {
134                 return cbi_validators.cidr4.apply(this) ||
135                         cbi_validators.cidr6.apply(this);
136         },
137
138         'cidr4': function()
139         {
140                 var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/);
141                 return !!(m && IPv4(m[1]) && cbi_validators.ip4prefix.apply(m[2]));
142         },
143
144         'cidr6': function()
145         {
146                 var m = this.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/);
147                 return !!(m && IPv6(m[1]) && cbi_validators.ip6prefix.apply(m[2]));
148         },
149
150         'ipnet4': function()
151         {
152                 var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
153                 return !!(m && IPv4(m[1]) && IPv4(m[2]));
154         },
155
156         'ipnet6': function()
157         {
158                 var m = this.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
159                 return !!(m && IPv6(m[1]) && IPv6(m[2]));
160         },
161
162         'ip6hostid': function()
163         {
164                 if (this == "eui64" || this == "random")
165                         return true;
166
167                 var v6 = IPv6(this);
168                 return !(!v6 || v6[0] || v6[1] || v6[2] || v6[3]);
169         },
170
171         'ipmask': function()
172         {
173                 return cbi_validators.ipmask4.apply(this) ||
174                         cbi_validators.ipmask6.apply(this);
175         },
176
177         'ipmask4': function()
178         {
179                 return cbi_validators.cidr4.apply(this) ||
180                         cbi_validators.ipnet4.apply(this) ||
181                         cbi_validators.ip4addr.apply(this);
182         },
183
184         'ipmask6': function()
185         {
186                 return cbi_validators.cidr6.apply(this) ||
187                         cbi_validators.ipnet6.apply(this) ||
188                         cbi_validators.ip6addr.apply(this);
189         },
190
191         'port': function()
192         {
193                 var p = Int(this);
194                 return (p >= 0 && p <= 65535);
195         },
196
197         'portrange': function()
198         {
199                 if (this.match(/^(\d+)-(\d+)$/))
200                 {
201                         var p1 = +RegExp.$1;
202                         var p2 = +RegExp.$2;
203                         return (p1 <= p2 && p2 <= 65535);
204                 }
205
206                 return cbi_validators.port.apply(this);
207         },
208
209         'macaddr': function()
210         {
211                 return (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null);
212         },
213
214         'host': function(ipv4only)
215         {
216                 return cbi_validators.hostname.apply(this) ||
217                         ((ipv4only != 1) && cbi_validators.ipaddr.apply(this)) ||
218                         ((ipv4only == 1) && cbi_validators.ip4addr.apply(this));
219         },
220
221         'hostname': function(strict)
222         {
223                 if (this.length <= 253)
224                         return (this.match(/^[a-zA-Z0-9_]+$/) != null ||
225                                 (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
226                                  this.match(/[^0-9.]/))) &&
227                                (!strict || !this.match(/^_/));
228
229                 return false;
230         },
231
232         'network': function()
233         {
234                 return cbi_validators.uciname.apply(this) ||
235                         cbi_validators.host.apply(this);
236         },
237
238         'hostport': function(ipv4only)
239         {
240                 var hp = this.split(/:/);
241
242                 if (hp.length == 2)
243                         return (cbi_validators.host.apply(hp[0], ipv4only) &&
244                                 cbi_validators.port.apply(hp[1]));
245
246                 return false;
247         },
248
249         'ip4addrport': function()
250         {
251                 var hp = this.split(/:/);
252
253                 if (hp.length == 2)
254                         return (cbi_validators.ipaddr.apply(hp[0]) &&
255                                 cbi_validators.port.apply(hp[1]));
256                 return false;
257         },
258
259         'ipaddrport': function(bracket)
260         {
261                 if (this.match(/^([^\[\]:]+):([^:]+)$/)) {
262                         var addr = RegExp.$1
263                         var port = RegExp.$2
264                         return (cbi_validators.ip4addr.apply(addr) &&
265                                 cbi_validators.port.apply(port));
266                 } else if ((bracket == 1) && (this.match(/^\[(.+)\]:([^:]+)$/))) {
267                         var addr = RegExp.$1
268                         var port = RegExp.$2
269                         return (cbi_validators.ip6addr.apply(addr) &&
270                                 cbi_validators.port.apply(port));
271                 } else if ((bracket != 1) && (this.match(/^([^\[\]]+):([^:]+)$/))) {
272                         var addr = RegExp.$1
273                         var port = RegExp.$2
274                         return (cbi_validators.ip6addr.apply(addr) &&
275                                 cbi_validators.port.apply(port));
276                 } else {
277                         return false;
278                 }
279         },
280
281         'wpakey': function()
282         {
283                 var v = this;
284
285                 if( v.length == 64 )
286                         return (v.match(/^[a-fA-F0-9]{64}$/) != null);
287                 else
288                         return (v.length >= 8) && (v.length <= 63);
289         },
290
291         'wepkey': function()
292         {
293                 var v = this;
294
295                 if ( v.substr(0,2) == 's:' )
296                         v = v.substr(2);
297
298                 if( (v.length == 10) || (v.length == 26) )
299                         return (v.match(/^[a-fA-F0-9]{10,26}$/) != null);
300                 else
301                         return (v.length == 5) || (v.length == 13);
302         },
303
304         'uciname': function()
305         {
306                 return (this.match(/^[a-zA-Z0-9_]+$/) != null);
307         },
308
309         'range': function(min, max)
310         {
311                 var val = Dec(this);
312                 return (val >= +min && val <= +max);
313         },
314
315         'min': function(min)
316         {
317                 return (Dec(this) >= +min);
318         },
319
320         'max': function(max)
321         {
322                 return (Dec(this) <= +max);
323         },
324
325         'rangelength': function(min, max)
326         {
327                 var val = '' + this;
328                 return ((val.length >= +min) && (val.length <= +max));
329         },
330
331         'minlength': function(min)
332         {
333                 return ((''+this).length >= +min);
334         },
335
336         'maxlength': function(max)
337         {
338                 return ((''+this).length <= +max);
339         },
340
341         'or': function()
342         {
343                 for (var i = 0; i < arguments.length; i += 2)
344                 {
345                         if (typeof arguments[i] != 'function')
346                         {
347                                 if (arguments[i] == this)
348                                         return true;
349                                 i--;
350                         }
351                         else if (arguments[i].apply(this, arguments[i+1]))
352                         {
353                                 return true;
354                         }
355                 }
356                 return false;
357         },
358
359         'and': function()
360         {
361                 for (var i = 0; i < arguments.length; i += 2)
362                 {
363                         if (typeof arguments[i] != 'function')
364                         {
365                                 if (arguments[i] != this)
366                                         return false;
367                                 i--;
368                         }
369                         else if (!arguments[i].apply(this, arguments[i+1]))
370                         {
371                                 return false;
372                         }
373                 }
374                 return true;
375         },
376
377         'neg': function()
378         {
379                 return cbi_validators.or.apply(
380                         this.replace(/^[ \t]*![ \t]*/, ''), arguments);
381         },
382
383         'list': function(subvalidator, subargs)
384         {
385                 if (typeof subvalidator != 'function')
386                         return false;
387
388                 var tokens = this.match(/[^ \t]+/g);
389                 for (var i = 0; i < tokens.length; i++)
390                         if (!subvalidator.apply(tokens[i], subargs))
391                                 return false;
392
393                 return true;
394         },
395         'phonedigit': function()
396         {
397                 return (this.match(/^[0-9\*#!\.]+$/) != null);
398         },
399         'timehhmmss': function()
400         {
401                 return (this.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/) != null);
402         },
403         'dateyyyymmdd': function()
404         {
405                 if (this == null) {
406                         return false;
407                 }
408                 if (this.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
409                         var year = RegExp.$1;
410                         var month = RegExp.$2;
411                         var day = RegExp.$2
412
413                         var days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30 , 31, 30, 31 ];
414                         function is_leap_year(year) {
415                                 return ((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0);
416                         }
417                         function get_days_in_month(month, year) {
418                                 if ((month == 2) && is_leap_year(year)) {
419                                         return 29;
420                                 } else {
421                                         return days_in_month[month];
422                                 }
423                         }
424                         /* Firewall rules in the past don't make sense */
425                         if (year < 2015) {
426                                 return false;
427                         }
428                         if ((month <= 0) || (month > 12)) {
429                                 return false;
430                         }
431                         if ((day <= 0) || (day > get_days_in_month(month, year))) {
432                                 return false;
433                         }
434                         return true;
435
436                 } else {
437                         return false;
438                 }
439         }
440 };
441
442
443 function cbi_d_add(field, dep, index) {
444         var obj = (typeof(field) === 'string') ? document.getElementById(field) : field;
445         if (obj) {
446                 var entry
447                 for (var i=0; i<cbi_d.length; i++) {
448                         if (cbi_d[i].id == obj.id) {
449                                 entry = cbi_d[i];
450                                 break;
451                         }
452                 }
453                 if (!entry) {
454                         entry = {
455                                 "node": obj,
456                                 "id": obj.id,
457                                 "parent": obj.parentNode.id,
458                                 "deps": [],
459                                 "index": index
460                         };
461                         cbi_d.unshift(entry);
462                 }
463                 entry.deps.push(dep)
464         }
465 }
466
467 function cbi_d_checkvalue(target, ref) {
468         var value = null,
469             query = 'input[id="'+target+'"], input[name="'+target+'"], ' +
470                     'select[id="'+target+'"], select[name="'+target+'"]';
471
472         document.querySelectorAll(query).forEach(function(i) {
473                 if (value === null && ((i.type !== 'radio' && i.type !== 'checkbox') || i.checked === true))
474                         value = i.value;
475         });
476
477         return (((value !== null) ? value : "") == ref);
478 }
479
480 function cbi_d_check(deps) {
481         var reverse;
482         var def = false;
483         for (var i=0; i<deps.length; i++) {
484                 var istat = true;
485                 reverse = false;
486                 for (var j in deps[i]) {
487                         if (j == "!reverse") {
488                                 reverse = true;
489                         } else if (j == "!default") {
490                                 def = true;
491                                 istat = false;
492                         } else {
493                                 istat = (istat && cbi_d_checkvalue(j, deps[i][j]))
494                         }
495                 }
496
497                 if (istat ^ reverse) {
498                         return true;
499                 }
500         }
501         return def;
502 }
503
504 function cbi_d_update() {
505         var state = false;
506         for (var i=0; i<cbi_d.length; i++) {
507                 var entry = cbi_d[i];
508                 var node  = document.getElementById(entry.id);
509                 var parent = document.getElementById(entry.parent);
510
511                 if (node && node.parentNode && !cbi_d_check(entry.deps)) {
512                         node.parentNode.removeChild(node);
513                         state = true;
514                 } else if (parent && (!node || !node.parentNode) && cbi_d_check(entry.deps)) {
515                         var next = undefined;
516
517                         for (next = parent.firstChild; next; next = next.nextSibling) {
518                                 if (next.getAttribute && parseInt(next.getAttribute('data-index'), 10) > entry.index) {
519                                         break;
520                                 }
521                         }
522
523                         if (!next) {
524                                 parent.appendChild(entry.node);
525                         } else {
526                                 parent.insertBefore(entry.node, next);
527                         }
528
529                         state = true;
530                 }
531
532                 // hide optionals widget if no choices remaining
533                 if (parent && parent.parentNode && parent.getAttribute('data-optionals'))
534                         parent.parentNode.style.display = (parent.options.length <= 1) ? 'none' : '';
535         }
536
537         if (entry && entry.parent) {
538                 if (!cbi_t_update())
539                         cbi_tag_last(parent);
540         }
541
542         if (state) {
543                 cbi_d_update();
544         }
545 }
546
547 function cbi_init() {
548         var nodes;
549
550         nodes = document.querySelectorAll('[data-strings]');
551
552         for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
553                 var str = JSON.parse(node.getAttribute('data-strings'));
554                 for (var key in str) {
555                         for (var key2 in str[key]) {
556                                 var dst = cbi_strings[key] || (cbi_strings[key] = { });
557                                     dst[key2] = str[key][key2];
558                         }
559                 }
560         }
561
562         nodes = document.querySelectorAll('[data-depends]');
563
564         for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
565                 var index = parseInt(node.getAttribute('data-index'), 10);
566                 var depends = JSON.parse(node.getAttribute('data-depends'));
567                 if (!isNaN(index) && depends.length > 0) {
568                         for (var alt = 0; alt < depends.length; alt++) {
569                                 cbi_d_add(node, depends[alt], index);
570                         }
571                 }
572         }
573
574         nodes = document.querySelectorAll('[data-update]');
575
576         for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
577                 var events = node.getAttribute('data-update').split(' ');
578                 for (var j = 0, event; (event = events[j]) !== undefined; j++) {
579                         cbi_bind(node, event, cbi_d_update);
580                 }
581         }
582
583         nodes = document.querySelectorAll('[data-choices]');
584
585         for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
586                 var choices = JSON.parse(node.getAttribute('data-choices'));
587                 var options = {};
588
589                 for (var j = 0; j < choices[0].length; j++)
590                         options[choices[0][j]] = choices[1][j];
591
592                 var def = (node.getAttribute('data-optional') === 'true')
593                         ? node.placeholder || '' : null;
594
595                 cbi_combobox_init(node, options, def,
596                                   node.getAttribute('data-manual'));
597         }
598
599         nodes = document.querySelectorAll('[data-dynlist]');
600
601         for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
602                 var choices = JSON.parse(node.getAttribute('data-dynlist'));
603                 var options = null;
604
605                 if (choices[0] && choices[0].length) {
606                         options = {};
607
608                         for (var j = 0; j < choices[0].length; j++)
609                                 options[choices[0][j]] = choices[1][j];
610                 }
611
612                 cbi_dynlist_init(node, choices[2], choices[3], options);
613         }
614
615         nodes = document.querySelectorAll('[data-type]');
616
617         for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
618                 cbi_validate_field(node, node.getAttribute('data-optional') === 'true',
619                                    node.getAttribute('data-type'));
620         }
621
622         document.querySelectorAll('.cbi-dropdown').forEach(function(s) {
623                 cbi_dropdown_init(s);
624         });
625
626         document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) {
627                 s.parentNode.classList.add('cbi-tooltip-container');
628         });
629
630         document.querySelectorAll('.cbi-section-remove > input[name^="cbi.rts"]').forEach(function(i) {
631                 var handler = function(ev) {
632                         var bits = this.name.split(/\./),
633                             section = document.getElementById('cbi-' + bits[2] + '-' + bits[3]);
634
635                     section.style.opacity = (ev.type === 'mouseover') ? 0.5 : '';
636                 };
637
638                 i.addEventListener('mouseover', handler);
639                 i.addEventListener('mouseout', handler);
640         });
641
642         cbi_d_update();
643 }
644
645 function cbi_bind(obj, type, callback, mode) {
646         if (!obj.addEventListener) {
647                 obj.attachEvent('on' + type,
648                         function(){
649                                 var e = window.event;
650
651                                 if (!e.target && e.srcElement)
652                                         e.target = e.srcElement;
653
654                                 return !!callback(e);
655                         }
656                 );
657         } else {
658                 obj.addEventListener(type, callback, !!mode);
659         }
660         return obj;
661 }
662
663 function cbi_combobox(id, values, def, man, focus) {
664         var selid = "cbi.combobox." + id;
665         if (document.getElementById(selid)) {
666                 return
667         }
668
669         var obj = document.getElementById(id)
670         var sel = document.createElement("select");
671                 sel.id = selid;
672                 sel.index = obj.index;
673                 sel.className = obj.className.replace(/cbi-input-text/, 'cbi-input-select');
674
675         if (obj.nextSibling) {
676                 obj.parentNode.insertBefore(sel, obj.nextSibling);
677         } else {
678                 obj.parentNode.appendChild(sel);
679         }
680
681         var dt = obj.getAttribute('cbi_datatype');
682         var op = obj.getAttribute('cbi_optional');
683
684         if (!values[obj.value]) {
685                 if (obj.value == "") {
686                         var optdef = document.createElement("option");
687                         optdef.value = "";
688                         optdef.appendChild(document.createTextNode(typeof(def) === 'string' ? def : cbi_strings.label.choose));
689                         sel.appendChild(optdef);
690                 } else {
691                         var opt = document.createElement("option");
692                         opt.value = obj.value;
693                         opt.selected = "selected";
694                         opt.appendChild(document.createTextNode(obj.value));
695                         sel.appendChild(opt);
696                 }
697         }
698
699         for (var i in values) {
700                 var opt = document.createElement("option");
701                 opt.value = i;
702
703                 if (obj.value == i) {
704                         opt.selected = "selected";
705                 }
706
707                 opt.appendChild(document.createTextNode(values[i]));
708                 sel.appendChild(opt);
709         }
710
711         var optman = document.createElement("option");
712         optman.value = "";
713         optman.appendChild(document.createTextNode(typeof(man) === 'string' ? man : cbi_strings.label.custom));
714         sel.appendChild(optman);
715
716         obj.style.display = "none";
717
718         if (dt)
719                 cbi_validate_field(sel, op == 'true', dt);
720
721         cbi_bind(sel, "change", function() {
722                 if (sel.selectedIndex == sel.options.length - 1) {
723                         obj.style.display = "inline";
724                         sel.blur();
725                         sel.parentNode.removeChild(sel);
726                         obj.focus();
727                 } else {
728                         obj.value = sel.options[sel.selectedIndex].value;
729                 }
730
731                 try {
732                         cbi_d_update();
733                 } catch (e) {
734                         //Do nothing
735                 }
736         })
737
738         // Retrigger validation in select
739         if (focus) {
740                 sel.focus();
741                 sel.blur();
742         }
743 }
744
745 function cbi_combobox_init(id, values, def, man) {
746         var obj = (typeof(id) === 'string') ? document.getElementById(id) : id;
747         cbi_bind(obj, "blur", function() {
748                 cbi_combobox(obj.id, values, def, man, true);
749         });
750         cbi_combobox(obj.id, values, def, man, false);
751 }
752
753 function cbi_filebrowser(id, defpath) {
754         var field   = document.getElementById(id);
755         var browser = window.open(
756                 cbi_strings.path.browser + ( field.value || defpath || '' ) + '?field=' + id,
757                 "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes"
758         );
759
760         browser.focus();
761 }
762
763 function cbi_browser_init(id, resource, defpath)
764 {
765         function cbi_browser_btnclick(e) {
766                 cbi_filebrowser(id, defpath);
767                 return false;
768         }
769
770         var field = document.getElementById(id);
771
772         var btn = document.createElement('img');
773         btn.className = 'cbi-image-button';
774         btn.src = (resource || cbi_strings.path.resource) + '/cbi/folder.gif';
775         field.parentNode.insertBefore(btn, field.nextSibling);
776
777         cbi_bind(btn, 'click', cbi_browser_btnclick);
778 }
779
780 function cbi_dynlist_init(parent, datatype, optional, choices)
781 {
782         var prefix = parent.getAttribute('data-prefix');
783         var holder = parent.getAttribute('data-placeholder');
784
785         var values;
786
787         function cbi_dynlist_redraw(focus, add, del)
788         {
789                 values = [ ];
790
791                 while (parent.firstChild)
792                 {
793                         var n = parent.firstChild;
794                         var i = +n.index;
795
796                         if (i != del)
797                         {
798                                 if (n.nodeName.toLowerCase() == 'input')
799                                         values.push(n.value || '');
800                                 else if (n.nodeName.toLowerCase() == 'select')
801                                         values[values.length-1] = n.options[n.selectedIndex].value;
802                         }
803
804                         parent.removeChild(n);
805                 }
806
807                 if (add >= 0)
808                 {
809                         focus = add+1;
810                         values.splice(focus, 0, '');
811                 }
812                 else if (values.length == 0)
813                 {
814                         focus = 0;
815                         values.push('');
816                 }
817
818                 for (var i = 0; i < values.length; i++)
819                 {
820                         var t = document.createElement('input');
821                                 t.id = prefix + '.' + (i+1);
822                                 t.name = prefix;
823                                 t.value = values[i];
824                                 t.type = 'text';
825                                 t.index = i;
826                                 t.className = 'cbi-input-text';
827
828                         if (i == 0 && holder)
829                         {
830                                 t.placeholder = holder;
831                         }
832
833                         var b = E('div', {
834                                 class: 'cbi-button cbi-button-' + ((i+1) < values.length ? 'remove' : 'add')
835                         }, (i+1) < values.length ? '×' : '+');
836
837                         parent.appendChild(t);
838                         parent.appendChild(b);
839                         if (datatype == 'file')
840                         {
841                                 cbi_browser_init(t.id, null, parent.getAttribute('data-browser-path'));
842                         }
843
844                         parent.appendChild(document.createElement('br'));
845
846                         if (datatype)
847                         {
848                                 cbi_validate_field(t.id, ((i+1) == values.length) || optional, datatype);
849                         }
850
851                         if (choices)
852                         {
853                                 cbi_combobox_init(t.id, choices, '', cbi_strings.label.custom);
854                                 b.index = i;
855
856                                 cbi_bind(b, 'keydown',  cbi_dynlist_keydown);
857                                 cbi_bind(b, 'keypress', cbi_dynlist_keypress);
858
859                                 if (i == focus || -i == focus)
860                                         b.focus();
861                         }
862                         else
863                         {
864                                 cbi_bind(t, 'keydown',  cbi_dynlist_keydown);
865                                 cbi_bind(t, 'keypress', cbi_dynlist_keypress);
866
867                                 if (i == focus)
868                                 {
869                                         t.focus();
870                                 }
871                                 else if (-i == focus)
872                                 {
873                                         t.focus();
874
875                                         /* force cursor to end */
876                                         var v = t.value;
877                                         t.value = ' '
878                                         t.value = v;
879                                 }
880                         }
881
882                         cbi_bind(b, 'click', cbi_dynlist_btnclick);
883                 }
884         }
885
886         function cbi_dynlist_keypress(ev)
887         {
888                 ev = ev ? ev : window.event;
889
890                 var se = ev.target ? ev.target : ev.srcElement;
891
892                 if (se.nodeType == 3)
893                         se = se.parentNode;
894
895                 switch (ev.keyCode)
896                 {
897                         /* backspace, delete */
898                         case 8:
899                         case 46:
900                                 if (se.value.length == 0)
901                                 {
902                                         if (ev.preventDefault)
903                                                 ev.preventDefault();
904
905                                         return false;
906                                 }
907
908                                 return true;
909
910                         /* enter, arrow up, arrow down */
911                         case 13:
912                         case 38:
913                         case 40:
914                                 if (ev.preventDefault)
915                                         ev.preventDefault();
916
917                                 return false;
918                 }
919
920                 return true;
921         }
922
923         function cbi_dynlist_keydown(ev)
924         {
925                 ev = ev ? ev : window.event;
926
927                 var se = ev.target ? ev.target : ev.srcElement;
928
929                 if (se.nodeType == 3)
930                         se = se.parentNode;
931
932                 var prev = se.previousSibling;
933                 while (prev && prev.name != prefix)
934                         prev = prev.previousSibling;
935
936                 var next = se.nextSibling;
937                 while (next && next.name != prefix)
938                         next = next.nextSibling;
939
940                 /* advance one further in combobox case */
941                 if (next && next.nextSibling.name == prefix)
942                         next = next.nextSibling;
943
944                 switch (ev.keyCode)
945                 {
946                         /* backspace, delete */
947                         case 8:
948                         case 46:
949                                 var del = (se.nodeName.toLowerCase() == 'select')
950                                         ? true : (se.value.length == 0);
951
952                                 if (del)
953                                 {
954                                         if (ev.preventDefault)
955                                                 ev.preventDefault();
956
957                                         var focus = se.index;
958                                         if (ev.keyCode == 8)
959                                                 focus = -focus+1;
960
961                                         cbi_dynlist_redraw(focus, -1, se.index);
962
963                                         return false;
964                                 }
965
966                                 break;
967
968                         /* enter */
969                         case 13:
970                                 cbi_dynlist_redraw(-1, se.index, -1);
971                                 break;
972
973                         /* arrow up */
974                         case 38:
975                                 if (prev)
976                                         prev.focus();
977
978                                 break;
979
980                         /* arrow down */
981                         case 40:
982                                 if (next)
983                                         next.focus();
984
985                                 break;
986                 }
987
988                 return true;
989         }
990
991         function cbi_dynlist_btnclick(ev)
992         {
993                 ev = ev ? ev : window.event;
994
995                 var se = ev.target ? ev.target : ev.srcElement;
996                 var input = se.previousSibling;
997                 while (input && input.name != prefix) {
998                         input = input.previousSibling;
999                 }
1000
1001                 if (se.classList.contains('cbi-button-remove')) {
1002                         input.value = '';
1003
1004                         cbi_dynlist_keydown({
1005                                 target:  input,
1006                                 keyCode: 8
1007                         });
1008                 }
1009                 else {
1010                         cbi_dynlist_keydown({
1011                                 target:  input,
1012                                 keyCode: 13
1013                         });
1014                 }
1015
1016                 return false;
1017         }
1018
1019         cbi_dynlist_redraw(NaN, -1, -1);
1020 }
1021
1022
1023 function cbi_t_add(section, tab) {
1024         var t = document.getElementById('tab.' + section + '.' + tab);
1025         var c = document.getElementById('container.' + section + '.' + tab);
1026
1027         if( t && c ) {
1028                 cbi_t[section] = (cbi_t[section] || [ ]);
1029                 cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
1030         }
1031 }
1032
1033 function cbi_t_switch(section, tab) {
1034         if( cbi_t[section] && cbi_t[section][tab] ) {
1035                 var o = cbi_t[section][tab];
1036                 var h = document.getElementById('tab.' + section);
1037                 for( var tid in cbi_t[section] ) {
1038                         var o2 = cbi_t[section][tid];
1039                         if( o.tab.id != o2.tab.id ) {
1040                                 o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
1041                                 o2.container.style.display = 'none';
1042                         }
1043                         else {
1044                                 if(h) h.value = tab;
1045                                 o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
1046                                 o2.container.style.display = 'block';
1047                         }
1048                 }
1049         }
1050         return false
1051 }
1052
1053 function cbi_t_update() {
1054         var hl_tabs = [ ];
1055         var updated = false;
1056
1057         for( var sid in cbi_t )
1058                 for( var tid in cbi_t[sid] )
1059                 {
1060                         var t = cbi_t[sid][tid].tab;
1061                         var c = cbi_t[sid][tid].container;
1062
1063                         if (!c.firstElementChild) {
1064                                 t.style.display = 'none';
1065                         }
1066                         else if (t.style.display == 'none') {
1067                                 t.style.display = '';
1068                                 t.className += ' cbi-tab-highlighted';
1069                                 hl_tabs.push(t);
1070                         }
1071
1072                         cbi_tag_last(c);
1073                         updated = true;
1074                 }
1075
1076         if (hl_tabs.length > 0)
1077                 window.setTimeout(function() {
1078                         for( var i = 0; i < hl_tabs.length; i++ )
1079                                 hl_tabs[i].className = hl_tabs[i].className.replace(/ cbi-tab-highlighted/g, '');
1080                 }, 750);
1081
1082         return updated;
1083 }
1084
1085
1086 function cbi_validate_form(form, errmsg)
1087 {
1088         /* if triggered by a section removal or addition, don't validate */
1089         if( form.cbi_state == 'add-section' || form.cbi_state == 'del-section' )
1090                 return true;
1091
1092         if( form.cbi_validators )
1093         {
1094                 for( var i = 0; i < form.cbi_validators.length; i++ )
1095                 {
1096                         var validator = form.cbi_validators[i];
1097                         if( !validator() && errmsg )
1098                         {
1099                                 alert(errmsg);
1100                                 return false;
1101                         }
1102                 }
1103         }
1104
1105         return true;
1106 }
1107
1108 function cbi_validate_reset(form)
1109 {
1110         window.setTimeout(
1111                 function() { cbi_validate_form(form, null) }, 100
1112         );
1113
1114         return true;
1115 }
1116
1117 function cbi_validate_compile(code)
1118 {
1119         var pos = 0;
1120         var esc = false;
1121         var depth = 0;
1122         var stack = [ ];
1123
1124         code += ',';
1125
1126         for (var i = 0; i < code.length; i++)
1127         {
1128                 if (esc)
1129                 {
1130                         esc = false;
1131                         continue;
1132                 }
1133
1134                 switch (code.charCodeAt(i))
1135                 {
1136                 case 92:
1137                         esc = true;
1138                         break;
1139
1140                 case 40:
1141                 case 44:
1142                         if (depth <= 0)
1143                         {
1144                                 if (pos < i)
1145                                 {
1146                                         var label = code.substring(pos, i);
1147                                                 label = label.replace(/\\(.)/g, '$1');
1148                                                 label = label.replace(/^[ \t]+/g, '');
1149                                                 label = label.replace(/[ \t]+$/g, '');
1150
1151                                         if (label && !isNaN(label))
1152                                         {
1153                                                 stack.push(parseFloat(label));
1154                                         }
1155                                         else if (label.match(/^(['"]).*\1$/))
1156                                         {
1157                                                 stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
1158                                         }
1159                                         else if (typeof cbi_validators[label] == 'function')
1160                                         {
1161                                                 stack.push(cbi_validators[label]);
1162                                                 stack.push(null);
1163                                         }
1164                                         else
1165                                         {
1166                                                 throw "Syntax error, unhandled token '"+label+"'";
1167                                         }
1168                                 }
1169                                 pos = i+1;
1170                         }
1171                         depth += (code.charCodeAt(i) == 40);
1172                         break;
1173
1174                 case 41:
1175                         if (--depth <= 0)
1176                         {
1177                                 if (typeof stack[stack.length-2] != 'function')
1178                                         throw "Syntax error, argument list follows non-function";
1179
1180                                 stack[stack.length-1] =
1181                                         arguments.callee(code.substring(pos, i));
1182
1183                                 pos = i+1;
1184                         }
1185                         break;
1186                 }
1187         }
1188
1189         return stack;
1190 }
1191
1192 function cbi_validate_field(cbid, optional, type)
1193 {
1194         var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid;
1195         var vstack; try { vstack = cbi_validate_compile(type); } catch(e) { };
1196
1197         if (field && vstack && typeof vstack[0] == "function")
1198         {
1199                 var validator = function()
1200                 {
1201                         // is not detached
1202                         if( field.form )
1203                         {
1204                                 field.className = field.className.replace(/ cbi-input-invalid/g, '');
1205
1206                                 // validate value
1207                                 var value = (field.options && field.options.selectedIndex > -1)
1208                                         ? field.options[field.options.selectedIndex].value : field.value;
1209
1210                                 if (!(((value.length == 0) && optional) || vstack[0].apply(value, vstack[1])))
1211                                 {
1212                                         // invalid
1213                                         field.className += ' cbi-input-invalid';
1214                                         return false;
1215                                 }
1216                         }
1217
1218                         return true;
1219                 };
1220
1221                 if( ! field.form.cbi_validators )
1222                         field.form.cbi_validators = [ ];
1223
1224                 field.form.cbi_validators.push(validator);
1225
1226                 cbi_bind(field, "blur",  validator);
1227                 cbi_bind(field, "keyup", validator);
1228
1229                 if (field.nodeName == 'SELECT')
1230                 {
1231                         cbi_bind(field, "change", validator);
1232                         cbi_bind(field, "click",  validator);
1233                 }
1234
1235                 field.setAttribute("cbi_validate", validator);
1236                 field.setAttribute("cbi_datatype", type);
1237                 field.setAttribute("cbi_optional", (!!optional).toString());
1238
1239                 validator();
1240
1241                 var fcbox = document.getElementById('cbi.combobox.' + field.id);
1242                 if (fcbox)
1243                         cbi_validate_field(fcbox, optional, type);
1244         }
1245 }
1246
1247 function cbi_row_swap(elem, up, store)
1248 {
1249         var tr = findParent(elem.parentNode, '.cbi-section-table-row');
1250
1251         if (!tr)
1252                 return false;
1253
1254         tr.classList.remove('flash');
1255
1256         if (up) {
1257                 var prev = tr.previousElementSibling;
1258
1259                 if (prev && prev.classList.contains('cbi-section-table-row'))
1260                         tr.parentNode.insertBefore(tr, prev);
1261                 else
1262                         return;
1263         }
1264         else {
1265                 var next = tr.nextElementSibling ? tr.nextElementSibling.nextElementSibling : null;
1266
1267                 if (next && next.classList.contains('cbi-section-table-row'))
1268                         tr.parentNode.insertBefore(tr, next);
1269                 else if (!next)
1270                         tr.parentNode.appendChild(tr);
1271                 else
1272                         return;
1273         }
1274
1275         var ids = [ ];
1276
1277         for (var i = 0, n = 0; i < tr.parentNode.childNodes.length; i++) {
1278                 var node = tr.parentNode.childNodes[i];
1279                 if (node.classList && node.classList.contains('cbi-section-table-row')) {
1280                         node.classList.remove('cbi-rowstyle-1');
1281                         node.classList.remove('cbi-rowstyle-2');
1282                         node.classList.add((n++ % 2) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1');
1283
1284                         if (/-([^\-]+)$/.test(node.id))
1285                                 ids.push(RegExp.$1);
1286                 }
1287         }
1288
1289         var input = document.getElementById(store);
1290         if (input)
1291                 input.value = ids.join(' ');
1292
1293         window.scrollTo(0, tr.offsetTop);
1294         window.setTimeout(function() { tr.classList.add('flash'); }, 1);
1295
1296         return false;
1297 }
1298
1299 function cbi_tag_last(container)
1300 {
1301         var last;
1302
1303         for (var i = 0; i < container.childNodes.length; i++)
1304         {
1305                 var c = container.childNodes[i];
1306                 if (c.nodeType == 1 && c.nodeName.toLowerCase() == 'div')
1307                 {
1308                         c.className = c.className.replace(/ cbi-value-last$/, '');
1309                         last = c;
1310                 }
1311         }
1312
1313         if (last)
1314         {
1315                 last.className += ' cbi-value-last';
1316         }
1317 }
1318
1319 function cbi_submit(elem, name, value, action)
1320 {
1321         var form = elem.form || findParent(elem, 'form');
1322
1323         if (!form)
1324                 return false;
1325
1326         if (action)
1327                 form.action = action;
1328
1329         if (name) {
1330                 var hidden = form.querySelector('input[type="hidden"][name="%s"]'.format(name)) ||
1331                         E('input', { type: 'hidden', name: name });
1332
1333                 hidden.value = value || '1';
1334                 form.appendChild(hidden);
1335         }
1336
1337         form.submit();
1338         return true;
1339 }
1340
1341 String.prototype.format = function()
1342 {
1343         if (!RegExp)
1344                 return;
1345
1346         var html_esc = [/&/g, '&#38;', /"/g, '&#34;', /'/g, '&#39;', /</g, '&#60;', />/g, '&#62;'];
1347         var quot_esc = [/"/g, '&#34;', /'/g, '&#39;'];
1348
1349         function esc(s, r) {
1350                 if (typeof(s) !== 'string' && !(s instanceof String))
1351                         return '';
1352
1353                 for( var i = 0; i < r.length; i += 2 )
1354                         s = s.replace(r[i], r[i+1]);
1355                 return s;
1356         }
1357
1358         var str = this;
1359         var out = '';
1360         var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/;
1361         var a = b = [], numSubstitutions = 0, numMatches = 0;
1362
1363         while (a = re.exec(str))
1364         {
1365                 var m = a[1];
1366                 var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5];
1367                 var pPrecision = a[6], pType = a[7];
1368
1369                 numMatches++;
1370
1371                 if (pType == '%')
1372                 {
1373                         subst = '%';
1374                 }
1375                 else
1376                 {
1377                         if (numSubstitutions < arguments.length)
1378                         {
1379                                 var param = arguments[numSubstitutions++];
1380
1381                                 var pad = '';
1382                                 if (pPad && pPad.substr(0,1) == "'")
1383                                         pad = leftpart.substr(1,1);
1384                                 else if (pPad)
1385                                         pad = pPad;
1386                                 else
1387                                         pad = ' ';
1388
1389                                 var justifyRight = true;
1390                                 if (pJustify && pJustify === "-")
1391                                         justifyRight = false;
1392
1393                                 var minLength = -1;
1394                                 if (pMinLength)
1395                                         minLength = +pMinLength;
1396
1397                                 var precision = -1;
1398                                 if (pPrecision && pType == 'f')
1399                                         precision = +pPrecision.substring(1);
1400
1401                                 var subst = param;
1402
1403                                 switch(pType)
1404                                 {
1405                                         case 'b':
1406                                                 subst = (+param || 0).toString(2);
1407                                                 break;
1408
1409                                         case 'c':
1410                                                 subst = String.fromCharCode(+param || 0);
1411                                                 break;
1412
1413                                         case 'd':
1414                                                 subst = ~~(+param || 0);
1415                                                 break;
1416
1417                                         case 'u':
1418                                                 subst = ~~Math.abs(+param || 0);
1419                                                 break;
1420
1421                                         case 'f':
1422                                                 subst = (precision > -1)
1423                                                         ? ((+param || 0.0)).toFixed(precision)
1424                                                         : (+param || 0.0);
1425                                                 break;
1426
1427                                         case 'o':
1428                                                 subst = (+param || 0).toString(8);
1429                                                 break;
1430
1431                                         case 's':
1432                                                 subst = param;
1433                                                 break;
1434
1435                                         case 'x':
1436                                                 subst = ('' + (+param || 0).toString(16)).toLowerCase();
1437                                                 break;
1438
1439                                         case 'X':
1440                                                 subst = ('' + (+param || 0).toString(16)).toUpperCase();
1441                                                 break;
1442
1443                                         case 'h':
1444                                                 subst = esc(param, html_esc);
1445                                                 break;
1446
1447                                         case 'q':
1448                                                 subst = esc(param, quot_esc);
1449                                                 break;
1450
1451                                         case 't':
1452                                                 var td = 0;
1453                                                 var th = 0;
1454                                                 var tm = 0;
1455                                                 var ts = (param || 0);
1456
1457                                                 if (ts > 60) {
1458                                                         tm = Math.floor(ts / 60);
1459                                                         ts = (ts % 60);
1460                                                 }
1461
1462                                                 if (tm > 60) {
1463                                                         th = Math.floor(tm / 60);
1464                                                         tm = (tm % 60);
1465                                                 }
1466
1467                                                 if (th > 24) {
1468                                                         td = Math.floor(th / 24);
1469                                                         th = (th % 24);
1470                                                 }
1471
1472                                                 subst = (td > 0)
1473                                                         ? String.format('%dd %dh %dm %ds', td, th, tm, ts)
1474                                                         : String.format('%dh %dm %ds', th, tm, ts);
1475
1476                                                 break;
1477
1478                                         case 'm':
1479                                                 var mf = pMinLength ? +pMinLength : 1000;
1480                                                 var pr = pPrecision ? ~~(10 * +('0' + pPrecision)) : 2;
1481
1482                                                 var i = 0;
1483                                                 var val = (+param || 0);
1484                                                 var units = [ ' ', ' K', ' M', ' G', ' T', ' P', ' E' ];
1485
1486                                                 for (i = 0; (i < units.length) && (val > mf); i++)
1487                                                         val /= mf;
1488
1489                                                 subst = (i ? val.toFixed(pr) : val) + units[i];
1490                                                 pMinLength = null;
1491                                                 break;
1492                                 }
1493                         }
1494                 }
1495
1496                 if (pMinLength) {
1497                         subst = subst.toString();
1498                         for (var i = subst.length; i < pMinLength; i++)
1499                                 if (pJustify == '-')
1500                                         subst = subst + ' ';
1501                                 else
1502                                         subst = pad + subst;
1503                 }
1504
1505                 out += leftpart + subst;
1506                 str = str.substr(m.length);
1507         }
1508
1509         return out + str;
1510 }
1511
1512 String.prototype.nobr = function()
1513 {
1514         return this.replace(/[\s\n]+/g, '&#160;');
1515 }
1516
1517 String.format = function()
1518 {
1519         var a = [ ];
1520         for (var i = 1; i < arguments.length; i++)
1521                 a.push(arguments[i]);
1522         return ''.format.apply(arguments[0], a);
1523 }
1524
1525 String.nobr = function()
1526 {
1527         var a = [ ];
1528         for (var i = 1; i < arguments.length; i++)
1529                 a.push(arguments[i]);
1530         return ''.nobr.apply(arguments[0], a);
1531 }
1532
1533 if (window.NodeList && !NodeList.prototype.forEach) {
1534         NodeList.prototype.forEach = function (callback, thisArg) {
1535                 thisArg = thisArg || window;
1536                 for (var i = 0; i < this.length; i++) {
1537                         callback.call(thisArg, this[i], i, this);
1538                 }
1539         };
1540 }
1541
1542
1543 var dummyElem, domParser;
1544
1545 function isElem(e)
1546 {
1547         return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
1548 }
1549
1550 function toElem(s)
1551 {
1552         var elem;
1553
1554         try {
1555                 domParser = domParser || new DOMParser();
1556                 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
1557         }
1558         catch(e) {}
1559
1560         if (!elem) {
1561                 try {
1562                         dummyElem = dummyElem || document.createElement('div');
1563                         dummyElem.innerHTML = s;
1564                         elem = dummyElem.firstChild;
1565                 }
1566                 catch (e) {}
1567         }
1568
1569         return elem || null;
1570 }
1571
1572 function findParent(node, selector)
1573 {
1574         while (node)
1575                 if (node.msMatchesSelector && node.msMatchesSelector(selector))
1576                         return node;
1577                 else if (node.matches && node.matches(selector))
1578                         return node;
1579                 else
1580                         node = node.parentNode;
1581
1582         return null;
1583 }
1584
1585 function E()
1586 {
1587         var html = arguments[0],
1588             attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
1589             data = attr ? arguments[2] : arguments[1],
1590             elem;
1591
1592         if (isElem(html))
1593                 elem = html;
1594         else if (html.charCodeAt(0) === 60)
1595                 elem = toElem(html);
1596         else
1597                 elem = document.createElement(html);
1598
1599         if (!elem)
1600                 return null;
1601
1602         if (attr)
1603                 for (var key in attr)
1604                         if (attr.hasOwnProperty(key) && attr[key] !== null && attr[key] !== undefined)
1605                                 elem.setAttribute(key, attr[key]);
1606
1607         if (typeof(data) === 'function')
1608                 data = data(elem);
1609
1610         if (isElem(data)) {
1611                 elem.appendChild(data);
1612         }
1613         else if (Array.isArray(data)) {
1614                 for (var i = 0; i < data.length; i++)
1615                         if (isElem(data[i]))
1616                                 elem.appendChild(data[i]);
1617                         else
1618                                 elem.appendChild(document.createTextNode('' + data[i]));
1619         }
1620         else if (data !== null && data !== undefined) {
1621                 elem.innerHTML = '' + data;
1622         }
1623
1624         return elem;
1625 }
1626
1627 if (typeof(window.CustomEvent) !== 'function') {
1628         function CustomEvent(event, params) {
1629                 params = params || { bubbles: false, cancelable: false, detail: undefined };
1630                 var evt = document.createEvent('CustomEvent');
1631                     evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
1632                 return evt;
1633         }
1634
1635         CustomEvent.prototype = window.Event.prototype;
1636         window.CustomEvent = CustomEvent;
1637 }
1638
1639 CBIDropdown = {
1640         openDropdown: function(sb) {
1641                 var st = window.getComputedStyle(sb, null),
1642                     ul = sb.querySelector('ul'),
1643                     li = ul.querySelectorAll('li'),
1644                     sel = ul.querySelector('[selected]'),
1645                     rect = sb.getBoundingClientRect(),
1646                     h = sb.clientHeight - parseFloat(st.paddingTop) - parseFloat(st.paddingBottom),
1647                     mh = this.dropdown_items * h,
1648                     eh = Math.min(mh, li.length * h);
1649
1650                 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
1651                         s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1652                 });
1653
1654                 ul.style.maxHeight = mh + 'px';
1655                 sb.setAttribute('open', '');
1656
1657                 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
1658                 ul.querySelectorAll('[selected] input[type="checkbox"]').forEach(function(c) {
1659                         c.checked = true;
1660                 });
1661
1662                 ul.style.top = ul.style.bottom = '';
1663                 ul.style[((sb.getBoundingClientRect().top + eh) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
1664                 ul.classList.add('dropdown');
1665
1666                 var pv = ul.cloneNode(true);
1667                     pv.classList.remove('dropdown');
1668                     pv.classList.add('preview');
1669
1670                 sb.insertBefore(pv, ul.nextElementSibling);
1671
1672                 li.forEach(function(l) {
1673                         l.setAttribute('tabindex', 0);
1674                 });
1675
1676                 sb.lastElementChild.setAttribute('tabindex', 0);
1677
1678                 this.setFocus(sb, sel || li[0], true);
1679         },
1680
1681         closeDropdown: function(sb, no_focus) {
1682                 if (!sb.hasAttribute('open'))
1683                         return;
1684
1685                 var pv = sb.querySelector('ul.preview'),
1686                     ul = sb.querySelector('ul.dropdown'),
1687                     li = ul.querySelectorAll('li');
1688
1689                 li.forEach(function(l) { l.removeAttribute('tabindex'); });
1690                 sb.lastElementChild.removeAttribute('tabindex');
1691
1692                 sb.removeChild(pv);
1693                 sb.removeAttribute('open');
1694                 sb.style.width = sb.style.height = '';
1695
1696                 ul.classList.remove('dropdown');
1697
1698                 if (!no_focus)
1699                         this.setFocus(sb, sb);
1700
1701                 this.saveValues(sb, ul);
1702         },
1703
1704         toggleItem: function(sb, li, force_state) {
1705                 if (li.hasAttribute('unselectable'))
1706                         return;
1707
1708                 if (this.multi) {
1709                         var cbox = li.querySelector('input[type="checkbox"]'),
1710                             items = li.parentNode.querySelectorAll('li'),
1711                             label = sb.querySelector('ul.preview'),
1712                             sel = li.parentNode.querySelectorAll('[selected]').length,
1713                             more = sb.querySelector('.more'),
1714                             ndisplay = this.display_items,
1715                             n = 0;
1716
1717                         if (li.hasAttribute('selected')) {
1718                                 if (force_state !== true) {
1719                                         if (sel > 1 || this.optional) {
1720                                                 li.removeAttribute('selected');
1721                                                 cbox.checked = cbox.disabled = false;
1722                                                 sel--;
1723                                         }
1724                                         else {
1725                                                 cbox.disabled = true;
1726                                         }
1727                                 }
1728                         }
1729                         else {
1730                                 if (force_state !== false) {
1731                                         li.setAttribute('selected', '');
1732                                         cbox.checked = true;
1733                                         cbox.disabled = false;
1734                                         sel++;
1735                                 }
1736                         }
1737
1738                         while (label.firstElementChild)
1739                                 label.removeChild(label.firstElementChild);
1740
1741                         for (var i = 0; i < items.length; i++) {
1742                                 items[i].removeAttribute('display');
1743                                 if (items[i].hasAttribute('selected')) {
1744                                         if (ndisplay-- > 0) {
1745                                                 items[i].setAttribute('display', n++);
1746                                                 label.appendChild(items[i].cloneNode(true));
1747                                         }
1748                                         var c = items[i].querySelector('input[type="checkbox"]');
1749                                         if (c)
1750                                                 c.disabled = (sel == 1 && !this.optional);
1751                                 }
1752                         }
1753
1754                         if (ndisplay < 0)
1755                                 sb.setAttribute('more', '');
1756                         else
1757                                 sb.removeAttribute('more');
1758
1759                         if (ndisplay === this.display_items)
1760                                 sb.setAttribute('empty', '');
1761                         else
1762                                 sb.removeAttribute('empty');
1763
1764                         more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···';
1765                 }
1766                 else {
1767                         var sel = li.parentNode.querySelector('[selected]');
1768                         if (sel) {
1769                                 sel.removeAttribute('display');
1770                                 sel.removeAttribute('selected');
1771                         }
1772
1773                         li.setAttribute('display', 0);
1774                         li.setAttribute('selected', '');
1775
1776                         this.closeDropdown(sb, true);
1777                 }
1778
1779                 this.saveValues(sb, li.parentNode);
1780         },
1781
1782         transformItem: function(sb, li) {
1783                 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
1784                     label = E('label');
1785
1786                 while (li.firstChild)
1787                         label.appendChild(li.firstChild);
1788
1789                 li.appendChild(cbox);
1790                 li.appendChild(label);
1791         },
1792
1793         saveValues: function(sb, ul) {
1794                 var sel = ul.querySelectorAll('[selected]'),
1795                     div = sb.lastElementChild;
1796
1797                 while (div.lastElementChild)
1798                         div.removeChild(div.lastElementChild);
1799
1800                 sel.forEach(function (s) {
1801                         div.appendChild(E('input', {
1802                                 type: 'hidden',
1803                                 name: s.hasAttribute('name') ? s.getAttribute('name') : (sb.getAttribute('name') || ''),
1804                                 value: s.hasAttribute('value') ? s.getAttribute('value') : s.innerText
1805                         }));
1806                 });
1807
1808                 cbi_d_update();
1809         },
1810
1811         setFocus: function(sb, elem, scroll) {
1812                 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
1813                         return;
1814
1815                 document.querySelectorAll('.focus').forEach(function(e) {
1816                         if (e.nodeName.toLowerCase() !== 'input') {
1817                                 e.classList.remove('focus');
1818                                 e.blur();
1819                         }
1820                 });
1821
1822                 if (elem) {
1823                         elem.focus();
1824                         elem.classList.add('focus');
1825
1826                         if (scroll)
1827                                 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
1828                 }
1829         },
1830
1831         createItems: function(sb, value) {
1832                 var sbox = this,
1833                     val = (value || '').trim().split(/\s+/),
1834                     ul = sb.querySelector('ul');
1835
1836                 if (!sbox.multi)
1837                         val.length = Math.min(val.length, 1);
1838
1839                 val.forEach(function(item) {
1840                         var new_item = null;
1841
1842                         ul.childNodes.forEach(function(li) {
1843                                 if (li.getAttribute && li.getAttribute('value') === item)
1844                                         new_item = li;
1845                         });
1846
1847                         if (!new_item) {
1848                                 var markup,
1849                                     tpl = sb.querySelector(sbox.template);
1850
1851                                 if (tpl)
1852                                         markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
1853                                 else
1854                                         markup = '<li value="{{value}}">{{value}}</li>';
1855
1856                                 new_item = E(markup.replace(/{{value}}/g, item));
1857
1858                                 if (sbox.multi) {
1859                                         sbox.transformItem(sb, new_item);
1860                                 }
1861                                 else {
1862                                         var old = ul.querySelector('li[created]');
1863                                         if (old)
1864                                                 ul.removeChild(old);
1865
1866                                         new_item.setAttribute('created', '');
1867                                 }
1868
1869                                 new_item = ul.insertBefore(new_item, ul.lastElementChild);
1870                         }
1871
1872                         sbox.toggleItem(sb, new_item, true);
1873                         sbox.setFocus(sb, new_item, true);
1874                 });
1875         },
1876
1877         closeAllDropdowns: function() {
1878                 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
1879                         s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
1880                 });
1881         }
1882 };
1883
1884 function cbi_dropdown_init(sb) {
1885         if (!(this instanceof cbi_dropdown_init))
1886                 return new cbi_dropdown_init(sb);
1887
1888         this.multi = sb.hasAttribute('multiple');
1889         this.optional = sb.hasAttribute('optional');
1890         this.placeholder = sb.getAttribute('placeholder') || '---';
1891         this.display_items = parseInt(sb.getAttribute('display-items') || 3);
1892         this.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || 5);
1893         this.create = sb.getAttribute('item-create') || '.create-item-input';
1894         this.template = sb.getAttribute('item-template') || 'script[type="item-template"]';
1895
1896         var sbox = this,
1897             ul = sb.querySelector('ul'),
1898             items = ul.querySelectorAll('li'),
1899             more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
1900             open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, 'â–¾')),
1901             canary = sb.appendChild(E('div')),
1902             create = sb.querySelector(this.create),
1903             ndisplay = this.display_items,
1904             n = 0;
1905
1906         if (this.multi) {
1907                 for (var i = 0; i < items.length; i++) {
1908                         sbox.transformItem(sb, items[i]);
1909
1910                         if (items[i].hasAttribute('selected') && ndisplay-- > 0)
1911                                 items[i].setAttribute('display', n++);
1912                 }
1913         }
1914         else {
1915                 var sel = sb.querySelectorAll('[selected]');
1916
1917                 sel.forEach(function(s) {
1918                         s.removeAttribute('selected');
1919                 });
1920
1921                 var s = sel[0] || items[0];
1922                 if (s) {
1923                         s.setAttribute('selected', '');
1924                         s.setAttribute('display', n++);
1925                 }
1926
1927                 ndisplay--;
1928
1929                 if (this.optional && !ul.querySelector('li[value=""]')) {
1930                         var placeholder = E('li', { placeholder: '' }, this.placeholder);
1931                         ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder);
1932                 }
1933         }
1934
1935         sbox.saveValues(sb, ul);
1936
1937         ul.setAttribute('tabindex', -1);
1938         sb.setAttribute('tabindex', 0);
1939
1940         if (ndisplay < 0)
1941                 sb.setAttribute('more', '')
1942         else
1943                 sb.removeAttribute('more');
1944
1945         if (ndisplay === this.display_items)
1946                 sb.setAttribute('empty', '')
1947         else
1948                 sb.removeAttribute('empty');
1949
1950         more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···';
1951
1952
1953         sb.addEventListener('click', function(ev) {
1954                 if (!this.hasAttribute('open')) {
1955                         if (ev.target.nodeName.toLowerCase() !== 'input')
1956                                 sbox.openDropdown(this);
1957                 }
1958                 else {
1959                         var li = findParent(ev.target, 'li');
1960                         if (li && li.parentNode.classList.contains('dropdown'))
1961                                 sbox.toggleItem(this, li);
1962                 }
1963
1964                 ev.preventDefault();
1965                 ev.stopPropagation();
1966         });
1967
1968         sb.addEventListener('keydown', function(ev) {
1969                 if (ev.target.nodeName.toLowerCase() === 'input')
1970                         return;
1971
1972                 if (!this.hasAttribute('open')) {
1973                         switch (ev.keyCode) {
1974                         case 37:
1975                         case 38:
1976                         case 39:
1977                         case 40:
1978                                 sbox.openDropdown(this);
1979                                 ev.preventDefault();
1980                         }
1981                 }
1982                 else
1983                 {
1984                         var active = findParent(document.activeElement, 'li');
1985
1986                         switch (ev.keyCode) {
1987                         case 27:
1988                                 sbox.closeDropdown(this);
1989                                 break;
1990
1991                         case 13:
1992                                 if (active) {
1993                                         if (!active.hasAttribute('selected'))
1994                                                 sbox.toggleItem(this, active);
1995                                         sbox.closeDropdown(this);
1996                                         ev.preventDefault();
1997                                 }
1998                                 break;
1999
2000                         case 32:
2001                                 if (active) {
2002                                         sbox.toggleItem(this, active);
2003                                         ev.preventDefault();
2004                                 }
2005                                 break;
2006
2007                         case 38:
2008                                 if (active && active.previousElementSibling) {
2009                                         sbox.setFocus(this, active.previousElementSibling);
2010                                         ev.preventDefault();
2011                                 }
2012                                 break;
2013
2014                         case 40:
2015                                 if (active && active.nextElementSibling) {
2016                                         sbox.setFocus(this, active.nextElementSibling);
2017                                         ev.preventDefault();
2018                                 }
2019                                 break;
2020                         }
2021                 }
2022         });
2023
2024         sb.addEventListener('cbi-dropdown-close', function(ev) {
2025                 sbox.closeDropdown(this, true);
2026         });
2027
2028         if ('ontouchstart' in window) {
2029                 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
2030                 window.addEventListener('touchstart', sbox.closeAllDropdowns);
2031         }
2032         else {
2033                 sb.addEventListener('mouseover', function(ev) {
2034                         if (!this.hasAttribute('open'))
2035                                 return;
2036
2037                         var li = findParent(ev.target, 'li');
2038                         if (li) {
2039                                 if (li.parentNode.classList.contains('dropdown'))
2040                                         sbox.setFocus(this, li);
2041
2042                                 ev.stopPropagation();
2043                         }
2044                 });
2045
2046                 sb.addEventListener('focus', function(ev) {
2047                         document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
2048                                 if (s !== this || this.hasAttribute('open'))
2049                                         s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
2050                         });
2051                 });
2052
2053                 canary.addEventListener('focus', function(ev) {
2054                         sbox.closeDropdown(this.parentNode);
2055                 });
2056
2057                 window.addEventListener('mouseover', sbox.setFocus);
2058                 window.addEventListener('click', sbox.closeAllDropdowns);
2059         }
2060
2061         if (create) {
2062                 create.addEventListener('keydown', function(ev) {
2063                         switch (ev.keyCode) {
2064                         case 13:
2065                                 ev.preventDefault();
2066
2067                                 if (this.classList.contains('cbi-input-invalid'))
2068                                         return;
2069
2070                                 sbox.createItems(sb, this.value);
2071                                 this.value = '';
2072                                 this.blur();
2073                                 break;
2074                         }
2075                 });
2076
2077                 create.addEventListener('focus', function(ev) {
2078                         var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]');
2079                         if (cbox) cbox.checked = true;
2080                         sb.setAttribute('locked-in', '');
2081                 });
2082
2083                 create.addEventListener('blur', function(ev) {
2084                         var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]');
2085                         if (cbox) cbox.checked = false;
2086                         sb.removeAttribute('locked-in');
2087                 });
2088
2089                 var li = findParent(create, 'li');
2090
2091                 li.setAttribute('unselectable', '');
2092                 li.addEventListener('click', function(ev) {
2093                         this.querySelector(sbox.create).focus();
2094                 });
2095         }
2096 }
2097
2098 cbi_dropdown_init.prototype = CBIDropdown;
2099
2100 function cbi_update_table(table, data, placeholder) {
2101         target = isElem(table) ? table : document.querySelector(table);
2102
2103         if (!isElem(target))
2104                 return;
2105
2106         target.querySelectorAll('.tr.table-titles, .cbi-section-table-titles').forEach(function(thead) {
2107                 var titles = [];
2108
2109                 thead.querySelectorAll('.th').forEach(function(th) {
2110                         titles.push(th);
2111                 });
2112
2113                 if (Array.isArray(data)) {
2114                         var n = 0, rows = target.querySelectorAll('.tr');
2115
2116                         data.forEach(function(row) {
2117                                 var trow = E('div', { 'class': 'tr' });
2118
2119                                 for (var i = 0; i < titles.length; i++) {
2120                                         var text = (titles[i].innerText || '').trim();
2121                                         var td = trow.appendChild(E('div', {
2122                                                 'class': titles[i].className,
2123                                                 'data-title': (text !== '') ? text : null
2124                                         }, row[i] || ''));
2125
2126                                         td.classList.remove('th');
2127                                         td.classList.add('td');
2128                                 }
2129
2130                                 trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
2131
2132                                 if (rows[n])
2133                                         target.replaceChild(trow, rows[n]);
2134                                 else
2135                                         target.appendChild(trow);
2136                         });
2137
2138                         while (rows[++n])
2139                                 target.removeChild(rows[n]);
2140
2141                         if (placeholder && target.firstElementChild === target.lastElementChild) {
2142                                 var trow = target.appendChild(E('div', { 'class': 'tr placeholder' }));
2143                                 var td = trow.appendChild(E('div', { 'class': titles[0].className }, placeholder));
2144
2145                                 td.classList.remove('th');
2146                                 td.classList.add('td');
2147                         }
2148                 }
2149                 else {
2150                         thead.parentNode.style.display = 'none';
2151
2152                         thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) {
2153                                 if (trow !== thead) {
2154                                         var n = 0;
2155                                         trow.querySelectorAll('.th, .td').forEach(function(td) {
2156                                                 if (n < titles.length) {
2157                                                         var text = (titles[n++].innerText || '').trim();
2158                                                         if (text !== '')
2159                                                                 td.setAttribute('data-title', text);
2160                                                 }
2161                                         });
2162                                 }
2163                         });
2164
2165                         thead.parentNode.style.display = '';
2166                 }
2167         });
2168 }
2169
2170 document.addEventListener('DOMContentLoaded', function() {
2171         document.querySelectorAll('.table').forEach(cbi_update_table);
2172 });