2966fac9736f0e7d187e29b37ac6051c3e0d921d
[clamp.git] / src / main / resources / META-INF / resources / designer / lib / jsoneditor.js
1 /**
2  * @name JSON Editor
3  * @description JSON Schema Based Editor
4  * Deprecation notice
5  * This repo is no longer maintained (see also https://github.com/jdorn/json-editor/issues/800)
6  * Development is continued at https://github.com/json-editor/json-editor
7  * For details please visit https://github.com/json-editor/json-editor/issues/5
8  * @version 1.1.0-beta.2
9  * @author Jeremy Dorn
10  * @see https://github.com/jdorn/json-editor/
11  * @see https://github.com/json-editor/json-editor
12  * @license MIT
13  * @example see README.md and docs/ for requirements, examples and usage info
14  */
15
16 (function() {
17
18 /*jshint loopfunc: true */
19 /* Simple JavaScript Inheritance
20  * By John Resig http://ejohn.org/
21  * MIT Licensed.
22  */
23 // Inspired by base2 and Prototype
24 var Class;
25 (function(){
26   var initializing = false, fnTest = /xyz/.test(function(){window.postMessage("xyz");}) ? /\b_super\b/ : /.*/;
27  
28   // The base Class implementation (does nothing)
29   Class = function(){};
30  
31   // Create a new Class that inherits from this class
32   Class.extend = function extend(prop) {
33     var _super = this.prototype;
34    
35     // Instantiate a base class (but only create the instance,
36     // don't run the init constructor)
37     initializing = true;
38     var prototype = new this();
39     initializing = false;
40    
41     // Copy the properties over onto the new prototype
42     for (var name in prop) {
43       // Check if we're overwriting an existing function
44       prototype[name] = typeof prop[name] == "function" &&
45         typeof _super[name] == "function" && fnTest.test(prop[name]) ?
46         (function(name, fn){
47           return function() {
48             var tmp = this._super;
49            
50             // Add a new ._super() method that is the same method
51             // but on the super-class
52             this._super = _super[name];
53            
54             // The method only need to be bound temporarily, so we
55             // remove it when we're done executing
56             var ret = fn.apply(this, arguments);        
57             this._super = tmp;
58            
59             return ret;
60           };
61         })(name, prop[name]) :
62         prop[name];
63     }
64    
65     // The dummy class constructor
66     function Class() {
67       // All construction is actually done in the init method
68       if ( !initializing && this.init )
69         this.init.apply(this, arguments);
70     }
71    
72     // Populate our constructed prototype object
73     Class.prototype = prototype;
74    
75     // Enforce the constructor to be what we expect
76     Class.prototype.constructor = Class;
77  
78     // And make this class extendable
79     Class.extend = extend;
80    
81     return Class;
82   };
83   
84   return Class;
85 })();
86
87 // CustomEvent constructor polyfill
88 // From MDN
89 (function () {
90   function CustomEvent ( event, params ) {
91     params = params || { bubbles: false, cancelable: false, detail: undefined };
92     var evt = document.createEvent( 'CustomEvent' );
93     evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
94     return evt;
95   }
96
97   CustomEvent.prototype = window.Event.prototype;
98
99   window.CustomEvent = CustomEvent;
100 })();
101
102 // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
103 // MIT license
104 (function() {
105     var lastTime = 0;
106     var vendors = ['ms', 'moz', 'webkit', 'o'];
107     for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
108         window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
109         window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || 
110                                       window[vendors[x]+'CancelRequestAnimationFrame'];
111     }
112  
113     if (!window.requestAnimationFrame)
114         window.requestAnimationFrame = function(callback, element) {
115             var currTime = new Date().getTime();
116             var timeToCall = Math.max(0, 16 - (currTime - lastTime));
117             var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
118               timeToCall);
119             lastTime = currTime + timeToCall;
120             return id;
121         };
122  
123     if (!window.cancelAnimationFrame)
124         window.cancelAnimationFrame = function(id) {
125             clearTimeout(id);
126         };
127 }());
128
129 // Array.isArray polyfill
130 // From MDN
131 (function() {
132         if(!Array.isArray) {
133           Array.isArray = function(arg) {
134                 return Object.prototype.toString.call(arg) === '[object Array]';
135           };
136         }
137 }());
138 /**
139  * Taken from jQuery 2.1.3
140  *
141  * @param obj
142  * @returns {boolean}
143  */
144 var $isplainobject = function( obj ) {
145   // Not plain objects:
146   // - Any object or value whose internal [[Class]] property is not "[object Object]"
147   // - DOM nodes
148   // - window
149   if (typeof obj !== "object" || obj.nodeType || (obj !== null && obj === obj.window)) {
150     return false;
151   }
152
153   if (obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
154     return false;
155   }
156
157   // If the function hasn't returned already, we're confident that
158   // |obj| is a plain object, created by {} or constructed with new Object
159   return true;
160 };
161
162 var $extend = function(destination) {
163   var source, i,property;
164   for(i=1; i<arguments.length; i++) {
165     source = arguments[i];
166     for (property in source) {
167       if(!source.hasOwnProperty(property)) continue;
168       if(source[property] && $isplainobject(source[property])) {
169         if(!destination.hasOwnProperty(property)) destination[property] = {};
170         $extend(destination[property], source[property]);
171       }
172       else {
173         destination[property] = source[property];
174       }
175     }
176   }
177   return destination;
178 };
179
180 var $each = function(obj,callback) {
181   if(!obj || typeof obj !== "object") return;
182   var i;
183   if(Array.isArray(obj) || (typeof obj.length === 'number' && obj.length > 0 && (obj.length - 1) in obj)) {
184     for(i=0; i<obj.length; i++) {
185       if(callback(i,obj[i])===false) return;
186     }
187   }
188   else {
189     if (Object.keys) {
190       var keys = Object.keys(obj);
191       for(i=0; i<keys.length; i++) {
192         if(callback(keys[i],obj[keys[i]])===false) return;
193       }
194     }
195     else {
196       for(i in obj) {
197         if(!obj.hasOwnProperty(i)) continue;
198         if(callback(i,obj[i])===false) return;
199       }
200     }
201   }
202 };
203
204 var $trigger = function(el,event) {
205   var e = document.createEvent('HTMLEvents');
206   e.initEvent(event, true, true);
207   el.dispatchEvent(e);
208 };
209 var $triggerc = function(el,event) {
210   var e = new CustomEvent(event,{
211     bubbles: true,
212     cancelable: true
213   });
214
215   el.dispatchEvent(e);
216 };
217
218 var JSONEditor = function(element,options) {
219   if (!(element instanceof Element)) {
220     throw new Error('element should be an instance of Element');
221   }
222   options = $extend({},JSONEditor.defaults.options,options||{});
223   this.element = element;
224   this.options = options;
225   this.init();
226 };
227 JSONEditor.prototype = {
228   // necessary since we remove the ctor property by doing a literal assignment. Without this
229   // the $isplainobject function will think that this is a plain object.
230   constructor: JSONEditor,
231   init: function() {
232     var self = this;
233     
234     this.ready = false;
235     this.copyClipboard = null;
236
237     var theme_class = JSONEditor.defaults.themes[this.options.theme || JSONEditor.defaults.theme];
238     if(!theme_class) throw "Unknown theme " + (this.options.theme || JSONEditor.defaults.theme);
239     
240     this.schema = this.options.schema;
241     this.theme = new theme_class();
242     this.template = this.options.template;
243     this.refs = this.options.refs || {};
244     this.uuid = 0;
245     this.__data = {};
246     
247     var icon_class = JSONEditor.defaults.iconlibs[this.options.iconlib || JSONEditor.defaults.iconlib];
248     if(icon_class) this.iconlib = new icon_class();
249
250     this.root_container = this.theme.getContainer();
251     this.element.appendChild(this.root_container);
252     
253     this.translate = this.options.translate || JSONEditor.defaults.translate;
254
255     // Fetch all external refs via ajax
256     this._loadExternalRefs(this.schema, function() {
257       self._getDefinitions(self.schema);
258       
259       // Validator options
260       var validator_options = {};
261       if(self.options.custom_validators) {
262         validator_options.custom_validators = self.options.custom_validators;
263       }
264       self.validator = new JSONEditor.Validator(self,null,validator_options);
265       
266       // Create the root editor
267       var schema = self.expandRefs(self.schema);
268       var editor_class = self.getEditorClass(schema);
269       self.root = self.createEditor(editor_class, {
270         jsoneditor: self,
271         schema: schema,
272         required: true,
273         container: self.root_container
274       });
275       
276       self.root.preBuild();
277       self.root.build();
278       self.root.postBuild();
279
280       // Starting data
281       if(self.options.hasOwnProperty('startval')) self.root.setValue(self.options.startval, true);
282
283       self.validation_results = self.validator.validate(self.root.getValue());
284       self.root.showValidationErrors(self.validation_results);
285       self.ready = true;
286
287       // Fire ready event asynchronously
288       window.requestAnimationFrame(function() {
289         if(!self.ready) return;
290         self.validation_results = self.validator.validate(self.root.getValue());
291         self.root.showValidationErrors(self.validation_results);
292         self.trigger('ready');
293         self.trigger('change');
294       });
295     });
296   },
297   getValue: function() {
298     if(!this.ready) throw "JSON Editor not ready yet.  Listen for 'ready' event before getting the value";
299
300     return this.root.getValue();
301   },
302   setValue: function(value) {
303     if(!this.ready) throw "JSON Editor not ready yet.  Listen for 'ready' event before setting the value";
304
305     this.root.setValue(value);
306     return this;
307   },
308   validate: function(value) {
309     if(!this.ready) throw "JSON Editor not ready yet.  Listen for 'ready' event before validating";
310     
311     // Custom value
312     if(arguments.length === 1) {
313       return this.validator.validate(value);
314     }
315     // Current value (use cached result)
316     else {
317       return this.validation_results;
318     }
319   },
320   destroy: function() {
321     if(this.destroyed) return;
322     if(!this.ready) return;
323     
324     this.schema = null;
325     this.options = null;
326     this.root.destroy();
327     this.root = null;
328     this.root_container = null;
329     this.validator = null;
330     this.validation_results = null;
331     this.theme = null;
332     this.iconlib = null;
333     this.template = null;
334     this.__data = null;
335     this.ready = false;
336     this.element.innerHTML = '';
337     
338     this.destroyed = true;
339   },
340   on: function(event, callback) {
341     this.callbacks = this.callbacks || {};
342     this.callbacks[event] = this.callbacks[event] || [];
343     this.callbacks[event].push(callback);
344     
345     return this;
346   },
347   off: function(event, callback) {
348     // Specific callback
349     if(event && callback) {
350       this.callbacks = this.callbacks || {};
351       this.callbacks[event] = this.callbacks[event] || [];
352       var newcallbacks = [];
353       for(var i=0; i<this.callbacks[event].length; i++) {
354         if(this.callbacks[event][i]===callback) continue;
355         newcallbacks.push(this.callbacks[event][i]);
356       }
357       this.callbacks[event] = newcallbacks;
358     }
359     // All callbacks for a specific event
360     else if(event) {
361       this.callbacks = this.callbacks || {};
362       this.callbacks[event] = [];
363     }
364     // All callbacks for all events
365     else {
366       this.callbacks = {};
367     }
368     
369     return this;
370   },
371   trigger: function(event) {
372     if(this.callbacks && this.callbacks[event] && this.callbacks[event].length) {
373       for(var i=0; i<this.callbacks[event].length; i++) {
374         this.callbacks[event][i].apply(this, []);
375       }
376     }
377     
378     return this;
379   },
380   setOption: function(option, value) {
381     if(option === "show_errors") {
382       this.options.show_errors = value;
383       this.onChange();
384     }
385     // Only the `show_errors` option is supported for now
386     else {
387       throw "Option "+option+" must be set during instantiation and cannot be changed later";
388     }
389     
390     return this;
391   },
392   getEditorClass: function(schema) {
393     var classname;
394
395     schema = this.expandSchema(schema);
396
397     $each(JSONEditor.defaults.resolvers,function(i,resolver) {
398       var tmp = resolver(schema);
399       if(tmp) {
400         if(JSONEditor.defaults.editors[tmp]) {
401           classname = tmp;
402           return false;
403         }
404       }
405     });
406
407     if(!classname) throw "Unknown editor for schema "+JSON.stringify(schema);
408     if(!JSONEditor.defaults.editors[classname]) throw "Unknown editor "+classname;
409
410     return JSONEditor.defaults.editors[classname];
411   },
412   createEditor: function(editor_class, options) {
413     options = $extend({},editor_class.options||{},options);
414     return new editor_class(options);
415   },
416   onChange: function() {
417     if(!this.ready) return;
418     
419     if(this.firing_change) return;
420     this.firing_change = true;
421     
422     var self = this;
423     
424     window.requestAnimationFrame(function() {
425       self.firing_change = false;
426       if(!self.ready) return;
427
428       // Validate and cache results
429       self.validation_results = self.validator.validate(self.root.getValue());
430       
431       if(self.options.show_errors !== "never") {
432         self.root.showValidationErrors(self.validation_results);
433       }
434       else {
435         self.root.showValidationErrors([]);
436       }
437       
438       // Fire change event
439       self.trigger('change');
440     });
441     
442     return this;
443   },
444   compileTemplate: function(template, name) {
445     name = name || JSONEditor.defaults.template;
446
447     var engine;
448
449     // Specifying a preset engine
450     if(typeof name === 'string') {
451       if(!JSONEditor.defaults.templates[name]) throw "Unknown template engine "+name;
452       engine = JSONEditor.defaults.templates[name]();
453
454       if(!engine) throw "Template engine "+name+" missing required library.";
455     }
456     // Specifying a custom engine
457     else {
458       engine = name;
459     }
460
461     if(!engine) throw "No template engine set";
462     if(!engine.compile) throw "Invalid template engine set";
463
464     return engine.compile(template);
465   },
466   _data: function(el,key,value) {
467     // Setting data
468     if(arguments.length === 3) {
469       var uuid;
470       if(el.hasAttribute('data-jsoneditor-'+key)) {
471         uuid = el.getAttribute('data-jsoneditor-'+key);
472       }
473       else {
474         uuid = this.uuid++;
475         el.setAttribute('data-jsoneditor-'+key,uuid);
476       }
477
478       this.__data[uuid] = value;
479     }
480     // Getting data
481     else {
482       // No data stored
483       if(!el.hasAttribute('data-jsoneditor-'+key)) return null;
484       
485       return this.__data[el.getAttribute('data-jsoneditor-'+key)];
486     }
487   },
488   registerEditor: function(editor) {
489     this.editors = this.editors || {};
490     this.editors[editor.path] = editor;
491     return this;
492   },
493   unregisterEditor: function(editor) {
494     this.editors = this.editors || {};
495     this.editors[editor.path] = null;
496     return this;
497   },
498   getEditor: function(path) {
499     if(!this.editors) return;
500     return this.editors[path];
501   },
502   watch: function(path,callback) {
503     this.watchlist = this.watchlist || {};
504     this.watchlist[path] = this.watchlist[path] || [];
505     this.watchlist[path].push(callback);
506     
507     return this;
508   },
509   unwatch: function(path,callback) {
510     if(!this.watchlist || !this.watchlist[path]) return this;
511     // If removing all callbacks for a path
512     if(!callback) {
513       this.watchlist[path] = null;
514       return this;
515     }
516     
517     var newlist = [];
518     for(var i=0; i<this.watchlist[path].length; i++) {
519       if(this.watchlist[path][i] === callback) continue;
520       else newlist.push(this.watchlist[path][i]);
521     }
522     this.watchlist[path] = newlist.length? newlist : null;
523     return this;
524   },
525   notifyWatchers: function(path) {
526     if(!this.watchlist || !this.watchlist[path]) return this;
527     for(var i=0; i<this.watchlist[path].length; i++) {
528       this.watchlist[path][i]();
529     }
530   },
531   isEnabled: function() {
532     return !this.root || this.root.isEnabled();
533   },
534   enable: function() {
535     this.root.enable();
536   },
537   disable: function() {
538     this.root.disable();
539   },
540   _getDefinitions: function(schema,path) {
541     path = path || '#/definitions/';
542     if(schema.definitions) {
543       for(var i in schema.definitions) {
544         if(!schema.definitions.hasOwnProperty(i)) continue;
545         this.refs[path+i] = schema.definitions[i];
546         if(schema.definitions[i].definitions) {
547           this._getDefinitions(schema.definitions[i],path+i+'/definitions/');
548         }
549       }
550     }
551   },
552   _getExternalRefs: function(schema) {
553     var refs = {};
554     var merge_refs = function(newrefs) {
555       for(var i in newrefs) {
556         if(newrefs.hasOwnProperty(i)) {
557           refs[i] = true;
558         }
559       }
560     };
561     
562     if(schema.$ref && typeof schema.$ref !== "object" && schema.$ref.substr(0,1) !== "#" && !this.refs[schema.$ref]) {
563       refs[schema.$ref] = true;
564     }
565     
566     for(var i in schema) {
567       if(!schema.hasOwnProperty(i)) continue;
568       if(schema[i] && typeof schema[i] === "object" && Array.isArray(schema[i])) {
569         for(var j=0; j<schema[i].length; j++) {
570           if(schema[i][j] && typeof schema[i][j]==="object") {
571             merge_refs(this._getExternalRefs(schema[i][j]));
572           }
573         }
574       }
575       else if(schema[i] && typeof schema[i] === "object") {
576         merge_refs(this._getExternalRefs(schema[i]));
577       }
578     }
579     
580     return refs;
581   },
582   _loadExternalRefs: function(schema, callback) {
583     var self = this;
584     var refs = this._getExternalRefs(schema);
585     
586     var done = 0, waiting = 0, callback_fired = false;
587     
588     $each(refs,function(url) {
589       if(self.refs[url]) return;
590       if(!self.options.ajax) throw "Must set ajax option to true to load external ref "+url;
591       self.refs[url] = 'loading';
592       waiting++;
593
594       var fetchUrl=url;
595       if( self.options.ajaxBase && self.options.ajaxBase!=url.substr(0,self.options.ajaxBase.length) && "http"!=url.substr(0,4)) fetchUrl=self.options.ajaxBase+url;
596
597       var r = new XMLHttpRequest(); 
598       r.open("GET", fetchUrl, true);
599       if(self.options.ajaxCredentials) r.withCredentials=self.options.ajaxCredentials;
600       r.onreadystatechange = function () {
601         if (r.readyState != 4) return; 
602         // Request succeeded
603         if(r.status === 200) {
604           var response;
605           try {
606             response = JSON.parse(r.responseText);
607           }
608           catch(e) {
609             window.console.log(e);
610             throw "Failed to parse external ref "+fetchUrl;
611           }
612           if(!response || typeof response !== "object") throw "External ref does not contain a valid schema - "+fetchUrl;
613           
614           self.refs[url] = response;
615           self._loadExternalRefs(response,function() {
616             done++;
617             if(done >= waiting && !callback_fired) {
618               callback_fired = true;
619               callback();
620             }
621           });
622         }
623         // Request failed
624         else {
625           window.console.log(r);
626           throw "Failed to fetch ref via ajax- "+url;
627         }
628       };
629       r.send();
630     });
631     
632     if(!waiting) {
633       callback();
634     }
635   },
636   expandRefs: function(schema) {
637     schema = $extend({},schema);
638     
639     while (schema.$ref) {
640       var ref = schema.$ref;
641       delete schema.$ref;
642       
643       if(!this.refs[ref]) ref = decodeURIComponent(ref);
644       
645       schema = this.extendSchemas(schema,this.refs[ref]);
646     }
647     return schema;
648   },
649   expandSchema: function(schema) {
650     var self = this;
651     var extended = $extend({},schema);
652     var i;
653
654     // Version 3 `type`
655     if(typeof schema.type === 'object') {
656       // Array of types
657       if(Array.isArray(schema.type)) {
658         $each(schema.type, function(key,value) {
659           // Schema
660           if(typeof value === 'object') {
661             schema.type[key] = self.expandSchema(value);
662           }
663         });
664       }
665       // Schema
666       else {
667         schema.type = self.expandSchema(schema.type);
668       }
669     }
670     // Version 3 `disallow`
671     if(typeof schema.disallow === 'object') {
672       // Array of types
673       if(Array.isArray(schema.disallow)) {
674         $each(schema.disallow, function(key,value) {
675           // Schema
676           if(typeof value === 'object') {
677             schema.disallow[key] = self.expandSchema(value);
678           }
679         });
680       }
681       // Schema
682       else {
683         schema.disallow = self.expandSchema(schema.disallow);
684       }
685     }
686     // Version 4 `anyOf`
687     if(schema.anyOf) {
688       $each(schema.anyOf, function(key,value) {
689         schema.anyOf[key] = self.expandSchema(value);
690       });
691     }
692     // Version 4 `dependencies` (schema dependencies)
693     if(schema.dependencies) {
694       $each(schema.dependencies,function(key,value) {
695         if(typeof value === "object" && !(Array.isArray(value))) {
696           schema.dependencies[key] = self.expandSchema(value);
697         }
698       });
699     }
700     // Version 4 `not`
701     if(schema.not) {
702       schema.not = this.expandSchema(schema.not);
703     }
704     
705     // allOf schemas should be merged into the parent
706     if(schema.allOf) {
707       for(i=0; i<schema.allOf.length; i++) {
708         extended = this.extendSchemas(extended,this.expandSchema(schema.allOf[i]));
709       }
710       delete extended.allOf;
711     }
712     // extends schemas should be merged into parent
713     if(schema["extends"]) {
714       // If extends is a schema
715       if(!(Array.isArray(schema["extends"]))) {
716         extended = this.extendSchemas(extended,this.expandSchema(schema["extends"]));
717       }
718       // If extends is an array of schemas
719       else {
720         for(i=0; i<schema["extends"].length; i++) {
721           extended = this.extendSchemas(extended,this.expandSchema(schema["extends"][i]));
722         }
723       }
724       delete extended["extends"];
725     }
726     // parent should be merged into oneOf schemas
727     if(schema.oneOf) {
728       var tmp = $extend({},extended);
729       delete tmp.oneOf;
730       for(i=0; i<schema.oneOf.length; i++) {
731         extended.oneOf[i] = this.extendSchemas(this.expandSchema(schema.oneOf[i]),tmp);
732       }
733     }
734     
735     return this.expandRefs(extended);
736   },
737   extendSchemas: function(obj1, obj2) {
738     obj1 = $extend({},obj1);
739     obj2 = $extend({},obj2);
740
741     var self = this;
742     var extended = {};
743     $each(obj1, function(prop,val) {
744       // If this key is also defined in obj2, merge them
745       if(typeof obj2[prop] !== "undefined") {
746         // Required and defaultProperties arrays should be unioned together
747         if((prop === 'required'||prop === 'defaultProperties') && typeof val === "object" && Array.isArray(val)) {
748           // Union arrays and unique
749           extended[prop] = val.concat(obj2[prop]).reduce(function(p, c) {
750             if (p.indexOf(c) < 0) p.push(c);
751             return p;
752           }, []);
753         }
754         // Type should be intersected and is either an array or string
755         else if(prop === 'type' && (typeof val === "string" || Array.isArray(val))) {
756           // Make sure we're dealing with arrays
757           if(typeof val === "string") val = [val];
758           if(typeof obj2.type === "string") obj2.type = [obj2.type];
759
760           // If type is only defined in the first schema, keep it
761           if(!obj2.type || !obj2.type.length) {
762             extended.type = val;
763           }
764           // If type is defined in both schemas, do an intersect
765           else {
766             extended.type = val.filter(function(n) {
767               return obj2.type.indexOf(n) !== -1;
768             });
769           }
770
771           // If there's only 1 type and it's a primitive, use a string instead of array
772           if(extended.type.length === 1 && typeof extended.type[0] === "string") {
773             extended.type = extended.type[0];
774           }
775           // Remove the type property if it's empty
776           else if(extended.type.length === 0) {
777             delete extended.type;
778           }
779         }
780         // All other arrays should be intersected (enum, etc.)
781         else if(typeof val === "object" && Array.isArray(val)){
782           extended[prop] = val.filter(function(n) {
783             return obj2[prop].indexOf(n) !== -1;
784           });
785         }
786         // Objects should be recursively merged
787         else if(typeof val === "object" && val !== null) {
788           extended[prop] = self.extendSchemas(val,obj2[prop]);
789         }
790         // Otherwise, use the first value
791         else {
792           extended[prop] = val;
793         }
794       }
795       // Otherwise, just use the one in obj1
796       else {
797         extended[prop] = val;
798       }
799     });
800     // Properties in obj2 that aren't in obj1
801     $each(obj2, function(prop,val) {
802       if(typeof obj1[prop] === "undefined") {
803         extended[prop] = val;
804       }
805     });
806
807     return extended;
808   },
809   setCopyClipboardContents: function(value) {
810     this.copyClipboard = value;
811   },
812   getCopyClipboardContents: function() {
813     return this.copyClipboard;
814   }
815 };
816
817 JSONEditor.defaults = {
818   themes: {},
819   templates: {},
820   iconlibs: {},
821   editors: {},
822   languages: {},
823   resolvers: [],
824   custom_validators: []
825 };
826
827 JSONEditor.Validator = Class.extend({
828   init: function(jsoneditor,schema,options) {
829     this.jsoneditor = jsoneditor;
830     this.schema = schema || this.jsoneditor.schema;
831     this.options = options || {};
832     this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
833   },
834   validate: function(value) {
835     return this._validateSchema(this.schema, value);
836   },
837   _validateSchema: function(schema,value,path) {
838     var self = this;
839     var errors = [];
840     var valid, i, j;
841     var stringified = JSON.stringify(value);
842
843     path = path || 'root';
844
845     // Work on a copy of the schema
846     schema = $extend({},this.jsoneditor.expandRefs(schema));
847
848     /*
849      * Type Agnostic Validation
850      */
851
852     // Version 3 `required` and `required_by_default`
853     if(typeof value === "undefined" || value === null) {
854       if((typeof schema.required !== "undefined" && schema.required === true) || (typeof schema.required === "undefined" && this.jsoneditor.options.required_by_default === true)) {
855         errors.push({
856           path: path,
857           property: 'required',
858           message: this.translate("error_notset", [schema.title ? schema.title : path.split('-').pop().trim()])
859         });
860       }
861
862       return errors;
863     }
864
865     // `enum`
866     if(schema["enum"]) {
867       valid = false;
868       for(i=0; i<schema["enum"].length; i++) {
869         if(stringified === JSON.stringify(schema["enum"][i])) valid = true;
870       }
871       if(!valid) {
872         errors.push({
873           path: path,
874           property: 'enum',
875           message: this.translate("error_enum", [schema.title ? schema.title : path.split('-').pop().trim()])
876         });
877       }
878     }
879
880     // `extends` (version 3)
881     if(schema["extends"]) {
882       for(i=0; i<schema["extends"].length; i++) {
883         errors = errors.concat(this._validateSchema(schema["extends"][i],value,path));
884       }
885     }
886
887     // `allOf`
888     if(schema.allOf) {
889       for(i=0; i<schema.allOf.length; i++) {
890         errors = errors.concat(this._validateSchema(schema.allOf[i],value,path));
891       }
892     }
893
894     // `anyOf`
895     if(schema.anyOf) {
896       valid = false;
897       for(i=0; i<schema.anyOf.length; i++) {
898         if(!this._validateSchema(schema.anyOf[i],value,path).length) {
899           valid = true;
900           break;
901         }
902       }
903       if(!valid) {
904         errors.push({
905           path: path,
906           property: 'anyOf',
907           message: this.translate('error_anyOf')
908         });
909       }
910     }
911
912     // `oneOf`
913     if(schema.oneOf) {
914       valid = 0;
915       var oneof_errors = [];
916       for(i=0; i<schema.oneOf.length; i++) {
917         // Set the error paths to be path.oneOf[i].rest.of.path
918         var tmp = this._validateSchema(schema.oneOf[i],value,path);
919         if(!tmp.length) {
920           valid++;
921         }
922
923         for(j=0; j<tmp.length; j++) {
924           tmp[j].path = path+'.oneOf['+i+']'+tmp[j].path.substr(path.length);
925         }
926         oneof_errors = oneof_errors.concat(tmp);
927
928       }
929       if(valid !== 1) {
930         errors.push({
931           path: path,
932           property: 'oneOf',
933           message: this.translate('error_oneOf', [valid])
934         });
935         errors = errors.concat(oneof_errors);
936       }
937     }
938
939     // `not`
940     if(schema.not) {
941       if(!this._validateSchema(schema.not,value,path).length) {
942         errors.push({
943           path: path,
944           property: 'not',
945           message: this.translate('error_not')
946         });
947       }
948     }
949
950     // `type` (both Version 3 and Version 4 support)
951     if(schema.type) {
952       // Union type
953       if(Array.isArray(schema.type)) {
954         valid = false;
955         for(i=0;i<schema.type.length;i++) {
956           if(this._checkType(schema.type[i], value)) {
957             valid = true;
958             break;
959           }
960         }
961         if(!valid) {
962           errors.push({
963             path: path,
964             property: 'type',
965             message: this.translate('error_type_union')
966           });
967         }
968       }
969       // Simple type
970       else {
971         if(!this._checkType(schema.type, value)) {
972           errors.push({
973             path: path,
974             property: 'type',
975             message: this.translate('error_type', [schema.type])
976           });
977         }
978       }
979     }
980
981
982     // `disallow` (version 3)
983     if(schema.disallow) {
984       // Union type
985       if(Array.isArray(schema.disallow)) {
986         valid = true;
987         for(i=0;i<schema.disallow.length;i++) {
988           if(this._checkType(schema.disallow[i], value)) {
989             valid = false;
990             break;
991           }
992         }
993         if(!valid) {
994           errors.push({
995             path: path,
996             property: 'disallow',
997             message: this.translate('error_disallow_union')
998           });
999         }
1000       }
1001       // Simple type
1002       else {
1003         if(this._checkType(schema.disallow, value)) {
1004           errors.push({
1005             path: path,
1006             property: 'disallow',
1007             message: this.translate('error_disallow', [schema.disallow])
1008           });
1009         }
1010       }
1011     }
1012
1013     /*
1014      * Type Specific Validation
1015      */
1016
1017     // Number Specific Validation
1018     if(typeof value === "number") {
1019       // `multipleOf` and `divisibleBy`
1020       if(schema.multipleOf || schema.divisibleBy) {
1021         var divisor = schema.multipleOf || schema.divisibleBy;
1022         // Vanilla JS, prone to floating point rounding errors (e.g. 1.14 / .01 == 113.99999)
1023         valid = (value/divisor === Math.floor(value/divisor));
1024
1025         // Use math.js is available
1026         if(window.math) {
1027           valid = window.math.mod(window.math.bignumber(value), window.math.bignumber(divisor)).equals(0);
1028         }
1029         // Use decimal.js is available
1030         else if(window.Decimal) {
1031           valid = (new window.Decimal(value)).mod(new window.Decimal(divisor)).equals(0);
1032         }
1033
1034         if(!valid) {
1035           errors.push({
1036             path: path,
1037             property: schema.multipleOf? 'multipleOf' : 'divisibleBy',
1038             message: this.translate('error_multipleOf', [divisor])
1039           });
1040         }
1041       }
1042
1043       // `maximum`
1044       if(schema.hasOwnProperty('maximum')) {
1045         // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
1046         valid = schema.exclusiveMaximum? (value < schema.maximum) : (value <= schema.maximum);
1047
1048         // Use math.js is available
1049         if(window.math) {
1050           valid = window.math[schema.exclusiveMaximum?'smaller':'smallerEq'](
1051             window.math.bignumber(value),
1052             window.math.bignumber(schema.maximum)
1053           );
1054         }
1055         // Use Decimal.js if available
1056         else if(window.Decimal) {
1057           valid = (new window.Decimal(value))[schema.exclusiveMaximum?'lt':'lte'](new window.Decimal(schema.maximum));
1058         }
1059
1060         if(!valid) {
1061           errors.push({
1062             path: path,
1063             property: 'maximum',
1064             message: this.translate(
1065               (schema.exclusiveMaximum?'error_maximum_excl':'error_maximum_incl'),
1066               [schema.title ? schema.title : path.split('-').pop().trim(), schema.maximum]
1067             )
1068           });
1069         }
1070       }
1071
1072       // `minimum`
1073       if(schema.hasOwnProperty('minimum')) {
1074         // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
1075         valid = schema.exclusiveMinimum? (value > schema.minimum) : (value >= schema.minimum);
1076
1077         // Use math.js is available
1078         if(window.math) {
1079           valid = window.math[schema.exclusiveMinimum?'larger':'largerEq'](
1080             window.math.bignumber(value),
1081             window.math.bignumber(schema.minimum)
1082           );
1083         }
1084         // Use Decimal.js if available
1085         else if(window.Decimal) {
1086           valid = (new window.Decimal(value))[schema.exclusiveMinimum?'gt':'gte'](new window.Decimal(schema.minimum));
1087         }
1088
1089         if(!valid) {
1090           errors.push({
1091             path: path,
1092             property: 'minimum',
1093             message: this.translate(
1094               (schema.exclusiveMinimum?'error_minimum_excl':'error_minimum_incl'),
1095               [schema.title ? schema.title : path.split('-').pop().trim(), schema.minimum]
1096             )
1097           });
1098         }
1099       }
1100     }
1101     // String specific validation
1102     else if(typeof value === "string") {
1103       // `maxLength`
1104       if(schema.maxLength) {
1105         if((value+"").length > schema.maxLength) {
1106           errors.push({
1107             path: path,
1108             property: 'maxLength',
1109             message: this.translate('error_maxLength', 
1110                         [schema.title ? schema.title : path.split('-').pop().trim(), schema.maxLength])
1111           });
1112         }
1113       }
1114
1115       // `minLength` -- Commented because we are validating required field. 
1116       if(schema.minLength) {
1117         if((value+"").length < schema.minLength) {
1118           errors.push({
1119             path: path,
1120             property: 'minLength',
1121             message: this.translate((schema.minLength===1?'error_notempty':'error_minLength'), 
1122                         [schema.title ? schema.title : path.split('-').pop().trim(), schema.minLength])
1123           });
1124         }
1125       }
1126
1127       // `pattern`
1128       if(schema.pattern) {
1129         if(!(new RegExp(schema.pattern)).test(value)) {
1130           errors.push({
1131             path: path,
1132             property: 'pattern',
1133             message: this.translate('error_pattern', 
1134                         [schema.title ? schema.title : path.split('-').pop().trim(), schema.pattern])
1135           });
1136         }
1137       }
1138     }
1139     // Array specific validation
1140     else if(typeof value === "object" && value !== null && Array.isArray(value)) {
1141       // `items` and `additionalItems`
1142       if(schema.items) {
1143         // `items` is an array
1144         if(Array.isArray(schema.items)) {
1145           for(i=0; i<value.length; i++) {
1146             // If this item has a specific schema tied to it
1147             // Validate against it
1148             if(schema.items[i]) {
1149               errors = errors.concat(this._validateSchema(schema.items[i],value[i],path+'.'+i));
1150             }
1151             // If all additional items are allowed
1152             else if(schema.additionalItems === true) {
1153               break;
1154             }
1155             // If additional items is a schema
1156             // TODO: Incompatibility between version 3 and 4 of the spec
1157             else if(schema.additionalItems) {
1158               errors = errors.concat(this._validateSchema(schema.additionalItems,value[i],path+'.'+i));
1159             }
1160             // If no additional items are allowed
1161             else if(schema.additionalItems === false) {
1162               errors.push({
1163                 path: path,
1164                 property: 'additionalItems',
1165                 message: this.translate('error_additionalItems')
1166               });
1167               break;
1168             }
1169             // Default for `additionalItems` is an empty schema
1170             else {
1171               break;
1172             }
1173           }
1174         }
1175         // `items` is a schema
1176         else {
1177           // Each item in the array must validate against the schema
1178           for(i=0; i<value.length; i++) {
1179             errors = errors.concat(this._validateSchema(schema.items,value[i],path+'.'+i));
1180           }
1181         }
1182       }
1183
1184       // `maxItems`
1185       if(schema.maxItems) {
1186         if(value.length > schema.maxItems) {
1187           errors.push({
1188             path: path,
1189             property: 'maxItems',
1190             message: this.translate('error_maxItems', [schema.maxItems])
1191           });
1192         }
1193       }
1194
1195       // `minItems`
1196       if(schema.minItems) {
1197         if(value.length < schema.minItems) {
1198           errors.push({
1199             path: path,
1200             property: 'minItems',
1201             message: this.translate('error_minItems', [schema.minItems])
1202           });
1203         }
1204       }
1205
1206       // `uniqueItems`
1207       if(schema.uniqueItems) {
1208         var seen = {};
1209         for(i=0; i<value.length; i++) {
1210           valid = JSON.stringify(value[i]);
1211           if(seen[valid]) {
1212             errors.push({
1213               path: path,
1214               property: 'uniqueItems',
1215               message: this.translate('error_uniqueItems', 
1216                                 [schema.title ? schema.title : path.split('-').pop().trim()])
1217             });
1218             break;
1219           }
1220           seen[valid] = true;
1221         }
1222       }
1223     }
1224     // Object specific validation
1225     else if(typeof value === "object" && value !== null) {
1226       // `maxProperties`
1227       if(schema.maxProperties) {
1228         valid = 0;
1229         for(i in value) {
1230           if(!value.hasOwnProperty(i)) continue;
1231           valid++;
1232         }
1233         if(valid > schema.maxProperties) {
1234           errors.push({
1235             path: path,
1236             property: 'maxProperties',
1237             message: this.translate('error_maxProperties', [schema.maxProperties])
1238           });
1239         }
1240       }
1241
1242       // `minProperties`
1243       if(schema.minProperties) {
1244         valid = 0;
1245         for(i in value) {
1246           if(!value.hasOwnProperty(i)) continue;
1247           valid++;
1248         }
1249         if(valid < schema.minProperties) {
1250           errors.push({
1251             path: path,
1252             property: 'minProperties',
1253             message: this.translate('error_minProperties', [schema.minProperties])
1254           });
1255         }
1256       }
1257
1258       // Version 4 `required`
1259       if(typeof schema.required !== "undefined" && Array.isArray(schema.required)) {
1260         for(i=0; i<schema.required.length; i++) {
1261           // Arrays are the only missing "required" thing we report in the "object" 
1262           // level control group error message area; all others appear in their own form control 
1263           // control message area.
1264           if((typeof value[schema.required[i]] === "undefined") ||
1265                  (Array.isArray(value[schema.required[i]]) && value[schema.required[i]].length == 0)) {
1266             var parm_name;
1267             if(typeof schema.properties[schema.required[i]].title !== "undefined") {
1268               parm_name = schema.properties[schema.required[i]].title;
1269             } 
1270             else {
1271               parm_name = schema.required[i];
1272             }
1273             errors.push({
1274               path: path,
1275               property: 'required',
1276               message: this.translate('error_required', [parm_name])
1277             });
1278           }
1279         }
1280       }
1281
1282       // `properties`
1283       var validated_properties = {};
1284       if(schema.properties) {
1285         if(typeof schema.required !== "undefined" && Array.isArray(schema.required)) {
1286           for(i=0; i<schema.required.length; i++) {
1287             var property = schema.required[i];
1288             validated_properties[property] = true;
1289             errors = errors.concat(this._validateSchema(schema.properties[property],value[property],path+'.'+property));
1290           }
1291         }
1292
1293         // If an optional property is not an object and is not empty, we must run validation
1294         // on it as the user may have entered some data into it.
1295
1296         for(i in schema.properties) {
1297           if(!schema.properties.hasOwnProperty(i) || validated_properties[i] === true) continue;
1298           if((typeof value[i] !== "object" && typeof value[i] !== "undefined" && value[i] !== null) ||
1299              (schema.properties[i].type === "array" && Array.isArray(value[i]) && value[i].length > 0)) {
1300                   
1301             errors = errors.concat(this._validateSchema(schema.properties[i],value[i],path+'.'+i));
1302           }
1303           validated_properties[i] = true;
1304         }
1305       }
1306
1307       // `patternProperties`
1308       if(schema.patternProperties) {
1309         for(i in schema.patternProperties) {
1310           if(!schema.patternProperties.hasOwnProperty(i)) continue;
1311           var regex = new RegExp(i);
1312
1313           // Check which properties match
1314           for(j in value) {
1315             if(!value.hasOwnProperty(j)) continue;
1316             if(regex.test(j)) {
1317               validated_properties[j] = true;
1318               errors = errors.concat(this._validateSchema(schema.patternProperties[i],value[j],path+'.'+j));
1319             }
1320           }
1321         }
1322       }
1323
1324       // The no_additional_properties option currently doesn't work with extended schemas that use oneOf or anyOf
1325       if(typeof schema.additionalProperties === "undefined" && this.jsoneditor.options.no_additional_properties && !schema.oneOf && !schema.anyOf) {
1326         schema.additionalProperties = false;
1327       }
1328
1329       // `additionalProperties`
1330       if(typeof schema.additionalProperties !== "undefined") {
1331         for(i in value) {
1332           if(!value.hasOwnProperty(i)) continue;
1333           if(!validated_properties[i]) {
1334             // No extra properties allowed
1335             if(!schema.additionalProperties) {
1336               errors.push({
1337                 path: path,
1338                 property: 'additionalProperties',
1339                 message: this.translate('error_additional_properties', [i])
1340               });
1341               break;
1342             }
1343             // Allowed
1344             else if(schema.additionalProperties === true) {
1345               break;
1346             }
1347             // Must match schema
1348             // TODO: incompatibility between version 3 and 4 of the spec
1349             else {
1350               errors = errors.concat(this._validateSchema(schema.additionalProperties,value[i],path+'.'+i));
1351             }
1352           }
1353         }
1354       }
1355
1356       // `dependencies`
1357       if(schema.dependencies) {
1358         for(i in schema.dependencies) {
1359           if(!schema.dependencies.hasOwnProperty(i)) continue;
1360
1361           // Doesn't need to meet the dependency
1362           if(typeof value[i] === "undefined") continue;
1363
1364           // Property dependency
1365           if(Array.isArray(schema.dependencies[i])) {
1366             for(j=0; j<schema.dependencies[i].length; j++) {
1367               if(typeof value[schema.dependencies[i][j]] === "undefined") {
1368                 errors.push({
1369                   path: path,
1370                   property: 'dependencies',
1371                   message: this.translate('error_dependency', [schema.dependencies[i][j]])
1372                 });
1373               }
1374             }
1375           }
1376           // Schema dependency
1377           else {
1378             errors = errors.concat(this._validateSchema(schema.dependencies[i],value,path));
1379           }
1380         }
1381       }
1382     }
1383
1384     // Custom type validation (global)
1385     $each(JSONEditor.defaults.custom_validators,function(i,validator) {
1386       errors = errors.concat(validator.call(self,schema,value,path));
1387     });
1388     // Custom type validation (instance specific)
1389     if(this.options.custom_validators) {
1390       $each(this.options.custom_validators,function(i,validator) {
1391         errors = errors.concat(validator.call(self,schema,value,path));
1392       });
1393     }
1394
1395     return errors;
1396   },
1397   _checkType: function(type, value) {
1398     // Simple types
1399     if(typeof type === "string") {
1400       if(type==="string") return typeof value === "string";
1401       else if(type==="number") return typeof value === "number";
1402       else if(type==="qbldr") return typeof value === "string";
1403       else if(type==="integer") return typeof value === "number" && value === Math.floor(value);
1404       else if(type==="boolean") return typeof value === "boolean";
1405       else if(type==="array") return Array.isArray(value);
1406       else if(type === "object") return value !== null && !(Array.isArray(value)) && typeof value === "object";
1407       else if(type === "null") return value === null;
1408       else return true;
1409     }
1410     // Schema
1411     else {
1412       return !this._validateSchema(type,value).length;
1413     }
1414   }
1415 });
1416
1417 /**
1418  * All editors should extend from this class
1419  */
1420 JSONEditor.AbstractEditor = Class.extend({
1421   onChildEditorChange: function(editor) {
1422     this.onChange(true);
1423   },
1424   notify: function() {
1425     if(this.path) this.jsoneditor.notifyWatchers(this.path);
1426   },
1427   change: function() {
1428     if(this.parent) this.parent.onChildEditorChange(this);
1429     else if(this.jsoneditor) this.jsoneditor.onChange();
1430   },
1431   onChange: function(bubble) {
1432     this.notify();
1433     if(this.watch_listener) this.watch_listener();
1434     if(bubble) this.change();
1435   },
1436   register: function() {
1437     this.jsoneditor.registerEditor(this);
1438     this.onChange();
1439   },
1440   unregister: function() {
1441     if(!this.jsoneditor) return;
1442     this.jsoneditor.unregisterEditor(this);
1443   },
1444   getNumColumns: function() {
1445     return 12;
1446   },
1447   init: function(options) {
1448     this.jsoneditor = options.jsoneditor;
1449     
1450     this.theme = this.jsoneditor.theme;
1451     this.template_engine = this.jsoneditor.template;
1452     this.iconlib = this.jsoneditor.iconlib;
1453     
1454     this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
1455
1456     this.original_schema = options.schema;
1457     this.schema = this.jsoneditor.expandSchema(this.original_schema);
1458
1459     this.options = $extend({}, (this.options || {}), (this.schema.options || {}), (options.schema.options || {}), options);
1460
1461     if(!options.path && !this.schema.id) this.schema.id = 'root';
1462     this.path = options.path || 'root';
1463     this.formname = options.formname || this.path.replace(/\.([^.]+)/g,'[$1]');
1464     if(this.jsoneditor.options.form_name_root) this.formname = this.formname.replace(/^root\[/,this.jsoneditor.options.form_name_root+'[');
1465     this.key = this.path.split('.').pop();
1466     this.parent = options.parent;
1467     
1468     this.link_watchers = [];
1469     
1470     if(options.container) this.setContainer(options.container);
1471     this.registerDependencies();
1472   },
1473   registerDependencies: function() {
1474     this.dependenciesFulfilled = true;
1475     var deps = this.options.dependencies;
1476     if (!deps) {
1477       return;
1478     }
1479     
1480     var self = this;
1481     Object.keys(deps).forEach(function(dependency) {
1482       var path = self.path.split('.');
1483       path[path.length - 1] = dependency;
1484       path = path.join('.');
1485       var choices = deps[dependency];
1486       self.jsoneditor.watch(path, function() {
1487         self.checkDependency(path, choices);
1488       });
1489     });
1490   },
1491   checkDependency: function(path, choices) {
1492     var wrapper = this.control || this.container;
1493     if (this.path === path || !wrapper) {
1494       return;
1495     }
1496     
1497     var self = this;
1498     var editor = this.jsoneditor.getEditor(path);
1499     var value = editor ? editor.getValue() : undefined;
1500     var previousStatus = this.dependenciesFulfilled;
1501     this.dependenciesFulfilled = false;
1502     
1503     if (!editor || !editor.dependenciesFulfilled) {
1504       this.dependenciesFulfilled = false;
1505     } else if (Array.isArray(choices)) {
1506       choices.some(function(choice) {
1507         if (value === choice) {
1508           self.dependenciesFulfilled = true;
1509           return true;
1510         }
1511       });
1512     } else if (typeof choices === 'object') {
1513       if (typeof value !== 'object') {
1514         this.dependenciesFulfilled = choices === value;
1515       } else {
1516         Object.keys(choices).some(function(key) {
1517           if (!choices.hasOwnProperty(key)) {
1518             return false;
1519           }
1520           if (!value.hasOwnProperty(key) || choices[key] !== value[key]) {
1521             self.dependenciesFulfilled = false;
1522             return true;
1523           }
1524           self.dependenciesFulfilled = true;
1525         });
1526       }
1527     } else if (typeof choices === 'string' || typeof choices === 'number') {
1528       this.dependenciesFulfilled = value === choices;
1529     } else if (typeof choices === 'boolean') {
1530       if (choices) {
1531         this.dependenciesFulfilled = value && value.length > 0;
1532       } else {
1533         this.dependenciesFulfilled = !value || value.length === 0;
1534       }
1535     }
1536     
1537     if (this.dependenciesFulfilled !== previousStatus) {
1538       this.notify();
1539     }
1540     
1541     if (this.dependenciesFulfilled) {
1542       wrapper.style.display = 'block';
1543     } else {
1544       wrapper.style.display = 'none';
1545     }
1546   },
1547   setContainer: function(container) {
1548     this.container = container;
1549     if(this.schema.id) this.container.setAttribute('data-schemaid',this.schema.id);
1550     if(this.schema.type && typeof this.schema.type === "string") this.container.setAttribute('data-schematype',this.schema.type);
1551     this.container.setAttribute('data-schemapath',this.path);
1552     this.container.style.padding = '4px';
1553   },
1554   
1555   preBuild: function() {
1556
1557   },
1558   build: function() {
1559     
1560   },
1561   postBuild: function() {
1562     this.setupWatchListeners();
1563     this.addLinks();
1564     this.setValue(this.getDefault(), true);
1565     this.updateHeaderText();
1566     this.register();
1567     this.onWatchedFieldChange();
1568   },
1569   
1570   setupWatchListeners: function() {
1571     var self = this;
1572     
1573     // Watched fields
1574     this.watched = {};
1575     if(this.schema.vars) this.schema.watch = this.schema.vars;
1576     this.watched_values = {};
1577     this.watch_listener = function() {
1578       if(self.refreshWatchedFieldValues()) {
1579         self.onWatchedFieldChange();
1580       }
1581     };
1582     
1583     if(this.schema.hasOwnProperty('watch')) {
1584       var path,path_parts,first,root,adjusted_path;
1585
1586       for(var name in this.schema.watch) {
1587         if(!this.schema.watch.hasOwnProperty(name)) continue;
1588         path = this.schema.watch[name];
1589
1590         if(Array.isArray(path)) {
1591           if(path.length<2) continue;
1592           path_parts = [path[0]].concat(path[1].split('.'));
1593         }
1594         else {
1595           path_parts = path.split('.');
1596           if(!self.theme.closest(self.container,'[data-schemaid="'+path_parts[0]+'"]')) path_parts.unshift('#');
1597         }
1598         first = path_parts.shift();
1599
1600         if(first === '#') first = self.jsoneditor.schema.id || 'root';
1601
1602         // Find the root node for this template variable
1603         root = self.theme.closest(self.container,'[data-schemaid="'+first+'"]');
1604         if(!root) throw "Could not find ancestor node with id "+first;
1605
1606         // Keep track of the root node and path for use when rendering the template
1607         adjusted_path = root.getAttribute('data-schemapath') + '.' + path_parts.join('.');
1608         
1609         self.jsoneditor.watch(adjusted_path,self.watch_listener);
1610         
1611         self.watched[name] = adjusted_path;
1612       }
1613     }
1614     
1615     // Dynamic header
1616     if(this.schema.headerTemplate) {
1617       this.header_template = this.jsoneditor.compileTemplate(this.schema.headerTemplate, this.template_engine);
1618     }
1619   },
1620   
1621   addLinks: function() {
1622     // Add links
1623     if(!this.no_link_holder) {
1624       this.link_holder = this.theme.getLinksHolder();
1625       this.container.appendChild(this.link_holder);
1626       if(this.schema.links) {
1627         for(var i=0; i<this.schema.links.length; i++) {
1628           this.addLink(this.getLink(this.schema.links[i]));
1629         }
1630       }
1631     }
1632   },
1633   
1634   
1635   getButton: function(text, icon, title) {
1636     var btnClass = 'json-editor-btn-'+icon;
1637     if(!this.iconlib) icon = null;
1638     else icon = this.iconlib.getIcon(icon);
1639     
1640     if(!icon && title) {
1641       text = title;
1642       title = null;
1643     }
1644     
1645     var btn = this.theme.getButton(text, icon, title);
1646     btn.className += ' ' + btnClass + ' ';
1647     return btn;
1648   },
1649   setButtonText: function(button, text, icon, title) {
1650     if(!this.iconlib) icon = null;
1651     else icon = this.iconlib.getIcon(icon);
1652     
1653     if(!icon && title) {
1654       text = title;
1655       title = null;
1656     }
1657     
1658     return this.theme.setButtonText(button, text, icon, title);
1659   },
1660   addLink: function(link) {
1661     if(this.link_holder) this.link_holder.appendChild(link);
1662   },
1663   getLink: function(data) {
1664     var holder, link;
1665         
1666     // Get mime type of the link
1667     var mime = data.mediaType || 'application/javascript';
1668     var type = mime.split('/')[0];
1669     
1670     // Template to generate the link href
1671     var href = this.jsoneditor.compileTemplate(data.href,this.template_engine);
1672     var relTemplate = this.jsoneditor.compileTemplate(data.rel ? data.rel : data.href,this.template_engine);
1673     
1674     // Template to generate the link's download attribute
1675     var download = null;
1676     if(data.download) download = data.download;
1677
1678     if(download && download !== true) {
1679       download = this.jsoneditor.compileTemplate(download, this.template_engine);
1680     }
1681
1682     // Image links
1683     if(type === 'image') {
1684       holder = this.theme.getBlockLinkHolder();
1685       link = document.createElement('a');
1686       link.setAttribute('target','_blank');
1687       var image = document.createElement('img');
1688       
1689       this.theme.createImageLink(holder,link,image);
1690     
1691       // When a watched field changes, update the url  
1692       this.link_watchers.push(function(vars) {
1693         var url = href(vars);
1694         var rel = relTemplate(vars);
1695         link.setAttribute('href',url);
1696         link.setAttribute('title',rel || url);
1697         image.setAttribute('src',url);
1698       });
1699     }
1700     // Audio/Video links
1701     else if(['audio','video'].indexOf(type) >=0) {
1702       holder = this.theme.getBlockLinkHolder();
1703       
1704       link = this.theme.getBlockLink();
1705       link.setAttribute('target','_blank');
1706       
1707       var media = document.createElement(type);
1708       media.setAttribute('controls','controls');
1709       
1710       this.theme.createMediaLink(holder,link,media);
1711       
1712       // When a watched field changes, update the url  
1713       this.link_watchers.push(function(vars) {
1714         var url = href(vars);
1715         var rel = relTemplate(vars);
1716         link.setAttribute('href',url);
1717         link.textContent = rel || url;
1718         media.setAttribute('src',url);
1719       });
1720     }
1721     // Text links
1722     else {
1723       link = holder = this.theme.getBlockLink();
1724       holder.setAttribute('target','_blank');
1725       holder.textContent = data.rel;
1726
1727       // When a watched field changes, update the url
1728       this.link_watchers.push(function(vars) {
1729         var url = href(vars);
1730         var rel = relTemplate(vars);
1731         holder.setAttribute('href',url);
1732         holder.textContent = rel || url;
1733       });
1734     }
1735
1736     if(download && link) {
1737       if(download === true) {
1738         link.setAttribute('download','');
1739       }
1740       else {
1741         this.link_watchers.push(function(vars) {
1742           link.setAttribute('download',download(vars));
1743         });
1744       }
1745     }
1746     
1747     if(data.class) link.className = link.className + ' ' + data.class;
1748
1749     return holder;
1750   },
1751   refreshWatchedFieldValues: function() {
1752     if(!this.watched_values) return;
1753     var watched = {};
1754     var changed = false;
1755     var self = this;
1756     
1757     if(this.watched) {
1758       var val,editor;
1759       for(var name in this.watched) {
1760         if(!this.watched.hasOwnProperty(name)) continue;
1761         editor = self.jsoneditor.getEditor(this.watched[name]);
1762         val = editor? editor.getValue() : null;
1763         if(self.watched_values[name] !== val) changed = true;
1764         watched[name] = val;
1765       }
1766     }
1767     
1768     watched.self = this.getValue();
1769     if(this.watched_values.self !== watched.self) changed = true;
1770     
1771     this.watched_values = watched;
1772     
1773     return changed;
1774   },
1775   getWatchedFieldValues: function() {
1776     return this.watched_values;
1777   },
1778   updateHeaderText: function() {
1779     if(this.header) {
1780       // If the header has children, only update the text node's value
1781       if(this.header.children.length) {
1782         for(var i=0; i<this.header.childNodes.length; i++) {
1783           if(this.header.childNodes[i].nodeType===3) {
1784             this.header.childNodes[i].nodeValue = this.getHeaderText();
1785             break;
1786           }
1787         }
1788       }
1789       // Otherwise, just update the entire node
1790       else {
1791         this.header.textContent = this.getHeaderText();
1792       }
1793     }
1794   },
1795   getHeaderText: function(title_only) {
1796     if(this.header_text) return this.header_text;
1797     else if(title_only) return this.schema.title;
1798     else return this.getTitle();
1799   },
1800   onWatchedFieldChange: function() {
1801     var vars;
1802     if(this.header_template) {      
1803       vars = $extend(this.getWatchedFieldValues(),{
1804         key: this.key,
1805         i: this.key,
1806         i0: (this.key*1),
1807         i1: (this.key*1+1),
1808         title: this.getTitle()
1809       });
1810       var header_text = this.header_template(vars);
1811       
1812       if(header_text !== this.header_text) {
1813         this.header_text = header_text;
1814         this.updateHeaderText();
1815         this.notify();
1816         //this.fireChangeHeaderEvent();
1817       }
1818     }
1819     if(this.link_watchers.length) {
1820       vars = this.getWatchedFieldValues();
1821       for(var i=0; i<this.link_watchers.length; i++) {
1822         this.link_watchers[i](vars);
1823       }
1824     }
1825   },
1826   setValue: function(value) {
1827     this.value = value;
1828   },
1829   getValue: function() {
1830     if (!this.dependenciesFulfilled) {
1831       return undefined;
1832     }
1833     return this.value;
1834   },
1835   refreshValue: function() {
1836
1837   },
1838   getChildEditors: function() {
1839     return false;
1840   },
1841   destroy: function() {
1842     var self = this;
1843     this.unregister(this);
1844     $each(this.watched,function(name,adjusted_path) {
1845       self.jsoneditor.unwatch(adjusted_path,self.watch_listener);
1846     });
1847     this.watched = null;
1848     this.watched_values = null;
1849     this.watch_listener = null;
1850     this.header_text = null;
1851     this.header_template = null;
1852     this.value = null;
1853     if(this.container && this.container.parentNode) this.container.parentNode.removeChild(this.container);
1854     this.container = null;
1855     this.jsoneditor = null;
1856     this.schema = null;
1857     this.path = null;
1858     this.key = null;
1859     this.parent = null;
1860   },
1861   getDefault: function() {
1862     if (typeof this.schema["default"] !== 'undefined') {
1863       return this.schema["default"];
1864     }
1865
1866     if (typeof this.schema["enum"] !== 'undefined') {
1867       return this.schema["enum"][0];
1868     }
1869     
1870     var type = this.schema.type || this.schema.oneOf;
1871     if(type && Array.isArray(type)) type = type[0];
1872     if(type && typeof type === "object") type = type.type;
1873     if(type && Array.isArray(type)) type = type[0];
1874     
1875     if(typeof type === "string") {
1876       if(type === "number") return 0.0;
1877       if(type === "boolean") return false;
1878       if(type === "integer") return 0;
1879       if(type === "string") return "";
1880       if(type === "object") return {};
1881       if(type === "array") return [];
1882     }
1883     
1884     return null;
1885   },
1886   getTitle: function() {
1887     return this.schema.title || this.key;
1888   },
1889   enable: function() {
1890     this.disabled = false;
1891   },
1892   disable: function() {
1893     this.disabled = true;
1894   },
1895   isEnabled: function() {
1896     return !this.disabled;
1897   },
1898   isRequired: function() {
1899     if(typeof this.schema.required === "boolean") return this.schema.required;
1900     else if(this.parent && this.parent.schema && Array.isArray(this.parent.schema.required)) return this.parent.schema.required.indexOf(this.key) > -1;
1901     else if(this.jsoneditor.options.required_by_default) return true;
1902     else return false;
1903   },  
1904   getDisplayText: function(arr) {
1905     var disp = [];
1906     var used = {};
1907     
1908     // Determine how many times each attribute name is used.
1909     // This helps us pick the most distinct display text for the schemas.
1910     $each(arr,function(i,el) {
1911       if(el.title) {
1912         used[el.title] = used[el.title] || 0;
1913         used[el.title]++;
1914       }
1915       if(el.description) {
1916         used[el.description] = used[el.description] || 0;
1917         used[el.description]++;
1918       }
1919       if(el.format) {
1920         used[el.format] = used[el.format] || 0;
1921         used[el.format]++;
1922       }
1923       if(el.type) {
1924         used[el.type] = used[el.type] || 0;
1925         used[el.type]++;
1926       }
1927     });
1928     
1929     // Determine display text for each element of the array
1930     $each(arr,function(i,el)  {
1931       var name;
1932       
1933       // If it's a simple string
1934       if(typeof el === "string") name = el;
1935       // Object
1936       else if(el.title && used[el.title]<=1) name = el.title;
1937       else if(el.format && used[el.format]<=1) name = el.format;
1938       else if(el.type && used[el.type]<=1) name = el.type;
1939       else if(el.description && used[el.description]<=1) name = el.descripton;
1940       else if(el.title) name = el.title;
1941       else if(el.format) name = el.format;
1942       else if(el.type) name = el.type;
1943       else if(el.description) name = el.description;
1944       else if(JSON.stringify(el).length < 50) name = JSON.stringify(el);
1945       else name = "type";
1946       
1947       disp.push(name);
1948     });
1949     
1950     // Replace identical display text with "text 1", "text 2", etc.
1951     var inc = {};
1952     $each(disp,function(i,name) {
1953       inc[name] = inc[name] || 0;
1954       inc[name]++;
1955       
1956       if(used[name] > 1) disp[i] = name + " " + inc[name];
1957     });
1958     
1959     return disp;
1960   },
1961   getOption: function(key) {
1962     try {
1963       throw "getOption is deprecated";
1964     }
1965     catch(e) {
1966       window.console.error(e);
1967     }
1968     
1969     return this.options[key];
1970   },
1971   showValidationErrors: function(errors) {
1972
1973   }
1974 });
1975
1976 JSONEditor.defaults.editors["null"] = JSONEditor.AbstractEditor.extend({
1977   getValue: function() {
1978     if (!this.dependenciesFulfilled) {
1979       return undefined;
1980     }
1981     return null;
1982   },
1983   setValue: function() {
1984     this.onChange();
1985   },
1986   getNumColumns: function() {
1987     return 2;
1988   }
1989 });
1990
1991 JSONEditor.defaults.editors.qbldr = JSONEditor.AbstractEditor.extend({
1992   register: function() {
1993         this._super();
1994         if(!this.input) return;
1995         this.input.setAttribute('name',this.formname);
1996   },
1997   unregister: function() {
1998         this._super();
1999         if(!this.input) return;
2000         this.input.removeAttribute('name');
2001   },
2002   setValue: function(value, initial) {
2003         var self = this;
2004             
2005         if(typeof value === "undefined" || typeof this.jqbldrId === "undefined" || value === this.value) {
2006           return;
2007         }
2008
2009     if ((initial === true) && (value !== "") && (value !== null)) {
2010           $(this.jqbldrId).queryBuilder('off','rulesChanged');
2011           $(this.jqbldrId).queryBuilder('setRulesFromSQL', value);
2012           var filter_result = $(this.jqbldrId).queryBuilder('getSQL');
2013           value = filter_result === null ? null : filter_result.sql;
2014           $(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
2015     }
2016     
2017         this.input.value = value;  
2018         this.value = value;
2019
2020         // Bubble this setValue to parents if the value changed
2021         this.onChange(true);
2022   },
2023   getValue: function() {
2024         var self = this;
2025
2026         if (this.value === "" || this.value === null) { 
2027           return undefined; 
2028         } else { 
2029           return this.value;
2030         }
2031   },
2032   
2033   getNumColumns: function() {   
2034     return 12;
2035   },
2036   
2037   qbldrRulesChangedCb: function(eventObj) {
2038     var self = this;
2039
2040     $(this.jqbldrId).queryBuilder('off','rulesChanged');
2041
2042     var filter_result = $(this.jqbldrId).queryBuilder('getSQL');
2043     
2044     if (filter_result !== null) {
2045         this.setValue(filter_result.sql);
2046     }
2047
2048     $(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
2049
2050     return;
2051   },
2052   preBuild: function() {
2053     var self = this;
2054     this._super();
2055   },
2056   build: function() {
2057     var self = this;
2058     
2059     this.qschema = this.schema.qschema;
2060     this.qbldrId = this.path;
2061     this.jqbldrId = '#' + this.qbldrId;
2062     this.jqbldrId = this.jqbldrId.replace(/\./g,'\\.');
2063
2064     this.qgrid = this.theme.getGridContainer();
2065     this.qgrid.style.padding = '4px';
2066     this.qgrid.style.border = '1px solid #e3e3e3';
2067
2068     this.gridrow1 = this.theme.getGridRow();
2069     this.gridrow1.style.padding = '4px';
2070     
2071     this.gridrow2 = this.theme.getGridRow();
2072     this.gridrow2.style.padding = '4px';
2073     
2074     this.title = this.getTitle();
2075     this.label = this.theme.getFormInputLabel(this.title);
2076
2077     this.input = this.theme.getTextareaInput();
2078     this.input.disabled = 'true';
2079
2080     this.control = this.theme.getFormControl(this.label, this.input, this.description);
2081     
2082     this.gridrow2.setAttribute('id',this.qbldrId);
2083     
2084     this.container.appendChild(this.qgrid);  // attach the grid to container
2085
2086     this.qgrid.appendChild(this.gridrow1);   // attach gridrow1 to grid
2087     this.gridrow1.appendChild(this.control); // attach control form to gridrow1
2088     
2089     this.qgrid.appendChild(this.gridrow2);
2090     
2091     var options = { conditions: [ 'AND', 'OR'], sort_filters: true };
2092     
2093     $.extend(this.qschema, options);
2094     
2095     $(this.jqbldrId).queryBuilder(this.qschema);
2096     
2097     //$(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
2098     //$(this.jqbldrId).queryBuilder('on', 'afterUpdateRuleValue', this.qbldrRulesChangedCb.bind(this));
2099     $(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
2100   },
2101   enable: function() {
2102     this._super();
2103   },
2104   disable: function() {
2105     this._super();
2106   },
2107   afterInputReady: function() {
2108     var self = this, options;
2109     self.theme.afterInputReady(self.input);
2110   },
2111   refreshValue: function() {
2112     this.value = this.input.value;
2113     if(typeof this.value !== "string") this.value = '';
2114   },
2115   destroy: function() {
2116         var self = this;
2117     this._super();
2118   },
2119   /**
2120    * This is overridden in derivative editors
2121    */
2122   sanitize: function(value) {
2123     return value;
2124   },
2125   /**
2126    * Re-calculates the value if needed
2127    */
2128   onWatchedFieldChange: function() {    
2129     var self = this, vars, j;
2130     
2131     this._super();
2132   },
2133   showValidationErrors: function(errors) {
2134     var self = this;
2135
2136     if(this.jsoneditor.options.show_errors === "always") {}
2137     else if(this.previous_error_setting===this.jsoneditor.options.show_errors) return;
2138     
2139     this.previous_error_setting = this.jsoneditor.options.show_errors;
2140
2141     var messages = [];
2142     $each(errors,function(i,error) {
2143       if(error.path === self.path) {
2144         messages.push(error.message);
2145       }
2146     });
2147
2148     this.input.controlgroup = this.control;
2149
2150     if(messages.length) {
2151       this.theme.addInputError(this.input, messages.join('. ')+'.');
2152     }
2153     else {
2154       this.theme.removeInputError(this.input);
2155     }
2156   }
2157 });
2158
2159 JSONEditor.defaults.editors.string = JSONEditor.AbstractEditor.extend({
2160   register: function() {
2161     this._super();
2162     if(!this.input) return;
2163     this.input.setAttribute('name',this.formname);
2164   },
2165   unregister: function() {
2166     this._super();
2167     if(!this.input) return;
2168     this.input.removeAttribute('name');
2169   },
2170   setValue: function(value,initial,from_template) {
2171     var self = this;
2172     
2173     if(this.template && !from_template) {
2174       return;
2175     }
2176     
2177     if(value === null || typeof value === 'undefined') value = "";
2178     else if(typeof value === "object") value = JSON.stringify(value);
2179     else if(typeof value !== "string") value = ""+value;
2180     
2181     if(value === this.serialized) return;
2182
2183     // Sanitize value before setting it
2184     var sanitized = this.sanitize(value);
2185
2186     if(this.input.value === sanitized) {
2187       return;
2188     }
2189
2190     this.input.value = sanitized;
2191     
2192     // If using SCEditor, update the WYSIWYG
2193     if(this.sceditor_instance) {
2194       this.sceditor_instance.val(sanitized);
2195     }
2196     else if(this.SimpleMDE) {
2197       this.SimpleMDE.value(sanitized);
2198     }
2199     else if(this.ace_editor) {
2200       this.ace_editor.setValue(sanitized);
2201     }
2202     
2203     var changed = from_template || this.getValue() !== value;
2204     
2205     this.refreshValue();
2206     
2207     if(initial) this.is_dirty = false;
2208     else if(this.jsoneditor.options.show_errors === "change") this.is_dirty = true;
2209     
2210     if(this.adjust_height) this.adjust_height(this.input);
2211
2212     // Bubble this setValue to parents if the value changed
2213     this.onChange(changed);
2214   },
2215   getNumColumns: function() {
2216     var min = Math.ceil(Math.max(this.getTitle().length,this.schema.maxLength||0,this.schema.minLength||0)/5);
2217     var num;
2218     
2219     if(this.input_type === 'textarea') num = 6;
2220     else if(['text','email'].indexOf(this.input_type) >= 0) num = 4;
2221     else num = 2;
2222     
2223     return Math.min(12,Math.max(min,num));
2224   },
2225   build: function() {
2226     var self = this, i;
2227     if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
2228     if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
2229     if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
2230
2231     this.format = this.schema.format;
2232     if(!this.format && this.schema.media && this.schema.media.type) {
2233       this.format = this.schema.media.type.replace(/(^(application|text)\/(x-)?(script\.)?)|(-source$)/g,'');
2234     }
2235     if(!this.format && this.options.default_format) {
2236       this.format = this.options.default_format;
2237     }
2238     if(this.options.format) {
2239       this.format = this.options.format;
2240     }
2241
2242     // Specific format
2243     if(this.format) {
2244       // Text Area
2245       if(this.format === 'textarea') {
2246         this.input_type = 'textarea';
2247         this.input = this.theme.getTextareaInput();
2248       }
2249       // Range Input
2250       else if(this.format === 'range') {
2251         this.input_type = 'range';
2252         var min = this.schema.minimum || 0;
2253         var max = this.schema.maximum || Math.max(100,min+1);
2254         var step = 1;
2255         if(this.schema.multipleOf) {
2256           if(min%this.schema.multipleOf) min = Math.ceil(min/this.schema.multipleOf)*this.schema.multipleOf;
2257           if(max%this.schema.multipleOf) max = Math.floor(max/this.schema.multipleOf)*this.schema.multipleOf;
2258           step = this.schema.multipleOf;
2259         }
2260
2261         this.input = this.theme.getRangeInput(min,max,step);
2262       }
2263       // Source Code
2264       else if([
2265           'actionscript',
2266           'batchfile',
2267           'bbcode',
2268           'c',
2269           'c++',
2270           'cpp',
2271           'coffee',
2272           'csharp',
2273           'css',
2274           'dart',
2275           'django',
2276           'ejs',
2277           'erlang',
2278           'golang',
2279           'groovy',
2280           'handlebars',
2281           'haskell',
2282           'haxe',
2283           'html',
2284           'ini',
2285           'jade',
2286           'java',
2287           'javascript',
2288           'json',
2289           'less',
2290           'lisp',
2291           'lua',
2292           'makefile',
2293           'markdown',
2294           'matlab',
2295           'mysql',
2296           'objectivec',
2297           'pascal',
2298           'perl',
2299           'pgsql',
2300           'php',
2301           'python',
2302           'r',
2303           'ruby',
2304           'sass',
2305           'scala',
2306           'scss',
2307           'smarty',
2308           'sql',
2309           'stylus',
2310           'svg',
2311           'twig',
2312           'vbscript',
2313           'xml',
2314           'yaml'
2315         ].indexOf(this.format) >= 0
2316       ) {
2317         this.input_type = this.format;
2318         this.source_code = true;
2319         
2320         this.input = this.theme.getTextareaInput();
2321       }
2322       // HTML5 Input type
2323       else {
2324         this.input_type = this.format;
2325         this.input = this.theme.getFormInputField(this.input_type);
2326       }
2327     }
2328     // Normal text input
2329     else {
2330       this.input_type = 'text';
2331       this.input = this.theme.getFormInputField(this.input_type);
2332     }
2333     
2334     // minLength, maxLength, and pattern
2335     if(typeof this.schema.maxLength !== "undefined") this.input.setAttribute('maxlength',this.schema.maxLength);
2336     if(typeof this.schema.pattern !== "undefined") this.input.setAttribute('pattern',this.schema.pattern);
2337     else if(typeof this.schema.minLength !== "undefined") this.input.setAttribute('pattern','.{'+this.schema.minLength+',}');
2338
2339     if(this.options.compact) {
2340       this.container.className += ' compact';
2341     }
2342     else {
2343       if(this.options.input_width) this.input.style.width = this.options.input_width;
2344     }
2345
2346     if(this.schema.readOnly || this.schema.readonly || this.schema.template) {
2347       this.always_disabled = true;
2348       this.input.disabled = true;
2349     }
2350
2351     this.input
2352       .addEventListener('change',function(e) {        
2353         e.preventDefault();
2354         e.stopPropagation();
2355         
2356         // Don't allow changing if this field is a template
2357         if(self.schema.template) {
2358           this.value = self.value;
2359           return;
2360         }
2361
2362         var val = this.value;
2363         
2364         // sanitize value
2365         var sanitized = self.sanitize(val);
2366         if(val !== sanitized) {
2367           this.value = sanitized;
2368         }
2369         
2370         self.is_dirty = true;
2371
2372         self.refreshValue();
2373         self.onChange(true);
2374       });
2375       
2376     if(this.options.input_height) this.input.style.height = this.options.input_height;
2377     if(this.options.expand_height) {
2378       this.adjust_height = function(el) {
2379         if(!el) return;
2380         var i, ch=el.offsetHeight;
2381         // Input too short
2382         if(el.offsetHeight < el.scrollHeight) {
2383           i=0;
2384           while(el.offsetHeight < el.scrollHeight+3) {
2385             if(i>100) break;
2386             i++;
2387             ch++;
2388             el.style.height = ch+'px';
2389           }
2390         }
2391         else {
2392           i=0;
2393           while(el.offsetHeight >= el.scrollHeight+3) {
2394             if(i>100) break;
2395             i++;
2396             ch--;
2397             el.style.height = ch+'px';
2398           }
2399           el.style.height = (ch+1)+'px';
2400         }
2401       };
2402       
2403       this.input.addEventListener('keyup',function(e) {
2404         self.adjust_height(this);
2405       });
2406       this.input.addEventListener('change',function(e) {
2407         self.adjust_height(this);
2408       });
2409       this.adjust_height();
2410     }
2411
2412     if(this.format) this.input.setAttribute('data-schemaformat',this.format);
2413
2414     this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
2415     this.container.appendChild(this.control);
2416
2417     // Any special formatting that needs to happen after the input is added to the dom
2418     window.requestAnimationFrame(function() {
2419       // Skip in case the input is only a temporary editor,
2420       // otherwise, in the case of an ace_editor creation,
2421       // it will generate an error trying to append it to the missing parentNode
2422       if(self.input.parentNode) self.afterInputReady();
2423       if(self.adjust_height) self.adjust_height(self.input);
2424     });
2425
2426     // Compile and store the template
2427     if(this.schema.template) {
2428       this.template = this.jsoneditor.compileTemplate(this.schema.template, this.template_engine);
2429       this.refreshValue();
2430     }
2431     else {
2432       this.refreshValue();
2433     }
2434   },
2435   enable: function() {
2436     if(!this.always_disabled) {
2437       this.input.disabled = false;
2438       // TODO: WYSIWYG and Markdown editors
2439       this._super();
2440     }
2441   },
2442   disable: function(always_disabled) {
2443     if(always_disabled) this.always_disabled = true;
2444     this.input.disabled = true;
2445     // TODO: WYSIWYG and Markdown editors
2446     this._super();
2447   },
2448   afterInputReady: function() {
2449     var self = this, options;
2450     
2451     // Code editor
2452     if(this.source_code) {      
2453       // WYSIWYG html and bbcode editor
2454       if(this.options.wysiwyg && 
2455         ['html','bbcode'].indexOf(this.input_type) >= 0 && 
2456         window.jQuery && window.jQuery.fn && window.jQuery.fn.sceditor
2457       ) {
2458         options = $extend({},{
2459           plugins: self.input_type==='html'? 'xhtml' : 'bbcode',
2460           emoticonsEnabled: false,
2461           width: '100%',
2462           height: 300
2463         },JSONEditor.plugins.sceditor,self.options.sceditor_options||{});
2464         
2465         window.jQuery(self.input).sceditor(options);
2466         
2467         self.sceditor_instance = window.jQuery(self.input).sceditor('instance');
2468         
2469         self.sceditor_instance.blur(function() {
2470           // Get editor's value
2471           var val = window.jQuery("<div>"+self.sceditor_instance.val()+"</div>");
2472           // Remove sceditor spans/divs
2473           window.jQuery('#sceditor-start-marker,#sceditor-end-marker,.sceditor-nlf',val).remove();
2474           // Set the value and update
2475           self.input.value = val.html();
2476           self.value = self.input.value;
2477           self.is_dirty = true;
2478           self.onChange(true);
2479         });
2480       }
2481       // SimpleMDE for markdown (if it's loaded)
2482       else if (this.input_type === 'markdown' && window.SimpleMDE) {
2483         options = $extend({},JSONEditor.plugins.SimpleMDE,{
2484           element: this.input
2485         });
2486
2487         this.SimpleMDE = new window.SimpleMDE((options));
2488
2489         this.SimpleMDE.codemirror.on("change",function() {
2490           self.value = self.SimpleMDE.value();
2491           self.is_dirty = true;
2492           self.onChange(true);
2493         });
2494       }
2495       // ACE editor for everything else
2496       else if(window.ace) {
2497         var mode = this.input_type;
2498         // aliases for c/cpp
2499         if(mode === 'cpp' || mode === 'c++' || mode === 'c') {
2500           mode = 'c_cpp';
2501         }
2502         
2503         this.ace_container = document.createElement('div');
2504         this.ace_container.style.width = '100%';
2505         this.ace_container.style.position = 'relative';
2506         this.ace_container.style.height = '400px';
2507         this.input.parentNode.insertBefore(this.ace_container,this.input);
2508         this.input.style.display = 'none';
2509         this.ace_editor = window.ace.edit(this.ace_container);
2510         
2511         this.ace_editor.setValue(this.getValue());
2512
2513         // The theme
2514         if(JSONEditor.plugins.ace.theme) this.ace_editor.setTheme('ace/theme/'+JSONEditor.plugins.ace.theme);
2515         // The mode
2516         this.ace_editor.getSession().setMode('ace/mode/' + this.schema.format);
2517
2518         // Listen for changes
2519         this.ace_editor.on('change',function() {
2520           var val = self.ace_editor.getValue();
2521           self.input.value = val;
2522           self.refreshValue();
2523           self.is_dirty = true;
2524           self.onChange(true);
2525         });
2526       }
2527     }
2528     
2529     self.theme.afterInputReady(self.input);
2530   },
2531   refreshValue: function() {
2532     this.value = this.input.value;
2533     if(typeof this.value !== "string") this.value = '';
2534     this.serialized = this.value;
2535   },
2536   destroy: function() {
2537     // If using SCEditor, destroy the editor instance
2538     if(this.sceditor_instance) {
2539       this.sceditor_instance.destroy();
2540     }
2541     else if(this.SimpleMDE) {
2542       this.SimpleMDE.destroy();
2543     }
2544     else if(this.ace_editor) {
2545       this.ace_editor.destroy();
2546     }
2547     
2548     
2549     this.template = null;
2550     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
2551     if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
2552     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
2553
2554     this._super();
2555   },
2556   /**
2557    * This is overridden in derivative editors
2558    */
2559   sanitize: function(value) {
2560     return value;
2561   },
2562   /**
2563    * Re-calculates the value if needed
2564    */
2565   onWatchedFieldChange: function() {    
2566     var self = this, vars, j;
2567     
2568     // If this editor needs to be rendered by a macro template
2569     if(this.template) {
2570       vars = this.getWatchedFieldValues();
2571       this.setValue(this.template(vars),false,true);
2572     }
2573     
2574     this._super();
2575   },
2576   showValidationErrors: function(errors) {
2577     var self = this;
2578     
2579     if(this.jsoneditor.options.show_errors === "always") {}
2580     else if(!this.is_dirty && this.previous_error_setting===this.jsoneditor.options.show_errors) return;
2581     
2582     this.previous_error_setting = this.jsoneditor.options.show_errors;
2583
2584     var messages = [];
2585     $each(errors,function(i,error) {
2586       if(error.path === self.path) {
2587         messages.push(error.message);
2588       }
2589     });
2590
2591     this.input.controlgroup = this.control;
2592     
2593     if(messages.length) {
2594       this.theme.addInputError(this.input, messages.join('. ')+'.');
2595     }
2596     else {
2597       this.theme.removeInputError(this.input);
2598     }
2599   }
2600 });
2601
2602 /**
2603  * Created by Mehmet Baker on 12.04.2017
2604  */
2605 JSONEditor.defaults.editors.hidden = JSONEditor.AbstractEditor.extend({
2606   register: function () {
2607     this._super();
2608     if (!this.input) return;
2609     this.input.setAttribute('name', this.formname);
2610   },
2611   unregister: function () {
2612     this._super();
2613     if (!this.input) return;
2614     this.input.removeAttribute('name');
2615   },
2616   setValue: function (value, initial, from_template) {
2617     var self = this;
2618
2619     if(this.template && !from_template) {
2620       return;
2621     }
2622
2623     if(value === null || typeof value === 'undefined') value = "";
2624     else if(typeof value === "object") value = JSON.stringify(value);
2625     else if(typeof value !== "string") value = ""+value;
2626
2627     if(value === this.serialized) return;
2628
2629     // Sanitize value before setting it
2630     var sanitized = this.sanitize(value);
2631
2632     if(this.input.value === sanitized) {
2633       return;
2634     }
2635
2636     this.input.value = sanitized;
2637
2638     var changed = from_template || this.getValue() !== value;
2639
2640     this.refreshValue();
2641
2642     if(initial) this.is_dirty = false;
2643     else if(this.jsoneditor.options.show_errors === "change") this.is_dirty = true;
2644
2645     if(this.adjust_height) this.adjust_height(this.input);
2646
2647     // Bubble this setValue to parents if the value changed
2648     this.onChange(changed);
2649   },
2650   getNumColumns: function () {
2651     return 2;
2652   },
2653   enable: function () {
2654     this._super();
2655   },
2656   disable: function () {
2657     this._super();
2658   },
2659   refreshValue: function () {
2660     this.value = this.input.value;
2661     if (typeof this.value !== "string") this.value = '';
2662     this.serialized = this.value;
2663   },
2664   destroy: function () {
2665     this.template = null;
2666     if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
2667     if (this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
2668     if (this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
2669
2670     this._super();
2671   },
2672   /**
2673    * This is overridden in derivative editors
2674    */
2675   sanitize: function (value) {
2676     return value;
2677   },
2678   /**
2679    * Re-calculates the value if needed
2680    */
2681   onWatchedFieldChange: function () {
2682     var self = this, vars, j;
2683
2684     // If this editor needs to be rendered by a macro template
2685     if (this.template) {
2686       vars = this.getWatchedFieldValues();
2687       this.setValue(this.template(vars), false, true);
2688     }
2689
2690     this._super();
2691   },
2692   build: function () {
2693     var self = this;
2694
2695     this.format = this.schema.format;
2696     if (!this.format && this.options.default_format) {
2697       this.format = this.options.default_format;
2698     }
2699     if (this.options.format) {
2700       this.format = this.options.format;
2701     }
2702
2703     this.input_type = 'hidden';
2704     this.input = this.theme.getFormInputField(this.input_type);
2705
2706     if (this.format) this.input.setAttribute('data-schemaformat', this.format);
2707
2708     this.container.appendChild(this.input);
2709
2710     // Compile and store the template
2711     if (this.schema.template) {
2712       this.template = this.jsoneditor.compileTemplate(this.schema.template, this.template_engine);
2713       this.refreshValue();
2714     }
2715     else {
2716       this.refreshValue();
2717     }
2718   }
2719 });
2720 JSONEditor.defaults.editors.number = JSONEditor.defaults.editors.string.extend({
2721   build: function() {
2722     this._super();
2723
2724     if (typeof this.schema.minimum !== "undefined") {
2725       var minimum = this.schema.minimum;
2726
2727       if (typeof this.schema.exclusiveMinimum !== "undefined") {
2728         minimum += 1;
2729       }
2730
2731       this.input.setAttribute("min", minimum);
2732     }
2733
2734     if (typeof this.schema.maximum !== "undefined") {
2735       var maximum = this.schema.maximum;
2736
2737       if (typeof this.schema.exclusiveMaximum !== "undefined") {
2738         maximum -= 1;
2739       }
2740
2741       this.input.setAttribute("max", maximum);
2742     }
2743
2744     if (typeof this.schema.step !== "undefined") {
2745       var step = this.schema.step || 1;
2746       this.input.setAttribute("step", step);
2747     }
2748
2749   },
2750   sanitize: function(value) {
2751     return (value+"").replace(/[^0-9\.\-eE]/g,'');
2752   },
2753   getNumColumns: function() {
2754     return 2;
2755   },
2756   getValue: function() {
2757     if (!this.dependenciesFulfilled) {
2758       return undefined;
2759     }
2760     return this.value===''?undefined:this.value*1;
2761   }
2762 });
2763
2764 JSONEditor.defaults.editors.integer = JSONEditor.defaults.editors.number.extend({
2765   sanitize: function(value) {
2766     value = value + "";
2767     return value.replace(/[^0-9\-]/g,'');
2768   },
2769   getNumColumns: function() {
2770     return 2;
2771   }
2772 });
2773
2774 JSONEditor.defaults.editors.rating = JSONEditor.defaults.editors.integer.extend({
2775   build: function() {
2776     var self = this, i;
2777     if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
2778     if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
2779
2780     // Dynamically add the required CSS the first time this editor is used
2781     var styleId = 'json-editor-style-rating';
2782     var styles = document.getElementById(styleId);
2783     if (!styles) {
2784       var style = document.createElement('style');
2785       style.id = styleId;
2786       style.type = 'text/css';
2787       style.innerHTML =
2788         '      .rating-container {' +
2789         '        display: inline-block;' +
2790         '        clear: both;' +
2791         '      }' +
2792         '      ' +
2793         '      .rating {' +
2794         '        float:left;' +
2795         '      }' +
2796         '      ' +
2797         '      /* :not(:checked) is a filter, so that browsers that don’t support :checked don’t' +
2798         '         follow these rules. Every browser that supports :checked also supports :not(), so' +
2799         '         it doesn’t make the test unnecessarily selective */' +
2800         '      .rating:not(:checked) > input {' +
2801         '        position:absolute;' +
2802         '        top:-9999px;' +
2803         '        clip:rect(0,0,0,0);' +
2804         '      }' +
2805         '      ' +
2806         '      .rating:not(:checked) > label {' +
2807         '        float:right;' +
2808         '        width:1em;' +
2809         '        padding:0 .1em;' +
2810         '        overflow:hidden;' +
2811         '        white-space:nowrap;' +
2812         '        cursor:pointer;' +
2813         '        color:#ddd;' +
2814         '      }' +
2815         '      ' +
2816         '      .rating:not(:checked) > label:before {' +
2817         '        content: \'★ \';' +
2818         '      }' +
2819         '      ' +
2820         '      .rating > input:checked ~ label {' +
2821         '        color: #FFB200;' +
2822         '      }' +
2823         '      ' +
2824         '      .rating:not([readOnly]):not(:checked) > label:hover,' +
2825         '      .rating:not([readOnly]):not(:checked) > label:hover ~ label {' +
2826         '        color: #FFDA00;' +
2827         '      }' +
2828         '      ' +
2829         '      .rating:not([readOnly]) > input:checked + label:hover,' +
2830         '      .rating:not([readOnly]) > input:checked + label:hover ~ label,' +
2831         '      .rating:not([readOnly]) > input:checked ~ label:hover,' +
2832         '      .rating:not([readOnly]) > input:checked ~ label:hover ~ label,' +
2833         '      .rating:not([readOnly]) > label:hover ~ input:checked ~ label {' +
2834         '        color: #FF8C0D;' +
2835         '      }' +
2836         '      ' +
2837         '      .rating:not([readOnly])  > label:active {' +
2838         '        position:relative;' +
2839         '        top:2px;' +
2840         '        left:2px;' +
2841         '      }';
2842       document.getElementsByTagName('head')[0].appendChild(style);
2843     }
2844
2845     this.input = this.theme.getFormInputField('hidden');
2846     this.container.appendChild(this.input);
2847
2848     // Required to keep height
2849     var ratingContainer = document.createElement('div');
2850     ratingContainer.className = 'rating-container';
2851
2852     // Contains options for rating
2853     var group = document.createElement('div');
2854     group.setAttribute('name', this.formname);
2855     group.className = 'rating';
2856     ratingContainer.appendChild(group);
2857
2858     if(this.options.compact) this.container.setAttribute('class',this.container.getAttribute('class')+' compact');
2859
2860     var max = this.schema.maximum ? this.schema.maximum : 5;
2861     if (this.schema.exclusiveMaximum) max--;
2862
2863     this.inputs = [];
2864     for(i=max; i>0; i--) {
2865       var id = this.formname + i;
2866       var radioInput = this.theme.getFormInputField('radio');
2867       radioInput.setAttribute('id', id);
2868       radioInput.setAttribute('value', i);
2869       radioInput.setAttribute('name', this.formname);
2870       group.appendChild(radioInput);
2871       this.inputs.push(radioInput);
2872
2873       var label = document.createElement('label');
2874       label.setAttribute('for', id);
2875       label.appendChild(document.createTextNode(i + (i == 1 ? ' star' : ' stars')));
2876       group.appendChild(label);
2877     }
2878
2879     if(this.schema.readOnly || this.schema.readonly) {
2880       this.always_disabled = true;
2881       $each(this.inputs,function(i,input) {
2882         group.setAttribute("readOnly", "readOnly");
2883         input.disabled = true;
2884       });
2885     }
2886
2887     ratingContainer
2888       .addEventListener('change',function(e) {
2889         e.preventDefault();
2890         e.stopPropagation();
2891
2892         self.input.value = e.srcElement.value;
2893
2894         self.is_dirty = true;
2895
2896         self.refreshValue();
2897         self.watch_listener();
2898         self.jsoneditor.notifyWatchers(self.path);
2899         if(self.parent) self.parent.onChildEditorChange(self);
2900         else self.jsoneditor.onChange();
2901       });
2902
2903     this.control = this.theme.getFormControl(this.label, ratingContainer, this.description);
2904     this.container.appendChild(this.control);
2905
2906     this.refreshValue();
2907   },
2908   setValue: function(val) {
2909     var sanitized = this.sanitize(val);
2910     if(this.value === sanitized) {
2911       return;
2912     }
2913     var self = this;
2914     $each(this.inputs,function(i,input) {
2915       if (input.value === sanitized) {
2916         input.checked = true;
2917         self.value = sanitized;
2918         self.input.value = self.value;
2919         self.watch_listener();
2920         self.jsoneditor.notifyWatchers(self.path);
2921         return false;
2922       }
2923     });
2924   }
2925 });
2926
2927 JSONEditor.defaults.editors.object = JSONEditor.AbstractEditor.extend({
2928   getDefault: function() {
2929     return $extend({},this.schema["default"] || {});
2930   },
2931   getChildEditors: function() {
2932     return this.editors;
2933   },
2934   register: function() {
2935     this._super();
2936     if(this.editors) {
2937       for(var i in this.editors) {
2938         if(!this.editors.hasOwnProperty(i)) continue;
2939         this.editors[i].register();
2940       }
2941     }
2942   },
2943   unregister: function() {
2944     this._super();
2945     if(this.editors) {
2946       for(var i in this.editors) {
2947         if(!this.editors.hasOwnProperty(i)) continue;
2948         this.editors[i].unregister();
2949       }
2950     }
2951   },
2952   getNumColumns: function() {
2953     return Math.max(Math.min(12,this.maxwidth),3);
2954   },
2955   enable: function() {
2956     if(!this.always_disabled) {
2957       if(this.editjson_button) this.editjson_button.disabled = false;
2958       if(this.addproperty_button) this.addproperty_button.disabled = false;
2959
2960       this._super();
2961       if(this.editors) {
2962         for(var i in this.editors) {
2963           if(!this.editors.hasOwnProperty(i)) continue;
2964           this.editors[i].enable();
2965         }
2966       }
2967     }
2968   },
2969   disable: function(always_disabled) {
2970     if(always_disabled) this.always_disabled = true;
2971     if(this.editjson_button) this.editjson_button.disabled = true;
2972     if(this.addproperty_button) this.addproperty_button.disabled = true;
2973     this.hideEditJSON();
2974
2975     this._super();
2976     if(this.editors) {
2977       for(var i in this.editors) {
2978         if(!this.editors.hasOwnProperty(i)) continue;
2979         this.editors[i].disable(always_disabled);
2980       }
2981     }
2982   },
2983   layoutEditors: function() {
2984     var self = this, i, j;
2985
2986     if(!this.row_container) return;
2987
2988     // Sort editors by propertyOrder
2989     this.property_order = Object.keys(this.editors);
2990     this.property_order = this.property_order.sort(function(a,b) {
2991       var ordera = self.editors[a].schema.propertyOrder;
2992       var orderb = self.editors[b].schema.propertyOrder;
2993       if(typeof ordera !== "number") ordera = 1000;
2994       if(typeof orderb !== "number") orderb = 1000;
2995
2996       return ordera - orderb;
2997     });
2998
2999     var container = document.createElement('div');
3000     var isCategoriesFormat = (this.format === 'categories');
3001
3002     if(this.format === 'grid') {
3003       var rows = [];
3004       $each(this.property_order, function(j,key) {
3005         var editor = self.editors[key];
3006         if(editor.property_removed) return;
3007         var found = false;
3008         var width = editor.options.hidden? 0 : (editor.options.grid_columns || editor.getNumColumns());
3009         var height = editor.options.hidden? 0 : editor.container.offsetHeight;
3010         // See if the editor will fit in any of the existing rows first
3011         for(var i=0; i<rows.length; i++) {
3012           // If the editor will fit in the row horizontally
3013           if(rows[i].width + width <= 12) {
3014             // If the editor is close to the other elements in height
3015             // i.e. Don't put a really tall editor in an otherwise short row or vice versa
3016             if(!height || (rows[i].minh*0.5 < height && rows[i].maxh*2 > height)) {
3017               found = i;
3018             }
3019           }
3020         }
3021
3022         // If there isn't a spot in any of the existing rows, start a new row
3023         if(found === false) {
3024           rows.push({
3025             width: 0,
3026             minh: 999999,
3027             maxh: 0,
3028             editors: []
3029           });
3030           found = rows.length-1;
3031         }
3032
3033         rows[found].editors.push({
3034           key: key,
3035           //editor: editor,
3036           width: width,
3037           height: height
3038         });
3039         rows[found].width += width;
3040         rows[found].minh = Math.min(rows[found].minh,height);
3041         rows[found].maxh = Math.max(rows[found].maxh,height);
3042       });
3043
3044       // Make almost full rows width 12
3045       // Do this by increasing all editors' sizes proprotionately
3046       // Any left over space goes to the biggest editor
3047       // Don't touch rows with a width of 6 or less
3048       for(i=0; i<rows.length; i++) {
3049         if(rows[i].width < 12) {
3050           var biggest = false;
3051           var new_width = 0;
3052           for(j=0; j<rows[i].editors.length; j++) {
3053             if(biggest === false) biggest = j;
3054             else if(rows[i].editors[j].width > rows[i].editors[biggest].width) biggest = j;
3055             rows[i].editors[j].width *= 12/rows[i].width;
3056             rows[i].editors[j].width = Math.floor(rows[i].editors[j].width);
3057             new_width += rows[i].editors[j].width;
3058           }
3059           if(new_width < 12) rows[i].editors[biggest].width += 12-new_width;
3060           rows[i].width = 12;
3061         }
3062       }
3063
3064       // layout hasn't changed
3065       if(this.layout === JSON.stringify(rows)) return false;
3066       this.layout = JSON.stringify(rows);
3067
3068       // Layout the form
3069       for(i=0; i<rows.length; i++) {
3070         var row = this.theme.getGridRow();
3071         container.appendChild(row);
3072         for(j=0; j<rows[i].editors.length; j++) {
3073           var key = rows[i].editors[j].key;
3074           var editor = this.editors[key];
3075
3076           if(editor.options.hidden) editor.container.style.display = 'none';
3077           else this.theme.setGridColumnSize(editor.container,rows[i].editors[j].width);
3078           row.appendChild(editor.container);
3079         }
3080       }
3081     }
3082     // Normal layout
3083     else if(isCategoriesFormat) {
3084       //A container for properties not object nor arrays
3085       var containerSimple = document.createElement('div');
3086       //This will be the place to (re)build tabs and panes
3087       //tabs_holder has 2 childs, [0]: ul.nav.nav-tabs and [1]: div.tab-content
3088       var newTabs_holder = this.theme.getTopTabHolder(this.schema.title);
3089       //child [1] of previous, stores panes
3090       var newTabPanesContainer = this.theme.getTopTabContentHolder(newTabs_holder);
3091
3092       $each(this.property_order, function(i,key){
3093         var editor = self.editors[key];
3094         if(editor.property_removed) return;
3095         var aPane = self.theme.getTabContent();
3096         var isObjOrArray = editor.schema && (editor.schema.type === "object" || editor.schema.type === "array");
3097         //mark the pane
3098         aPane.isObjOrArray = isObjOrArray;
3099         var gridRow = self.theme.getGridRow();
3100
3101         //this happens with added properties, they don't have a tab
3102         if(!editor.tab){
3103           //Pass the pane which holds the editor
3104           if(typeof self.basicPane === 'undefined'){
3105             //There is no basicPane yet, so aPane will be it
3106             self.addRow(editor,newTabs_holder, aPane);
3107           }
3108           else {
3109             self.addRow(editor,newTabs_holder, self.basicPane);
3110           }
3111         }
3112
3113         aPane.id = editor.tab_text.textContent;
3114
3115         //For simple properties, add them on the same panel (Basic)
3116         if(!isObjOrArray){
3117           containerSimple.appendChild(gridRow);
3118           //There is already some panes
3119           if(newTabPanesContainer.childElementCount > 0){
3120             //If first pane is object or array, insert before a simple pane
3121             if(newTabPanesContainer.firstChild.isObjOrArray){
3122               //Append pane for simple properties
3123               aPane.appendChild(containerSimple);
3124               newTabPanesContainer.insertBefore(aPane,newTabPanesContainer.firstChild);
3125               //Add "Basic" tab
3126               self.theme.insertBasicTopTab(editor.tab,newTabs_holder);
3127               //newTabs_holder.firstChild.insertBefore(editor.tab,newTabs_holder.firstChild.firstChild);
3128               //Update the basicPane
3129               editor.basicPane = aPane;
3130             }
3131             else {
3132               //We already have a first "Basic" pane, just add the new property to it, so
3133               //do nothing;
3134             }
3135           }
3136           //There is no pane, so add the first (simple) pane
3137           else {
3138             //Append pane for simple properties
3139             aPane.appendChild(containerSimple);
3140             newTabPanesContainer.appendChild(aPane);
3141             //Add "Basic" tab
3142             //newTabs_holder.firstChild.appendChild(editor.tab);
3143             self.theme.addTopTab(newTabs_holder,editor.tab);
3144             //Update the basicPane
3145             editor.basicPane = aPane;
3146           }
3147         }
3148         //Objects and arrays earn it's own panes
3149         else {
3150           aPane.appendChild(gridRow);
3151           newTabPanesContainer.appendChild(aPane);
3152           //newTabs_holder.firstChild.appendChild(editor.tab);
3153           self.theme.addTopTab(newTabs_holder,editor.tab);
3154         }
3155
3156         if(editor.options.hidden) editor.container.style.display = 'none';
3157         else self.theme.setGridColumnSize(editor.container,12);
3158         //Now, add the property editor to the row
3159         gridRow.appendChild(editor.container);
3160         //Update the container (same as self.rows[x].container)
3161         editor.container = aPane;
3162
3163       });
3164
3165       //Erase old panes
3166       while (this.tabPanesContainer.firstChild) {
3167         this.tabPanesContainer.removeChild(this.tabPanesContainer.firstChild);
3168       }
3169
3170       //Erase old tabs and set the new ones
3171       var parentTabs_holder = this.tabs_holder.parentNode;
3172       parentTabs_holder.removeChild(parentTabs_holder.firstChild);
3173       parentTabs_holder.appendChild(newTabs_holder);
3174
3175       this.tabPanesContainer = newTabPanesContainer;
3176       this.tabs_holder = newTabs_holder;
3177
3178       //Activate the first tab
3179       var firstTab = this.theme.getFirstTab(this.tabs_holder);
3180       if(firstTab){
3181         $trigger(firstTab,'click');
3182       }
3183       return;
3184     }
3185     // !isCategoriesFormat
3186     else {
3187       $each(this.property_order, function(i,key) {
3188         var editor = self.editors[key];
3189         if(editor.property_removed) return;
3190         var row = self.theme.getGridRow();
3191         container.appendChild(row);
3192
3193         if(editor.options.hidden) editor.container.style.display = 'none';
3194         else self.theme.setGridColumnSize(editor.container,12);
3195         row.appendChild(editor.container);
3196       });
3197     }
3198     //for grid and normal layout
3199     while (this.row_container.firstChild) {
3200       this.row_container.removeChild(this.row_container.firstChild);
3201     }
3202     this.row_container.appendChild(container);
3203   },
3204   getPropertySchema: function(key) {
3205     // Schema declared directly in properties
3206     var schema = this.schema.properties[key] || {};
3207     schema = $extend({},schema);
3208     var matched = this.schema.properties[key]? true : false;
3209
3210     // Any matching patternProperties should be merged in
3211     if(this.schema.patternProperties) {
3212       for(var i in this.schema.patternProperties) {
3213         if(!this.schema.patternProperties.hasOwnProperty(i)) continue;
3214         var regex = new RegExp(i);
3215         if(regex.test(key)) {
3216           schema.allOf = schema.allOf || [];
3217           schema.allOf.push(this.schema.patternProperties[i]);
3218           matched = true;
3219         }
3220       }
3221     }
3222
3223     // Hasn't matched other rules, use additionalProperties schema
3224     if(!matched && this.schema.additionalProperties && typeof this.schema.additionalProperties === "object") {
3225       schema = $extend({},this.schema.additionalProperties);
3226     }
3227
3228     return schema;
3229   },
3230   preBuild: function() {
3231     this._super();
3232
3233     this.editors = {};
3234     this.cached_editors = {};
3235     var self = this;
3236
3237     this.format = this.options.layout || this.options.object_layout || this.schema.format || this.jsoneditor.options.object_layout || 'normal';
3238
3239     this.schema.properties = this.schema.properties || {};
3240
3241     this.minwidth = 0;
3242     this.maxwidth = 0;
3243
3244     // If the object should be rendered as a table row
3245     if(this.options.table_row) {
3246       $each(this.schema.properties, function(key,schema) {
3247         var editor = self.jsoneditor.getEditorClass(schema);
3248         self.editors[key] = self.jsoneditor.createEditor(editor,{
3249           jsoneditor: self.jsoneditor,
3250           schema: schema,
3251           path: self.path+'.'+key,
3252           parent: self,
3253           compact: true,
3254           required: true
3255         });
3256         self.editors[key].preBuild();
3257
3258         var width = self.editors[key].options.hidden? 0 : (self.editors[key].options.grid_columns || self.editors[key].getNumColumns());
3259
3260         self.minwidth += width;
3261         self.maxwidth += width;
3262       });
3263       this.no_link_holder = true;
3264     }
3265     // If the object should be rendered as a table
3266     else if(this.options.table) {
3267       // TODO: table display format
3268       throw "Not supported yet";
3269     }
3270     // If the object should be rendered as a div
3271     else {
3272       if(!this.schema.defaultProperties) {
3273         if(this.jsoneditor.options.display_required_only || this.options.display_required_only) {
3274           this.schema.defaultProperties = [];
3275           $each(this.schema.properties, function(k,s) {
3276             if(self.isRequired({key: k, schema: s})) {
3277               self.schema.defaultProperties.push(k);
3278             }
3279           });
3280         }
3281         else {
3282           self.schema.defaultProperties = Object.keys(self.schema.properties);
3283         }
3284       }
3285
3286       // Increase the grid width to account for padding
3287       self.maxwidth += 1;
3288
3289       $each(this.schema.defaultProperties, function(i,key) {
3290         self.addObjectProperty(key, true);
3291
3292         if(self.editors[key]) {
3293           self.minwidth = Math.max(self.minwidth,(self.editors[key].options.grid_columns || self.editors[key].getNumColumns()));
3294           self.maxwidth += (self.editors[key].options.grid_columns || self.editors[key].getNumColumns());
3295         }
3296       });
3297     }
3298
3299     // Sort editors by propertyOrder
3300     this.property_order = Object.keys(this.editors);
3301     this.property_order = this.property_order.sort(function(a,b) {
3302       var ordera = self.editors[a].schema.propertyOrder;
3303       var orderb = self.editors[b].schema.propertyOrder;
3304       if(typeof ordera !== "number") ordera = 1000;
3305       if(typeof orderb !== "number") orderb = 1000;
3306
3307       return ordera - orderb;
3308     });
3309   },
3310   //"Borrow" from arrays code
3311   addTab: function(idx){
3312       var self = this;
3313       var isObjOrArray = self.rows[idx].schema && (self.rows[idx].schema.type === "object" || self.rows[idx].schema.type === "array");
3314       if(self.tabs_holder) {
3315         self.rows[idx].tab_text = document.createElement('span');
3316
3317         if(!isObjOrArray){
3318           self.rows[idx].tab_text.textContent = (typeof self.schema.basicCategoryTitle === 'undefined') ? "Basic" : self.schema.basicCategoryTitle;
3319         } else {
3320           self.rows[idx].tab_text.textContent = self.rows[idx].getHeaderText();
3321         }
3322         self.rows[idx].tab = self.theme.getTopTab(self.rows[idx].tab_text,self.rows[idx].tab_text.textContent);
3323         self.rows[idx].tab.addEventListener('click', function(e) {
3324           self.active_tab = self.rows[idx].tab;
3325           self.refreshTabs();
3326           e.preventDefault();
3327           e.stopPropagation();
3328         });
3329
3330       }
3331
3332     },
3333   addRow: function(editor, tabHolder, holder) {
3334     var self = this;
3335     var rowsLen = this.rows.length;
3336     var isObjOrArray = editor.schema.type === "object" || editor.schema.type === "array";
3337
3338     //Add a row
3339     self.rows[rowsLen] = editor;
3340     //container stores the editor corresponding pane to set the display style when refreshing Tabs
3341     self.rows[rowsLen].container = holder;
3342
3343     if(!isObjOrArray){
3344
3345       //This is the first simple property to be added,
3346       //add a ("Basic") tab for it and save it's row number
3347       if(typeof self.basicTab === "undefined"){
3348         self.addTab(rowsLen);
3349         //Store the index row of the first simple property added
3350         self.basicTab = rowsLen;
3351         self.basicPane = holder;
3352         self.theme.addTopTab(tabHolder, self.rows[rowsLen].tab);
3353       }
3354
3355       else {
3356         //Any other simple property gets the same tab (and the same pane) as the first one,
3357         //so, when 'click' event is fired from a row, it gets the correct ("Basic") tab
3358         self.rows[rowsLen].tab = self.rows[self.basicTab].tab;
3359         self.rows[rowsLen].tab_text = self.rows[self.basicTab].tab_text;
3360         self.rows[rowsLen].container = self.rows[self.basicTab].container;
3361       }
3362     }
3363     else {
3364       self.addTab(rowsLen);
3365       self.theme.addTopTab(tabHolder, self.rows[rowsLen].tab);
3366     }
3367   },
3368   //Mark the active tab and make visible the corresponding pane, hide others
3369   refreshTabs: function(refresh_headers) {
3370     var self = this;
3371     var basicTabPresent = typeof self.basicTab !== 'undefined';
3372     var basicTabRefreshed = false;
3373
3374     $each(this.rows, function(i,row) {
3375       //If it's an orphan row (some property which has been deleted), return
3376       if(!row.tab || !row.container || !row.container.parentNode) return;
3377
3378       if(basicTabPresent && row.tab == self.rows[self.basicTab].tab && basicTabRefreshed) return;
3379
3380       if(refresh_headers) {
3381         row.tab_text.textContent = row.getHeaderText();
3382       }
3383       else {
3384         //All rows of simple properties point to the same tab, so refresh just once
3385         if(basicTabPresent && row.tab == self.rows[self.basicTab].tab) basicTabRefreshed = true;
3386
3387         if(row.tab === self.active_tab) {
3388           self.theme.markTabActive(row);
3389         }
3390         else {
3391           self.theme.markTabInactive(row);
3392         }
3393       }
3394     });
3395   },
3396   build: function() {
3397     var self = this;
3398
3399     var isCategoriesFormat = (this.format === 'categories');
3400     this.rows=[];
3401     this.active_tab = null;
3402
3403     // If the object should be rendered as a table row
3404     if(this.options.table_row) {
3405       this.editor_holder = this.container;
3406       $each(this.editors, function(key,editor) {
3407         var holder = self.theme.getTableCell();
3408         self.editor_holder.appendChild(holder);
3409
3410         editor.setContainer(holder);
3411         editor.build();
3412         editor.postBuild();
3413
3414         if(self.editors[key].options.hidden) {
3415           holder.style.display = 'none';
3416         }
3417         if(self.editors[key].options.input_width) {
3418           holder.style.width = self.editors[key].options.input_width;
3419         }
3420       });
3421     }
3422     // If the object should be rendered as a table
3423     else if(this.options.table) {
3424       // TODO: table display format
3425       throw "Not supported yet";
3426     }
3427     // If the object should be rendered as a div
3428     else {
3429       this.header = document.createElement('span');
3430       this.header.textContent = this.getTitle();
3431       this.title = this.theme.getHeader(this.header);
3432       this.container.appendChild(this.title);
3433       this.container.style.position = 'relative';
3434
3435       // Edit JSON modal
3436       this.editjson_holder = this.theme.getModal();
3437       this.editjson_textarea = this.theme.getTextareaInput();
3438       this.editjson_textarea.style.height = '170px';
3439       this.editjson_textarea.style.width = '300px';
3440       this.editjson_textarea.style.display = 'block';
3441       this.editjson_save = this.getButton('Save','save','Save');
3442       this.editjson_save.addEventListener('click',function(e) {
3443         e.preventDefault();
3444         e.stopPropagation();
3445         self.saveJSON();
3446       });
3447       this.editjson_cancel = this.getButton('Cancel','cancel','Cancel');
3448       this.editjson_cancel.addEventListener('click',function(e) {
3449         e.preventDefault();
3450         e.stopPropagation();
3451         self.hideEditJSON();
3452       });
3453       this.editjson_holder.appendChild(this.editjson_textarea);
3454       this.editjson_holder.appendChild(this.editjson_save);
3455       this.editjson_holder.appendChild(this.editjson_cancel);
3456
3457       // Manage Properties modal
3458       this.addproperty_holder = this.theme.getModal();
3459       this.addproperty_list = document.createElement('div');
3460       this.addproperty_list.style.width = '295px';
3461       this.addproperty_list.style.maxHeight = '160px';
3462       this.addproperty_list.style.padding = '5px 0';
3463       this.addproperty_list.style.overflowY = 'auto';
3464       this.addproperty_list.style.overflowX = 'hidden';
3465       this.addproperty_list.style.paddingLeft = '5px';
3466       this.addproperty_list.setAttribute('class', 'property-selector');
3467       this.addproperty_add = this.getButton('add','add','add');
3468       this.addproperty_input = this.theme.getFormInputField('text');
3469       this.addproperty_input.setAttribute('placeholder','Property name...');
3470       this.addproperty_input.style.width = '220px';
3471       this.addproperty_input.style.marginBottom = '0';
3472       this.addproperty_input.style.display = 'inline-block';
3473       this.addproperty_add.addEventListener('click',function(e) {
3474         e.preventDefault();
3475         e.stopPropagation();
3476         if(self.addproperty_input.value) {
3477           if(self.editors[self.addproperty_input.value]) {
3478             window.alert('there is already a property with that name');
3479             return;
3480           }
3481
3482           self.addObjectProperty(self.addproperty_input.value);
3483           if(self.editors[self.addproperty_input.value]) {
3484             self.editors[self.addproperty_input.value].disable();
3485           }
3486           self.onChange(true);
3487         }
3488       });
3489       this.addproperty_holder.appendChild(this.addproperty_list);
3490       this.addproperty_holder.appendChild(this.addproperty_input);
3491       this.addproperty_holder.appendChild(this.addproperty_add);
3492       var spacer = document.createElement('div');
3493       spacer.style.clear = 'both';
3494       this.addproperty_holder.appendChild(spacer);
3495
3496
3497       // Description
3498       if(this.schema.description) {
3499         this.description = this.theme.getDescription(this.schema.description);
3500         this.container.appendChild(this.description);
3501       }
3502
3503       // Validation error placeholder area
3504       this.error_holder = document.createElement('div');
3505       this.container.appendChild(this.error_holder);
3506
3507       // Container for child editor area
3508       this.editor_holder = this.theme.getIndentedPanel();
3509       this.container.appendChild(this.editor_holder);
3510
3511       // Container for rows of child editors
3512       this.row_container = this.theme.getGridContainer();
3513
3514       if(isCategoriesFormat) {
3515         this.tabs_holder = this.theme.getTopTabHolder(this.schema.title);
3516         this.tabPanesContainer = this.theme.getTopTabContentHolder(this.tabs_holder);
3517         this.editor_holder.appendChild(this.tabs_holder);
3518       }
3519       else {
3520         this.tabs_holder = this.theme.getTabHolder(this.schema.title);
3521         this.tabPanesContainer = this.theme.getTabContentHolder(this.tabs_holder);
3522         this.editor_holder.appendChild(this.row_container);
3523       }
3524
3525       $each(this.editors, function(key,editor) {
3526         var aPane = self.theme.getTabContent();
3527         var holder = self.theme.getGridColumn();
3528         var isObjOrArray = (editor.schema && (editor.schema.type === 'object' || editor.schema.type === 'array')) ? true : false;
3529         aPane.isObjOrArray = isObjOrArray;
3530
3531         if(isCategoriesFormat){
3532           if(isObjOrArray) {
3533             var single_row_container = self.theme.getGridContainer();
3534             single_row_container.appendChild(holder);
3535             aPane.appendChild(single_row_container);
3536             self.tabPanesContainer.appendChild(aPane);
3537             self.row_container = single_row_container;
3538           }
3539           else {
3540             if(typeof self.row_container_basic === 'undefined'){
3541               self.row_container_basic = self.theme.getGridContainer();
3542               aPane.appendChild(self.row_container_basic);
3543               if(self.tabPanesContainer.childElementCount == 0){
3544                 self.tabPanesContainer.appendChild(aPane);
3545               }
3546               else {
3547                 self.tabPanesContainer.insertBefore(aPane,self.tabPanesContainer.childNodes[1]);
3548               }
3549             }
3550             self.row_container_basic.appendChild(holder);
3551           }
3552
3553           self.addRow(editor,self.tabs_holder,aPane);
3554
3555           aPane.id = editor.schema.title; //editor.schema.path//tab_text.textContent
3556
3557         }
3558         else {
3559           self.row_container.appendChild(holder);
3560         }
3561
3562         editor.setContainer(holder);
3563         editor.build();
3564         editor.postBuild();
3565       });
3566
3567       if(this.rows[0]){
3568         $trigger(this.rows[0].tab,'click');
3569       }
3570
3571       // Control buttons
3572       this.title_controls = this.theme.getHeaderButtonHolder();
3573       this.editjson_controls = this.theme.getHeaderButtonHolder();
3574       this.addproperty_controls = this.theme.getHeaderButtonHolder();
3575       this.title.appendChild(this.title_controls);
3576       this.title.appendChild(this.editjson_controls);
3577       this.title.appendChild(this.addproperty_controls);
3578
3579       // Show/Hide button
3580       this.collapsed = false;
3581       this.toggle_button = this.getButton('', 'collapse', this.translate('button_collapse'));
3582       this.title_controls.appendChild(this.toggle_button);
3583       this.toggle_button.addEventListener('click',function(e) {
3584         e.preventDefault();
3585         e.stopPropagation();
3586         if(self.collapsed) {
3587           self.editor_holder.style.display = '';
3588           self.collapsed = false;
3589           self.setButtonText(self.toggle_button,'','collapse',self.translate('button_collapse'));
3590         }
3591         else {
3592           self.editor_holder.style.display = 'none';
3593           self.collapsed = true;
3594           self.setButtonText(self.toggle_button,'','expand',self.translate('button_expand'));
3595         }
3596       });
3597
3598       // If it should start collapsed
3599       if(this.options.collapsed) {
3600         $trigger(this.toggle_button,'click');
3601       }
3602
3603       // Collapse button disabled
3604       if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
3605         if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
3606       }
3607       else if(this.jsoneditor.options.disable_collapse) {
3608         this.toggle_button.style.display = 'none';
3609       }
3610
3611       // Edit JSON Button
3612       this.editjson_button = this.getButton('JSON','edit','Edit JSON');
3613       this.editjson_button.addEventListener('click',function(e) {
3614         e.preventDefault();
3615         e.stopPropagation();
3616         self.toggleEditJSON();
3617       });
3618       this.editjson_controls.appendChild(this.editjson_button);
3619       this.editjson_controls.appendChild(this.editjson_holder);
3620
3621       // Edit JSON Buttton disabled
3622       if(this.schema.options && typeof this.schema.options.disable_edit_json !== "undefined") {
3623         if(this.schema.options.disable_edit_json) this.editjson_button.style.display = 'none';
3624       }
3625       else if(this.jsoneditor.options.disable_edit_json) {
3626         this.editjson_button.style.display = 'none';
3627       }
3628
3629       // Object Properties Button
3630       this.addproperty_button = this.getButton('Properties','edit','Object Properties');
3631       this.addproperty_button.addEventListener('click',function(e) {
3632         e.preventDefault();
3633         e.stopPropagation();
3634         self.toggleAddProperty();
3635       });
3636       this.addproperty_controls.appendChild(this.addproperty_button);
3637       this.addproperty_controls.appendChild(this.addproperty_holder);
3638       this.refreshAddProperties();
3639     }
3640
3641     // Fix table cell ordering
3642     if(this.options.table_row) {
3643       this.editor_holder = this.container;
3644       $each(this.property_order,function(i,key) {
3645         self.editor_holder.appendChild(self.editors[key].container);
3646       });
3647     }
3648     // Layout object editors in grid if needed
3649     else {
3650       // Initial layout
3651       this.layoutEditors();
3652       // Do it again now that we know the approximate heights of elements
3653       this.layoutEditors();
3654     }
3655   },
3656   showEditJSON: function() {
3657     if(!this.editjson_holder) return;
3658     this.hideAddProperty();
3659
3660     // Position the form directly beneath the button
3661     // TODO: edge detection
3662     this.editjson_holder.style.left = this.editjson_button.offsetLeft+"px";
3663     this.editjson_holder.style.top = this.editjson_button.offsetTop + this.editjson_button.offsetHeight+"px";
3664
3665     // Start the textarea with the current value
3666     this.editjson_textarea.value = JSON.stringify(this.getValue(),null,2);
3667
3668     // Disable the rest of the form while editing JSON
3669     this.disable();
3670
3671     this.editjson_holder.style.display = '';
3672     this.editjson_button.disabled = false;
3673     this.editing_json = true;
3674   },
3675   hideEditJSON: function() {
3676     if(!this.editjson_holder) return;
3677     if(!this.editing_json) return;
3678
3679     this.editjson_holder.style.display = 'none';
3680     this.enable();
3681     this.editing_json = false;
3682   },
3683   saveJSON: function() {
3684     if(!this.editjson_holder) return;
3685
3686     try {
3687       var json = JSON.parse(this.editjson_textarea.value);
3688       this.setValue(json);
3689       this.hideEditJSON();
3690     }
3691     catch(e) {
3692       window.alert('invalid JSON');
3693       throw e;
3694     }
3695   },
3696   toggleEditJSON: function() {
3697     if(this.editing_json) this.hideEditJSON();
3698     else this.showEditJSON();
3699   },
3700   insertPropertyControlUsingPropertyOrder: function (property, control, container) {
3701     var propertyOrder;
3702     if (this.schema.properties[property])
3703       propertyOrder = this.schema.properties[property].propertyOrder;
3704     if (typeof propertyOrder !== "number") propertyOrder = 1000;
3705     control.propertyOrder = propertyOrder;
3706
3707     for (var i = 0; i < container.childNodes.length; i++) {
3708       var child = container.childNodes[i];
3709       if (control.propertyOrder < child.propertyOrder) {
3710         this.addproperty_list.insertBefore(control, child);
3711         control = null;
3712         break;
3713       }
3714     }
3715     if (control) {
3716       this.addproperty_list.appendChild(control);
3717     }
3718   },
3719   addPropertyCheckbox: function(key) {
3720     var self = this;
3721     var checkbox, label, labelText, control;
3722
3723     checkbox = self.theme.getCheckbox();
3724     checkbox.style.width = 'auto';
3725
3726     if (this.schema.properties[key] && this.schema.properties[key].title)
3727       labelText = this.schema.properties[key].title;
3728     else
3729       labelText = key;
3730
3731     label = self.theme.getCheckboxLabel(labelText);
3732
3733     control = self.theme.getFormControl(label,checkbox);
3734     control.style.paddingBottom = control.style.marginBottom = control.style.paddingTop = control.style.marginTop = 0;
3735     control.style.height = 'auto';
3736     //control.style.overflowY = 'hidden';
3737
3738     this.insertPropertyControlUsingPropertyOrder(key, control, this.addproperty_list);
3739
3740     checkbox.checked = key in this.editors;
3741     checkbox.addEventListener('change',function() {
3742       if(checkbox.checked) {
3743         self.addObjectProperty(key);
3744       }
3745       else {
3746         self.removeObjectProperty(key);
3747       }
3748       self.onChange(true);
3749     });
3750     self.addproperty_checkboxes[key] = checkbox;
3751
3752     return checkbox;
3753   },
3754   showAddProperty: function() {
3755     if(!this.addproperty_holder) return;
3756     this.hideEditJSON();
3757
3758     // Position the form directly beneath the button
3759     // TODO: edge detection
3760     this.addproperty_holder.style.left = this.addproperty_button.offsetLeft+"px";
3761     this.addproperty_holder.style.top = this.addproperty_button.offsetTop + this.addproperty_button.offsetHeight+"px";
3762
3763     // Disable the rest of the form while editing JSON
3764     this.disable();
3765
3766     this.adding_property = true;
3767     this.addproperty_button.disabled = false;
3768     this.addproperty_holder.style.display = '';
3769     this.refreshAddProperties();
3770   },
3771   hideAddProperty: function() {
3772     if(!this.addproperty_holder) return;
3773     if(!this.adding_property) return;
3774
3775     this.addproperty_holder.style.display = 'none';
3776     this.enable();
3777
3778     this.adding_property = false;
3779   },
3780   toggleAddProperty: function() {
3781     if(this.adding_property) this.hideAddProperty();
3782     else this.showAddProperty();
3783   },
3784   removeObjectProperty: function(property) {
3785     if(this.editors[property]) {
3786       this.editors[property].unregister();
3787       delete this.editors[property];
3788
3789       this.refreshValue();
3790       this.layoutEditors();
3791     }
3792   },
3793   addObjectProperty: function(name, prebuild_only) {
3794     var self = this;
3795
3796     // Property is already added
3797     if(this.editors[name]) return;
3798
3799     // Property was added before and is cached
3800     if(this.cached_editors[name]) {
3801       this.editors[name] = this.cached_editors[name];
3802       if(prebuild_only) return;
3803       this.editors[name].register();
3804     }
3805     // New property
3806     else {
3807       if(!this.canHaveAdditionalProperties() && (!this.schema.properties || !this.schema.properties[name])) {
3808         return;
3809       }
3810
3811       var schema = self.getPropertySchema(name);
3812       if(typeof schema.propertyOrder !== 'number'){
3813         // if the propertyOrder undefined, then set a smart default value.
3814         schema.propertyOrder = Object.keys(self.editors).length + 1000;
3815       }
3816
3817
3818       // Add the property
3819       var editor = self.jsoneditor.getEditorClass(schema);
3820
3821       self.editors[name] = self.jsoneditor.createEditor(editor,{
3822         jsoneditor: self.jsoneditor,
3823         schema: schema,
3824         path: self.path+'.'+name,
3825         parent: self
3826       });
3827       self.editors[name].preBuild();
3828
3829       if(!prebuild_only) {
3830         var holder = self.theme.getChildEditorHolder();
3831         self.editor_holder.appendChild(holder);
3832         self.editors[name].setContainer(holder);
3833         self.editors[name].build();
3834         self.editors[name].postBuild();
3835       }
3836
3837       self.cached_editors[name] = self.editors[name];
3838     }
3839
3840     // If we're only prebuilding the editors, don't refresh values
3841     if(!prebuild_only) {
3842       self.refreshValue();
3843       self.layoutEditors();
3844     }
3845   },
3846   onChildEditorChange: function(editor) {
3847     this.refreshValue();
3848     this._super(editor);
3849   },
3850   canHaveAdditionalProperties: function() {
3851     if (typeof this.schema.additionalProperties === "boolean") {//# sourceMappingURL=jsoneditor.js.map
3852       return this.schema.additionalProperties;
3853     }
3854     return !this.jsoneditor.options.no_additional_properties;
3855   },
3856   destroy: function() {
3857     $each(this.cached_editors, function(i,el) {
3858       el.destroy();
3859     });
3860     if(this.editor_holder) this.editor_holder.innerHTML = '';
3861     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
3862     if(this.error_holder && this.error_holder.parentNode) this.error_holder.parentNode.removeChild(this.error_holder);
3863
3864     this.editors = null;
3865     this.cached_editors = null;
3866     if(this.editor_holder && this.editor_holder.parentNode) this.editor_holder.parentNode.removeChild(this.editor_holder);
3867     this.editor_holder = null;
3868
3869     this._super();
3870   },
3871   getValue: function() {
3872     if (!this.dependenciesFulfilled) {
3873       return undefined;
3874     }
3875     var result = this._super();
3876     if(this.jsoneditor.options.remove_empty_properties || this.options.remove_empty_properties) {
3877       for (var i in result) {
3878         if (result.hasOwnProperty(i)) {
3879           if ((typeof result[i] === 'undefined' || result[i] === '') || 
3880               (Object.keys(result[i]).length == 0 && result[i].constructor == Object) ||
3881               (Array.isArray(result[i]) && result[i].length == 0)) {
3882               delete result[i];
3883           }
3884         }
3885       }
3886     }
3887
3888     return result;
3889   },
3890   refreshValue: function() {
3891     this.value = {};
3892     var self = this;
3893
3894     for(var i in this.editors) {
3895       if(!this.editors.hasOwnProperty(i)) continue;
3896       this.value[i] = this.editors[i].getValue();
3897     }
3898
3899     if(this.adding_property) this.refreshAddProperties();
3900   },
3901   refreshAddProperties: function() {
3902     if(this.options.disable_properties || (this.options.disable_properties !== false && this.jsoneditor.options.disable_properties)) {
3903       this.addproperty_controls.style.display = 'none';
3904       return;
3905     }
3906
3907     var can_add = false, can_remove = false, num_props = 0, i, show_modal = false;
3908
3909     // Get number of editors
3910     for(i in this.editors) {
3911       if(!this.editors.hasOwnProperty(i)) continue;
3912       num_props++;
3913     }
3914
3915     // Determine if we can add back removed properties
3916     can_add = this.canHaveAdditionalProperties() && !(typeof this.schema.maxProperties !== "undefined" && num_props >= this.schema.maxProperties);
3917
3918     if(this.addproperty_checkboxes) {
3919       this.addproperty_list.innerHTML = '';
3920     }
3921     this.addproperty_checkboxes = {};
3922
3923     // Check for which editors can't be removed or added back
3924     for(i in this.cached_editors) {
3925       if(!this.cached_editors.hasOwnProperty(i)) continue;
3926
3927       this.addPropertyCheckbox(i);
3928
3929       if(this.isRequired(this.cached_editors[i]) && i in this.editors) {
3930         this.addproperty_checkboxes[i].disabled = true;
3931       }
3932
3933       if(typeof this.schema.minProperties !== "undefined" && num_props <= this.schema.minProperties) {
3934         this.addproperty_checkboxes[i].disabled = this.addproperty_checkboxes[i].checked;
3935         if(!this.addproperty_checkboxes[i].checked) show_modal = true;
3936       }
3937       else if(!(i in this.editors)) {
3938         if(!can_add  && !this.schema.properties.hasOwnProperty(i)) {
3939           this.addproperty_checkboxes[i].disabled = true;
3940         }
3941         else {
3942           this.addproperty_checkboxes[i].disabled = false;
3943           show_modal = true;
3944         }
3945       }
3946       else {
3947         show_modal = true;
3948         can_remove = true;
3949       }
3950     }
3951
3952     if(this.canHaveAdditionalProperties()) {
3953       show_modal = true;
3954     }
3955
3956     // Additional addproperty checkboxes not tied to a current editor
3957     for(i in this.schema.properties) {
3958       if(!this.schema.properties.hasOwnProperty(i)) continue;
3959       if(this.cached_editors[i]) continue;
3960       show_modal = true;
3961       this.addPropertyCheckbox(i);
3962     }
3963
3964     // If no editors can be added or removed, hide the modal button
3965     if(!show_modal) {
3966       this.hideAddProperty();
3967       this.addproperty_controls.style.display = 'none';
3968     }
3969     // If additional properties are disabled
3970     else if(!this.canHaveAdditionalProperties()) {
3971       this.addproperty_add.style.display = 'none';
3972       this.addproperty_input.style.display = 'none';
3973     }
3974     // If no new properties can be added
3975     else if(!can_add) {
3976       this.addproperty_add.disabled = true;
3977     }
3978     // If new properties can be added
3979     else {
3980       this.addproperty_add.disabled = false;
3981     }
3982   },
3983   isRequired: function(editor) {
3984     if(typeof editor.schema.required === "boolean") return editor.schema.required;
3985     else if(Array.isArray(this.schema.required)) return this.schema.required.indexOf(editor.key) > -1;
3986     else if(this.jsoneditor.options.required_by_default) return true;
3987     else return false;
3988   },
3989   setValue: function(value, initial) {
3990     var self = this;
3991     value = value || {};
3992
3993     if(typeof value !== "object" || Array.isArray(value)) value = {};
3994
3995     // First, set the values for all of the defined properties
3996     $each(this.cached_editors, function(i,editor) {
3997       // Value explicitly set
3998       if(typeof value[i] !== "undefined") {
3999         self.addObjectProperty(i);
4000         editor.setValue(value[i],initial);
4001       }
4002       // Otherwise, remove value unless this is the initial set or it's required
4003       else if(!initial && !self.isRequired(editor)) {
4004         self.removeObjectProperty(i);
4005       }
4006       // Otherwise, set the value to the default
4007       else {
4008         editor.setValue(editor.getDefault(),initial);
4009       }
4010     });
4011
4012     $each(value, function(i,val) {
4013       if(!self.cached_editors[i]) {
4014         self.addObjectProperty(i);
4015         if(self.editors[i]) self.editors[i].setValue(val,initial);
4016       }
4017     });
4018
4019     this.refreshValue();
4020     this.layoutEditors();
4021     this.onChange();
4022   },
4023   showValidationErrors: function(errors) {
4024     var self = this;
4025
4026     // Get all the errors that pertain to this editor
4027     var my_errors = [];
4028     var other_errors = [];
4029     $each(errors, function(i,error) {
4030       if(error.path === self.path) {
4031         my_errors.push(error);
4032       }
4033       else {
4034         other_errors.push(error);
4035       }
4036     });
4037
4038     // Show errors for this editor
4039     if(this.error_holder) {
4040       if(my_errors.length) {
4041         var message = [];
4042         this.error_holder.innerHTML = '';
4043         this.error_holder.style.display = '';
4044         $each(my_errors, function(i,error) {
4045           self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
4046         });
4047       }
4048       // Hide error area
4049       else {
4050         this.error_holder.style.display = 'none';
4051       }
4052     }
4053
4054     // Show error for the table row if this is inside a table
4055     if(this.options.table_row) {
4056       if(my_errors.length) {
4057         this.theme.addTableRowError(this.container);
4058       }
4059       else {
4060         this.theme.removeTableRowError(this.container);
4061       }
4062     }
4063
4064     // Show errors for child editors
4065     $each(this.editors, function(i,editor) {
4066       editor.showValidationErrors(other_errors);
4067     });
4068   }
4069 });
4070
4071 JSONEditor.defaults.editors.array = JSONEditor.AbstractEditor.extend({
4072   getDefault: function() {
4073     return this.schema["default"] || [];
4074   },
4075   register: function() {
4076     this._super();
4077     if(this.rows) {
4078       for(var i=0; i<this.rows.length; i++) {
4079         this.rows[i].register();
4080       }
4081     }
4082   },
4083   unregister: function() {
4084     this._super();
4085     if(this.rows) {
4086       for(var i=0; i<this.rows.length; i++) {
4087         this.rows[i].unregister();
4088       }
4089     }
4090   },
4091   getNumColumns: function() {
4092     var info = this.getItemInfo(0);
4093     // Tabs require extra horizontal space
4094     if(this.tabs_holder && this.schema.format !== 'tabs-top') {
4095       return Math.max(Math.min(12,info.width+2),4);
4096     }
4097     else {
4098       return info.width;
4099     }
4100   },
4101   enable: function() {
4102     if(!this.always_disabled) {
4103       if(this.add_row_button) this.add_row_button.disabled = false;
4104       if(this.remove_all_rows_button) this.remove_all_rows_button.disabled = false;
4105       if(this.delete_last_row_button) this.delete_last_row_button.disabled = false;
4106
4107       if(this.rows) {
4108         for(var i=0; i<this.rows.length; i++) {
4109           this.rows[i].enable();
4110
4111           if(this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = false;
4112           if(this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = false;
4113           if(this.rows[i].delete_button) this.rows[i].delete_button.disabled = false;
4114         }
4115       }
4116       this._super();
4117     }
4118   },
4119   disable: function(always_disabled) {
4120     if(always_disabled) this.always_disabled = true;
4121     if(this.add_row_button) this.add_row_button.disabled = true;
4122     if(this.remove_all_rows_button) this.remove_all_rows_button.disabled = true;
4123     if(this.delete_last_row_button) this.delete_last_row_button.disabled = true;
4124
4125     if(this.rows) {
4126       for(var i=0; i<this.rows.length; i++) {
4127         this.rows[i].disable(always_disabled);
4128
4129         if(this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = true;
4130         if(this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = true;
4131         if(this.rows[i].delete_button) this.rows[i].delete_button.disabled = true;
4132       }
4133     }
4134     this._super();
4135   },
4136   preBuild: function() {
4137     this._super();
4138
4139     this.rows = [];
4140     this.row_cache = [];
4141
4142     this.hide_delete_buttons = this.options.disable_array_delete || this.jsoneditor.options.disable_array_delete;
4143     this.hide_delete_all_rows_buttons = this.hide_delete_buttons || this.options.disable_array_delete_all_rows || this.jsoneditor.options.disable_array_delete_all_rows;
4144     this.hide_delete_last_row_buttons = this.hide_delete_buttons || this.options.disable_array_delete_last_row || this.jsoneditor.options.disable_array_delete_last_row;
4145     this.hide_move_buttons = this.options.disable_array_reorder || this.jsoneditor.options.disable_array_reorder;
4146     this.hide_add_button = this.options.disable_array_add || this.jsoneditor.options.disable_array_add;
4147         this.show_copy_button = this.options.enable_array_copy || this.jsoneditor.options.enable_array_copy;
4148   },
4149   build: function() {
4150     var self = this;
4151
4152     if(!this.options.compact) {
4153       this.header = document.createElement('span');
4154       this.header.textContent = this.getTitle();
4155       this.title = this.theme.getHeader(this.header);
4156       this.container.appendChild(this.title);
4157       this.title_controls = this.theme.getHeaderButtonHolder();
4158       this.title.appendChild(this.title_controls);
4159       if(this.schema.description) {
4160         this.description = this.theme.getDescription(this.schema.description);
4161         this.container.appendChild(this.description);
4162       }
4163       this.error_holder = document.createElement('div');
4164       this.container.appendChild(this.error_holder);
4165
4166       if(this.schema.format === 'tabs-top') {
4167         this.controls = this.theme.getHeaderButtonHolder();
4168         this.title.appendChild(this.controls);
4169         this.tabs_holder = this.theme.getTopTabHolder(this.getItemTitle());
4170         this.container.appendChild(this.tabs_holder);
4171         this.row_holder = this.theme.getTopTabContentHolder(this.tabs_holder);
4172
4173         this.active_tab = null;
4174       }
4175       else if(this.schema.format === 'tabs') {
4176         this.controls = this.theme.getHeaderButtonHolder();
4177         this.title.appendChild(this.controls);
4178         this.tabs_holder = this.theme.getTabHolder(this.getItemTitle());
4179         this.container.appendChild(this.tabs_holder);
4180         this.row_holder = this.theme.getTabContentHolder(this.tabs_holder);
4181
4182         this.active_tab = null;
4183       }
4184       else {
4185         this.panel = this.theme.getIndentedPanel();
4186         this.container.appendChild(this.panel);
4187         this.row_holder = document.createElement('div');
4188         this.panel.appendChild(this.row_holder);
4189         this.controls = this.theme.getButtonHolder();
4190         this.panel.appendChild(this.controls);
4191       }
4192     }
4193     else {
4194         this.panel = this.theme.getIndentedPanel();
4195         this.container.appendChild(this.panel);
4196         this.controls = this.theme.getButtonHolder();
4197         this.panel.appendChild(this.controls);
4198         this.row_holder = document.createElement('div');
4199         this.panel.appendChild(this.row_holder);
4200     }
4201
4202     // Add controls
4203     this.addControls();
4204   },
4205   onChildEditorChange: function(editor) {
4206     this.refreshValue();
4207     this.refreshTabs(true);
4208     this._super(editor);
4209   },
4210   getItemTitle: function() {
4211     if(!this.item_title) {
4212       if(this.schema.items && !Array.isArray(this.schema.items)) {
4213         var tmp = this.jsoneditor.expandRefs(this.schema.items);
4214         this.item_title = tmp.title || 'item';
4215       }
4216       else {
4217         this.item_title = 'item';
4218       }
4219     }
4220     return this.item_title;
4221   },
4222   getItemSchema: function(i) {
4223     if(Array.isArray(this.schema.items)) {
4224       if(i >= this.schema.items.length) {
4225         if(this.schema.additionalItems===true) {
4226           return {};
4227         }
4228         else if(this.schema.additionalItems) {
4229           return $extend({},this.schema.additionalItems);
4230         }
4231       }
4232       else {
4233         return $extend({},this.schema.items[i]);
4234       }
4235     }
4236     else if(this.schema.items) {
4237       return $extend({},this.schema.items);
4238     }
4239     else {
4240       return {};
4241     }
4242   },
4243   getItemInfo: function(i) {
4244     var schema = this.getItemSchema(i);
4245
4246     // Check if it's cached
4247     this.item_info = this.item_info || {};
4248     var stringified = JSON.stringify(schema);
4249     if(typeof this.item_info[stringified] !== "undefined") return this.item_info[stringified];
4250
4251     // Get the schema for this item
4252     schema = this.jsoneditor.expandRefs(schema);
4253
4254     this.item_info[stringified] = {
4255       title: schema.title || "item",
4256       'default': schema["default"],
4257       width: 12,
4258       child_editors: schema.properties || schema.items
4259     };
4260
4261     return this.item_info[stringified];
4262   },
4263   getElementEditor: function(i) {
4264     var item_info = this.getItemInfo(i);
4265     var schema = this.getItemSchema(i);
4266     schema = this.jsoneditor.expandRefs(schema);
4267     schema.title = item_info.title+' '+(i+1);
4268
4269     var editor = this.jsoneditor.getEditorClass(schema);
4270
4271     var holder;
4272     if(this.tabs_holder) {
4273       if(this.schema.format === 'tabs-top') {
4274         holder = this.theme.getTopTabContent();
4275       }
4276       else {
4277         holder = this.theme.getTabContent();
4278       }
4279       holder.id = this.path+'.'+i;
4280     }
4281     else if(item_info.child_editors) {
4282       holder = this.theme.getChildEditorHolder();
4283     }
4284     else {
4285       holder = this.theme.getIndentedPanel();
4286     }
4287
4288     this.row_holder.appendChild(holder);
4289
4290     var ret = this.jsoneditor.createEditor(editor,{
4291       jsoneditor: this.jsoneditor,
4292       schema: schema,
4293       container: holder,
4294       path: this.path+'.'+i,
4295       parent: this,
4296       required: true
4297     });
4298     ret.preBuild();
4299     ret.build();
4300     ret.postBuild();
4301
4302     if(!ret.title_controls) {
4303       ret.array_controls = this.theme.getButtonHolder();
4304       holder.appendChild(ret.array_controls);
4305     }
4306
4307     return ret;
4308   },
4309   destroy: function() {
4310     this.empty(true);
4311     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
4312     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
4313     if(this.row_holder && this.row_holder.parentNode) this.row_holder.parentNode.removeChild(this.row_holder);
4314     if(this.controls && this.controls.parentNode) this.controls.parentNode.removeChild(this.controls);
4315     if(this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
4316
4317     this.rows = this.row_cache = this.title = this.description = this.row_holder = this.panel = this.controls = null;
4318
4319     this._super();
4320   },
4321   empty: function(hard) {
4322     if(!this.rows) return;
4323     var self = this;
4324     $each(this.rows,function(i,row) {
4325       if(hard) {
4326         if(row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
4327         self.destroyRow(row,true);
4328         self.row_cache[i] = null;
4329       }
4330       self.rows[i] = null;
4331     });
4332     self.rows = [];
4333     if(hard) self.row_cache = [];
4334   },
4335   destroyRow: function(row,hard) {
4336     var holder = row.container;
4337     if(hard) {
4338       row.destroy();
4339       if(holder.parentNode) holder.parentNode.removeChild(holder);
4340       if(row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
4341     }
4342     else {
4343       if(row.tab) row.tab.style.display = 'none';
4344       holder.style.display = 'none';
4345       row.unregister();
4346     }
4347   },
4348   getMax: function() {
4349     if((Array.isArray(this.schema.items)) && this.schema.additionalItems === false) {
4350       return Math.min(this.schema.items.length,this.schema.maxItems || Infinity);
4351     }
4352     else {
4353       return this.schema.maxItems || Infinity;
4354     }
4355   },
4356   refreshTabs: function(refresh_headers) {
4357     var self = this;
4358     $each(this.rows, function(i,row) {
4359       if(!row.tab) return;
4360
4361       if(refresh_headers) {
4362         row.tab_text.textContent = row.getHeaderText();
4363       }
4364       else {
4365         if(row.tab === self.active_tab) {
4366           self.theme.markTabActive(row);
4367         }
4368         else {
4369           self.theme.markTabInactive(row);
4370         }
4371       }
4372     });
4373   },
4374   setValue: function(value, initial) {
4375     // Update the array's value, adding/removing rows when necessary
4376     value = value || [];
4377
4378     if(!(Array.isArray(value))) value = [value];
4379
4380     var serialized = JSON.stringify(value);
4381     if(serialized === this.serialized) return;
4382
4383     // Make sure value has between minItems and maxItems items in it
4384     if(this.schema.minItems) {
4385       while(value.length < this.schema.minItems) {
4386         value.push(this.getItemInfo(value.length)["default"]);
4387       }
4388     }
4389     if(this.getMax() && value.length > this.getMax()) {
4390       value = value.slice(0,this.getMax());
4391     }
4392
4393     var self = this;
4394     $each(value,function(i,val) {
4395       if(self.rows[i]) {
4396         // TODO: don't set the row's value if it hasn't changed
4397         self.rows[i].setValue(val,initial);
4398       }
4399       else if(self.row_cache[i]) {
4400         self.rows[i] = self.row_cache[i];
4401         self.rows[i].setValue(val,initial);
4402         self.rows[i].container.style.display = '';
4403         if(self.rows[i].tab) self.rows[i].tab.style.display = '';
4404         self.rows[i].register();
4405       }
4406       else {
4407         self.addRow(val,initial);
4408       }
4409     });
4410
4411     for(var j=value.length; j<self.rows.length; j++) {
4412       self.destroyRow(self.rows[j]);
4413       self.rows[j] = null;
4414     }
4415     self.rows = self.rows.slice(0,value.length);
4416
4417     // Set the active tab
4418     var new_active_tab = null;
4419     $each(self.rows, function(i,row) {
4420       if(row.tab === self.active_tab) {
4421         new_active_tab = row.tab;
4422         return false;
4423       }
4424     });
4425     if(!new_active_tab && self.rows.length) new_active_tab = self.rows[0].tab;
4426
4427     self.active_tab = new_active_tab;
4428
4429     self.refreshValue(initial);
4430     self.refreshTabs(true);
4431     self.refreshTabs();
4432
4433     self.onChange();
4434
4435     // TODO: sortable
4436   },
4437   refreshValue: function(force) {
4438     var self = this;
4439     var oldi = this.value? this.value.length : 0;
4440     this.value = [];
4441
4442     $each(this.rows,function(i,editor) {
4443       // Get the value for this editor
4444       self.value[i] = editor.getValue();
4445     });
4446
4447     if(oldi !== this.value.length || force) {
4448       // If we currently have minItems items in the array
4449       var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
4450
4451       $each(this.rows,function(i,editor) {
4452         // Hide the move down button for the last row
4453         if(editor.movedown_button) {
4454           if(i === self.rows.length - 1) {
4455             editor.movedown_button.style.display = 'none';
4456           }
4457           else {
4458             editor.movedown_button.style.display = '';
4459           }
4460         }
4461
4462         // Hide the delete button if we have minItems items
4463         if(editor.delete_button) {
4464           if(minItems) {
4465             editor.delete_button.style.display = 'none';
4466           }
4467           else {
4468             editor.delete_button.style.display = '';
4469           }
4470         }
4471
4472         // Get the value for this editor
4473         self.value[i] = editor.getValue();
4474       });
4475
4476       var controls_needed = false;
4477
4478       if(!this.value.length) {
4479         this.delete_last_row_button.style.display = 'none';
4480         this.remove_all_rows_button.style.display = 'none';
4481       }
4482       else if(this.value.length === 1) {
4483         this.remove_all_rows_button.style.display = 'none';
4484
4485         // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
4486         if(minItems || this.hide_delete_last_row_buttons) {
4487           this.delete_last_row_button.style.display = 'none';
4488         }
4489         else {
4490           this.delete_last_row_button.style.display = '';
4491           controls_needed = true;
4492         }
4493       }
4494       else {
4495         if(minItems || this.hide_delete_last_row_buttons) {
4496           this.delete_last_row_button.style.display = 'none';
4497         }
4498         else {
4499           this.delete_last_row_button.style.display = '';
4500           controls_needed = true;
4501         }
4502
4503         if(minItems || this.hide_delete_all_rows_buttons) {
4504           this.remove_all_rows_button.style.display = 'none';
4505         }
4506         else {
4507           this.remove_all_rows_button.style.display = '';
4508           controls_needed = true;
4509         }
4510       }
4511
4512       // If there are maxItems in the array, hide the add button beneath the rows
4513       if((this.getMax() && this.getMax() <= this.rows.length) || this.hide_add_button){
4514         this.add_row_button.style.display = 'none';
4515       }
4516       else {
4517         this.add_row_button.style.display = '';
4518         controls_needed = true;
4519       }
4520
4521       if(!this.collapsed && controls_needed) {
4522         this.controls.style.display = 'inline-block';
4523       }
4524       else {
4525         this.controls.style.display = 'none';
4526       }
4527     }
4528   },
4529   addRow: function(value, initial) {
4530     var self = this;
4531     var i = this.rows.length;
4532
4533     self.rows[i] = this.getElementEditor(i);
4534     self.row_cache[i] = self.rows[i];
4535
4536     if(self.tabs_holder) {
4537       self.rows[i].tab_text = document.createElement('span');
4538       self.rows[i].tab_text.textContent = self.rows[i].getHeaderText();
4539       if(self.schema.format === 'tabs-top'){
4540         self.rows[i].tab = self.theme.getTopTab(self.rows[i].tab_text,self.rows[i].path);
4541         self.theme.addTopTab(self.tabs_holder, self.rows[i].tab);
4542       }
4543       else {
4544         self.rows[i].tab = self.theme.getTab(self.rows[i].tab_text,self.rows[i].path);
4545         self.theme.addTab(self.tabs_holder, self.rows[i].tab);
4546       }
4547       self.rows[i].tab.addEventListener('click', function(e) {
4548         self.active_tab = self.rows[i].tab;
4549         self.refreshTabs();
4550         e.preventDefault();
4551         e.stopPropagation();
4552       });
4553
4554     }
4555
4556     var controls_holder = self.rows[i].title_controls || self.rows[i].array_controls;
4557
4558     // Buttons to delete row, move row up, and move row down
4559     if(!self.hide_delete_buttons) {
4560       self.rows[i].delete_button = this.getButton(self.getItemTitle(),'delete',this.translate('button_delete_row_title',[self.getItemTitle()]));
4561       self.rows[i].delete_button.className += ' delete';
4562       self.rows[i].delete_button.setAttribute('data-i',i);
4563       self.rows[i].delete_button.addEventListener('click',function(e) {
4564         e.preventDefault();
4565         e.stopPropagation();
4566
4567         if (self.jsoneditor.options.prompt_before_delete === true) {
4568           if (confirm("Confirm to remove.") === false) {
4569             return false;
4570           }
4571         }
4572
4573         var i = this.getAttribute('data-i')*1;
4574
4575         var value = self.getValue();
4576
4577         var newval = [];
4578         var new_active_tab = null;
4579         $each(value,function(j,row) {
4580           if(j===i) {
4581             // If the one we're deleting is the active tab
4582             if(self.rows[j].tab === self.active_tab) {
4583               // Make the next tab active if there is one
4584               // Note: the next tab is going to be the current tab after deletion
4585               if(self.rows[j+1]) new_active_tab = self.rows[j].tab;
4586               // Otherwise, make the previous tab active if there is one
4587               else if(j) new_active_tab = self.rows[j-1].tab;
4588             }
4589
4590             return; // If this is the one we're deleting
4591           }
4592           newval.push(row);
4593         });
4594         self.setValue(newval);
4595         if(new_active_tab) {
4596           self.active_tab = new_active_tab;
4597           self.refreshTabs();
4598         }
4599
4600         self.onChange(true);
4601       });
4602
4603       if(controls_holder) {
4604         controls_holder.appendChild(self.rows[i].delete_button);
4605       }
4606     }
4607
4608         //Button to copy an array element and add it as last element
4609         if(self.show_copy_button){
4610         self.rows[i].copy_button = this.getButton(self.getItemTitle(),'copy','Copy '+self.getItemTitle());
4611         self.rows[i].copy_button.className += ' copy';
4612         self.rows[i].copy_button.setAttribute('data-i',i);
4613         self.rows[i].copy_button.addEventListener('click',function(e) {
4614             var value = self.getValue();
4615             e.preventDefault();
4616             e.stopPropagation();
4617             var i = this.getAttribute('data-i')*1;
4618
4619             $each(value,function(j,row) {
4620               if(j===i) {
4621                 value.push(row);
4622                 return;
4623               }
4624             });
4625
4626             self.setValue(value);
4627             self.refreshValue(true);
4628             self.onChange(true);
4629
4630         });
4631
4632         controls_holder.appendChild(self.rows[i].copy_button);
4633     }
4634
4635
4636     if(i && !self.hide_move_buttons) {
4637       self.rows[i].moveup_button = this.getButton('','moveup',this.translate('button_move_up_title'));
4638       self.rows[i].moveup_button.className += ' moveup';
4639       self.rows[i].moveup_button.setAttribute('data-i',i);
4640       self.rows[i].moveup_button.addEventListener('click',function(e) {
4641         e.preventDefault();
4642         e.stopPropagation();
4643         var i = this.getAttribute('data-i')*1;
4644
4645         if(i<=0) return;
4646         var rows = self.getValue();
4647         var tmp = rows[i-1];
4648         rows[i-1] = rows[i];
4649         rows[i] = tmp;
4650
4651         self.setValue(rows);
4652         self.active_tab = self.rows[i-1].tab;
4653         self.refreshTabs();
4654
4655         self.onChange(true);
4656       });
4657
4658       if(controls_holder) {
4659         controls_holder.appendChild(self.rows[i].moveup_button);
4660       }
4661     }
4662
4663     if(!self.hide_move_buttons) {
4664       self.rows[i].movedown_button = this.getButton('','movedown',this.translate('button_move_down_title'));
4665       self.rows[i].movedown_button.className += ' movedown';
4666       self.rows[i].movedown_button.setAttribute('data-i',i);
4667       self.rows[i].movedown_button.addEventListener('click',function(e) {
4668         e.preventDefault();
4669         e.stopPropagation();
4670         var i = this.getAttribute('data-i')*1;
4671
4672         var rows = self.getValue();
4673         if(i>=rows.length-1) return;
4674         var tmp = rows[i+1];
4675         rows[i+1] = rows[i];
4676         rows[i] = tmp;
4677
4678         self.setValue(rows);
4679         self.active_tab = self.rows[i+1].tab;
4680         self.refreshTabs();
4681         self.onChange(true);
4682       });
4683
4684       if(controls_holder) {
4685         controls_holder.appendChild(self.rows[i].movedown_button);
4686       }
4687     }
4688
4689     if(value) self.rows[i].setValue(value, initial);
4690     self.refreshTabs();
4691   },
4692   addControls: function() {
4693     var self = this;
4694
4695     this.collapsed = false;
4696     this.toggle_button = this.getButton('','collapse',this.translate('button_collapse'));
4697     this.title_controls.appendChild(this.toggle_button);
4698     var row_holder_display = self.row_holder.style.display;
4699     var controls_display = self.controls.style.display;
4700     this.toggle_button.addEventListener('click',function(e) {
4701       e.preventDefault();
4702       e.stopPropagation();
4703       if(self.collapsed) {
4704         self.collapsed = false;
4705         if(self.panel) self.panel.style.display = '';
4706         self.row_holder.style.display = row_holder_display;
4707         if(self.tabs_holder) self.tabs_holder.style.display = '';
4708         self.controls.style.display = controls_display;
4709         self.setButtonText(this,'','collapse',self.translate('button_collapse'));
4710       }
4711       else {
4712         self.collapsed = true;
4713         self.row_holder.style.display = 'none';
4714         if(self.tabs_holder) self.tabs_holder.style.display = 'none';
4715         self.controls.style.display = 'none';
4716         if(self.panel) self.panel.style.display = 'none';
4717         self.setButtonText(this,'','expand',self.translate('button_expand'));
4718       }
4719     });
4720
4721     // If it should start collapsed
4722     if(this.options.collapsed) {
4723       $trigger(this.toggle_button,'click');
4724     }
4725
4726     // Collapse button disabled
4727     if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
4728       if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
4729     }
4730     else if(this.jsoneditor.options.disable_collapse) {
4731       this.toggle_button.style.display = 'none';
4732     }
4733
4734     // Add "new row" and "delete last" buttons below editor
4735     this.add_row_button = this.getButton(this.getItemTitle(),'add',this.translate('button_add_row_title',[this.getItemTitle()]));
4736
4737     this.add_row_button.addEventListener('click',function(e) {
4738       e.preventDefault();
4739       e.stopPropagation();
4740       var i = self.rows.length;
4741       if(self.row_cache[i]) {
4742         self.rows[i] = self.row_cache[i];
4743         self.rows[i].setValue(self.rows[i].getDefault(), true);
4744         self.rows[i].container.style.display = '';
4745         if(self.rows[i].tab) self.rows[i].tab.style.display = '';
4746         self.rows[i].register();
4747       }
4748       else {
4749         self.addRow();
4750       }
4751       self.active_tab = self.rows[i].tab;
4752       self.refreshTabs();
4753       self.refreshValue();
4754       self.onChange(true);
4755     });
4756     self.controls.appendChild(this.add_row_button);
4757
4758     this.delete_last_row_button = this.getButton(this.translate('button_delete_last',[this.getItemTitle()]),'delete',this.translate('button_delete_last_title',[this.getItemTitle()]));
4759     this.delete_last_row_button.addEventListener('click',function(e) {
4760       e.preventDefault();
4761       e.stopPropagation();
4762
4763       if (self.jsoneditor.options.prompt_before_delete === true) {
4764         if (confirm("Confirm to remove.") === false) {
4765           return false;
4766         }
4767       }
4768
4769       var rows = self.getValue();
4770
4771       var new_active_tab = null;
4772       if(self.rows.length > 1 && self.rows[self.rows.length-1].tab === self.active_tab) new_active_tab = self.rows[self.rows.length-2].tab;
4773
4774       rows.pop();
4775       self.setValue(rows);
4776       if(new_active_tab) {
4777         self.active_tab = new_active_tab;
4778         self.refreshTabs();
4779       }
4780       self.onChange(true);
4781     });
4782     self.controls.appendChild(this.delete_last_row_button);
4783
4784     this.remove_all_rows_button = this.getButton(this.translate('button_delete_all'),'delete',this.translate('button_delete_all_title'));
4785     this.remove_all_rows_button.addEventListener('click',function(e) {
4786       e.preventDefault();
4787       e.stopPropagation();
4788
4789       if (self.jsoneditor.options.prompt_before_delete === true) {
4790         if (confirm("Confirm to remove.") === false) {
4791           return false;
4792         }
4793       }
4794
4795       self.setValue([]);
4796       self.onChange(true);
4797     });
4798     self.controls.appendChild(this.remove_all_rows_button);
4799
4800     if(self.tabs) {
4801       this.add_row_button.style.width = '100%';
4802       this.add_row_button.style.textAlign = 'left';
4803       this.add_row_button.style.marginBottom = '3px';
4804
4805       this.delete_last_row_button.style.width = '100%';
4806       this.delete_last_row_button.style.textAlign = 'left';
4807       this.delete_last_row_button.style.marginBottom = '3px';
4808
4809       this.remove_all_rows_button.style.width = '100%';
4810       this.remove_all_rows_button.style.textAlign = 'left';
4811       this.remove_all_rows_button.style.marginBottom = '3px';
4812     }
4813   },
4814   showValidationErrors: function(errors) {
4815     var self = this;
4816
4817     // Get all the errors that pertain to this editor
4818     var my_errors = [];
4819     var other_errors = [];
4820     $each(errors, function(i,error) {
4821       if(error.path === self.path) {
4822         my_errors.push(error);
4823       }
4824       else {
4825         other_errors.push(error);
4826       }
4827     });
4828
4829     // Show errors for this editor
4830     if(this.error_holder) {
4831       if(my_errors.length) {
4832         var message = [];
4833         this.error_holder.innerHTML = '';
4834         this.error_holder.style.display = '';
4835         $each(my_errors, function(i,error) {
4836           self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
4837         });
4838       }
4839       // Hide error area
4840       else {
4841         this.error_holder.style.display = 'none';
4842       }
4843     }
4844
4845     // Show errors for child editors
4846     $each(this.rows, function(i,row) {
4847       row.showValidationErrors(other_errors);
4848     });
4849   }
4850 });
4851
4852 JSONEditor.defaults.editors.table = JSONEditor.defaults.editors.array.extend({
4853   register: function() {
4854     this._super();
4855     if(this.rows) {
4856       for(var i=0; i<this.rows.length; i++) {
4857         this.rows[i].register();
4858       }
4859     }
4860   },
4861   unregister: function() {
4862     this._super();
4863     if(this.rows) {
4864       for(var i=0; i<this.rows.length; i++) {
4865         this.rows[i].unregister();
4866       }
4867     }
4868   },
4869   getNumColumns: function() {
4870     return Math.max(Math.min(12,this.width),3);
4871   },
4872   preBuild: function() {
4873     var item_schema = this.jsoneditor.expandRefs(this.schema.items || {});
4874
4875     this.item_title = item_schema.title || 'row';
4876     this.item_default = item_schema["default"] || null;
4877     this.item_has_child_editors = item_schema.properties || item_schema.items;
4878     this.width = 12;
4879     this._super();
4880   },
4881   build: function() {
4882     var self = this;
4883     this.table = this.theme.getTable();
4884     this.container.appendChild(this.table);
4885     this.thead = this.theme.getTableHead();
4886     this.table.appendChild(this.thead);
4887     this.header_row = this.theme.getTableRow();
4888     this.thead.appendChild(this.header_row);
4889     this.row_holder = this.theme.getTableBody();
4890     this.table.appendChild(this.row_holder);
4891
4892     // Determine the default value of array element
4893     var tmp = this.getElementEditor(0,true);
4894     this.item_default = tmp.getDefault();
4895     this.width = tmp.getNumColumns() + 2;
4896
4897     if(!this.options.compact) {
4898       this.title = this.theme.getHeader(this.getTitle());
4899       this.container.appendChild(this.title);
4900       this.title_controls = this.theme.getHeaderButtonHolder();
4901       this.title.appendChild(this.title_controls);
4902       if(this.schema.description) {
4903         this.description = this.theme.getDescription(this.schema.description);
4904         this.container.appendChild(this.description);
4905       }
4906       this.panel = this.theme.getIndentedPanel();
4907       this.container.appendChild(this.panel);
4908       this.error_holder = document.createElement('div');
4909       this.panel.appendChild(this.error_holder);
4910     }
4911     else {
4912       this.panel = document.createElement('div');
4913       this.container.appendChild(this.panel);
4914     }
4915
4916     this.panel.appendChild(this.table);
4917     this.controls = this.theme.getButtonHolder();
4918     this.panel.appendChild(this.controls);
4919
4920     if(this.item_has_child_editors) {
4921       var ce = tmp.getChildEditors();
4922       var order = tmp.property_order || Object.keys(ce);
4923       for(var i=0; i<order.length; i++) {
4924         var th = self.theme.getTableHeaderCell(ce[order[i]].getTitle());
4925         if(ce[order[i]].options.hidden) th.style.display = 'none';
4926         self.header_row.appendChild(th);
4927       }
4928     }
4929     else {
4930       self.header_row.appendChild(self.theme.getTableHeaderCell(this.item_title));
4931     }
4932
4933     tmp.destroy();
4934     this.row_holder.innerHTML = '';
4935
4936     // Row Controls column
4937     this.controls_header_cell = self.theme.getTableHeaderCell(" ");
4938     self.header_row.appendChild(this.controls_header_cell);
4939
4940     // Add controls
4941     this.addControls();
4942   },
4943   onChildEditorChange: function(editor) {
4944     this.refreshValue();
4945     this._super();
4946   },
4947   getItemDefault: function() {
4948     return $extend({},{"default":this.item_default})["default"];
4949   },
4950   getItemTitle: function() {
4951     return this.item_title;
4952   },
4953   getElementEditor: function(i,ignore) {
4954     var schema_copy = $extend({},this.schema.items);
4955     var editor = this.jsoneditor.getEditorClass(schema_copy, this.jsoneditor);
4956     var row = this.row_holder.appendChild(this.theme.getTableRow());
4957     var holder = row;
4958     if(!this.item_has_child_editors) {
4959       holder = this.theme.getTableCell();
4960       row.appendChild(holder);
4961     }
4962
4963     var ret = this.jsoneditor.createEditor(editor,{
4964       jsoneditor: this.jsoneditor,
4965       schema: schema_copy,
4966       container: holder,
4967       path: this.path+'.'+i,
4968       parent: this,
4969       compact: true,
4970       table_row: true
4971     });
4972
4973     ret.preBuild();
4974     if(!ignore) {
4975       ret.build();
4976       ret.postBuild();
4977
4978       ret.controls_cell = row.appendChild(this.theme.getTableCell());
4979       ret.row = row;
4980       ret.table_controls = this.theme.getButtonHolder();
4981       ret.controls_cell.appendChild(ret.table_controls);
4982       ret.table_controls.style.margin = 0;
4983       ret.table_controls.style.padding = 0;
4984     }
4985
4986     return ret;
4987   },
4988   destroy: function() {
4989     this.innerHTML = '';
4990     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
4991     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
4992     if(this.row_holder && this.row_holder.parentNode) this.row_holder.parentNode.removeChild(this.row_holder);
4993     if(this.table && this.table.parentNode) this.table.parentNode.removeChild(this.table);
4994     if(this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
4995
4996     this.rows = this.title = this.description = this.row_holder = this.table = this.panel = null;
4997
4998     this._super();
4999   },
5000   setValue: function(value, initial) {
5001     // Update the array's value, adding/removing rows when necessary
5002     value = value || [];
5003
5004     // Make sure value has between minItems and maxItems items in it
5005     if(this.schema.minItems) {
5006       while(value.length < this.schema.minItems) {
5007         value.push(this.getItemDefault());
5008       }
5009     }
5010     if(this.schema.maxItems && value.length > this.schema.maxItems) {
5011       value = value.slice(0,this.schema.maxItems);
5012     }
5013
5014     var serialized = JSON.stringify(value);
5015     if(serialized === this.serialized) return;
5016
5017     var numrows_changed = false;
5018
5019     var self = this;
5020     $each(value,function(i,val) {
5021       if(self.rows[i]) {
5022         // TODO: don't set the row's value if it hasn't changed
5023         self.rows[i].setValue(val);
5024       }
5025       else {
5026         self.addRow(val);
5027         numrows_changed = true;
5028       }
5029     });
5030
5031     for(var j=value.length; j<self.rows.length; j++) {
5032       var holder = self.rows[j].container;
5033       if(!self.item_has_child_editors) {
5034         self.rows[j].row.parentNode.removeChild(self.rows[j].row);
5035       }
5036       self.rows[j].destroy();
5037       if(holder.parentNode) holder.parentNode.removeChild(holder);
5038       self.rows[j] = null;
5039       numrows_changed = true;
5040     }
5041     self.rows = self.rows.slice(0,value.length);
5042
5043     self.refreshValue();
5044     if(numrows_changed || initial) self.refreshRowButtons();
5045
5046     self.onChange();
5047
5048     // TODO: sortable
5049   },
5050   refreshRowButtons: function() {
5051     var self = this;
5052
5053     // If we currently have minItems items in the array
5054     var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
5055
5056     var need_row_buttons = false;
5057     $each(this.rows,function(i,editor) {
5058       // Hide the move down button for the last row
5059       if(editor.movedown_button) {
5060         if(i === self.rows.length - 1) {
5061           editor.movedown_button.style.display = 'none';
5062         }
5063         else {
5064           need_row_buttons = true;
5065           editor.movedown_button.style.display = '';
5066         }
5067       }
5068
5069       // Hide the delete button if we have minItems items
5070       if(editor.delete_button) {
5071         if(minItems) {
5072           editor.delete_button.style.display = 'none';
5073         }
5074         else {
5075           need_row_buttons = true;
5076           editor.delete_button.style.display = '';
5077         }
5078       }
5079
5080       if(editor.moveup_button) {
5081         need_row_buttons = true;
5082       }
5083     });
5084
5085     // Show/hide controls column in table
5086     $each(this.rows,function(i,editor) {
5087       if(need_row_buttons) {
5088         editor.controls_cell.style.display = '';
5089       }
5090       else {
5091         editor.controls_cell.style.display = 'none';
5092       }
5093     });
5094     if(need_row_buttons) {
5095       this.controls_header_cell.style.display = '';
5096     }
5097     else {
5098       this.controls_header_cell.style.display = 'none';
5099     }
5100
5101     var controls_needed = false;
5102
5103     if(!this.value.length) {
5104       this.delete_last_row_button.style.display = 'none';
5105       this.remove_all_rows_button.style.display = 'none';
5106       this.table.style.display = 'none';
5107     }
5108     else if(this.value.length === 1) {
5109       this.table.style.display = '';
5110       this.remove_all_rows_button.style.display = 'none';
5111
5112       // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
5113       if(minItems || this.hide_delete_last_row_buttons) {
5114         this.delete_last_row_button.style.display = 'none';
5115       }
5116       else {
5117         this.delete_last_row_button.style.display = '';
5118         controls_needed = true;
5119       }
5120     }
5121     else {
5122       this.table.style.display = '';
5123
5124       if(minItems || this.hide_delete_last_row_buttons) {
5125         this.delete_last_row_button.style.display = 'none';
5126       }
5127       else {
5128         this.delete_last_row_button.style.display = '';
5129         controls_needed = true;
5130       }
5131
5132       if(minItems || this.hide_delete_all_rows_buttons) {
5133         this.remove_all_rows_button.style.display = 'none';
5134       }
5135       else {
5136         this.remove_all_rows_button.style.display = '';
5137         controls_needed = true;
5138       }
5139     }
5140
5141     // If there are maxItems in the array, hide the add button beneath the rows
5142     if((this.schema.maxItems && this.schema.maxItems <= this.rows.length) || this.hide_add_button) {
5143       this.add_row_button.style.display = 'none';
5144     }
5145     else {
5146       this.add_row_button.style.display = '';
5147       controls_needed = true;
5148     }
5149
5150     if(!controls_needed) {
5151       this.controls.style.display = 'none';
5152     }
5153     else {
5154       this.controls.style.display = '';
5155     }
5156   },
5157   refreshValue: function() {
5158     var self = this;
5159     this.value = [];
5160
5161     $each(this.rows,function(i,editor) {
5162       // Get the value for this editor
5163       self.value[i] = editor.getValue();
5164     });
5165     this.serialized = JSON.stringify(this.value);
5166   },
5167   addRow: function(value) {
5168     var self = this;
5169     var i = this.rows.length;
5170
5171     self.rows[i] = this.getElementEditor(i);
5172
5173     var controls_holder = self.rows[i].table_controls;
5174
5175     // Buttons to delete row, move row up, and move row down
5176     if(!this.hide_delete_buttons) {
5177       self.rows[i].delete_button = this.getButton('','delete',this.translate('button_delete_row_title_short'));
5178       self.rows[i].delete_button.className += ' delete';
5179       self.rows[i].delete_button.setAttribute('data-i',i);
5180       self.rows[i].delete_button.addEventListener('click',function(e) {
5181         e.preventDefault();
5182         e.stopPropagation();
5183         var i = this.getAttribute('data-i')*1;
5184
5185         var value = self.getValue();
5186
5187         var newval = [];
5188         $each(value,function(j,row) {
5189           if(j===i) return; // If this is the one we're deleting
5190           newval.push(row);
5191         });
5192         self.setValue(newval);
5193         self.onChange(true);
5194       });
5195       controls_holder.appendChild(self.rows[i].delete_button);
5196     }
5197
5198
5199     if(i && !this.hide_move_buttons) {
5200       self.rows[i].moveup_button = this.getButton('','moveup',this.translate('button_move_up_title'));
5201       self.rows[i].moveup_button.className += ' moveup';
5202       self.rows[i].moveup_button.setAttribute('data-i',i);
5203       self.rows[i].moveup_button.addEventListener('click',function(e) {
5204         e.preventDefault();
5205         e.stopPropagation();
5206         var i = this.getAttribute('data-i')*1;
5207
5208         if(i<=0) return;
5209         var rows = self.getValue();
5210         var tmp = rows[i-1];
5211         rows[i-1] = rows[i];
5212         rows[i] = tmp;
5213
5214         self.setValue(rows);
5215         self.onChange(true);
5216       });
5217       controls_holder.appendChild(self.rows[i].moveup_button);
5218     }
5219
5220     if(!this.hide_move_buttons) {
5221       self.rows[i].movedown_button = this.getButton('','movedown',this.translate('button_move_down_title'));
5222       self.rows[i].movedown_button.className += ' movedown';
5223       self.rows[i].movedown_button.setAttribute('data-i',i);
5224       self.rows[i].movedown_button.addEventListener('click',function(e) {
5225         e.preventDefault();
5226         e.stopPropagation();
5227         var i = this.getAttribute('data-i')*1;
5228         var rows = self.getValue();
5229         if(i>=rows.length-1) return;
5230         var tmp = rows[i+1];
5231         rows[i+1] = rows[i];
5232         rows[i] = tmp;
5233
5234         self.setValue(rows);
5235         self.onChange(true);
5236       });
5237       controls_holder.appendChild(self.rows[i].movedown_button);
5238     }
5239
5240     if(value) self.rows[i].setValue(value);
5241   },
5242   addControls: function() {
5243     var self = this;
5244
5245     this.collapsed = false;
5246     this.toggle_button = this.getButton('','collapse',this.translate('button_collapse'));
5247     if(this.title_controls) {
5248       this.title_controls.appendChild(this.toggle_button);
5249       this.toggle_button.addEventListener('click',function(e) {
5250         e.preventDefault();
5251         e.stopPropagation();
5252
5253         if(self.collapsed) {
5254           self.collapsed = false;
5255           self.panel.style.display = '';
5256           self.setButtonText(this,'','collapse',self.translate('button_collapse'));
5257         }
5258         else {
5259           self.collapsed = true;
5260           self.panel.style.display = 'none';
5261           self.setButtonText(this,'','expand',self.translate('button_expand'));
5262         }
5263       });
5264
5265       // If it should start collapsed
5266       if(this.options.collapsed) {
5267         $trigger(this.toggle_button,'click');
5268       }
5269
5270       // Collapse button disabled
5271       if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
5272         if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
5273       }
5274       else if(this.jsoneditor.options.disable_collapse) {
5275         this.toggle_button.style.display = 'none';
5276       }
5277     }
5278
5279     // Add "new row" and "delete last" buttons below editor
5280     this.add_row_button = this.getButton(this.getItemTitle(),'add',this.translate('button_add_row_title',[this.getItemTitle()]));
5281     this.add_row_button.addEventListener('click',function(e) {
5282       e.preventDefault();
5283       e.stopPropagation();
5284
5285       self.addRow();
5286       self.refreshValue();
5287       self.refreshRowButtons();
5288       self.onChange(true);
5289     });
5290     self.controls.appendChild(this.add_row_button);
5291
5292     this.delete_last_row_button = this.getButton(this.translate('button_delete_last',[this.getItemTitle()]),'delete',this.translate('button_delete_last_title',[this.getItemTitle()]));
5293     this.delete_last_row_button.addEventListener('click',function(e) {
5294       e.preventDefault();
5295       e.stopPropagation();
5296
5297       var rows = self.getValue();
5298       rows.pop();
5299       self.setValue(rows);
5300       self.onChange(true);
5301     });
5302     self.controls.appendChild(this.delete_last_row_button);
5303
5304     this.remove_all_rows_button = this.getButton(this.translate('button_delete_all'),'delete',this.translate('button_delete_all_title'));
5305     this.remove_all_rows_button.addEventListener('click',function(e) {
5306       e.preventDefault();
5307       e.stopPropagation();
5308
5309       self.setValue([]);
5310       self.onChange(true);
5311     });
5312     self.controls.appendChild(this.remove_all_rows_button);
5313   }
5314 });
5315
5316 // Multiple Editor (for when `type` is an array, also when `oneOf` is present)
5317 JSONEditor.defaults.editors.multiple = JSONEditor.AbstractEditor.extend({
5318   register: function() {
5319     if(this.editors) {
5320       for(var i=0; i<this.editors.length; i++) {
5321         if(!this.editors[i]) continue;
5322         this.editors[i].unregister();
5323       }
5324       if(this.editors[this.type]) this.editors[this.type].register();
5325     }
5326     this._super();
5327   },
5328   unregister: function() {
5329     this._super();
5330     if(this.editors) {
5331       for(var i=0; i<this.editors.length; i++) {
5332         if(!this.editors[i]) continue;
5333         this.editors[i].unregister();
5334       }
5335     }
5336   },
5337   getNumColumns: function() {
5338     if(!this.editors[this.type]) return 4;
5339     return Math.max(this.editors[this.type].getNumColumns(),4);
5340   },
5341   enable: function() {
5342     if(!this.always_disabled) {
5343       if(this.editors) {
5344         for(var i=0; i<this.editors.length; i++) {
5345           if(!this.editors[i]) continue;
5346           this.editors[i].enable();
5347         }
5348       }
5349       this.switcher.disabled = false;
5350       this._super();
5351     }
5352   },
5353   disable: function(always_disabled) {
5354     if(always_disabled) this.always_disabled = true;
5355     if(this.editors) {
5356       for(var i=0; i<this.editors.length; i++) {
5357         if(!this.editors[i]) continue;
5358         this.editors[i].disable(always_disabled);
5359       }
5360     }
5361     this.switcher.disabled = true;
5362     this._super();
5363   },
5364   switchEditor: function(i) {
5365     var self = this;
5366
5367     if(!this.editors[i]) {
5368       this.buildChildEditor(i);
5369     }
5370     
5371     var current_value = self.getValue();
5372
5373     self.type = i;
5374
5375     self.register();
5376
5377     $each(self.editors,function(type,editor) {
5378       if(!editor) return;
5379       if(self.type === type) {
5380         if(self.keep_values) editor.setValue(current_value,true);
5381         editor.container.style.display = '';
5382       }
5383       else editor.container.style.display = 'none';
5384     });
5385     self.refreshValue();
5386     self.refreshHeaderText();
5387   },
5388   buildChildEditor: function(i) {
5389     var self = this;
5390     var type = this.types[i];
5391     var holder = self.theme.getChildEditorHolder();
5392     self.editor_holder.appendChild(holder);
5393
5394     var schema;
5395
5396     if(typeof type === "string") {
5397       schema = $extend({},self.schema);
5398       schema.type = type;
5399     }
5400     else {
5401       schema = $extend({},self.schema,type);
5402       schema = self.jsoneditor.expandRefs(schema);
5403
5404       // If we need to merge `required` arrays
5405       if(type && type.required && Array.isArray(type.required) && self.schema.required && Array.isArray(self.schema.required)) {
5406         schema.required = self.schema.required.concat(type.required);
5407       }
5408     }
5409
5410     var editor = self.jsoneditor.getEditorClass(schema);
5411
5412     self.editors[i] = self.jsoneditor.createEditor(editor,{
5413       jsoneditor: self.jsoneditor,
5414       schema: schema,
5415       container: holder,
5416       path: self.path,
5417       parent: self,
5418       required: true
5419     });
5420     self.editors[i].preBuild();
5421     self.editors[i].build();
5422     self.editors[i].postBuild();
5423
5424     if(self.editors[i].header) self.editors[i].header.style.display = 'none';
5425
5426     self.editors[i].option = self.switcher_options[i];
5427
5428     holder.addEventListener('change_header_text',function() {
5429       self.refreshHeaderText();
5430     });
5431
5432     if(i !== self.type) holder.style.display = 'none';
5433   },
5434   preBuild: function() {
5435     var self = this;
5436
5437     this.types = [];
5438     this.type = 0;
5439     this.editors = [];
5440     this.validators = [];
5441
5442     this.keep_values = true;
5443     if(typeof this.jsoneditor.options.keep_oneof_values !== "undefined") this.keep_values = this.jsoneditor.options.keep_oneof_values;
5444     if(typeof this.options.keep_oneof_values !== "undefined") this.keep_values = this.options.keep_oneof_values;
5445
5446     if(this.schema.oneOf) {
5447       this.oneOf = true;
5448       this.types = this.schema.oneOf;
5449       delete this.schema.oneOf;
5450     }
5451     else if(this.schema.anyOf) {
5452       this.anyOf = true;
5453       this.types = this.schema.anyOf;
5454       delete this.schema.anyOf;
5455     }
5456     else {
5457       if(!this.schema.type || this.schema.type === "any") {
5458         this.types = ['string','number','integer','boolean','object','array','null'];
5459
5460         // If any of these primitive types are disallowed
5461         if(this.schema.disallow) {
5462           var disallow = this.schema.disallow;
5463           if(typeof disallow !== 'object' || !(Array.isArray(disallow))) {
5464             disallow = [disallow];
5465           }
5466           var allowed_types = [];
5467           $each(this.types,function(i,type) {
5468             if(disallow.indexOf(type) === -1) allowed_types.push(type);
5469           });
5470           this.types = allowed_types;
5471         }
5472       }
5473       else if(Array.isArray(this.schema.type)) {
5474         this.types = this.schema.type;
5475       }
5476       else {
5477         this.types = [this.schema.type];
5478       }
5479       delete this.schema.type;
5480     }
5481
5482     this.display_text = this.getDisplayText(this.types);
5483   },
5484   build: function() {
5485     var self = this;
5486     var container = this.container;
5487
5488     this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5489     this.container.appendChild(this.header);
5490
5491     this.switcher = this.theme.getSwitcher(this.display_text);
5492     container.appendChild(this.switcher);
5493     this.switcher.addEventListener('change',function(e) {
5494       e.preventDefault();
5495       e.stopPropagation();
5496
5497       self.switchEditor(self.display_text.indexOf(this.value));
5498       self.onChange(true);
5499     });
5500
5501     this.editor_holder = document.createElement('div');
5502     container.appendChild(this.editor_holder);
5503     
5504       
5505     var validator_options = {};
5506     if(self.jsoneditor.options.custom_validators) {
5507       validator_options.custom_validators = self.jsoneditor.options.custom_validators;
5508     }
5509
5510     this.switcher_options = this.theme.getSwitcherOptions(this.switcher);
5511     $each(this.types,function(i,type) {
5512       self.editors[i] = false;
5513
5514       var schema;
5515
5516       if(typeof type === "string") {
5517         schema = $extend({},self.schema);
5518         schema.type = type;
5519       }
5520       else {
5521         schema = $extend({},self.schema,type);
5522
5523         // If we need to merge `required` arrays
5524         if(type.required && Array.isArray(type.required) && self.schema.required && Array.isArray(self.schema.required)) {
5525           schema.required = self.schema.required.concat(type.required);
5526         }
5527       }
5528
5529       self.validators[i] = new JSONEditor.Validator(self.jsoneditor,schema,validator_options);
5530     });
5531
5532     this.switchEditor(0);
5533   },
5534   onChildEditorChange: function(editor) {
5535     if(this.editors[this.type]) {
5536       this.refreshValue();
5537       this.refreshHeaderText();
5538     }
5539
5540     this._super();
5541   },
5542   refreshHeaderText: function() {
5543     var display_text = this.getDisplayText(this.types);
5544     $each(this.switcher_options, function(i,option) {
5545       option.textContent = display_text[i];
5546     });
5547   },
5548   refreshValue: function() {
5549     this.value = this.editors[this.type].getValue();
5550   },
5551   setValue: function(val,initial) {
5552     // Determine type by getting the first one that validates
5553     var self = this;
5554     var prev_type = this.type;
5555     $each(this.validators, function(i,validator) {
5556       if(!validator.validate(val).length) {
5557         self.type = i;
5558         self.switcher.value = self.display_text[i];
5559         return false;
5560       }
5561     });
5562
5563     var type_changed = this.type != prev_type;
5564     if (type_changed) {
5565         this.switchEditor(this.type);
5566     }
5567
5568     this.editors[this.type].setValue(val,initial);
5569
5570     this.refreshValue();
5571     self.onChange(type_changed);
5572   },
5573   destroy: function() {
5574     $each(this.editors, function(type,editor) {
5575       if(editor) editor.destroy();
5576     });
5577     if(this.editor_holder && this.editor_holder.parentNode) this.editor_holder.parentNode.removeChild(this.editor_holder);
5578     if(this.switcher && this.switcher.parentNode) this.switcher.parentNode.removeChild(this.switcher);
5579     this._super();
5580   },
5581   showValidationErrors: function(errors) {
5582     var self = this;
5583
5584     // oneOf and anyOf error paths need to remove the oneOf[i] part before passing to child editors
5585     if(this.oneOf || this.anyOf) {
5586       var check_part = this.oneOf? 'oneOf' : 'anyOf';
5587       $each(this.editors,function(i,editor) {
5588         if(!editor) return;
5589         var check = self.path+'.'+check_part+'['+i+']';
5590         var new_errors = [];
5591         $each(errors, function(j,error) {
5592           if(error.path.substr(0,check.length)===check) {
5593             var new_error = $extend({},error);
5594             new_error.path = self.path+new_error.path.substr(check.length);
5595             new_errors.push(new_error);
5596           }
5597         });
5598
5599         editor.showValidationErrors(new_errors);
5600       });
5601     }
5602     else {
5603       $each(this.editors,function(type,editor) {
5604         if(!editor) return;
5605         editor.showValidationErrors(errors);
5606       });
5607     }
5608   }
5609 });
5610
5611 // Enum Editor (used for objects and arrays with enumerated values)
5612 JSONEditor.defaults.editors["enum"] = JSONEditor.AbstractEditor.extend({
5613   getNumColumns: function() {
5614     return 4;
5615   },
5616   build: function() {
5617     var container = this.container;
5618     this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5619     this.container.appendChild(this.title);
5620
5621     this.options.enum_titles = this.options.enum_titles || [];
5622
5623     this["enum"] = this.schema["enum"];
5624     this.selected = 0;
5625     this.select_options = [];
5626     this.html_values = [];
5627
5628     var self = this;
5629     for(var i=0; i<this["enum"].length; i++) {
5630       this.select_options[i] = this.options.enum_titles[i] || "Value "+(i+1);
5631       this.html_values[i] = this.getHTML(this["enum"][i]);
5632     }
5633
5634     // Switcher
5635     this.switcher = this.theme.getSwitcher(this.select_options);
5636     this.container.appendChild(this.switcher);
5637
5638     // Display area
5639     this.display_area = this.theme.getIndentedPanel();
5640     this.container.appendChild(this.display_area);
5641
5642     if(this.options.hide_display) this.display_area.style.display = "none";
5643
5644     this.switcher.addEventListener('change',function() {
5645       self.selected = self.select_options.indexOf(this.value);
5646       self.value = self["enum"][self.selected];
5647       self.refreshValue();
5648       self.onChange(true);
5649     });
5650     this.value = this["enum"][0];
5651     this.refreshValue();
5652
5653     if(this["enum"].length === 1) this.switcher.style.display = 'none';
5654   },
5655   refreshValue: function() {
5656     var self = this;
5657     self.selected = -1;
5658     var stringified = JSON.stringify(this.value);
5659     $each(this["enum"], function(i, el) {
5660       if(stringified === JSON.stringify(el)) {
5661         self.selected = i;
5662         return false;
5663       }
5664     });
5665
5666     if(self.selected<0) {
5667       self.setValue(self["enum"][0]);
5668       return;
5669     }
5670
5671     this.switcher.value = this.select_options[this.selected];
5672     this.display_area.innerHTML = this.html_values[this.selected];
5673   },
5674   enable: function() {
5675     if(!this.always_disabled) {
5676       this.switcher.disabled = false;
5677       this._super();
5678     }
5679   },
5680   disable: function(always_disabled) {
5681     if(always_disabled) this.always_disabled = true;
5682     this.switcher.disabled = true;
5683     this._super();
5684   },
5685   getHTML: function(el) {
5686     var self = this;
5687
5688     if(el === null) {
5689       return '<em>null</em>';
5690     }
5691     // Array or Object
5692     else if(typeof el === "object") {
5693       // TODO: use theme
5694       var ret = '';
5695
5696       $each(el,function(i,child) {
5697         var html = self.getHTML(child);
5698
5699         // Add the keys to object children
5700         if(!(Array.isArray(el))) {
5701           // TODO: use theme
5702           html = '<div><em>'+i+'</em>: '+html+'</div>';
5703         }
5704
5705         // TODO: use theme
5706         ret += '<li>'+html+'</li>';
5707       });
5708
5709       if(Array.isArray(el)) ret = '<ol>'+ret+'</ol>';
5710       else ret = "<ul style='margin-top:0;margin-bottom:0;padding-top:0;padding-bottom:0;'>"+ret+'</ul>';
5711
5712       return ret;
5713     }
5714     // Boolean
5715     else if(typeof el === "boolean") {
5716       return el? 'true' : 'false';
5717     }
5718     // String
5719     else if(typeof el === "string") {
5720       return el.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
5721     }
5722     // Number
5723     else {
5724       return el;
5725     }
5726   },
5727   setValue: function(val) {
5728     if(this.value !== val) {
5729       this.value = val;
5730       this.refreshValue();
5731       this.onChange();
5732     }
5733   },
5734   destroy: function() {
5735     if(this.display_area && this.display_area.parentNode) this.display_area.parentNode.removeChild(this.display_area);
5736     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
5737     if(this.switcher && this.switcher.parentNode) this.switcher.parentNode.removeChild(this.switcher);
5738
5739     this._super();
5740   }
5741 });
5742
5743 JSONEditor.defaults.editors.select = JSONEditor.AbstractEditor.extend({
5744   setValue: function(value,initial) {
5745     value = this.typecast(value||'');
5746
5747     // Sanitize value before setting it
5748     var sanitized = value;
5749     if(this.enum_values.indexOf(sanitized) < 0) {
5750       sanitized = this.enum_values[0];
5751     }
5752
5753     if(this.value === sanitized) {
5754       return;
5755     }
5756
5757     this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
5758     if(this.select2) {
5759       if(this.select2v4)
5760         this.select2.val(this.input.value).trigger("change"); 
5761       else
5762         this.select2.select2('val',this.input.value);
5763     }
5764     this.value = sanitized;
5765     this.onChange();
5766     this.change();
5767   },
5768   register: function() {
5769     this._super();
5770     if(!this.input) return;
5771     this.input.setAttribute('name',this.formname);
5772   },
5773   unregister: function() {
5774     this._super();
5775     if(!this.input) return;
5776     this.input.removeAttribute('name');
5777   },
5778   getNumColumns: function() {
5779     if(!this.enum_options) return 3;
5780     var longest_text = this.getTitle().length;
5781     for(var i=0; i<this.enum_options.length; i++) {
5782       longest_text = Math.max(longest_text,this.enum_options[i].length+4);
5783     }
5784     return Math.min(12,Math.max(longest_text/7,2));
5785   },
5786   typecast: function(value) {
5787     if(this.schema.type === "boolean") {
5788       return !!value;
5789     }
5790     else if(this.schema.type === "number") {
5791       return 1*value;
5792     }
5793     else if(this.schema.type === "integer") {
5794       return Math.floor(value*1);
5795     }
5796     else {
5797       return ""+value;
5798     }
5799   },
5800   getValue: function() {
5801     if (!this.dependenciesFulfilled) {
5802       return undefined;
5803     }
5804     return this.typecast(this.value);
5805   },
5806   preBuild: function() {
5807     var self = this;
5808     this.input_type = 'select';
5809     this.enum_options = [];
5810     this.enum_values = [];
5811     this.enum_display = [];
5812     var i;
5813
5814     // Enum options enumerated
5815     if(this.schema["enum"]) {
5816       var display = this.schema.options && this.schema.options.enum_titles || [];
5817
5818       $each(this.schema["enum"],function(i,option) {
5819         self.enum_options[i] = ""+option;
5820         self.enum_display[i] = ""+(display[i] || option);
5821         self.enum_values[i] = self.typecast(option);
5822       });
5823
5824       if(!this.isRequired()){
5825         self.enum_display.unshift(' ');
5826         self.enum_options.unshift('undefined');
5827         self.enum_values.unshift(undefined);
5828       }
5829
5830     }
5831     // Boolean
5832     else if(this.schema.type === "boolean") {
5833       self.enum_display = this.schema.options && this.schema.options.enum_titles || ['true','false'];
5834       self.enum_options = ['1',''];
5835       self.enum_values = [true,false];
5836
5837       if(!this.isRequired()){
5838         self.enum_display.unshift(' ');
5839         self.enum_options.unshift('undefined');
5840         self.enum_values.unshift(undefined);
5841       }
5842
5843     }
5844     // Dynamic Enum
5845     else if(this.schema.enumSource) {
5846       this.enumSource = [];
5847       this.enum_display = [];
5848       this.enum_options = [];
5849       this.enum_values = [];
5850
5851       // Shortcut declaration for using a single array
5852       if(!(Array.isArray(this.schema.enumSource))) {
5853         if(this.schema.enumValue) {
5854           this.enumSource = [
5855             {
5856               source: this.schema.enumSource,
5857               value: this.schema.enumValue
5858             }
5859           ];
5860         }
5861         else {
5862           this.enumSource = [
5863             {
5864               source: this.schema.enumSource
5865             }
5866           ];
5867         }
5868       }
5869       else {
5870         for(i=0; i<this.schema.enumSource.length; i++) {
5871           // Shorthand for watched variable
5872           if(typeof this.schema.enumSource[i] === "string") {
5873             this.enumSource[i] = {
5874               source: this.schema.enumSource[i]
5875             };
5876           }
5877           // Make a copy of the schema
5878           else if(!(Array.isArray(this.schema.enumSource[i]))) {
5879             this.enumSource[i] = $extend({},this.schema.enumSource[i]);
5880           }
5881           else {
5882             this.enumSource[i] = this.schema.enumSource[i];
5883           }
5884         }
5885       }
5886
5887       // Now, enumSource is an array of sources
5888       // Walk through this array and fix up the values
5889       for(i=0; i<this.enumSource.length; i++) {
5890         if(this.enumSource[i].value) {
5891           this.enumSource[i].value = this.jsoneditor.compileTemplate(this.enumSource[i].value, this.template_engine);
5892         }
5893         if(this.enumSource[i].title) {
5894           this.enumSource[i].title = this.jsoneditor.compileTemplate(this.enumSource[i].title, this.template_engine);
5895         }
5896         if(this.enumSource[i].filter) {
5897           this.enumSource[i].filter = this.jsoneditor.compileTemplate(this.enumSource[i].filter, this.template_engine);
5898         }
5899       }
5900     }
5901     // Other, not supported
5902     else {
5903       throw "'select' editor requires the enum property to be set.";
5904     }
5905   },
5906   build: function() {
5907     var self = this;
5908     if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5909     if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
5910     if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
5911     if(this.options.compact) this.container.className += ' compact';
5912
5913     this.input = this.theme.getSelectInput(this.enum_options);
5914     this.theme.setSelectOptions(this.input,this.enum_options,this.enum_display);
5915
5916     if(this.schema.readOnly || this.schema.readonly) {
5917       this.always_disabled = true;
5918       this.input.disabled = true;
5919     }
5920
5921     this.input.addEventListener('change',function(e) {
5922       e.preventDefault();
5923       e.stopPropagation();
5924       self.onInputChange();
5925     });
5926
5927     this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
5928     this.input.controlgroup = this.control;
5929     this.container.appendChild(this.control);
5930
5931     this.value = this.enum_values[0];
5932   },
5933   onInputChange: function() {
5934     var val = this.typecast(this.input.value);
5935
5936     var new_val;
5937     // Invalid option, use first option instead
5938     if(this.enum_options.indexOf(val) === -1) {
5939       new_val = this.enum_values[0];
5940     }
5941     else {
5942       new_val = this.enum_values[this.enum_options.indexOf(val)];
5943     }
5944
5945     // If valid hasn't changed
5946     if(new_val === this.value) return;
5947
5948     // Store new value and propogate change event
5949     this.value = new_val;
5950     this.onChange(true);
5951   },
5952   setupSelect2: function() {
5953     // If the Select2 library is loaded use it when we have lots of items
5954     if(window.jQuery && window.jQuery.fn && window.jQuery.fn.select2 && (this.enum_options.length > 2 || (this.enum_options.length && this.enumSource))) {
5955       var options = $extend({},JSONEditor.plugins.select2);
5956       if(this.schema.options && this.schema.options.select2_options) options = $extend(options,this.schema.options.select2_options);
5957       this.select2 = window.jQuery(this.input).select2(options);
5958       this.select2v4 = this.select2.select2.hasOwnProperty("amd");
5959
5960       var self = this;
5961       this.select2.on('select2-blur',function() {
5962         if(self.select2v4)
5963           self.input.value = self.select2.val();
5964         else
5965           self.input.value = self.select2.select2('val');
5966
5967         self.onInputChange();
5968       });
5969
5970       this.select2.on('change',function() {
5971         if(self.select2v4)
5972           self.input.value = self.select2.val();
5973         else
5974           self.input.value = self.select2.select2('val');
5975
5976         self.onInputChange();
5977       });
5978     }
5979     else {
5980       this.select2 = null;
5981     }
5982   },
5983   postBuild: function() {
5984     this._super();
5985     this.theme.afterInputReady(this.input);
5986     this.setupSelect2();
5987   },
5988   onWatchedFieldChange: function() {
5989     var self = this, vars, j;
5990
5991     // If this editor uses a dynamic select box
5992     if(this.enumSource) {
5993       vars = this.getWatchedFieldValues();
5994       var select_options = [];
5995       var select_titles = [];
5996
5997       for(var i=0; i<this.enumSource.length; i++) {
5998         // Constant values
5999         if(Array.isArray(this.enumSource[i])) {
6000           select_options = select_options.concat(this.enumSource[i]);
6001           select_titles = select_titles.concat(this.enumSource[i]);
6002         }
6003         else {
6004           var items = [];
6005           // Static list of items
6006           if(Array.isArray(this.enumSource[i].source)) {
6007             items = this.enumSource[i].source;
6008           // A watched field
6009           } else {
6010             items = vars[this.enumSource[i].source];
6011           }
6012
6013           if(items) {
6014             // Only use a predefined part of the array
6015             if(this.enumSource[i].slice) {
6016               items = Array.prototype.slice.apply(items,this.enumSource[i].slice);
6017             }
6018             // Filter the items
6019             if(this.enumSource[i].filter) {
6020               var new_items = [];
6021               for(j=0; j<items.length; j++) {
6022                 if(this.enumSource[i].filter({i:j,item:items[j],watched:vars})) new_items.push(items[j]);
6023               }
6024               items = new_items;
6025             }
6026
6027             var item_titles = [];
6028             var item_values = [];
6029             for(j=0; j<items.length; j++) {
6030               var item = items[j];
6031
6032               // Rendered value
6033               if(this.enumSource[i].value) {
6034                 item_values[j] = this.enumSource[i].value({
6035                   i: j,
6036                   item: item
6037                 });
6038               }
6039               // Use value directly
6040               else {
6041                 item_values[j] = items[j];
6042               }
6043
6044               // Rendered title
6045               if(this.enumSource[i].title) {
6046                 item_titles[j] = this.enumSource[i].title({
6047                   i: j,
6048                   item: item
6049                 });
6050               }
6051               // Use value as the title also
6052               else {
6053                 item_titles[j] = item_values[j];
6054               }
6055             }
6056
6057             // TODO: sort
6058
6059             select_options = select_options.concat(item_values);
6060             select_titles = select_titles.concat(item_titles);
6061           }
6062         }
6063       }
6064
6065       var prev_value = this.value;
6066
6067       this.theme.setSelectOptions(this.input, select_options, select_titles);
6068       this.enum_options = select_options;
6069       this.enum_display = select_titles;
6070       this.enum_values = select_options;
6071
6072       if(this.select2) {
6073         this.select2.select2('destroy');
6074       }
6075
6076       // If the previous value is still in the new select options, stick with it
6077       if(select_options.indexOf(prev_value) !== -1) {
6078         this.input.value = prev_value;
6079         this.value = prev_value;
6080       }
6081       // Otherwise, set the value to the first select option
6082       else {
6083         this.input.value = select_options[0];
6084         this.value = this.typecast(select_options[0] || "");  
6085         if(this.parent) this.parent.onChildEditorChange(this);
6086         else this.jsoneditor.onChange();
6087         this.jsoneditor.notifyWatchers(this.path);
6088       }
6089
6090       this.setupSelect2();
6091     }
6092
6093     this._super();
6094   },
6095   enable: function() {
6096     if(!this.always_disabled) {
6097       this.input.disabled = false;
6098       if(this.select2) {
6099         if(this.select2v4)
6100           this.select2.prop("disabled",false);
6101         else
6102           this.select2.select2("enable",true);
6103       }
6104     }
6105     this._super();
6106   },
6107   disable: function(always_disabled) {
6108     if(always_disabled) this.always_disabled = true;
6109     this.input.disabled = true;
6110     if(this.select2) {
6111       if(this.select2v4)
6112         this.select2.prop("disabled",true);
6113       else
6114         this.select2.select2("enable",false);
6115     }
6116     this._super();
6117   },
6118   destroy: function() {
6119     if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
6120     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
6121     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
6122     if(this.select2) {
6123       this.select2.select2('destroy');
6124       this.select2 = null;
6125     }
6126
6127     this._super();
6128   },
6129   showValidationErrors: function (errors) {
6130             var self = this;
6131
6132             if (this.jsoneditor.options.show_errors === "always") {}
6133             else if (!this.is_dirty && this.previous_error_setting === this.jsoneditor.options.show_errors) {
6134               return;
6135             }
6136
6137             this.previous_error_setting = this.jsoneditor.options.show_errors;
6138
6139             var messages = [];
6140             $each(errors, function (i, error) {
6141               if (error.path === self.path) {
6142                 messages.push(error.message);
6143               }
6144             });
6145
6146             this.input.controlgroup = this.control;
6147
6148             if (messages.length) {
6149               this.theme.addInputError(this.input, messages.join('. ') + '.');
6150             }
6151             else {
6152               this.theme.removeInputError(this.input);
6153             }
6154           }
6155 });
6156
6157 JSONEditor.defaults.editors.selectize = JSONEditor.AbstractEditor.extend({
6158   setValue: function(value,initial) {
6159     value = this.typecast(value||'');
6160
6161     // Sanitize value before setting it
6162     var sanitized = value;
6163     if(this.enum_values.indexOf(sanitized) < 0) {
6164       sanitized = this.enum_values[0];
6165     }
6166
6167     if(this.value === sanitized) {
6168       return;
6169     }
6170
6171     this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
6172
6173     if(this.selectize) {
6174       this.selectize[0].selectize.addItem(sanitized);
6175     }
6176
6177     this.value = sanitized;
6178     this.onChange();
6179   },
6180   register: function() {
6181     this._super();
6182     if(!this.input) return;
6183     this.input.setAttribute('name',this.formname);
6184   },
6185   unregister: function() {
6186     this._super();
6187     if(!this.input) return;
6188     this.input.removeAttribute('name');
6189   },
6190   getNumColumns: function() {
6191     if(!this.enum_options) return 3;
6192     var longest_text = this.getTitle().length;
6193     for(var i=0; i<this.enum_options.length; i++) {
6194       longest_text = Math.max(longest_text,this.enum_options[i].length+4);
6195     }
6196     return Math.min(12,Math.max(longest_text/7,2));
6197   },
6198   typecast: function(value) {
6199     if(this.schema.type === "boolean") {
6200       return !!value;
6201     }
6202     else if(this.schema.type === "number") {
6203       return 1*value;
6204     }
6205     else if(this.schema.type === "integer") {
6206       return Math.floor(value*1);
6207     }
6208     else {
6209       return ""+value;
6210     }
6211   },
6212   getValue: function() {
6213     if (!this.dependenciesFulfilled) {
6214       return undefined;
6215     }
6216     return this.value;
6217   },
6218   preBuild: function() {
6219     var self = this;
6220     this.input_type = 'select';
6221     this.enum_options = [];
6222     this.enum_values = [];
6223     this.enum_display = [];
6224     var i;
6225
6226     // Enum options enumerated
6227     if(this.schema.enum) {
6228       var display = this.schema.options && this.schema.options.enum_titles || [];
6229
6230       $each(this.schema.enum,function(i,option) {
6231         self.enum_options[i] = ""+option;
6232         self.enum_display[i] = ""+(display[i] || option);
6233         self.enum_values[i] = self.typecast(option);
6234       });
6235     }
6236     // Boolean
6237     else if(this.schema.type === "boolean") {
6238       self.enum_display = this.schema.options && this.schema.options.enum_titles || ['true','false'];
6239       self.enum_options = ['1','0'];
6240       self.enum_values = [true,false];
6241     }
6242     // Dynamic Enum
6243     else if(this.schema.enumSource) {
6244       this.enumSource = [];
6245       this.enum_display = [];
6246       this.enum_options = [];
6247       this.enum_values = [];
6248
6249       // Shortcut declaration for using a single array
6250       if(!(Array.isArray(this.schema.enumSource))) {
6251         if(this.schema.enumValue) {
6252           this.enumSource = [
6253             {
6254               source: this.schema.enumSource,
6255               value: this.schema.enumValue
6256             }
6257           ];
6258         }
6259         else {
6260           this.enumSource = [
6261             {
6262               source: this.schema.enumSource
6263             }
6264           ];
6265         }
6266       }
6267       else {
6268         for(i=0; i<this.schema.enumSource.length; i++) {
6269           // Shorthand for watched variable
6270           if(typeof this.schema.enumSource[i] === "string") {
6271             this.enumSource[i] = {
6272               source: this.schema.enumSource[i]
6273             };
6274           }
6275           // Make a copy of the schema
6276           else if(!(Array.isArray(this.schema.enumSource[i]))) {
6277             this.enumSource[i] = $extend({},this.schema.enumSource[i]);
6278           }
6279           else {
6280             this.enumSource[i] = this.schema.enumSource[i];
6281           }
6282         }
6283       }
6284
6285       // Now, enumSource is an array of sources
6286       // Walk through this array and fix up the values
6287       for(i=0; i<this.enumSource.length; i++) {
6288         if(this.enumSource[i].value) {
6289           this.enumSource[i].value = this.jsoneditor.compileTemplate(this.enumSource[i].value, this.template_engine);
6290         }
6291         if(this.enumSource[i].title) {
6292           this.enumSource[i].title = this.jsoneditor.compileTemplate(this.enumSource[i].title, this.template_engine);
6293         }
6294         if(this.enumSource[i].filter) {
6295           this.enumSource[i].filter = this.jsoneditor.compileTemplate(this.enumSource[i].filter, this.template_engine);
6296         }
6297       }
6298     }
6299     // Other, not supported
6300     else {
6301       throw "'select' editor requires the enum property to be set.";
6302     }
6303   },
6304   build: function() {
6305     var self = this;
6306     if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
6307     if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
6308     if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
6309
6310     if(this.options.compact) this.container.className += ' compact';
6311
6312     this.input = this.theme.getSelectInput(this.enum_options);
6313     this.theme.setSelectOptions(this.input,this.enum_options,this.enum_display);
6314
6315     if(this.schema.readOnly || this.schema.readonly) {
6316       this.always_disabled = true;
6317       this.input.disabled = true;
6318     }
6319
6320     this.input.addEventListener('change',function(e) {
6321       e.preventDefault();
6322       e.stopPropagation();
6323       self.onInputChange();
6324     });
6325
6326     this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
6327     this.container.appendChild(this.control);
6328
6329     this.value = this.enum_values[0];
6330   },
6331   onInputChange: function() {
6332     //console.log("onInputChange");
6333     var val = this.input.value;
6334
6335     var sanitized = val;
6336     if(this.enum_options.indexOf(val) === -1) {
6337       sanitized = this.enum_options[0];
6338     }
6339
6340     //this.value = this.enum_values[this.enum_options.indexOf(val)];
6341     this.value = val;
6342     this.onChange(true);
6343   },
6344   setupSelectize: function() {
6345     // If the Selectize library is loaded use it when we have lots of items
6346     var self = this;
6347     if(window.jQuery && window.jQuery.fn && window.jQuery.fn.selectize && (this.enum_options.length >= 2 || (this.enum_options.length && this.enumSource))) {
6348       var options = $extend({},JSONEditor.plugins.selectize);
6349       if(this.schema.options && this.schema.options.selectize_options) options = $extend(options,this.schema.options.selectize_options);
6350       this.selectize = window.jQuery(this.input).selectize($extend(options,
6351       {
6352         // set the create option to true by default, or to the user specified value if defined
6353         create: ( options.create === undefined ? true : options.create),
6354         onChange : function() {
6355           self.onInputChange();
6356         }
6357       }));
6358     }
6359     else {
6360       this.selectize = null;
6361     }
6362   },
6363   postBuild: function() {
6364     this._super();
6365     this.theme.afterInputReady(this.input);
6366     this.setupSelectize();
6367   },
6368   onWatchedFieldChange: function() {
6369     var self = this, vars, j;
6370
6371     // If this editor uses a dynamic select box
6372     if(this.enumSource) {
6373       vars = this.getWatchedFieldValues();
6374       var select_options = [];
6375       var select_titles = [];
6376
6377       for(var i=0; i<this.enumSource.length; i++) {
6378         // Constant values
6379         if(Array.isArray(this.enumSource[i])) {
6380           select_options = select_options.concat(this.enumSource[i]);
6381           select_titles = select_titles.concat(this.enumSource[i]);
6382         }
6383         // A watched field
6384         else if(vars[this.enumSource[i].source]) {
6385           var items = vars[this.enumSource[i].source];
6386
6387           // Only use a predefined part of the array
6388           if(this.enumSource[i].slice) {
6389             items = Array.prototype.slice.apply(items,this.enumSource[i].slice);
6390           }
6391           // Filter the items
6392           if(this.enumSource[i].filter) {
6393             var new_items = [];
6394             for(j=0; j<items.length; j++) {
6395               if(this.enumSource[i].filter({i:j,item:items[j]})) new_items.push(items[j]);
6396             }
6397             items = new_items;
6398           }
6399
6400           var item_titles = [];
6401           var item_values = [];
6402           for(j=0; j<items.length; j++) {
6403             var item = items[j];
6404
6405             // Rendered value
6406             if(this.enumSource[i].value) {
6407               item_values[j] = this.enumSource[i].value({
6408                 i: j,
6409                 item: item
6410               });
6411             }
6412             // Use value directly
6413             else {
6414               item_values[j] = items[j];
6415             }
6416
6417             // Rendered title
6418             if(this.enumSource[i].title) {
6419               item_titles[j] = this.enumSource[i].title({
6420                 i: j,
6421                 item: item
6422               });
6423             }
6424             // Use value as the title also
6425             else {
6426               item_titles[j] = item_values[j];
6427             }
6428           }
6429
6430           // TODO: sort
6431
6432           select_options = select_options.concat(item_values);
6433           select_titles = select_titles.concat(item_titles);
6434         }
6435       }
6436
6437       var prev_value = this.value;
6438
6439       // Check to see if this item is in the list
6440       // Note: We have to skip empty string for watch lists to work properly
6441       if ((prev_value !== undefined) && (prev_value !== "") && (select_options.indexOf(prev_value) === -1)) {
6442         // item is not in the list. Add it.
6443         select_options = select_options.concat(prev_value);
6444         select_titles = select_titles.concat(prev_value);
6445       }
6446
6447       this.theme.setSelectOptions(this.input, select_options, select_titles);
6448       this.enum_options = select_options;
6449       this.enum_display = select_titles;
6450       this.enum_values = select_options;
6451
6452       // If the previous value is still in the new select options, stick with it
6453       if(select_options.indexOf(prev_value) !== -1) {
6454         this.input.value = prev_value;
6455         this.value = prev_value;
6456       }
6457
6458       // Otherwise, set the value to the first select option
6459       else {
6460         this.input.value = select_options[0];
6461         this.value = select_options[0] || "";
6462         if(this.parent) this.parent.onChildEditorChange(this);
6463         else this.jsoneditor.onChange();
6464         this.jsoneditor.notifyWatchers(this.path);
6465       }
6466
6467       if(this.selectize) {
6468         // Update the Selectize options
6469         this.updateSelectizeOptions(select_options);
6470       }
6471       else {
6472         this.setupSelectize();
6473       }
6474
6475       this._super();
6476     }
6477   },
6478   updateSelectizeOptions: function(select_options) {
6479     var selectized = this.selectize[0].selectize,
6480         self = this;
6481
6482     selectized.off();
6483     selectized.clearOptions();
6484     for(var n in select_options) {
6485       selectized.addOption({value:select_options[n],text:select_options[n]});
6486     }
6487     selectized.addItem(this.value);
6488     selectized.on('change',function() {
6489       self.onInputChange();
6490     });
6491   },
6492   enable: function() {
6493     if(!this.always_disabled) {
6494       this.input.disabled = false;
6495       if(this.selectize) {
6496         this.selectize[0].selectize.unlock();
6497       }
6498       this._super();
6499     }
6500   },
6501   disable: function(always_disabled) {
6502     if(always_disabled) this.always_disabled = true;
6503     this.input.disabled = true;
6504     if(this.selectize) {
6505       this.selectize[0].selectize.lock();
6506     }
6507     this._super();
6508   },
6509   destroy: function() {
6510     if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
6511     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
6512     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
6513     if(this.selectize) {
6514       this.selectize[0].selectize.destroy();
6515       this.selectize = null;
6516     }
6517     this._super();
6518   }
6519 });
6520
6521 JSONEditor.defaults.editors.multiselect = JSONEditor.AbstractEditor.extend({
6522   preBuild: function() {
6523     this._super();
6524     var i;
6525
6526     this.select_options = {};
6527     this.select_values = {};
6528
6529     var items_schema = this.jsoneditor.expandRefs(this.schema.items || {});
6530
6531     var e = items_schema["enum"] || [];
6532     var t = items_schema.options? items_schema.options.enum_titles || [] : [];
6533     this.option_keys = [];
6534     this.option_titles = [];
6535     for(i=0; i<e.length; i++) {
6536       // If the sanitized value is different from the enum value, don't include it
6537       if(this.sanitize(e[i]) !== e[i]) continue;
6538
6539       this.option_keys.push(e[i]+"");
6540       this.option_titles.push((t[i]||e[i])+"");
6541       this.select_values[e[i]+""] = e[i];
6542     }
6543   },
6544   build: function() {
6545     var self = this, i;
6546     if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
6547     if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
6548
6549     if((!this.schema.format && this.option_keys.length < 8) || this.schema.format === "checkbox") {
6550       this.input_type = 'checkboxes';
6551
6552       this.inputs = {};
6553       this.controls = {};
6554       for(i=0; i<this.option_keys.length; i++) {
6555         this.inputs[this.option_keys[i]] = this.theme.getCheckbox();
6556         this.select_options[this.option_keys[i]] = this.inputs[this.option_keys[i]];
6557         var label = this.theme.getCheckboxLabel(this.option_titles[i]);
6558         this.controls[this.option_keys[i]] = this.theme.getFormControl(label, this.inputs[this.option_keys[i]]);
6559       }
6560
6561       this.control = this.theme.getMultiCheckboxHolder(this.controls,this.label,this.description);
6562     }
6563     else {
6564       this.input_type = 'select';
6565       this.input = this.theme.getSelectInput(this.option_keys);
6566       this.theme.setSelectOptions(this.input,this.option_keys,this.option_titles);
6567       this.input.multiple = true;
6568       this.input.size = Math.min(10,this.option_keys.length);
6569
6570       for(i=0; i<this.option_keys.length; i++) {
6571         this.select_options[this.option_keys[i]] = this.input.children[i];
6572       }
6573
6574       if(this.schema.readOnly || this.schema.readonly) {
6575         this.always_disabled = true;
6576         this.input.disabled = true;
6577       }
6578
6579       this.control = this.theme.getFormControl(this.label, this.input, this.description);
6580     }
6581
6582     this.container.appendChild(this.control);
6583     this.control.addEventListener('change',function(e) {
6584       e.preventDefault();
6585       e.stopPropagation();
6586
6587       var new_value = [];
6588       for(i = 0; i<self.option_keys.length; i++) {
6589         if(self.select_options[self.option_keys[i]].selected || self.select_options[self.option_keys[i]].checked) new_value.push(self.select_values[self.option_keys[i]]);
6590       }
6591
6592       self.updateValue(new_value);
6593       self.onChange(true);
6594     });
6595   },
6596   setValue: function(value, initial) {
6597     var i;
6598     value = value || [];
6599     if(typeof value !== "object") value = [value];
6600     else if(!(Array.isArray(value))) value = [];
6601
6602     // Make sure we are dealing with an array of strings so we can check for strict equality
6603     for(i=0; i<value.length; i++) {
6604       if(typeof value[i] !== "string") value[i] += "";
6605     }
6606
6607     // Update selected status of options
6608     for(i in this.select_options) {
6609       if(!this.select_options.hasOwnProperty(i)) continue;
6610
6611       this.select_options[i][this.input_type === "select"? "selected" : "checked"] = (value.indexOf(i) !== -1);
6612     }
6613
6614     this.updateValue(value);
6615     this.onChange();
6616   },
6617   setupSelect2: function() {
6618     if(window.jQuery && window.jQuery.fn && window.jQuery.fn.select2) {
6619         var options = window.jQuery.extend({},JSONEditor.plugins.select2);
6620         if(this.schema.options && this.schema.options.select2_options) options = $extend(options,this.schema.options.select2_options);
6621         this.select2 = window.jQuery(this.input).select2(options);
6622         this.select2v4 = this.select2.select2.hasOwnProperty("amd");
6623
6624         var self = this;
6625         this.select2.on('select2-blur',function() {
6626           if(self.select2v4)
6627             self.value = self.select2.val();
6628           else
6629             self.value = self.select2.select2('val');
6630
6631           self.onChange(true);
6632         });
6633
6634         this.select2.on('change',function() {
6635           if(self.select2v4)
6636             self.value = self.select2.val();
6637           else
6638             self.value = self.select2.select2('val');
6639
6640           self.onChange(true);
6641         });
6642     }
6643     else {
6644         this.select2 = null;
6645     }
6646   },
6647   onInputChange: function() {
6648       this.value = this.input.value;
6649       this.onChange(true);
6650   },
6651   postBuild: function() {
6652       this._super();
6653       this.setupSelect2();
6654   },
6655   register: function() {
6656     this._super();
6657     if(!this.input) return;
6658     this.input.setAttribute('name',this.formname);
6659   },
6660   unregister: function() {
6661     this._super();
6662     if(!this.input) return;
6663     this.input.removeAttribute('name');
6664   },
6665   getNumColumns: function() {
6666     var longest_text = this.getTitle().length;
6667     for(var i in this.select_values) {
6668       if(!this.select_values.hasOwnProperty(i)) continue;
6669       longest_text = Math.max(longest_text,(this.select_values[i]+"").length+4);
6670     }
6671
6672     return Math.min(12,Math.max(longest_text/7,2));
6673   },
6674   updateValue: function(value) {
6675     var changed = false;
6676     var new_value = [];
6677     for(var i=0; i<value.length; i++) {
6678       if(!this.select_options[value[i]+""]) {
6679         changed = true;
6680         continue;
6681       }
6682       var sanitized = this.sanitize(this.select_values[value[i]]);
6683       new_value.push(sanitized);
6684       if(sanitized !== value[i]) changed = true;
6685     }
6686     this.value = new_value;
6687
6688     if(this.select2) {
6689       if(this.select2v4)
6690         this.select2.val(this.value).trigger("change"); 
6691       else
6692         this.select2.select2('val',this.value);
6693     }
6694
6695     return changed;
6696   },
6697   sanitize: function(value) {
6698     if(this.schema.items.type === "number") {
6699       return 1*value;
6700     }
6701     else if(this.schema.items.type === "integer") {
6702       return Math.floor(value*1);
6703     }
6704     else {
6705       return ""+value;
6706     }
6707   },
6708   enable: function() {
6709     if(!this.always_disabled) {
6710       if(this.input) {
6711         this.input.disabled = false;
6712       }
6713       else if(this.inputs) {
6714         for(var i in this.inputs) {
6715           if(!this.inputs.hasOwnProperty(i)) continue;
6716           this.inputs[i].disabled = false;
6717         }
6718       }
6719       if(this.select2) {
6720         if(this.select2v4)
6721           this.select2.prop("disabled",false);
6722         else
6723           this.select2.select2("enable",true);
6724       }
6725       this._super();
6726     }
6727   },
6728   disable: function(always_disabled) {
6729     if(always_disabled) this.always_disabled = true;
6730     if(this.input) {
6731       this.input.disabled = true;
6732     }
6733     else if(this.inputs) {
6734       for(var i in this.inputs) {
6735         if(!this.inputs.hasOwnProperty(i)) continue;
6736         this.inputs[i].disabled = true;
6737       }
6738     }
6739     if(this.select2) {
6740       if(this.select2v4)
6741         this.select2.prop("disabled",true);
6742       else
6743         this.select2.select2("enable",false);
6744     }
6745     this._super();
6746   },
6747   destroy: function() {
6748     if(this.select2) {
6749         this.select2.select2('destroy');
6750         this.select2 = null;
6751     }
6752     this._super();
6753   }
6754 });
6755
6756 JSONEditor.defaults.editors.base64 = JSONEditor.AbstractEditor.extend({
6757   getNumColumns: function() {
6758     return 4;
6759   },
6760   build: function() {    
6761     var self = this;
6762     this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
6763     if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
6764
6765     // Input that holds the base64 string
6766     this.input = this.theme.getFormInputField('hidden');
6767     this.container.appendChild(this.input);
6768     
6769     // Don't show uploader if this is readonly
6770     if(!this.schema.readOnly && !this.schema.readonly) {
6771       if(!window.FileReader) throw "FileReader required for base64 editor";
6772       
6773       // File uploader
6774       this.uploader = this.theme.getFormInputField('file');
6775       
6776       this.uploader.addEventListener('change',function(e) {
6777         e.preventDefault();
6778         e.stopPropagation();
6779         
6780         if(this.files && this.files.length) {
6781           var fr = new FileReader();
6782           fr.onload = function(evt) {
6783             self.value = evt.target.result;
6784             self.refreshPreview();
6785             self.onChange(true);
6786             fr = null;
6787           };
6788           fr.readAsDataURL(this.files[0]);
6789         }
6790       });
6791     }
6792
6793     this.preview = this.theme.getFormInputDescription(this.schema.description);
6794     this.container.appendChild(this.preview);
6795
6796     this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview, this.infoButton);
6797     this.container.appendChild(this.control);
6798   },
6799   refreshPreview: function() {
6800     if(this.last_preview === this.value) return;
6801     this.last_preview = this.value;
6802     
6803     this.preview.innerHTML = '';
6804     
6805     if(!this.value) return;
6806     
6807     var mime = this.value.match(/^data:([^;,]+)[;,]/);
6808     if(mime) mime = mime[1];
6809     
6810     if(!mime) {
6811       this.preview.innerHTML = '<em>Invalid data URI</em>';
6812     }
6813     else {
6814       this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+Math.floor((this.value.length-this.value.split(',')[0].length-1)/1.33333)+' bytes';
6815       if(mime.substr(0,5)==="image") {
6816         this.preview.innerHTML += '<br>';
6817         var img = document.createElement('img');
6818         img.style.maxWidth = '100%';
6819         img.style.maxHeight = '100px';
6820         img.src = this.value;
6821         this.preview.appendChild(img);
6822       }
6823     }
6824   },
6825   enable: function() {
6826     if(!this.always_disabled) {
6827       if(this.uploader) this.uploader.disabled = false;
6828       this._super();
6829     }
6830   },
6831   disable: function(always_disabled) {
6832     if(always_disabled) this.always_disabled = true;
6833     if(this.uploader) this.uploader.disabled = true;
6834     this._super();
6835   },
6836   setValue: function(val) {
6837     if(this.value !== val) {
6838       this.value = val;
6839       this.input.value = this.value;
6840       this.refreshPreview();
6841       this.onChange();
6842     }
6843   },
6844   destroy: function() {
6845     if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
6846     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
6847     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
6848     if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
6849
6850     this._super();
6851   }
6852 });
6853
6854 JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({
6855   getNumColumns: function() {
6856     return 4;
6857   },
6858   build: function() {    
6859     var self = this;
6860     this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
6861
6862     // Input that holds the base64 string
6863     this.input = this.theme.getFormInputField('hidden');
6864     this.container.appendChild(this.input);
6865     
6866     // Don't show uploader if this is readonly
6867     if(!this.schema.readOnly && !this.schema.readonly) {
6868
6869       if(!this.jsoneditor.options.upload) throw "Upload handler required for upload editor";
6870
6871       // File uploader
6872       this.uploader = this.theme.getFormInputField('file');
6873       
6874       this.uploader.addEventListener('change',function(e) {
6875         e.preventDefault();
6876         e.stopPropagation();
6877         
6878         if(this.files && this.files.length) {
6879           var fr = new FileReader();
6880           fr.onload = function(evt) {
6881             self.preview_value = evt.target.result;
6882             self.refreshPreview();
6883             self.onChange(true);
6884             fr = null;
6885           };
6886           fr.readAsDataURL(this.files[0]);
6887         }
6888       });
6889     }
6890
6891     var description = this.schema.description;
6892     if (!description) description = '';
6893
6894     this.preview = this.theme.getFormInputDescription(description);
6895     this.container.appendChild(this.preview);
6896
6897     this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview);
6898     this.container.appendChild(this.control);
6899   },
6900   refreshPreview: function() {
6901     if(this.last_preview === this.preview_value) return;
6902     this.last_preview = this.preview_value;
6903
6904     this.preview.innerHTML = '';
6905     
6906     if(!this.preview_value) return;
6907
6908     var self = this;
6909
6910     var mime = this.preview_value.match(/^data:([^;,]+)[;,]/);
6911     if(mime) mime = mime[1];
6912     if(!mime) mime = 'unknown';
6913
6914     var file = this.uploader.files[0];
6915
6916     this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+file.size+' bytes';
6917     if(mime.substr(0,5)==="image") {
6918       this.preview.innerHTML += '<br>';
6919       var img = document.createElement('img');
6920       img.style.maxWidth = '100%';
6921       img.style.maxHeight = '100px';
6922       img.src = this.preview_value;
6923       this.preview.appendChild(img);
6924     }
6925
6926     this.preview.innerHTML += '<br>';
6927     var uploadButton = this.getButton('Upload', 'upload', 'Upload');
6928     this.preview.appendChild(uploadButton);
6929     uploadButton.addEventListener('click',function(event) {
6930       event.preventDefault();
6931
6932       uploadButton.setAttribute("disabled", "disabled");
6933       self.theme.removeInputError(self.uploader);
6934
6935       if (self.theme.getProgressBar) {
6936         self.progressBar = self.theme.getProgressBar();
6937         self.preview.appendChild(self.progressBar);
6938       }
6939
6940       self.jsoneditor.options.upload(self.path, file, {
6941         success: function(url) {
6942           self.setValue(url);
6943
6944           if(self.parent) self.parent.onChildEditorChange(self);
6945           else self.jsoneditor.onChange();
6946
6947           if (self.progressBar) self.preview.removeChild(self.progressBar);
6948           uploadButton.removeAttribute("disabled");
6949         },
6950         failure: function(error) {
6951           self.theme.addInputError(self.uploader, error);
6952           if (self.progressBar) self.preview.removeChild(self.progressBar);
6953           uploadButton.removeAttribute("disabled");
6954         },
6955         updateProgress: function(progress) {
6956           if (self.progressBar) {
6957             if (progress) self.theme.updateProgressBar(self.progressBar, progress);
6958             else self.theme.updateProgressBarUnknown(self.progressBar);
6959           }
6960         }
6961       });
6962     });
6963
6964     if(this.jsoneditor.options.auto_upload || this.schema.options.auto_upload) {
6965       uploadButton.dispatchEvent(new MouseEvent('click'));
6966       this.preview.removeChild(uploadButton);
6967     }
6968   },
6969   enable: function() {
6970     if(!this.always_disabled) {
6971       if(this.uploader) this.uploader.disabled = false;
6972       this._super();
6973     }
6974   },
6975   disable: function(always_disabled) {
6976     if(always_disabled) this.always_disabled = true;
6977     if(this.uploader) this.uploader.disabled = true;
6978     this._super();
6979   },
6980   setValue: function(val) {
6981     if(this.value !== val) {
6982       this.value = val;
6983       this.input.value = this.value;
6984       this.onChange();
6985     }
6986   },
6987   destroy: function() {
6988     if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
6989     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
6990     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
6991     if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
6992
6993     this._super();
6994   }
6995 });
6996
6997 JSONEditor.defaults.editors.checkbox = JSONEditor.AbstractEditor.extend({
6998   setValue: function(value,initial) {
6999     this.value = !!value;
7000     this.input.checked = this.value;
7001     this.onChange();
7002   },
7003   register: function() {
7004     this._super();
7005     if(!this.input) return;
7006     this.input.setAttribute('name',this.formname);
7007   },
7008   unregister: function() {
7009     this._super();
7010     if(!this.input) return;
7011     this.input.removeAttribute('name');
7012   },
7013   getNumColumns: function() {
7014     return Math.min(12,Math.max(this.getTitle().length/7,2));
7015   },
7016   build: function() {
7017     var self = this;
7018     if(!this.options.compact) {
7019       this.label = this.header = this.theme.getCheckboxLabel(this.getTitle());
7020     }
7021     if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
7022     if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
7023     if(this.options.compact) this.container.className += ' compact';
7024
7025     this.input = this.theme.getCheckbox();
7026     this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
7027
7028     if(this.schema.readOnly || this.schema.readonly) {
7029       this.always_disabled = true;
7030       this.input.disabled = true;
7031     }
7032
7033     this.input.addEventListener('change',function(e) {
7034       e.preventDefault();
7035       e.stopPropagation();
7036       self.value = this.checked;
7037       self.onChange(true);
7038     });
7039
7040     this.container.appendChild(this.control);
7041   },
7042   enable: function() {
7043     if(!this.always_disabled) {
7044       this.input.disabled = false;
7045       this._super();
7046     }
7047   },
7048   disable: function(always_disabled) {
7049     if(always_disabled) this.always_disabled = true;
7050     this.input.disabled = true;
7051     this._super();
7052   },
7053   destroy: function() {
7054     if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
7055     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
7056     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
7057     this._super();
7058   },
7059   showValidationErrors: function (errors) {
7060     var self = this;
7061
7062     if (this.jsoneditor.options.show_errors === "always") {}
7063
7064     else if (!this.is_dirty && this.previous_error_setting === this.jsoneditor.options.show_errors) {
7065       return;
7066     }
7067
7068     this.previous_error_setting = this.jsoneditor.options.show_errors;
7069
7070     var messages = [];
7071     $each(errors, function (i, error) {
7072       if (error.path === self.path) {
7073         messages.push(error.message);
7074       }
7075     });
7076
7077     this.input.controlgroup = this.control;
7078
7079     if (messages.length) {
7080       this.theme.addInputError(this.input, messages.join('. ') + '.');
7081     }
7082     else {
7083       this.theme.removeInputError(this.input);
7084     }
7085   }
7086 });
7087
7088 JSONEditor.defaults.editors.arraySelectize = JSONEditor.AbstractEditor.extend({
7089   build: function() {
7090     this.title = this.theme.getFormInputLabel(this.getTitle());
7091
7092     this.title_controls = this.theme.getHeaderButtonHolder();
7093     this.title.appendChild(this.title_controls);
7094     this.error_holder = document.createElement('div');
7095
7096     if(this.schema.description) {
7097       this.description = this.theme.getDescription(this.schema.description);
7098     }
7099
7100     this.input = document.createElement('select');
7101     this.input.setAttribute('multiple', 'multiple');
7102
7103     var group = this.theme.getFormControl(this.title, this.input, this.description);
7104
7105     this.container.appendChild(group);
7106     this.container.appendChild(this.error_holder);
7107
7108     window.jQuery(this.input).selectize({
7109       delimiter: false,
7110       createOnBlur: true,
7111       create: true
7112     });
7113   },
7114   postBuild: function() {
7115       var self = this;
7116       this.input.selectize.on('change', function(event) {
7117           self.refreshValue();
7118           self.onChange(true);
7119       });
7120   },
7121   destroy: function() {
7122     this.empty(true);
7123     if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
7124     if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
7125     if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
7126
7127     this._super();
7128   },
7129   empty: function(hard) {},
7130   setValue: function(value, initial) {
7131     var self = this;
7132     // Update the array's value, adding/removing rows when necessary
7133     value = value || [];
7134     if(!(Array.isArray(value))) value = [value];
7135
7136     this.input.selectize.clearOptions();
7137     this.input.selectize.clear(true);
7138
7139     value.forEach(function(item) {
7140       self.input.selectize.addOption({text: item, value: item});
7141     });
7142     this.input.selectize.setValue(value);
7143
7144     this.refreshValue(initial);
7145   },
7146   refreshValue: function(force) {
7147     this.value = this.input.selectize.getValue();
7148   },
7149   showValidationErrors: function(errors) {
7150     var self = this;
7151
7152     // Get all the errors that pertain to this editor
7153     var my_errors = [];
7154     var other_errors = [];
7155     $each(errors, function(i,error) {
7156       if(error.path === self.path) {
7157         my_errors.push(error);
7158       }
7159       else {
7160         other_errors.push(error);
7161       }
7162     });
7163
7164     // Show errors for this editor
7165     if(this.error_holder) {
7166
7167       if(my_errors.length) {
7168         var message = [];
7169         this.error_holder.innerHTML = '';
7170         this.error_holder.style.display = '';
7171         $each(my_errors, function(i,error) {
7172           self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
7173         });
7174       }
7175       // Hide error area
7176       else {
7177         this.error_holder.style.display = 'none';
7178       }
7179     }
7180   }
7181 });
7182
7183 var matchKey = (function () {
7184   var elem = document.documentElement;
7185
7186   if (elem.matches) return 'matches';
7187   else if (elem.webkitMatchesSelector) return 'webkitMatchesSelector';
7188   else if (elem.mozMatchesSelector) return 'mozMatchesSelector';
7189   else if (elem.msMatchesSelector) return 'msMatchesSelector';
7190   else if (elem.oMatchesSelector) return 'oMatchesSelector';
7191 })();
7192
7193 JSONEditor.AbstractTheme = Class.extend({
7194   getContainer: function() {
7195     return document.createElement('div');
7196   },
7197   getFloatRightLinkHolder: function() {
7198     var el = document.createElement('div');
7199     el.style = el.style || {};
7200     el.style.cssFloat = 'right';
7201     el.style.marginLeft = '10px';
7202     return el;
7203   },
7204   getModal: function() {
7205     var el = document.createElement('div');
7206     el.style.backgroundColor = 'white';
7207     el.style.border = '1px solid black';
7208     el.style.boxShadow = '3px 3px black';
7209     el.style.position = 'absolute';
7210     el.style.zIndex = '10';
7211     el.style.display = 'none';
7212     return el;
7213   },
7214   getGridContainer: function() {
7215     var el = document.createElement('div');
7216     return el;
7217   },
7218   getGridRow: function() {
7219     var el = document.createElement('div');
7220     el.className = 'row';
7221     return el;
7222   },
7223   getGridColumn: function() {
7224     var el = document.createElement('div');
7225     return el;
7226   },
7227   setGridColumnSize: function(el,size) {
7228
7229   },
7230   getLink: function(text) {
7231     var el = document.createElement('a');
7232     el.setAttribute('href','#');
7233     el.appendChild(document.createTextNode(text));
7234     return el;
7235   },
7236   disableHeader: function(header) {
7237     header.style.color = '#ccc';
7238   },
7239   disableLabel: function(label) {
7240     label.style.color = '#ccc';
7241   },
7242   enableHeader: function(header) {
7243     header.style.color = '';
7244   },
7245   enableLabel: function(label) {
7246     label.style.color = '';
7247   },
7248   getInfoButton: function(text) {
7249     var icon = document.createElement('span');
7250     icon.innerText = "ⓘ";
7251     icon.style.fontSize = "16px";
7252     icon.style.fontWeight = "bold";
7253     icon.style.padding = ".25rem";
7254     icon.style.position = "relative";
7255     icon.style.display = "inline-block";
7256
7257     var tooltip = document.createElement('span');
7258     tooltip.style.fontSize = "12px";
7259     icon.style.fontWeight = "normal";
7260     tooltip.style["font-family"] = "sans-serif";
7261     tooltip.style.visibility = "hidden";
7262     tooltip.style["background-color"] = "rgba(50, 50, 50, .75)";
7263     tooltip.style.margin = "0 .25rem";
7264     tooltip.style.color = "#FAFAFA";
7265     tooltip.style.padding = ".5rem 1rem";
7266     tooltip.style["border-radius"] = ".25rem";
7267     tooltip.style.width = "20rem";
7268     tooltip.style.position = "absolute";
7269     tooltip.innerText = text;
7270     icon.onmouseover = function() {
7271       tooltip.style.visibility = "visible";
7272     };
7273     icon.onmouseleave = function() {
7274       tooltip.style.visibility = "hidden";
7275     };
7276
7277     icon.appendChild(tooltip);
7278
7279     return icon;
7280   },
7281   getFormInputLabel: function(text) {
7282     var el = document.createElement('label');
7283     el.appendChild(document.createTextNode(text));
7284     return el;
7285   },
7286   getCheckboxLabel: function(text) {
7287     var el = this.getFormInputLabel(text);
7288     el.style.fontWeight = 'normal';
7289     return el;
7290   },
7291   getHeader: function(text) {
7292     var el = document.createElement('h3');
7293     if(typeof text === "string") {
7294       el.textContent = text;
7295       el.style.fontWeight = 'bold';
7296       el.style.fontSize = '12px';
7297       el.style.padding = '4px';
7298     }
7299     else {
7300       el.appendChild(text);
7301     }
7302
7303     return el;
7304   },
7305   getCheckbox: function() {
7306     var el = this.getFormInputField('checkbox');
7307     el.style.display = 'inline-block';
7308     el.style.width = 'auto';
7309     return el;
7310   },
7311   getMultiCheckboxHolder: function(controls,label,description) {
7312     var el = document.createElement('div');
7313
7314     if(label) {
7315       label.style.display = 'block';
7316       el.appendChild(label);
7317     }
7318
7319     for(var i in controls) {
7320       if(!controls.hasOwnProperty(i)) continue;
7321       controls[i].style.display = 'inline-block';
7322       controls[i].style.marginRight = '20px';
7323       el.appendChild(controls[i]);
7324     }
7325
7326     if(description) el.appendChild(description);
7327
7328     return el;
7329   },
7330   getSelectInput: function(options) {
7331     var select = document.createElement('select');
7332     if(options) this.setSelectOptions(select, options);
7333     return select;
7334   },
7335   getSwitcher: function(options) {
7336     var switcher = this.getSelectInput(options);
7337     switcher.style.backgroundColor = 'transparent';
7338     switcher.style.display = 'inline-block';
7339     switcher.style.fontStyle = 'italic';
7340     switcher.style.fontWeight = 'normal';
7341     switcher.style.height = 'auto';
7342     switcher.style.marginBottom = 0;
7343     switcher.style.marginLeft = '5px';
7344     switcher.style.padding = '0 0 0 3px';
7345     switcher.style.width = 'auto';
7346     return switcher;
7347   },
7348   getSwitcherOptions: function(switcher) {
7349     return switcher.getElementsByTagName('option');
7350   },
7351   setSwitcherOptions: function(switcher, options, titles) {
7352     this.setSelectOptions(switcher, options, titles);
7353   },
7354   setSelectOptions: function(select, options, titles) {
7355     titles = titles || [];
7356     select.innerHTML = '';
7357     for(var i=0; i<options.length; i++) {
7358       var option = document.createElement('option');
7359       option.setAttribute('value',options[i]);
7360       option.textContent = titles[i] || options[i];
7361       select.appendChild(option);
7362     }
7363   },
7364   getTextareaInput: function(rows, cols) {
7365     var el = document.createElement('textarea');
7366     el.style = el.style || {};
7367     el.style.width = '100%';
7368     el.style.height = '50px';
7369     el.style.fontWeight = 'bold';
7370     el.style.fontSize = '1em';
7371     el.style.boxSizing = 'border-box';
7372     if(typeof rows === undefined) { rows = 1 };
7373     if(typeof cols === undefined) { cols = 80 };
7374     el.rows = rows;
7375     el.cols = cols;
7376     el.wrap = 'soft';
7377     el.readonly = 'true';
7378     return el;
7379   },
7380   getRangeInput: function(min,max,step) {
7381     var el = this.getFormInputField('range');
7382     el.setAttribute('min',min);
7383     el.setAttribute('max',max);
7384     el.setAttribute('step',step);
7385     return el;
7386   },
7387   getFormInputField: function(type) {
7388     var el = document.createElement('input');
7389     el.setAttribute('type',type);
7390     return el;
7391   },
7392   afterInputReady: function(input) {
7393
7394   },
7395   getFormControl: function(label, input, description, infoText) {
7396     var el = document.createElement('div');
7397     el.className = 'form-control';
7398     if(label) el.appendChild(label);
7399     if(input.type === 'checkbox' && label) {
7400       label.insertBefore(input,label.firstChild);
7401       if(infoText) label.appendChild(infoText);
7402     }
7403     else {
7404       if(infoText) label.appendChild(infoText);
7405       el.appendChild(input);
7406     }
7407
7408     if(description) el.appendChild(description);
7409     return el;
7410   },
7411   getIndentedPanel: function() {
7412     var el = document.createElement('div');
7413     el.style = el.style || {};
7414     el.style.paddingLeft = '10px';
7415     el.style.marginLeft = '10px';
7416     el.style.borderLeft = '1px solid #ccc';
7417     return el;
7418   },
7419   getTopIndentedPanel: function() {
7420     var el = document.createElement('div');
7421     el.style = el.style || {};
7422     el.style.paddingLeft = '10px';
7423     el.style.marginLeft = '10px';
7424     return el;
7425   },
7426   getChildEditorHolder: function() {
7427     return document.createElement('div');
7428   },
7429   getDescription: function(text) {
7430     var el = document.createElement('p');
7431     el.innerHTML = text;
7432     return el;
7433   },
7434   getCheckboxDescription: function(text) {
7435     return this.getDescription(text);
7436   },
7437   getFormInputDescription: function(text) {
7438     return this.getDescription(text);
7439   },
7440   getHeaderButtonHolder: function() {
7441     return this.getButtonHolder();
7442   },
7443   getButtonHolder: function() {
7444     return document.createElement('div');
7445   },
7446   getButton: function(text, icon, title) {
7447     var el = document.createElement('button');
7448     el.type = 'button';
7449     this.setButtonText(el,text,icon,title);
7450     return el;
7451   },
7452   setButtonText: function(button, text, icon, title) {
7453     button.innerHTML = '';
7454     if(icon) {
7455       button.appendChild(icon);
7456       button.innerHTML += ' ';
7457     }
7458     button.appendChild(document.createTextNode(text));
7459     if(title) button.setAttribute('title',title);
7460   },
7461   getTable: function() {
7462     return document.createElement('table');
7463   },
7464   getTableRow: function() {
7465     return document.createElement('tr');
7466   },
7467   getTableHead: function() {
7468     return document.createElement('thead');
7469   },
7470   getTableBody: function() {
7471     return document.createElement('tbody');
7472   },
7473   getTableHeaderCell: function(text) {
7474     var el = document.createElement('th');
7475     el.textContent = text;
7476     return el;
7477   },
7478   getTableCell: function() {
7479     var el = document.createElement('td');
7480     return el;
7481   },
7482   getErrorMessage: function(text) {
7483     var el = document.createElement('p');
7484     el.style = el.style || {};
7485     el.style.color = 'red';
7486     el.appendChild(document.createTextNode(text));
7487     return el;
7488   },
7489   addInputError: function(input, text) {
7490   },
7491   removeInputError: function(input) {
7492   },
7493   addTableRowError: function(row) {
7494   },
7495   removeTableRowError: function(row) {
7496   },
7497   getTabHolder: function(propertyName) {
7498     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
7499     var el = document.createElement('div');
7500     el.innerHTML = "<div style='float: left; width: 130px;' class='tabs' id='" + pName + "'></div><div class='content' style='margin-left: 120px;' id='" + pName + "'></div><div style='clear:both;'></div>";
7501     return el;
7502   },
7503   getTopTabHolder: function(propertyName) {
7504     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
7505     var el = document.createElement('div');
7506     el.innerHTML = "<div class='tabs' style='margin-left: 10px;' id='" + pName + "'></div><div style='clear:both;'></div><div class='content' id='" + pName + "'></div>";
7507     return el;
7508   },
7509   applyStyles: function(el,styles) {
7510     for(var i in styles) {
7511       if(!styles.hasOwnProperty(i)) continue;
7512       el.style[i] = styles[i];
7513     }
7514   },
7515   closest: function(elem, selector) {
7516     while (elem && elem !== document) {
7517       if (elem[matchKey]) {
7518         if (elem[matchKey](selector)) {
7519           return elem;
7520         } else {
7521           elem = elem.parentNode;
7522         }
7523       }
7524       else {
7525         return false;
7526       }
7527     }
7528     return false;
7529   },
7530   insertBasicTopTab: function(tab, newTabs_holder ) {
7531     newTabs_holder.firstChild.insertBefore(tab,newTabs_holder.firstChild.firstChild);
7532   },
7533   getTab: function(span, tabId) {
7534     var el = document.createElement('div');
7535     el.appendChild(span);
7536     el.id = tabId;
7537     el.style = el.style || {};
7538     this.applyStyles(el,{
7539       border: '1px solid #ccc',
7540       borderWidth: '1px 0 1px 1px',
7541       textAlign: 'center',
7542       lineHeight: '30px',
7543       borderRadius: '5px',
7544       borderBottomRightRadius: 0,
7545       borderTopRightRadius: 0,
7546       fontWeight: 'bold',
7547       cursor: 'pointer'
7548     });
7549     return el;
7550   },
7551   getTopTab: function(span, tabId) {
7552     var el = document.createElement('div');
7553     el.id = tabId;
7554     el.appendChild(span);
7555     el.style = el.style || {};
7556     this.applyStyles(el,{
7557       float: 'left',
7558       border: '1px solid #ccc',
7559       borderWidth: '1px 1px 0px 1px',
7560       textAlign: 'center',
7561       lineHeight: '30px',
7562       borderRadius: '5px',
7563       paddingLeft:'5px',
7564       paddingRight:'5px',
7565       borderBottomRightRadius: 0,
7566       borderBottomLeftRadius: 0,
7567       fontWeight: 'bold',
7568       cursor: 'pointer'
7569     });
7570     return el;
7571   },
7572   getTabContentHolder: function(tab_holder) {
7573     return tab_holder.children[1];
7574   },
7575   getTopTabContentHolder: function(tab_holder) {
7576     return tab_holder.children[1];
7577   },
7578   getTabContent: function() {
7579     return this.getIndentedPanel();
7580   },
7581   getTopTabContent: function() {
7582     return this.getTopIndentedPanel();
7583   },
7584   markTabActive: function(row) {
7585     this.applyStyles(row.tab,{
7586       opacity: 1,
7587       background: 'white'
7588     });
7589     row.container.style.display = '';
7590   },
7591   markTabInactive: function(row) {
7592     this.applyStyles(row.tab,{
7593       opacity:0.5,
7594       background: ''
7595     });
7596     row.container.style.display = 'none';
7597   },
7598   addTab: function(holder, tab) {
7599     holder.children[0].appendChild(tab);
7600   },
7601   addTopTab: function(holder, tab) {
7602     holder.children[0].appendChild(tab);
7603   },
7604   getBlockLink: function() {
7605     var link = document.createElement('a');
7606     link.style.display = 'block';
7607     return link;
7608   },
7609   getBlockLinkHolder: function() {
7610     var el = document.createElement('div');
7611     return el;
7612   },
7613   getLinksHolder: function() {
7614     var el = document.createElement('div');
7615     return el;
7616   },
7617   createMediaLink: function(holder,link,media) {
7618     holder.appendChild(link);
7619     media.style.width='100%';
7620     holder.appendChild(media);
7621   },
7622   createImageLink: function(holder,link,image) {
7623     holder.appendChild(link);
7624     link.appendChild(image);
7625   },
7626   getFirstTab: function(holder){
7627     return holder.firstChild.firstChild;
7628   }
7629 });
7630
7631 JSONEditor.defaults.themes.bootstrap2 = JSONEditor.AbstractTheme.extend({
7632   getRangeInput: function(min, max, step) {
7633     // TODO: use bootstrap slider
7634     return this._super(min, max, step);
7635   },
7636   getGridContainer: function() {
7637     var el = document.createElement('div');
7638     el.className = 'container-fluid';
7639     el.style.padding = '4px';
7640     return el;
7641   },
7642   getGridRow: function() {
7643     var el = document.createElement('div');
7644     el.className = 'row-fluid';
7645     return el;
7646   },
7647   getFormInputLabel: function(text) {
7648     var el = this._super(text);
7649     el.style.display = 'inline-block';
7650     el.style.fontWeight = 'bold';
7651     return el;
7652   },
7653   setGridColumnSize: function(el,size) {
7654     el.className = 'span'+size;
7655   },
7656   getSelectInput: function(options) {
7657     var input = this._super(options);
7658     input.style.width = 'auto';
7659     input.style.maxWidth = '98%';
7660     return input;
7661   },
7662   getFormInputField: function(type) {
7663     var el = this._super(type);
7664     el.style.width = '98%';
7665     return el;
7666   },
7667   afterInputReady: function(input) {
7668     if(input.controlgroup) return;
7669     input.controlgroup = this.closest(input,'.control-group');
7670     input.controls = this.closest(input,'.controls');
7671     if(this.closest(input,'.compact')) {
7672       input.controlgroup.className = input.controlgroup.className.replace(/control-group/g,'').replace(/[ ]{2,}/g,' ');
7673       input.controls.className = input.controlgroup.className.replace(/controls/g,'').replace(/[ ]{2,}/g,' ');
7674       input.style.marginBottom = 0;
7675     }
7676     if (this.queuedInputErrorText) {
7677         var text = this.queuedInputErrorText;
7678         delete this.queuedInputErrorText;
7679         this.addInputError(input,text);
7680     }
7681
7682     // TODO: use bootstrap slider
7683   },
7684   getIndentedPanel: function() {
7685     var el = document.createElement('div');
7686     el.className = 'well well-small';
7687     el.style.padding = '4px';
7688     return el;
7689   },
7690   getInfoButton: function(text) {
7691     var icon = document.createElement('span');
7692     icon.className = "icon-info-sign pull-right";
7693     icon.style.padding = ".25rem";
7694     icon.style.position = "relative";
7695     icon.style.display = "inline-block";
7696
7697     var tooltip = document.createElement('span');
7698     tooltip.style["font-family"] = "sans-serif";
7699     tooltip.style.visibility = "hidden";
7700     tooltip.style["background-color"] = "rgba(50, 50, 50, .75)";
7701     tooltip.style.margin = "0 .25rem";
7702     tooltip.style.color = "#FAFAFA";
7703     tooltip.style.padding = ".5rem 1rem";
7704     tooltip.style["border-radius"] = ".25rem";
7705     tooltip.style.width = "25rem";
7706     tooltip.style.transform = "translateX(-27rem) translateY(-.5rem)";
7707     tooltip.style.position = "absolute";
7708     tooltip.innerText = text;
7709     icon.onmouseover = function() {
7710       tooltip.style.visibility = "visible";
7711     };
7712     icon.onmouseleave = function() {
7713       tooltip.style.visibility = "hidden";
7714     };
7715
7716     icon.appendChild(tooltip);
7717
7718     return icon;
7719   },
7720   getFormInputDescription: function(text) {
7721     var el = document.createElement('p');
7722     el.className = 'help-inline';
7723     el.textContent = text;
7724     return el;
7725   },
7726   getFormControl: function(label, input, description, infoText) {
7727     var ret = document.createElement('div');
7728     ret.className = 'control-group';
7729
7730     var controls = document.createElement('div');
7731     controls.className = 'controls';
7732
7733     if(label && input.getAttribute('type') === 'checkbox') {
7734       ret.appendChild(controls);
7735       label.className += ' checkbox';
7736       label.appendChild(input);
7737       controls.appendChild(label);
7738       if(infoText) controls.appendChild(infoText);
7739       controls.style.height = '30px';
7740     }
7741     else {
7742       if(label) {
7743         label.className += ' control-label';
7744         ret.appendChild(label);
7745       }
7746       if(infoText) controls.appendChild(infoText);
7747       controls.appendChild(input);
7748       ret.appendChild(controls);
7749     }
7750
7751     if(description) controls.appendChild(description);
7752
7753     return ret;
7754   },
7755   getHeaderButtonHolder: function() {
7756     var el = this.getButtonHolder();
7757     el.style.marginLeft = '10px';
7758     return el;
7759   },
7760   getButtonHolder: function() {
7761     var el = document.createElement('div');
7762     el.className = 'btn-group';
7763     return el;
7764   },
7765   getButton: function(text, icon, title) {
7766     var el =  this._super(text, icon, title);
7767     el.className += ' btn btn-default';
7768     el.style.backgroundColor = '#f2bfab';
7769     el.style.border = '1px solid #ddd';
7770     return el;
7771   },
7772   getTable: function() {
7773     var el = document.createElement('table');
7774     el.className = 'table table-bordered';
7775     el.style.width = 'auto';
7776     el.style.maxWidth = 'none';
7777     return el;
7778   },
7779   addInputError: function(input,text) {
7780     if(!input.controlgroup) {
7781         this.queuedInputErrorText = text;
7782         return;
7783     }
7784     if(!input.controlgroup || !input.controls) return;
7785     input.controlgroup.className += ' error';
7786     if(!input.errmsg) {
7787       input.errmsg = document.createElement('p');
7788       input.errmsg.className = 'help-block errormsg';
7789       input.controls.appendChild(input.errmsg);
7790     }
7791     else {
7792       input.errmsg.style.display = '';
7793     }
7794
7795     input.errmsg.textContent = text;
7796   },
7797   removeInputError: function(input) {
7798     if(!input.controlgroup) {
7799         delete this.queuedInputErrorText;
7800     }
7801     if(!input.errmsg) return;
7802     input.errmsg.style.display = 'none';
7803     input.controlgroup.className = input.controlgroup.className.replace(/\s?error/g,'');
7804   },
7805   getTabHolder: function(propertyName) {
7806     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
7807     var el = document.createElement('div');
7808     el.className = 'tabbable tabs-left';
7809     el.innerHTML = "<ul class='nav nav-tabs'  id='" + pName + "'></ul><div class='tab-content well well-small' id='" + pName + "'></div>";
7810     return el;
7811   },
7812   getTopTabHolder: function(propertyName) {
7813     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
7814     var el = document.createElement('div');
7815     el.className = 'tabbable tabs-over';
7816     el.innerHTML = "<ul class='nav nav-tabs' id='" + pName + "'></ul><div class='tab-content well well-small'  id='" + pName + "'></div>";
7817     return el;
7818   },
7819   getTab: function(text,tabId) {
7820     var el = document.createElement('li');
7821     el.className = 'nav-item';
7822     var a = document.createElement('a');
7823     a.setAttribute('href','#' + tabId);
7824     a.appendChild(text);
7825     el.appendChild(a);
7826     return el;
7827   },
7828   getTopTab: function(text,tabId) {
7829     var el = document.createElement('li');
7830     el.className = 'nav-item';
7831     var a = document.createElement('a');
7832     a.setAttribute('href','#' + tabId);
7833     a.appendChild(text);
7834     el.appendChild(a);
7835     return el;
7836   },
7837   getTabContentHolder: function(tab_holder) {
7838     return tab_holder.children[1];
7839   },
7840   getTopTabContentHolder: function(tab_holder) {
7841     return tab_holder.children[1];
7842   },
7843   getTabContent: function() {
7844     var el = document.createElement('div');
7845     el.className = 'tab-pane';
7846     return el;
7847   },
7848   getTopTabContent: function() {
7849     var el = document.createElement('div');
7850     el.className = 'tab-pane';
7851     return el;
7852   },
7853   markTabActive: function(row) {
7854     row.tab.className = row.tab.className.replace(/\s?active/g,'');
7855     row.tab.className += ' active';
7856     row.container.className = row.container.className.replace(/\s?active/g,'');
7857     row.container.className += ' active';
7858   },
7859   markTabInactive: function(row) {
7860     row.tab.className = row.tab.className.replace(/\s?active/g,'');
7861     row.container.className = row.container.className.replace(/\s?active/g,'');
7862   },
7863   addTab: function(holder, tab) {
7864     holder.children[0].appendChild(tab);
7865   },
7866   addTopTab: function(holder, tab) {
7867     holder.children[0].appendChild(tab);
7868   },
7869   getProgressBar: function() {
7870     var container = document.createElement('div');
7871     container.className = 'progress';
7872
7873     var bar = document.createElement('div');
7874     bar.className = 'bar';
7875     bar.style.width = '0%';
7876     container.appendChild(bar);
7877
7878     return container;
7879   },
7880   updateProgressBar: function(progressBar, progress) {
7881     if (!progressBar) return;
7882
7883     progressBar.firstChild.style.width = progress + "%";
7884   },
7885   updateProgressBarUnknown: function(progressBar) {
7886     if (!progressBar) return;
7887
7888     progressBar.className = 'progress progress-striped active';
7889     progressBar.firstChild.style.width = '100%';
7890   }
7891 });
7892
7893 JSONEditor.defaults.themes.bootstrap3 = JSONEditor.AbstractTheme.extend({
7894   getSelectInput: function(options) {
7895     var el = this._super(options);
7896     el.className += 'form-control';
7897     //el.style.width = 'auto';
7898     return el;
7899   },
7900   getGridContainer: function() {
7901             var el = document.createElement('div');
7902             el.className = 'container-fluid';
7903             el.style.padding = '4px';
7904             return el;
7905   },
7906   getGridRow: function() {
7907             var el = document.createElement('div');
7908             el.className = 'row-fluid';
7909             el.style.padding = '4px';
7910             return el;
7911   },
7912   setGridColumnSize: function(el,size) {
7913     el.className = 'col-md-'+size;
7914   },
7915   afterInputReady: function(input) {
7916     if(input.controlgroup) return;
7917     input.controlgroup = this.closest(input,'.form-group');
7918     if(this.closest(input,'.compact')) {
7919       input.controlgroup.style.marginBottom = 0;
7920     }
7921     if (this.queuedInputErrorText) {
7922         var text = this.queuedInputErrorText;
7923         delete this.queuedInputErrorText;
7924         this.addInputError(input,text);
7925     }
7926
7927     // TODO: use bootstrap slider
7928   },
7929   getRangeInput: function(min, max, step) {
7930     // TODO: use better slider
7931     return this._super(min, max, step);
7932   },
7933   getFormInputField: function(type) {
7934     var el = this._super(type);
7935     if(type !== 'checkbox') {
7936       el.className += 'form-control';
7937     }
7938     return el;
7939   },
7940   getFormControl: function(label, input, description, infoText) {
7941     var group = document.createElement('div');
7942
7943     if(label && input.type === 'checkbox') {
7944       group.className += ' checkbox';
7945       label.appendChild(input);
7946       label.style.fontSize = '12px';
7947       group.style.marginTop = '0';
7948       if(infoText) group.appendChild(infoText);
7949       group.appendChild(label);
7950       input.style.position = 'relative';
7951       input.style.cssFloat = 'left';
7952     }
7953     else {
7954       group.className += ' form-group';
7955       if(label) {
7956         label.className += ' control-label';
7957         group.appendChild(label);
7958       }
7959
7960       if(infoText) group.appendChild(infoText);
7961       group.appendChild(input);
7962     }
7963
7964     if(description) group.appendChild(description);
7965
7966     return group;
7967   },
7968   getIndentedPanel: function() {
7969     var el = document.createElement('div');
7970     el.className = 'well well-sm';
7971     el.style.padding = '4px';
7972     return el;
7973   },
7974   getInfoButton: function(text) {
7975     var icon = document.createElement('span');
7976     icon.className = "glyphicon glyphicon-info-sign pull-right";
7977     icon.style.padding = ".25rem";
7978     icon.style.position = "relative";
7979     icon.style.display = "inline-block";
7980
7981     var tooltip = document.createElement('span');
7982     tooltip.style["font-family"] = "sans-serif";
7983     tooltip.style.visibility = "hidden";
7984     tooltip.style["background-color"] = "rgba(50, 50, 50, .75)";
7985     tooltip.style.margin = "0 .25rem";
7986     tooltip.style.color = "#FAFAFA";
7987     tooltip.style.padding = ".5rem 1rem";
7988     tooltip.style["border-radius"] = ".25rem";
7989     tooltip.style.width = "25rem";
7990     tooltip.style.transform = "translateX(-27rem) translateY(-.5rem)";
7991     tooltip.style.position = "absolute";
7992     tooltip.innerText = text;
7993     icon.onmouseover = function() {
7994       tooltip.style.visibility = "visible";
7995     };
7996     icon.onmouseleave = function() {
7997       tooltip.style.visibility = "hidden";
7998     };
7999
8000     icon.appendChild(tooltip);
8001
8002     return icon;
8003   },
8004   getFormInputDescription: function(text) {
8005     var el = document.createElement('p');
8006     el.className = 'help-block';
8007     el.innerHTML = text;
8008     return el;
8009   },
8010   getHeaderButtonHolder: function() {
8011     var el = this.getButtonHolder();
8012     el.style.marginLeft = '5px';
8013     return el;
8014   },
8015   getButtonHolder: function() {
8016     var el = document.createElement('div');
8017     el.className = 'btn-group';
8018     return el;
8019   },
8020   getButton: function(text, icon, title) {
8021         var el =  this._super(text, icon, title);
8022     el.className += ' btn btn-default';
8023         el.style.backgroundColor = '#f2bfab';
8024         el.style.border = '1px solid #ddd';
8025         return el;
8026   },
8027   getTable: function() {
8028     var el = document.createElement('table');
8029     el.className = 'table table-bordered';
8030     el.style.width = 'auto';
8031     el.style.maxWidth = 'none';
8032     return el;
8033   },
8034
8035   addInputError: function(input,text) {
8036     if(!input.controlgroup) {
8037         this.queuedInputErrorText = text;
8038         return;
8039     }
8040     input.controlgroup.className = input.controlgroup.className.replace(/\s?has-error/g,'');
8041     input.controlgroup.className += ' has-error';
8042     if(!input.errmsg) {
8043       input.errmsg = document.createElement('p');
8044       input.errmsg.className = 'help-block errormsg';
8045       input.controlgroup.appendChild(input.errmsg);
8046     }
8047     else {
8048       input.errmsg.style.display = '';
8049     }
8050
8051     input.errmsg.textContent = text;
8052   },
8053   removeInputError: function(input) {
8054     if(!input.controlgroup) {
8055         delete this.queuedInputErrorText;
8056     }
8057     if(!input.errmsg) return;
8058     input.errmsg.style.display = 'none';
8059     input.controlgroup.className = input.controlgroup.className.replace(/\s?has-error/g,'');
8060   },
8061   getTabHolder: function(propertyName) {
8062     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8063     var el = document.createElement('div');
8064     el.innerHTML = "<div class='list-group pull-left' id='" + pName + "'></div><div class='col-sm-10 pull-left' id='" + pName + "'></div>";
8065     return el;
8066   },
8067   getTopTabHolder: function(propertyName) {
8068     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8069     var el = document.createElement('div');
8070     el.innerHTML = "<ul class='nav nav-tabs' style='padding: 4px;' id='" + pName + "'></ul><div class='tab-content' style='overflow:visible;' id='" + pName + "'></div>";
8071     return el;
8072   },
8073   getTab: function(text, tabId) {
8074     var el = document.createElement('a');
8075     el.className = 'list-group-item';
8076     el.setAttribute('href','#'+tabId);
8077     el.appendChild(text);
8078     return el;
8079   },
8080   getTopTab: function(text, tabId) {
8081     var el = document.createElement('li');
8082     var a = document.createElement('a');
8083     a.setAttribute('href','#'+tabId);
8084     a.appendChild(text);
8085     el.appendChild(a);
8086     return el;
8087   },
8088   markTabActive: function(row) {
8089     row.tab.className = row.tab.className.replace(/\s?active/g,'');
8090     row.tab.className += ' active';
8091     row.container.style.display = '';
8092   },
8093   markTabInactive: function(row) {
8094     row.tab.className = row.tab.className.replace(/\s?active/g,'');
8095     row.container.style.display = 'none';
8096   },
8097   getProgressBar: function() {
8098     var min = 0, max = 100, start = 0;
8099
8100     var container = document.createElement('div');
8101     container.className = 'progress';
8102
8103     var bar = document.createElement('div');
8104     bar.className = 'progress-bar';
8105     bar.setAttribute('role', 'progressbar');
8106     bar.setAttribute('aria-valuenow', start);
8107     bar.setAttribute('aria-valuemin', min);
8108     bar.setAttribute('aria-valuenax', max);
8109     bar.innerHTML = start + "%";
8110     container.appendChild(bar);
8111
8112     return container;
8113   },
8114   updateProgressBar: function(progressBar, progress) {
8115     if (!progressBar) return;
8116
8117     var bar = progressBar.firstChild;
8118     var percentage = progress + "%";
8119     bar.setAttribute('aria-valuenow', progress);
8120     bar.style.width = percentage;
8121     bar.innerHTML = percentage;
8122   },
8123   updateProgressBarUnknown: function(progressBar) {
8124     if (!progressBar) return;
8125
8126     var bar = progressBar.firstChild;
8127     progressBar.className = 'progress progress-striped active';
8128     bar.removeAttribute('aria-valuenow');
8129     bar.style.width = '100%';
8130     bar.innerHTML = '';
8131   }
8132 });
8133
8134 JSONEditor.defaults.themes.bootstrap4 = JSONEditor.AbstractTheme.extend({
8135   getSelectInput: function(options) {
8136     var el = this._super(options);
8137     el.className += "form-control";
8138     //el.style.width = 'auto';
8139     return el;
8140   },
8141   setGridColumnSize: function(el, size) {
8142     el.className = "col-md-" + size;
8143   },
8144   afterInputReady: function(input) {
8145     if (input.controlgroup) return;
8146     input.controlgroup = this.closest(input, ".form-group");
8147     if (this.closest(input, ".compact")) {
8148       input.controlgroup.style.marginBottom = 0;
8149     }
8150
8151     // TODO: use bootstrap slider
8152   },
8153   getTextareaInput: function() {
8154     var el = document.createElement("textarea");
8155     el.className = "form-control";
8156     return el;
8157   },
8158   getRangeInput: function(min, max, step) {
8159     // TODO: use better slider
8160     return this._super(min, max, step);
8161   },
8162   getFormInputField: function(type) {
8163     var el = this._super(type);
8164     if (type !== "checkbox") {
8165       el.className += "form-control";
8166     }
8167     return el;
8168   },
8169   getFormControl: function(label, input, description) {
8170     var group = document.createElement("div");
8171
8172     if (label && input.type === "checkbox") {
8173       group.className += " checkbox";
8174       label.appendChild(input);
8175       label.style.fontSize = "12px";
8176       group.style.marginTop = "0";
8177       group.appendChild(label);
8178       input.style.position = "relative";
8179       input.style.cssFloat = "left";
8180     } else {
8181       group.className += " form-group";
8182       if (label) {
8183         label.className += " form-control-label";
8184         group.appendChild(label);
8185       }
8186       group.appendChild(input);
8187     }
8188
8189     if (description) group.appendChild(description);
8190
8191     return group;
8192   },
8193   getIndentedPanel: function() {
8194     var el = document.createElement("div");
8195     el.className = "card card-body bg-light";
8196     return el;
8197   },
8198   getFormInputDescription: function(text) {
8199     var el = document.createElement("p");
8200     el.className = "form-text";
8201     el.innerHTML = text;
8202     return el;
8203   },
8204   getHeaderButtonHolder: function() {
8205     var el = this.getButtonHolder();
8206     el.style.marginLeft = "10px";
8207     return el;
8208   },
8209   getButtonHolder: function() {
8210     var el = document.createElement("div");
8211     el.className = "btn-group";
8212     return el;
8213   },
8214   getButton: function(text, icon, title) {
8215     var el = this._super(text, icon, title);
8216     el.className += "btn btn-secondary";
8217     return el;
8218   },
8219   getTable: function() {
8220     var el = document.createElement("table");
8221     el.className = "table-bordered table-sm";
8222     el.style.width = "auto";
8223     el.style.maxWidth = "none";
8224     return el;
8225   },
8226
8227   addInputError: function(input, text) {
8228     if (!input.controlgroup) return;
8229     input.controlgroup.className += " has-error";
8230     if (!input.errmsg) {
8231       input.errmsg = document.createElement("p");
8232       input.errmsg.className = "form-text errormsg";
8233       input.controlgroup.appendChild(input.errmsg);
8234     } else {
8235       input.errmsg.style.display = "";
8236     }
8237
8238     input.errmsg.textContent = text;
8239   },
8240   removeInputError: function(input) {
8241     if (!input.errmsg) return;
8242     input.errmsg.style.display = "none";
8243     input.controlgroup.className = input.controlgroup.className.replace(
8244       /\s?has-error/g,
8245       ""
8246     );
8247   },
8248   getTabHolder: function(propertyName) {
8249     var el = document.createElement("div");
8250     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8251     el.innerHTML =
8252       "<ul class='nav flex-column nav-pills col-md-2' style='padding: 0px;' id='" + pName + "'></ul><div class='tab-content col-md-10' style='padding:5px;' id='" + pName + "'></div>";
8253 el.className = "row";
8254     return el;
8255   },
8256   getTopTabHolder: function(propertyName) {
8257     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8258     var el = document.createElement('div');
8259     el.innerHTML = "<ul class='nav nav-tabs' id='" + pName + "'></ul><div class='card-body' id='" + pName + "'></div>";
8260     return el;
8261   },
8262   getTab: function(text,tabId) {
8263     var liel = document.createElement('li');
8264     liel.className = 'nav-item';
8265     var ael = document.createElement("a");
8266     ael.className = "nav-link";
8267     ael.setAttribute("style",'padding:10px;');
8268     ael.setAttribute("href", "#" + tabId);
8269     ael.appendChild(text);
8270     liel.appendChild(ael);
8271     return liel;
8272   },
8273   getTopTab: function(text, tabId) {
8274     var el = document.createElement('li');
8275     el.className = 'nav-item';
8276     var a = document.createElement('a');
8277     a.className = 'nav-link';
8278     a.setAttribute('href','#'+tabId);
8279     a.appendChild(text);
8280     el.appendChild(a);
8281     return el;
8282   },
8283   markTabActive: function(row) {
8284     var el = row.tab.firstChild;
8285     el.className = el.className.replace(/\s?active/g,'');
8286     el.className += " active";
8287     row.container.style.display = '';
8288   },
8289   markTabInactive: function(row) {
8290     var el = row.tab.firstChild;
8291     el.className = el.className.replace(/\s?active/g,'');
8292     row.container.style.display = 'none';
8293   },
8294   getProgressBar: function() {
8295     var min = 0,
8296       max = 100,
8297       start = 0;
8298
8299     var container = document.createElement("div");
8300     container.className = "progress";
8301
8302     var bar = document.createElement("div");
8303     bar.className = "progress-bar";
8304     bar.setAttribute("role", "progressbar");
8305     bar.setAttribute("aria-valuenow", start);
8306     bar.setAttribute("aria-valuemin", min);
8307     bar.setAttribute("aria-valuenax", max);
8308     bar.innerHTML = start + "%";
8309     container.appendChild(bar);
8310
8311     return container;
8312   },
8313   updateProgressBar: function(progressBar, progress) {
8314     if (!progressBar) return;
8315
8316     var bar = progressBar.firstChild;
8317     var percentage = progress + "%";
8318     bar.setAttribute("aria-valuenow", progress);
8319     bar.style.width = percentage;
8320     bar.innerHTML = percentage;
8321   },
8322   updateProgressBarUnknown: function(progressBar) {
8323     if (!progressBar) return;
8324
8325     var bar = progressBar.firstChild;
8326     progressBar.className = "progress progress-striped active";
8327     bar.removeAttribute("aria-valuenow");
8328     bar.style.width = "100%";
8329     bar.innerHTML = "";
8330   }
8331 });
8332
8333 // Base Foundation theme
8334 JSONEditor.defaults.themes.foundation = JSONEditor.AbstractTheme.extend({
8335   getChildEditorHolder: function() {
8336     var el = document.createElement('div');
8337     el.style.marginBottom = '15px';
8338     return el;
8339   },
8340   getSelectInput: function(options) {
8341     var el = this._super(options);
8342     el.style.minWidth = 'none';
8343     el.style.padding = '5px';
8344     el.style.marginTop = '3px';
8345     return el;
8346   },
8347   getSwitcher: function(options) {
8348     var el = this._super(options);
8349     el.style.paddingRight = '8px';
8350     return el;
8351   },
8352   afterInputReady: function(input) {
8353     if(input.group) return;
8354     if(this.closest(input,'.compact')) {
8355       input.style.marginBottom = 0;
8356     }
8357     input.group = this.closest(input,'.form-control');
8358     if (this.queuedInputErrorText) {
8359         var text = this.queuedInputErrorText;
8360         delete this.queuedInputErrorText;
8361         this.addInputError(input,text);
8362     }
8363   },
8364   getFormInputLabel: function(text) {
8365     var el = this._super(text);
8366     el.style.display = 'inline-block';
8367     return el;
8368   },
8369   getFormInputField: function(type) {
8370     var el = this._super(type);
8371     el.style.width = '100%';
8372     el.style.marginBottom = type==='checkbox'? '0' : '12px';
8373     return el;
8374   },
8375   getFormInputDescription: function(text) {
8376     var el = document.createElement('p');
8377     el.textContent = text;
8378     el.style.marginTop = '-10px';
8379     el.style.fontStyle = 'italic';
8380     return el;
8381   },
8382   getIndentedPanel: function() {
8383     var el = document.createElement('div');
8384     el.className = 'panel';
8385     el.style.paddingBottom = 0;
8386     return el;
8387   },
8388   getHeaderButtonHolder: function() {
8389     var el = this.getButtonHolder();
8390     el.style.display = 'inline-block';
8391     el.style.marginLeft = '10px';
8392     el.style.verticalAlign = 'middle';
8393     return el;
8394   },
8395   getButtonHolder: function() {
8396     var el = document.createElement('div');
8397     el.className = 'button-group';
8398     return el;
8399   },
8400   getButton: function(text, icon, title) {
8401     var el = this._super(text, icon, title);
8402     el.className += ' small button';
8403     return el;
8404   },
8405   addInputError: function(input,text) {
8406     if(!input.group) {
8407         this.queuedInputErrorText = text;
8408         return;
8409     }
8410     input.group.className += ' error';
8411
8412     if(!input.errmsg) {
8413       input.insertAdjacentHTML('afterend','<small class="error"></small>');
8414       input.errmsg = input.parentNode.getElementsByClassName('error')[0];
8415     }
8416     else {
8417       input.errmsg.style.display = '';
8418     }
8419
8420     input.errmsg.textContent = text;
8421   },
8422   removeInputError: function(input) {
8423     if(!input.group) {
8424         delete this.queuedInputErrorText;
8425     }
8426     if(!input.errmsg) return;
8427     input.group.className = input.group.className.replace(/ error/g,'');
8428     input.errmsg.style.display = 'none';
8429   },
8430   getProgressBar: function() {
8431     var progressBar = document.createElement('div');
8432     progressBar.className = 'progress';
8433
8434     var meter = document.createElement('span');
8435     meter.className = 'meter';
8436     meter.style.width = '0%';
8437     progressBar.appendChild(meter);
8438     return progressBar;
8439   },
8440   updateProgressBar: function(progressBar, progress) {
8441     if (!progressBar) return;
8442     progressBar.firstChild.style.width = progress + '%';
8443   },
8444   updateProgressBarUnknown: function(progressBar) {
8445     if (!progressBar) return;
8446     progressBar.firstChild.style.width = '100%';
8447   }
8448 });
8449
8450 // Foundation 3 Specific Theme
8451 JSONEditor.defaults.themes.foundation3 = JSONEditor.defaults.themes.foundation.extend({
8452   getHeaderButtonHolder: function() {
8453     var el = this._super();
8454     el.style.fontSize = '.6em';
8455     return el;
8456   },
8457   getFormInputLabel: function(text) {
8458     var el = this._super(text);
8459     el.style.fontWeight = 'bold';
8460     return el;
8461   },
8462   getTabHolder: function(propertyName) {
8463     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8464     var el = document.createElement('div');
8465     el.className = 'row';
8466     el.innerHTML = '<dl class="tabs vertical two columns" id="' + pName + '"></dl><div class="tabs-content ten columns" id="' + pName + '"></div>';
8467     return el;
8468   },
8469   getTopTabHolder: function(propertyName) {
8470     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8471     var el = document.createElement('div');
8472     el.className = 'row';
8473     el.innerHTML = '<dl class="tabs horizontal" style="padding-left: 10px; margin-left: 10px;" id="' + pName + '"></dl><div class="tabs-content twelve columns" style="padding: 10px; margin-left: 10px;" id="' + pName + '"></div>';
8474     return el;
8475   },
8476   setGridColumnSize: function(el,size) {
8477     var sizes = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve'];
8478     el.className = 'columns '+sizes[size];
8479   },
8480   getTab: function(text, tabId) {
8481     var el = document.createElement('dd');
8482     var a = document.createElement('a');
8483     a.setAttribute('href','#'+tabId);
8484     a.appendChild(text);
8485     el.appendChild(a);
8486     return el;
8487   },
8488   getTopTab: function(text, tabId) {
8489     var el = document.createElement('dd');
8490     var a = document.createElement('a');
8491     a.setAttribute('href','#'+tabId);
8492     a.appendChild(text);
8493     el.appendChild(a);
8494     return el;
8495   },
8496   getTabContentHolder: function(tab_holder) {
8497     return tab_holder.children[1];
8498   },
8499   getTopTabContentHolder: function(tab_holder) {
8500     return tab_holder.children[1];
8501   },
8502   getTabContent: function() {
8503     var el = document.createElement('div');
8504     el.className = 'content active';
8505     el.style.paddingLeft = '5px';
8506     return el;
8507   },
8508   getTopTabContent: function() {
8509     var el = document.createElement('div');
8510     el.className = 'content active';
8511     el.style.paddingLeft = '5px';
8512     return el;
8513   },
8514   markTabActive: function(row) {
8515     row.tab.className = row.tab.className.replace(/\s?active/g,'');
8516     row.tab.className += ' active';
8517     row.container.style.display = '';
8518   },
8519   markTabInactive: function(row) {
8520     row.tab.className = row.tab.className.replace(/\s?active/g,'');
8521     row.container.style.display = 'none';
8522   },
8523   addTab: function(holder, tab) {
8524     holder.children[0].appendChild(tab);
8525   },
8526   addTopTab: function(holder, tab) {
8527     holder.children[0].appendChild(tab);
8528   }
8529 });
8530
8531 // Foundation 4 Specific Theme
8532 JSONEditor.defaults.themes.foundation4 = JSONEditor.defaults.themes.foundation.extend({
8533   getHeaderButtonHolder: function() {
8534     var el = this._super();
8535     el.style.fontSize = '.6em';
8536     return el;
8537   },
8538   setGridColumnSize: function(el,size) {
8539     el.className = 'columns large-'+size;
8540   },
8541   getFormInputDescription: function(text) {
8542     var el = this._super(text);
8543     el.style.fontSize = '.8rem';
8544     return el;
8545   },
8546   getFormInputLabel: function(text) {
8547     var el = this._super(text);
8548     el.style.fontWeight = 'bold';
8549     return el;
8550   }
8551 });
8552
8553 // Foundation 5 Specific Theme
8554 JSONEditor.defaults.themes.foundation5 = JSONEditor.defaults.themes.foundation.extend({
8555   getFormInputDescription: function(text) {
8556     var el = this._super(text);
8557     el.style.fontSize = '.8rem';
8558     return el;
8559   },
8560   setGridColumnSize: function(el,size) {
8561     el.className = 'columns medium-'+size;
8562   },
8563   getButton: function(text, icon, title) {
8564     var el = this._super(text,icon,title);
8565     el.className = el.className.replace(/\s*small/g,'') + ' tiny';
8566     return el;
8567   },
8568   getTabHolder: function(propertyName) {
8569     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8570     var el = document.createElement('div');
8571     el.innerHTML = '<dl class="tabs vertical" id="' + pName + '"></dl><div class="tabs-content vertical" id="' + pName + '"></div>';
8572     return el;
8573   },
8574   getTopTabHolder: function(propertyName) {
8575     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8576     var el = document.createElement('div');
8577     el.className = 'row';
8578     el.innerHTML = '<dl class="tabs horizontal" style="padding-left: 10px;" id="' + pName + '"></dl><div class="tabs-content horizontal" style="padding: 10px;" id="' + pName + '"></div>';
8579     return el;
8580   },
8581   getTab: function(text, tabId) {
8582     var el = document.createElement('dd');
8583     var a = document.createElement('a');
8584     a.setAttribute('href','#'+tabId);
8585     a.appendChild(text);
8586     el.appendChild(a);
8587     return el;
8588   },
8589   getTopTab: function(text, tabId) {
8590     var el = document.createElement('dd');
8591     var a = document.createElement('a');
8592     a.setAttribute('href','#'+tabId);
8593     a.appendChild(text);
8594     el.appendChild(a);
8595     return el;
8596   },
8597   getTabContentHolder: function(tab_holder) {
8598     return tab_holder.children[1];
8599   },
8600   getTopTabContentHolder: function(tab_holder) {
8601     return tab_holder.children[1];
8602   },
8603   getTabContent: function() {
8604     var el = document.createElement('div');
8605     el.className = 'tab-content active';
8606     el.style.paddingLeft = '5px';
8607     return el;
8608   },
8609   getTopTabContent: function() {
8610     var el = document.createElement('div');
8611     el.className = 'tab-content active';
8612     el.style.paddingLeft = '5px';
8613     return el;
8614   },
8615   markTabActive: function(row) {
8616     row.tab.className = row.tab.className.replace(/\s?active/g,'');
8617     row.tab.className += ' active';
8618     row.container.style.display = '';
8619   },
8620   markTabInactive: function(row) {
8621     row.tab.className = row.tab.className.replace(/\s?active/g,'');
8622     row.container.style.display = 'none';
8623   },
8624   addTab: function(holder, tab) {
8625     holder.children[0].appendChild(tab);
8626   },
8627   addTopTab: function(holder, tab) {
8628     holder.children[0].appendChild(tab);
8629   }
8630
8631 });
8632
8633 JSONEditor.defaults.themes.foundation6 = JSONEditor.defaults.themes.foundation5.extend({
8634   getIndentedPanel: function() {
8635     var el = document.createElement('div');
8636     el.className = 'callout secondary';
8637     el.className.style = 'padding-left: 10px; margin-left: 10px;';
8638     return el;
8639   },
8640   getButtonHolder: function() {
8641     var el = document.createElement('div');
8642     el.className = 'button-group tiny';
8643     el.style.marginBottom = 0;
8644     return el;
8645   },
8646   getFormInputLabel: function(text) {
8647     var el = this._super(text);
8648     el.style.display = 'block';
8649     return el;
8650   },
8651   getFormControl: function(label, input, description, infoText) {
8652     var el = document.createElement('div');
8653     el.className = 'form-control';
8654     if(label) el.appendChild(label);
8655     if(input.type === 'checkbox') {
8656       label.insertBefore(input,label.firstChild);
8657     }
8658     else if (label) {
8659       if(infoText) label.appendChild(infoText);
8660       label.appendChild(input);
8661     } else {
8662       if(infoText) el.appendChild(infoText);
8663       el.appendChild(input);
8664     }
8665
8666     if(description) label.appendChild(description);
8667     return el;
8668   },
8669   addInputError: function(input,text) {
8670     if(!input.group) return;
8671     input.group.className += ' error';
8672
8673     if(!input.errmsg) {
8674       var errorEl = document.createElement('span');
8675       errorEl.className = 'form-error is-visible';
8676       input.group.getElementsByTagName('label')[0].appendChild(errorEl);
8677
8678       input.className = input.className + ' is-invalid-input';
8679
8680       input.errmsg = errorEl;
8681     }
8682     else {
8683       input.errmsg.style.display = '';
8684       input.className = '';
8685     }
8686
8687     input.errmsg.textContent = text;
8688   },
8689   removeInputError: function(input) {
8690     if(!input.errmsg) return;
8691     input.className = input.className.replace(/ is-invalid-input/g,'');
8692     if(input.errmsg.parentNode) {
8693       input.errmsg.parentNode.removeChild(input.errmsg);
8694     }
8695   },
8696   getTabHolder: function(propertyName) {
8697     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8698     var el = document.createElement('div');
8699     el.className = 'grid-x';
8700     el.innerHTML = '<div class="medium-2 cell" style="float: left;"><ul class="vertical tabs" data-tabs id="' + pName + '"></ul></div><div class="medium-10 cell" style="float: left;"><div class="tabs-content" data-tabs-content="'+pName+'"></div></div>';
8701     return el;
8702   },
8703   getTopTabHolder: function(propertyName) {
8704     var pName = (typeof propertyName === 'undefined')? "" : propertyName;
8705     var el = document.createElement('div');
8706     el.className = 'grid-y';
8707     el.innerHTML = '<div className="cell"><ul class="tabs" data-tabs id="' + pName + '"></ul><div class="tabs-content" data-tabs-content="' + pName + '"></div></div>';
8708     return el;
8709
8710
8711   },
8712   insertBasicTopTab: function(tab, newTabs_holder ) {
8713     newTabs_holder.firstChild.firstChild.insertBefore(tab,newTabs_holder.firstChild.firstChild.firstChild);
8714   },
8715   getTab: function(text, tabId) {
8716     var el = document.createElement('li');
8717     el.className = 'tabs-title';
8718     var a = document.createElement('a');
8719     a.setAttribute('href','#'+tabId);
8720     a.appendChild(text);
8721     el.appendChild(a);
8722     return el;
8723   },
8724   getTopTab: function(text, tabId) {
8725     var el = document.createElement('li');
8726     el.className = 'tabs-title';
8727     var a = document.createElement('a');
8728     a.setAttribute('href','#' + tabId);
8729     a.appendChild(text);
8730     el.appendChild(a);
8731     return el;
8732   },
8733   getTabContentHolder: function(tab_holder) {
8734     return tab_holder.children[1].firstChild;
8735   },
8736   getTopTabContentHolder: function(tab_holder) {
8737     return tab_holder.firstChild.children[1];
8738   },
8739   getTabContent: function() {
8740     var el = document.createElement('div');
8741     el.className = 'tabs-panel';
8742     el.style.paddingLeft = '5px';
8743     return el;
8744   },
8745   getTopTabContent: function() {
8746     var el = document.createElement('div');
8747     el.className = 'tabs-panel';
8748     el.style.paddingLeft = '5px';
8749     return el;
8750   },
8751   markTabActive: function(row) {
8752     row.tab.className = row.tab.className.replace(/\s?is-active/g,'');
8753     row.tab.className += ' is-active';
8754     row.tab.firstChild.setAttribute('aria-selected', 'true');
8755
8756     row.container.className  = row.container.className.replace(/\s?is-active/g,'');
8757     row.container.className += ' is-active';
8758     row.container.setAttribute('aria-selected', 'true');
8759   },
8760   markTabInactive: function(row) {
8761     row.tab.className = row.tab.className.replace(/\s?is-active/g,'');
8762     row.tab.firstChild.removeAttribute('aria-selected');
8763
8764     row.container.className = row.container.className.replace(/\s?is-active/g,'');
8765     row.container.removeAttribute('aria-selected');
8766   },
8767   addTab: function(holder, tab) {
8768     holder.children[0].firstChild.appendChild(tab);
8769   },
8770   addTopTab: function(holder, tab) {
8771     holder.firstChild.children[0].appendChild(tab);
8772   },
8773   getFirstTab: function(holder){
8774     return holder.firstChild.firstChild.firstChild;
8775   }
8776 });
8777
8778 JSONEditor.defaults.themes.html = JSONEditor.AbstractTheme.extend({
8779   getFormInputLabel: function(text) {
8780     var el = this._super(text);
8781     el.style.display = 'block';
8782     el.style.marginBottom = '3px';
8783     el.style.fontWeight = 'bold';
8784     return el;
8785   },
8786   getFormInputDescription: function(text) {
8787     var el = this._super(text);
8788     el.style.fontSize = '.8em';
8789     el.style.margin = 0;
8790     el.style.display = 'inline-block';
8791     el.style.fontStyle = 'italic';
8792     return el;
8793   },
8794   getIndentedPanel: function() {
8795     var el = this._super();
8796     el.style.border = '1px solid #ddd';
8797     el.style.padding = '5px';
8798     el.style.margin = '10px';
8799     el.style.borderRadius = '3px';
8800     return el;
8801   },
8802   getTopIndentedPanel: function() {
8803     return this.getIndentedPanel();
8804   },
8805   getChildEditorHolder: function() {
8806     var el = this._super();
8807     el.style.marginBottom = '8px';
8808     return el;
8809   },
8810   getHeaderButtonHolder: function() {
8811     var el = this.getButtonHolder();
8812     el.style.display = 'inline-block';
8813     el.style.marginLeft = '10px';
8814     el.style.fontSize = '.8em';
8815     el.style.verticalAlign = 'middle';
8816     return el;
8817   },
8818   getTable: function() {
8819     var el = this._super();
8820     el.style.borderBottom = '1px solid #ccc';
8821     el.style.marginBottom = '5px';
8822     return el;
8823   },
8824   addInputError: function(input, text) {
8825     input.style.borderColor = 'red';
8826     
8827     if(!input.errmsg) {
8828       var group = this.closest(input,'.form-control');
8829       input.errmsg = document.createElement('div');
8830       input.errmsg.setAttribute('class','errmsg');
8831       input.errmsg.style = input.errmsg.style || {};
8832       input.errmsg.style.color = 'red';
8833       group.appendChild(input.errmsg);
8834     }
8835     else {
8836       input.errmsg.style.display = 'block';
8837     }
8838     
8839     input.errmsg.innerHTML = '';
8840     input.errmsg.appendChild(document.createTextNode(text));
8841   },
8842   removeInputError: function(input) {
8843     input.style.borderColor = '';
8844     if(input.errmsg) input.errmsg.style.display = 'none';
8845   },
8846   getProgressBar: function() {
8847     var max = 100, start = 0;
8848
8849     var progressBar = document.createElement('progress');
8850     progressBar.setAttribute('max', max);
8851     progressBar.setAttribute('value', start);
8852     return progressBar;
8853   },
8854   updateProgressBar: function(progressBar, progress) {
8855     if (!progressBar) return;
8856     progressBar.setAttribute('value', progress);
8857   },
8858   updateProgressBarUnknown: function(progressBar) {
8859     if (!progressBar) return;
8860     progressBar.removeAttribute('value');
8861   }
8862 });
8863
8864 JSONEditor.defaults.themes.jqueryui = JSONEditor.AbstractTheme.extend({
8865   getTable: function() {
8866     var el = this._super();
8867     el.setAttribute('cellpadding',5);
8868     el.setAttribute('cellspacing',0);
8869     return el;
8870   },
8871   getTableHeaderCell: function(text) {
8872     var el = this._super(text);
8873     el.className = 'ui-state-active';
8874     el.style.fontWeight = 'bold';
8875     return el;
8876   },
8877   getTableCell: function() {
8878     var el = this._super();
8879     el.className = 'ui-widget-content';
8880     return el;
8881   },
8882   getHeaderButtonHolder: function() {
8883     var el = this.getButtonHolder();
8884     el.style.marginLeft = '10px';
8885     el.style.fontSize = '.6em';
8886     el.style.display = 'inline-block';
8887     return el;
8888   },
8889   getFormInputDescription: function(text) {
8890     var el = this.getDescription(text);
8891     el.style.marginLeft = '10px';
8892     el.style.display = 'inline-block';
8893     return el;
8894   },
8895   getFormControl: function(label, input, description, infoText) {
8896     var el = this._super(label,input,description, infoText);
8897     if(input.type === 'checkbox') {
8898       el.style.lineHeight = '25px';
8899
8900       el.style.padding = '3px 0';
8901     }
8902     else {
8903       el.style.padding = '4px';
8904     }
8905     return el;
8906   },
8907   getDescription: function(text) {
8908     var el = document.createElement('span');
8909     el.style.fontSize = '.8em';
8910     el.style.fontStyle = 'italic';
8911     el.textContent = text;
8912     return el;
8913   },
8914   getButtonHolder: function() {
8915     var el = document.createElement('div');
8916     el.className = 'ui-buttonset';
8917     el.style.fontSize = '.7em';
8918     return el;
8919   },
8920   getFormInputLabel: function(text) {
8921     var el = document.createElement('label');
8922     el.style.fontWeight = 'bold';
8923     el.style.display = 'block';
8924     el.textContent = text;
8925     return el;
8926   },
8927   getButton: function(text, icon, title) {
8928     var button = document.createElement("button");
8929     button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
8930
8931     // Icon only
8932     if(icon && !text) {
8933       button.className += ' ui-button-icon-only';
8934       icon.className += ' ui-button-icon-primary ui-icon-primary';
8935       button.appendChild(icon);
8936     }
8937     // Icon and Text
8938     else if(icon) {
8939       button.className += ' ui-button-text-icon-primary';
8940       icon.className += ' ui-button-icon-primary ui-icon-primary';
8941       button.appendChild(icon);
8942     }
8943     // Text only
8944     else {
8945       button.className += ' ui-button-text-only';
8946     }
8947
8948     var el = document.createElement('span');
8949     el.className = 'ui-button-text';
8950     el.textContent = text||title||".";
8951     button.appendChild(el);
8952
8953     button.setAttribute('title',title);
8954
8955     return button;
8956   },
8957   setButtonText: function(button,text, icon, title) {
8958     button.innerHTML = '';
8959     button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
8960
8961     // Icon only
8962     if(icon && !text) {
8963       button.className += ' ui-button-icon-only';
8964       icon.className += ' ui-button-icon-primary ui-icon-primary';
8965       button.appendChild(icon);
8966     }
8967     // Icon and Text
8968     else if(icon) {
8969       button.className += ' ui-button-text-icon-primary';
8970       icon.className += ' ui-button-icon-primary ui-icon-primary';
8971       button.appendChild(icon);
8972     }
8973     // Text only
8974     else {
8975       button.className += ' ui-button-text-only';
8976     }
8977
8978     var el = document.createElement('span');
8979     el.className = 'ui-button-text';
8980     el.textContent = text||title||".";
8981     button.appendChild(el);
8982
8983     button.setAttribute('title',title);
8984   },
8985   getIndentedPanel: function() {
8986     var el = document.createElement('div');
8987     el.className = 'ui-widget-content ui-corner-all';
8988     el.style.padding = '1em 1.4em';
8989     el.style.marginBottom = '20px';
8990     return el;
8991   },
8992   afterInputReady: function(input) {
8993     if(input.controls) return;
8994     input.controls = this.closest(input,'.form-control');
8995     if (this.queuedInputErrorText) {
8996         var text = this.queuedInputErrorText;
8997         delete this.queuedInputErrorText;
8998         this.addInputError(input,text);
8999     }
9000   },
9001   addInputError: function(input,text) {
9002     if(!input.controls) {
9003         this.queuedInputErrorText = text;
9004         return;
9005     }
9006     if(!input.errmsg) {
9007       input.errmsg = document.createElement('div');
9008       input.errmsg.className = 'ui-state-error';
9009       input.controls.appendChild(input.errmsg);
9010     }
9011     else {
9012       input.errmsg.style.display = '';
9013     }
9014
9015     input.errmsg.textContent = text;
9016   },
9017   removeInputError: function(input) {
9018     if(!input.controls) {
9019         delete this.queuedInputErrorText;
9020     }
9021     if(!input.errmsg) return;
9022     input.errmsg.style.display = 'none';
9023   },
9024   markTabActive: function(row) {
9025     row.tab.className = row.tab.className.replace(/\s?ui-widget-header/g,'').replace(/\s?ui-state-active/g,'')+' ui-state-active';
9026     row.container.style.display = '';
9027   },
9028   markTabInactive: function(row) {
9029     row.tab.className = row.tab.className.replace(/\s?ui-state-active/g,'').replace(/\s?ui-widget-header/g,'')+' ui-widget-header';
9030     row.container.style.display = 'none';
9031   }
9032 });
9033
9034 JSONEditor.defaults.themes.barebones = JSONEditor.AbstractTheme.extend({
9035     getFormInputLabel: function (text) {
9036         var el = this._super(text);
9037         return el;
9038     },
9039     getFormInputDescription: function (text) {
9040         var el = this._super(text);
9041         return el;
9042     },
9043     getIndentedPanel: function () {
9044         var el = this._super();
9045         return el;
9046     },
9047     getChildEditorHolder: function () {
9048         var el = this._super();
9049         return el;
9050     },
9051     getHeaderButtonHolder: function () {
9052         var el = this.getButtonHolder();
9053         return el;
9054     },
9055     getTable: function () {
9056         var el = this._super();
9057         return el;
9058     },
9059     addInputError: function (input, text) {
9060         if (!input.errmsg) {
9061             var group = this.closest(input, '.form-control');
9062             input.errmsg = document.createElement('div');
9063             input.errmsg.setAttribute('class', 'errmsg');
9064             group.appendChild(input.errmsg);
9065         }
9066         else {
9067             input.errmsg.style.display = 'block';
9068         }
9069
9070         input.errmsg.innerHTML = '';
9071         input.errmsg.appendChild(document.createTextNode(text));
9072     },
9073     removeInputError: function (input) {
9074         input.style.borderColor = '';
9075         if (input.errmsg) input.errmsg.style.display = 'none';
9076     },
9077     getProgressBar: function () {
9078         var max = 100, start = 0;
9079
9080         var progressBar = document.createElement('progress');
9081         progressBar.setAttribute('max', max);
9082         progressBar.setAttribute('value', start);
9083         return progressBar;
9084     },
9085     updateProgressBar: function (progressBar, progress) {
9086         if (!progressBar) return;
9087         progressBar.setAttribute('value', progress);
9088     },
9089     updateProgressBarUnknown: function (progressBar) {
9090         if (!progressBar) return;
9091         progressBar.removeAttribute('value');
9092     }
9093 });
9094
9095 JSONEditor.defaults.themes.materialize = JSONEditor.AbstractTheme.extend({
9096
9097     /**
9098      * Applies grid size to specified element.
9099      * 
9100      * @param {HTMLElement} el The DOM element to have specified size applied.
9101      * @param {int} size The grid column size.
9102      * @see http://materializecss.com/grid.html
9103      */
9104     setGridColumnSize: function(el, size) {
9105         el.className = 'col s' + size;
9106     },
9107
9108     /**
9109      * Gets a wrapped button element for a header.
9110      * 
9111      * @returns {HTMLElement} The wrapped button element.
9112      */
9113     getHeaderButtonHolder: function() {
9114         return this.getButtonHolder();
9115     },
9116
9117     /**
9118      * Gets a wrapped button element.
9119      * 
9120      * @returns {HTMLElement} The wrapped button element.
9121      */
9122     getButtonHolder: function() {
9123         return document.createElement('span');
9124     },
9125
9126     /**
9127      * Gets a single button element.
9128      * 
9129      * @param {string} text The button text.
9130      * @param {HTMLElement} icon The icon object.
9131      * @param {string} title The button title.
9132      * @returns {HTMLElement} The button object.
9133      * @see http://materializecss.com/buttons.html
9134      */
9135     getButton: function(text, icon, title) {
9136
9137         // Prepare icon.
9138         if (text) {
9139             icon.className += ' left';
9140             icon.style.marginRight = '5px';
9141         }
9142
9143         // Create and return button.
9144         var el = this._super(text, icon, title);
9145         el.className = 'waves-effect waves-light btn';
9146         el.style.fontSize = '0.75rem';
9147         el.style.height = '20px';
9148         el.style.lineHeight = '20px';
9149         el.style.marginLeft = '4px';
9150         el.style.padding = '0 0.5rem';
9151         return el;
9152
9153     },
9154
9155     /**
9156      * Gets a form control object consisiting of several sub objects.
9157      * 
9158      * @param {HTMLElement} label The label element.
9159      * @param {HTMLElement} input The input element.
9160      * @param {string} description The element description.
9161      * @param {string} infoText The element information text.
9162      * @returns {HTMLElement} The assembled DOM element.
9163      * @see http://materializecss.com/forms.html
9164      */
9165     getFormControl: function(label, input, description, infoText) {
9166
9167         var ctrl,
9168             type = input.type;
9169
9170         // Checkboxes get wrapped in p elements.
9171         if (type && type === 'checkbox') {
9172
9173             ctrl = document.createElement('p');
9174             ctrl.appendChild(input);
9175             if (label) {
9176                 label.setAttribute('for', input.id);
9177                 ctrl.appendChild(label);
9178             }
9179             return ctrl;
9180
9181         }
9182
9183         // Anything else gets wrapped in divs.
9184         ctrl = this._super(label, input, description, infoText);
9185
9186         // Not .input-field for select wrappers.
9187         if (!type || !type.startsWith('select'))
9188             ctrl.className = 'input-field';
9189
9190         // Color needs special attention.
9191         if (type && type === 'color') {
9192             input.style.height = '3rem';
9193             input.style.width = '100%';
9194             input.style.margin = '5px 0 20px 0';
9195             input.style.padding = '3px';
9196
9197             if (label) {
9198                 label.style.transform = 'translateY(-14px) scale(0.8)';
9199                 label.style['-webkit-transform'] = 'translateY(-14px) scale(0.8)';
9200                 label.style['-webkit-transform-origin'] = '0 0';
9201                 label.style['transform-origin'] = '0 0';
9202             }
9203         }
9204
9205         return ctrl;
9206
9207     },
9208
9209     getDescription: function(text) {
9210         var el = document.createElement('div');
9211         el.className = 'grey-text';
9212         el.style.marginTop = '-15px';
9213         el.innerHTML = text;
9214         return el;
9215     },
9216
9217     /**
9218      * Gets a header element.
9219      * 
9220      * @param {string|HTMLElement} text The header text or element.
9221      * @returns {HTMLElement} The header element.
9222      */
9223     getHeader: function(text) {
9224
9225         var el = document.createElement('h5');
9226
9227         if (typeof text === 'string') {
9228           el.textContent = text;
9229         } else {
9230           el.appendChild(text);
9231         }
9232     
9233         return el;
9234
9235     },
9236
9237     getChildEditorHolder: function() {
9238
9239         var el = document.createElement('div');
9240         el.marginBottom = '10px';
9241         return el;
9242
9243     },
9244
9245     getIndentedPanel: function() {
9246         var el = document.createElement("div");
9247         el.className = "card-panel";
9248         return el;
9249     },
9250
9251     getTable: function() {
9252
9253         var el = document.createElement('table');
9254         el.className = 'striped bordered';
9255         el.style.marginBottom = '10px';
9256         return el;
9257
9258     },
9259
9260     getTableRow: function() {
9261         return document.createElement('tr');
9262     },
9263
9264     getTableHead: function() {
9265         return document.createElement('thead');
9266     },
9267
9268     getTableBody: function() {
9269         return document.createElement('tbody');
9270     },
9271
9272     getTableHeaderCell: function(text) {
9273
9274         var el = document.createElement('th');
9275         el.textContent = text;
9276         return el;
9277
9278     },
9279
9280     getTableCell: function() {
9281
9282         var el = document.createElement('td');
9283         return el;
9284
9285     },
9286
9287     /**
9288      * Gets the tab holder element.
9289      * 
9290      * @returns {HTMLElement} The tab holder component.
9291      * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
9292      */
9293     getTabHolder: function() {
9294
9295         var html = [
9296             '<div class="col s2">',
9297             '   <ul class="tabs" style="height: auto; margin-top: 0.82rem; -ms-flex-direction: column; -webkit-flex-direction: column; flex-direction: column; display: -webkit-flex; display: flex;">',
9298             '   </ul>',
9299             '</div>',
9300             '<div class="col s10">',
9301             '<div>'
9302         ].join("\n");
9303
9304         var el = document.createElement('div');
9305         el.className = 'row card-panel';
9306         el.innerHTML = html;
9307         return el;
9308
9309     },
9310
9311     /**
9312      * Add specified tab to specified holder element.
9313      * 
9314      * @param {HTMLElement} holder The tab holder element.
9315      * @param {HTMLElement} tab The tab to add.
9316      */
9317     addTab: function(holder, tab) {
9318         holder.children[0].children[0].appendChild(tab);
9319     },
9320
9321     /**
9322      * Gets a single tab element.
9323      * 
9324      * @param {HTMLElement} span The tab's content.
9325      * @returns {HTMLElement} The tab element.
9326      * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
9327      */
9328     getTab: function(span) {
9329
9330         var el = document.createElement('li');
9331         el.className = 'tab';
9332         this.applyStyles(el, {
9333             width: '100%',
9334             textAlign: 'left',
9335             lineHeight: '24px',
9336             height: '24px',
9337             fontSize: '14px',
9338             cursor: 'pointer'
9339         });
9340         el.appendChild(span);
9341         return el;
9342     },
9343
9344     /**
9345      * Marks specified tab as active.
9346      * 
9347      * @returns {HTMLElement} The tab element.
9348      * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
9349      */
9350     markTabActive: function(tab) {
9351
9352         this.applyStyles(tab, {
9353             width: '100%',
9354             textAlign: 'left',
9355             lineHeight: '24px',
9356             height: '24px',
9357             fontSize: '14px',
9358             cursor: 'pointer',
9359             color: 'rgba(238,110,115,1)',
9360             transition: 'border-color .5s ease',
9361             borderRight: '3px solid #424242'
9362         });
9363
9364     },
9365
9366     /**
9367      * Marks specified tab as inactive.
9368      * 
9369      * @returns {HTMLElement} The tab element.
9370      * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
9371      */
9372     markTabInactive: function(tab) {
9373
9374         this.applyStyles(tab, {
9375             width: '100%',
9376             textAlign: 'left',
9377             lineHeight: '24px',
9378             height: '24px',
9379             fontSize: '14px',
9380             cursor: 'pointer',
9381             color: 'rgba(238,110,115,0.7)'
9382         });
9383
9384     },
9385
9386     /**
9387      * Returns the element that holds the tab contents.
9388      * 
9389      * @param {HTMLElement} tabHolder The full tab holder element.
9390      * @returns {HTMLElement} The content element inside specified tab holder.
9391      */
9392     getTabContentHolder: function(tabHolder) {
9393         return tabHolder.children[1];
9394     },
9395
9396     /**
9397      * Creates and returns a tab content element.
9398      * 
9399      * @returns {HTMLElement} The new tab content element.
9400      */
9401     getTabContent: function() {
9402         return document.createElement('div');
9403     },
9404
9405     /**
9406      * Adds an error message to the specified input element.
9407      * 
9408      * @param {HTMLElement} input The input element that caused the error.
9409      * @param {string} text The error message.
9410      */
9411     addInputError: function(input, text) {
9412
9413         // Get the parent element. Should most likely be a <div class="input-field" ... />.
9414         var parent = input.parentNode,
9415             el;
9416
9417         if (!parent) return;
9418
9419         // Remove any previous error.
9420         this.removeInputError(input);
9421
9422         // Append an error message div.
9423         el = document.createElement('div');
9424         el.className = 'error-text red-text';
9425         el.textContent = text;
9426         parent.appendChild(el);
9427
9428     },
9429
9430     /**
9431      * Removes any error message from the specified input element.
9432      * 
9433      * @param {HTMLElement} input The input element that previously caused the error.
9434      */
9435     removeInputError: function(input) {
9436
9437         // Get the parent element. Should most likely be a <div class="input-field" ... />.
9438         var parent = input.parentElement,
9439             els;
9440
9441         if (!parent) return;
9442
9443         // Remove all elements having class .error-text.
9444         els = parent.getElementsByClassName('error-text');
9445         for (var i = 0; i < els.length; i++)
9446             parent.removeChild(els[i]);
9447
9448     },
9449
9450     addTableRowError: function(row) {
9451     },
9452
9453     removeTableRowError: function(row) {
9454     },
9455
9456     /**
9457      * Gets a select DOM element.
9458      * 
9459      * @param {object} options The option values.
9460      * @return {HTMLElement} The DOM element.
9461      * @see http://materializecss.com/forms.html#select
9462      */
9463     getSelectInput: function(options) {
9464
9465         var select = this._super(options);
9466         select.className = 'browser-default';
9467         return select;
9468
9469     },
9470
9471     /**
9472      * Gets a textarea DOM element.
9473      * 
9474      * @returns {HTMLElement} The DOM element.
9475      * @see http://materializecss.com/forms.html#textarea
9476      */
9477     getTextareaInput: function() {
9478         var el = document.createElement('textarea');
9479         el.style.marginBottom = '5px';
9480         el.style.fontSize = '1rem';
9481         el.style.fontFamily = 'monospace';
9482         return el;
9483     },
9484
9485     getCheckbox: function() {
9486
9487         var el = this.getFormInputField('checkbox');
9488         el.id = this.createUuid();
9489         return el;
9490
9491     },
9492
9493     /**
9494      * Gets the modal element for displaying Edit JSON and Properties dialogs.
9495      * 
9496      * @returns {HTMLElement} The modal DOM element.
9497      * @see http://materializecss.com/cards.html
9498      */
9499     getModal: function() {
9500
9501         var el = document.createElement('div');
9502         el.className = 'card-panel z-depth-3';
9503         el.style.padding = '5px';
9504         el.style.position = 'absolute';
9505         el.style.zIndex = '10';
9506         el.style.display = 'none';
9507         return el;
9508
9509     },
9510
9511     /**
9512      * Creates and returns a RFC4122 version 4 compliant unique id.
9513      * 
9514      * @returns {string} A GUID.
9515      * @see https://stackoverflow.com/a/2117523
9516      */
9517     createUuid: function() {
9518
9519         return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
9520             var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
9521             return v.toString(16);
9522         });
9523
9524     }
9525
9526 });
9527
9528 JSONEditor.AbstractIconLib = Class.extend({
9529   mapping: {
9530     collapse: '',
9531     expand: '',
9532     "delete": '',
9533     edit: '',
9534     add: '',
9535     cancel: '',
9536     save: '',
9537     moveup: '',
9538     movedown: ''
9539   },
9540   icon_prefix: '',
9541   getIconClass: function(key) {
9542     if(this.mapping[key]) return this.icon_prefix+this.mapping[key];
9543     else return null;
9544   },
9545   getIcon: function(key) {
9546     var iconclass = this.getIconClass(key);
9547     
9548     if(!iconclass) return null;
9549     
9550     var i = document.createElement('i');
9551     i.className = iconclass;
9552     return i;
9553   }
9554 });
9555
9556 JSONEditor.defaults.iconlibs.bootstrap2 = JSONEditor.AbstractIconLib.extend({
9557   mapping: {
9558     collapse: 'chevron-down',
9559     expand: 'chevron-up',
9560     "delete": 'trash',
9561     edit: 'pencil',
9562     add: 'plus',
9563     cancel: 'ban-circle',
9564     save: 'ok',
9565     moveup: 'arrow-up',
9566     movedown: 'arrow-down'
9567   },
9568   icon_prefix: 'glyphicon glyphicon-'
9569 });
9570
9571 JSONEditor.defaults.iconlibs.bootstrap3 = JSONEditor.AbstractIconLib.extend({
9572   mapping: {
9573     collapse: 'chevron-down',
9574     expand: 'chevron-right',
9575     "delete": 'remove',
9576     edit: 'pencil',
9577     add: 'plus',
9578     cancel: 'floppy-remove',
9579     save: 'floppy-saved',
9580     moveup: 'arrow-up',
9581     movedown: 'arrow-down'
9582   },
9583   icon_prefix: 'glyphicon glyphicon-'
9584 });
9585
9586 JSONEditor.defaults.iconlibs.fontawesome3 = JSONEditor.AbstractIconLib.extend({
9587   mapping: {
9588     collapse: 'chevron-down',
9589     expand: 'chevron-right',
9590     "delete": 'remove',
9591     edit: 'pencil',
9592     add: 'plus',
9593     cancel: 'ban-circle',
9594     save: 'save',
9595     moveup: 'arrow-up',
9596     movedown: 'arrow-down'
9597   },
9598   icon_prefix: 'icon-'
9599 });
9600
9601 JSONEditor.defaults.iconlibs.fontawesome4 = JSONEditor.AbstractIconLib.extend({
9602   mapping: {
9603     collapse: 'caret-square-o-down',
9604     expand: 'caret-square-o-right',
9605     "delete": 'times',
9606     edit: 'pencil',
9607     add: 'plus',
9608     cancel: 'ban',
9609     save: 'save',
9610     moveup: 'arrow-up',
9611     movedown: 'arrow-down',
9612     copy: 'files-o'
9613   },
9614   icon_prefix: 'fa fa-'
9615 });
9616
9617 JSONEditor.defaults.iconlibs.foundation2 = JSONEditor.AbstractIconLib.extend({
9618   mapping: {
9619     collapse: 'minus',
9620     expand: 'plus',
9621     "delete": 'remove',
9622     edit: 'edit',
9623     add: 'add-doc',
9624     cancel: 'error',
9625     save: 'checkmark',
9626     moveup: 'up-arrow',
9627     movedown: 'down-arrow'
9628   },
9629   icon_prefix: 'foundicon-'
9630 });
9631
9632 JSONEditor.defaults.iconlibs.foundation3 = JSONEditor.AbstractIconLib.extend({
9633   mapping: {
9634     collapse: 'minus',
9635     expand: 'plus',
9636     "delete": 'x',
9637     edit: 'pencil',
9638     add: 'page-add',
9639     cancel: 'x-circle',
9640     save: 'save',
9641     moveup: 'arrow-up',
9642     movedown: 'arrow-down'
9643   },
9644   icon_prefix: 'fi-'
9645 });
9646
9647 JSONEditor.defaults.iconlibs.jqueryui = JSONEditor.AbstractIconLib.extend({
9648   mapping: {
9649     collapse: 'triangle-1-s',
9650     expand: 'triangle-1-e',
9651     "delete": 'trash',
9652     edit: 'pencil',
9653     add: 'plusthick',
9654     cancel: 'closethick',
9655     save: 'disk',
9656     moveup: 'arrowthick-1-n',
9657     movedown: 'arrowthick-1-s'
9658   },
9659   icon_prefix: 'ui-icon ui-icon-'
9660 });
9661
9662 JSONEditor.defaults.iconlibs.materialicons = JSONEditor.AbstractIconLib.extend({
9663
9664     mapping: {
9665         collapse: 'arrow_drop_up',
9666         expand: 'arrow_drop_down',
9667         "delete": 'delete',
9668         edit: 'edit',
9669         add: 'add',
9670         cancel: 'cancel',
9671         save: 'save',
9672         moveup: 'arrow_upward',
9673         movedown: 'arrow_downward',
9674         copy: 'content_copy'
9675     },
9676
9677     icon_class: 'material-icons',
9678     icon_prefix: '',
9679
9680     getIconClass: function(key) {
9681
9682         // This method is unused.
9683
9684         return this.icon_class;
9685     },
9686
9687     getIcon: function(key) {
9688
9689         // Get the mapping.
9690         var mapping = this.mapping[key];
9691         if (!mapping) return null;
9692
9693         // @see http://materializecss.com/icons.html
9694         var i = document.createElement('i');
9695         i.className = this.icon_class;
9696         var t = document.createTextNode(mapping);
9697         i.appendChild(t);
9698         return i;
9699
9700     }
9701 });
9702
9703 JSONEditor.defaults.templates["default"] = function() {
9704   return {
9705     compile: function(template) {
9706       var matches = template.match(/{{\s*([a-zA-Z0-9\-_ \.]+)\s*}}/g);
9707       var l = matches && matches.length;
9708
9709       // Shortcut if the template contains no variables
9710       if(!l) return function() { return template; };
9711
9712       // Pre-compute the search/replace functions
9713       // This drastically speeds up template execution
9714       var replacements = [];
9715       var get_replacement = function(i) {
9716         var p = matches[i].replace(/[{}]+/g,'').trim().split('.');
9717         var n = p.length;
9718         var func;
9719         
9720         if(n > 1) {
9721           var cur;
9722           func = function(vars) {
9723             cur = vars;
9724             for(i=0; i<n; i++) {
9725               cur = cur[p[i]];
9726               if(!cur) break;
9727             }
9728             return cur;
9729           };
9730         }
9731         else {
9732           p = p[0];
9733           func = function(vars) {
9734             return vars[p];
9735           };
9736         }
9737         
9738         replacements.push({
9739           s: matches[i],
9740           r: func
9741         });
9742       };
9743       for(var i=0; i<l; i++) {
9744         get_replacement(i);
9745       }
9746
9747       // The compiled function
9748       return function(vars) {
9749         var ret = template+"";
9750         var r;
9751         for(i=0; i<l; i++) {
9752           r = replacements[i];
9753           ret = ret.replace(r.s, r.r(vars));
9754         }
9755         return ret;
9756       };
9757     }
9758   };
9759 };
9760
9761 JSONEditor.defaults.templates.ejs = function() {
9762   if(!window.EJS) return false;
9763
9764   return {
9765     compile: function(template) {
9766       var compiled = new window.EJS({
9767         text: template
9768       });
9769
9770       return function(context) {
9771         return compiled.render(context);
9772       };
9773     }
9774   };
9775 };
9776
9777 JSONEditor.defaults.templates.handlebars = function() {
9778   return window.Handlebars;
9779 };
9780
9781 JSONEditor.defaults.templates.hogan = function() {
9782   if(!window.Hogan) return false;
9783
9784   return {
9785     compile: function(template) {
9786       var compiled = window.Hogan.compile(template);
9787       return function(context) {
9788         return compiled.render(context);
9789       };
9790     }
9791   };
9792 };
9793
9794 JSONEditor.defaults.templates.lodash = function() {
9795   if(!window._) return false;
9796
9797   return {
9798     compile: function(template) {
9799       return function(context) {
9800         return window._.template(template)(context);
9801       };
9802     }
9803   };
9804 };
9805
9806 JSONEditor.defaults.templates.markup = function() {
9807   if(!window.Mark || !window.Mark.up) return false;
9808
9809   return {
9810     compile: function(template) {
9811       return function(context) {
9812         return window.Mark.up(template,context);
9813       };
9814     }
9815   };
9816 };
9817
9818 JSONEditor.defaults.templates.mustache = function() {
9819   if(!window.Mustache) return false;
9820
9821   return {
9822     compile: function(template) {
9823       return function(view) {
9824         return window.Mustache.render(template, view);
9825       };
9826     }
9827   };
9828 };
9829
9830 JSONEditor.defaults.templates.swig = function() {
9831   return window.swig;
9832 };
9833
9834 JSONEditor.defaults.templates.underscore = function() {
9835   if(!window._) return false;
9836
9837   return {
9838     compile: function(template) {
9839       return function(context) {
9840         return window._.template(template, context);
9841       };
9842     }
9843   };
9844 };
9845
9846 // Set the default theme
9847 JSONEditor.defaults.theme = 'html';
9848
9849 // Set the default template engine
9850 JSONEditor.defaults.template = 'default';
9851
9852 // Default options when initializing JSON Editor
9853 JSONEditor.defaults.options = {};
9854
9855 JSONEditor.defaults.options.prompt_before_delete = true;
9856
9857 // String translate function
9858 JSONEditor.defaults.translate = function(key, variables) {
9859   var lang = JSONEditor.defaults.languages[JSONEditor.defaults.language];
9860   if(!lang) throw "Unknown language "+JSONEditor.defaults.language;
9861   
9862   var string = lang[key] || JSONEditor.defaults.languages[JSONEditor.defaults.default_language][key];
9863   
9864   if(typeof string === "undefined") throw "Unknown translate string "+key;
9865   
9866   if(variables) {
9867     for(var i=0; i<variables.length; i++) {
9868       string = string.replace(new RegExp('\\{\\{'+i+'}}','g'),variables[i]);
9869     }
9870   }
9871   
9872   return string;
9873 };
9874
9875 // Translation strings and default languages
9876 JSONEditor.defaults.default_language = 'en';
9877 JSONEditor.defaults.language = JSONEditor.defaults.default_language;
9878 JSONEditor.defaults.languages.en = {
9879   /**
9880    * When a property is not set
9881    */
9882   error_notset: 'Please populate the required property "{{0}}"',
9883   /**
9884    * When a string must not be empty
9885    */
9886   error_notempty: 'Please populate the required property "{{0}}"',
9887   /**
9888    * When a value is not one of the enumerated values
9889    */
9890   error_enum: "{{0}} must be one of the enumerated values",
9891   /**
9892    * When a value doesn't validate any schema of a 'anyOf' combination
9893    */
9894   error_anyOf: "Value must validate against at least one of the provided schemas",
9895   /**
9896    * When a value doesn't validate
9897    * @variables This key takes one variable: The number of schemas the value does not validate
9898    */
9899   error_oneOf: 'Value must validate against exactly one of the provided schemas. It currently validates against {{0}} of the schemas.',
9900   /**
9901    * When a value does not validate a 'not' schema
9902    */
9903   error_not: "Value must not validate against the provided schema",
9904   /**
9905    * When a value does not match any of the provided types
9906    */
9907   error_type_union: "Value must be one of the provided types",
9908   /**
9909    * When a value does not match the given type
9910    * @variables This key takes one variable: The type the value should be of
9911    */
9912   error_type: "Value must be of type {{0}}",
9913   /**
9914    *  When the value validates one of the disallowed types
9915    */
9916   error_disallow_union: "Value must not be one of the provided disallowed types",
9917   /**
9918    *  When the value validates a disallowed type
9919    * @variables This key takes one variable: The type the value should not be of
9920    */
9921   error_disallow: "Value must not be of type {{0}}",
9922   /**
9923    * When a value is not a multiple of or divisible by a given number
9924    * @variables This key takes one variable: The number mentioned above
9925    */
9926   error_multipleOf: "Value must be a multiple of {{0}}",
9927   /**
9928    * When a value is greater than it's supposed to be (exclusive)
9929    * @variables This key takes one variable: The maximum
9930    */
9931   error_maximum_excl: "{{0}} must be less than {{1}}",
9932   /**
9933    * When a value is greater than it's supposed to be (inclusive)
9934    * @variables This key takes one variable: The maximum
9935    */
9936   error_maximum_incl: "{{0}} must be at most {{1}}",
9937   /**
9938    * When a value is lesser than it's supposed to be (exclusive)
9939    * @variables This key takes one variable: The minimum
9940    */
9941   error_minimum_excl: "{{0}} must be greater than {{1}}",
9942   /**
9943    * When a value is lesser than it's supposed to be (inclusive)
9944    * @variables This key takes one variable: The minimum
9945    */
9946   error_minimum_incl: "{{0}} must be at least {{1}}",
9947   /**
9948    * When a value have too many characters
9949    * @variables This key takes one variable: The maximum character count
9950    */
9951   error_maxLength: "{{0}} must be at most {{1}} characters long",
9952   /**
9953    * When a value does not have enough characters
9954    * @variables This key takes one variable: The minimum character count
9955    */
9956   error_minLength: "{{0}} must be at least {{1}} characters long",
9957   /**
9958    * When a value does not match a given pattern
9959    */
9960   error_pattern: "{{0}} must match the pattern {{1}}",
9961   /**
9962    * When an array has additional items whereas it is not supposed to
9963    */
9964   error_additionalItems: "No additional items allowed in this array",
9965   /**
9966    * When there are to many items in an array
9967    * @variables This key takes one variable: The maximum item count
9968    */
9969   error_maxItems: "{{0}} must have at most {{1}} items",
9970   /**
9971    * When there are not enough items in an array
9972    * @variables This key takes one variable: The minimum item count
9973    */
9974   error_minItems: "{{0}} must have at least {{1}} items",
9975   /**
9976    * When an array is supposed to have unique items but has duplicates
9977    */
9978   error_uniqueItems: "Each tab of {{0}} must specify a unique combination of parameters",
9979   /**
9980    * When there are too many properties in an object
9981    * @variables This key takes one variable: The maximum property count
9982    */
9983   error_maxProperties: "Object must have at most {{0}} properties",
9984   /**
9985    * When there are not enough properties in an object
9986    * @variables This key takes one variable: The minimum property count
9987    */
9988   error_minProperties: "Object must have at least {{0}} properties",
9989   /**
9990    * When a required property is not defined
9991    * @variables This key takes one variable: The name of the missing property
9992    */
9993   error_required: 'Please populate the required property "{{0}}"',
9994   /**
9995    * When there is an additional property is set whereas there should be none
9996    * @variables This key takes one variable: The name of the additional property
9997    */
9998   error_additional_properties: "No additional properties allowed, but property {{0}} is set",
9999   /**
10000    * When a dependency is not resolved
10001    * @variables This key takes one variable: The name of the missing property for the dependency
10002    */
10003   error_dependency: "Must have property {{0}}",
10004   /**
10005    * Text on Delete All buttons
10006    */
10007   button_delete_all: "All",
10008   /**
10009    * Title on Delete All buttons
10010    */
10011   button_delete_all_title: "Delete All",
10012   /**
10013     * Text on Delete Last buttons
10014     * @variable This key takes one variable: The title of object to delete
10015     */
10016   button_delete_last: "Last {{0}}",
10017   /**
10018     * Title on Delete Last buttons
10019     * @variable This key takes one variable: The title of object to delete
10020     */
10021   button_delete_last_title: "Delete Last {{0}}",
10022   /**
10023     * Title on Add Row buttons
10024     * @variable This key takes one variable: The title of object to add
10025     */
10026   button_add_row_title: "Add {{0}}",
10027   /**
10028     * Title on Move Down buttons
10029     */
10030   button_move_down_title: "Move down",
10031   /**
10032     * Title on Move Up buttons
10033     */
10034   button_move_up_title: "Move up",
10035   /**
10036     * Title on Delete Row buttons
10037     * @variable This key takes one variable: The title of object to delete
10038     */
10039   button_delete_row_title: "Delete {{0}}",
10040   /**
10041     * Title on Delete Row buttons, short version (no parameter with the object title)
10042     */
10043   button_delete_row_title_short: "Delete",
10044   /**
10045     * Title on Collapse buttons
10046     */
10047   button_collapse: "Collapse",
10048   /**
10049     * Title on Expand buttons
10050     */
10051   button_expand: "Expand"
10052 };
10053
10054 // Miscellaneous Plugin Settings
10055 JSONEditor.plugins = {
10056   ace: {
10057     theme: ''
10058   },
10059   SimpleMDE: {
10060
10061   },
10062   sceditor: {
10063
10064   },
10065   select2: {
10066     
10067   },
10068   selectize: {
10069   }
10070 };
10071
10072 // Default per-editor options
10073 $each(JSONEditor.defaults.editors, function(i,editor) {
10074   JSONEditor.defaults.editors[i].options = editor.options || {};
10075 });
10076
10077 // Set the default resolvers
10078 // Use "multiple" as a fall back for everything
10079 JSONEditor.defaults.resolvers.unshift(function(schema) {
10080   if(schema.type === "qbldr") return "qbldr";
10081 });
10082 JSONEditor.defaults.resolvers.unshift(function(schema) {
10083   if(typeof schema.type !== "string") return "multiple";
10084 });
10085 // If the type is not set but properties are defined, we can infer the type is actually object
10086 JSONEditor.defaults.resolvers.unshift(function(schema) {
10087   // If the schema is a simple type
10088   if(!schema.type && schema.properties ) return "object";
10089 });
10090 // If the type is set and it's a basic type, use the primitive editor
10091 JSONEditor.defaults.resolvers.unshift(function(schema) {
10092   // If the schema is a simple type
10093   if(typeof schema.type === "string") return schema.type;
10094 });
10095 // Use a specialized editor for ratings
10096 JSONEditor.defaults.resolvers.unshift(function(schema) {
10097   if(schema.type === "integer" && schema.format === "rating") return "rating";
10098 });
10099 // Use the select editor for all boolean values
10100 JSONEditor.defaults.resolvers.unshift(function(schema) {
10101   if(schema.type === 'boolean') {
10102     // If explicitly set to 'checkbox', use that
10103     if(schema.format === "checkbox" || (schema.options && schema.options.checkbox)) {
10104       return "checkbox";
10105     }
10106     // Otherwise, default to select menu
10107     return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
10108   }
10109 });
10110 // Use the multiple editor for schemas where the `type` is set to "any"
10111 JSONEditor.defaults.resolvers.unshift(function(schema) {
10112   // If the schema can be of any type
10113   if(schema.type === "any") return "multiple";
10114 });
10115 // Editor for base64 encoded files
10116 JSONEditor.defaults.resolvers.unshift(function(schema) {
10117   // If the schema can be of any type
10118   if(schema.type === "string" && schema.media && schema.media.binaryEncoding==="base64") {
10119     return "base64";
10120   }
10121 });
10122 // Editor for uploading files
10123 JSONEditor.defaults.resolvers.unshift(function(schema) {
10124   if(schema.type === "string" && schema.format === "url" && schema.options && schema.options.upload === true) {
10125     if(window.FileReader) return "upload";
10126   }
10127 });
10128 // Use the table editor for arrays with the format set to `table`
10129 JSONEditor.defaults.resolvers.unshift(function(schema) {
10130   // Type `array` with format set to `table`
10131   if(schema.type === "array" && schema.format === "table") {
10132     return "table";
10133   }
10134 });
10135 // Use the `select` editor for dynamic enumSource enums
10136 JSONEditor.defaults.resolvers.unshift(function(schema) {
10137   if(schema.enumSource) return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
10138 });
10139 // Use the `enum` or `select` editors for schemas with enumerated properties
10140 JSONEditor.defaults.resolvers.unshift(function(schema) {
10141   if(schema["enum"]) {
10142     if(schema.type === "array" || schema.type === "object") {
10143       return "enum";
10144     }
10145     else if(schema.type === "number" || schema.type === "integer" || schema.type === "string") {
10146       return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
10147     }
10148   }
10149 });
10150 // Specialized editors for arrays of strings
10151 JSONEditor.defaults.resolvers.unshift(function(schema) {
10152   if(schema.type === "array" && schema.items && !(Array.isArray(schema.items)) && schema.uniqueItems && ['string','number','integer'].indexOf(schema.items.type) >= 0) {
10153     // For enumerated strings, number, or integers
10154     if(schema.items.enum) {
10155       return 'multiselect';
10156     }
10157     // For non-enumerated strings (tag editor)
10158     else if(JSONEditor.plugins.selectize.enable && schema.items.type === "string") {
10159       return 'arraySelectize';
10160     }
10161   }
10162 });
10163 // Use the multiple editor for schemas with `oneOf` set
10164 JSONEditor.defaults.resolvers.unshift(function(schema) {
10165   // If this schema uses `oneOf` or `anyOf`
10166   if(schema.oneOf || schema.anyOf) return "multiple";
10167 });
10168
10169 /**
10170  * This is a small wrapper for using JSON Editor like a typical jQuery plugin.
10171  */
10172 (function() {
10173   if(window.jQuery || window.Zepto) {
10174     var $ = window.jQuery || window.Zepto;
10175     $.jsoneditor = JSONEditor.defaults;
10176     
10177     $.fn.jsoneditor = function(options) {
10178       var self = this;
10179       var editor = this.data('jsoneditor');
10180       if(options === 'value') {
10181         if(!editor) throw "Must initialize jsoneditor before getting/setting the value";
10182         
10183         // Set value
10184         if(arguments.length > 1) {
10185           editor.setValue(arguments[1]);
10186         }
10187         // Get value
10188         else {
10189           return editor.getValue();
10190         }
10191       }
10192       else if(options === 'validate') {
10193         if(!editor) throw "Must initialize jsoneditor before validating";
10194         
10195         // Validate a specific value
10196         if(arguments.length > 1) {
10197           return editor.validate(arguments[1]);
10198         }
10199         // Validate current value
10200         else {
10201           return editor.validate();
10202         }
10203       }
10204       else if(options === 'destroy') {
10205         if(editor) {
10206           editor.destroy();
10207           this.data('jsoneditor',null);
10208         }
10209       }
10210       else {
10211         // Destroy first
10212         if(editor) {
10213           editor.destroy();
10214         }
10215         
10216         // Create editor
10217         editor = new JSONEditor(this.get(0),options);
10218         this.data('jsoneditor',editor);
10219         
10220         // Setup event listeners
10221         editor.on('change',function() {
10222           self.trigger('change');
10223         });
10224         editor.on('ready',function() {
10225           self.trigger('ready');
10226         });
10227       }
10228       
10229       return this;
10230     };
10231   }
10232 })();
10233
10234   window.JSONEditor = JSONEditor;
10235 })();