blob: 1d0cf75561add137e6ada205d4e7e28acaca1151 [file] [log] [blame]
Andrey Pavlov9b47f322020-04-05 22:31:40 +03001/*! JSON Editor v0.7.25 - JSON Schema -> HTML Editor
2 * By Jeremy Dorn - https://github.com/jdorn/json-editor/
3 * Released under the MIT license
4 *
5 * Date: 2016-04-12
6 */
7
8/**
9 * See README.md for requirements and usage info
10 */
11
12(function() {
13
14/*jshint loopfunc: true */
15/* Simple JavaScript Inheritance
16 * By John Resig http://ejohn.org/
17 * MIT Licensed.
18 */
19// Inspired by base2 and Prototype
20var Class;
21(function(){
22 var initializing = false, fnTest = /xyz/.test(function(){window.postMessage("xyz");}) ? /\b_super\b/ : /.*/;
23
24 // The base Class implementation (does nothing)
25 Class = function(){};
26
27 // Create a new Class that inherits from this class
28 Class.extend = function(prop) {
29 var _super = this.prototype;
30
31 // Instantiate a base class (but only create the instance,
32 // don't run the init constructor)
33 initializing = true;
34 var prototype = new this();
35 initializing = false;
36
37 // Copy the properties over onto the new prototype
38 for (var name in prop) {
39 // Check if we're overwriting an existing function
40 prototype[name] = typeof prop[name] == "function" &&
41 typeof _super[name] == "function" && fnTest.test(prop[name]) ?
42 (function(name, fn){
43 return function() {
44 var tmp = this._super;
45
46 // Add a new ._super() method that is the same method
47 // but on the super-class
48 this._super = _super[name];
49
50 // The method only need to be bound temporarily, so we
51 // remove it when we're done executing
52 var ret = fn.apply(this, arguments);
53 this._super = tmp;
54
55 return ret;
56 };
57 })(name, prop[name]) :
58 prop[name];
59 }
60
61 // The dummy class constructor
62 function Class() {
63 // All construction is actually done in the init method
64 if ( !initializing && this.init )
65 this.init.apply(this, arguments);
66 }
67
68 // Populate our constructed prototype object
69 Class.prototype = prototype;
70
71 // Enforce the constructor to be what we expect
72 Class.prototype.constructor = Class;
73
74 // And make this class extendable
75 Class.extend = arguments.callee;
76
77 return Class;
78 };
79
80 return Class;
81})();
82
83// CustomEvent constructor polyfill
84// From MDN
85(function () {
86 function CustomEvent ( event, params ) {
87 params = params || { bubbles: false, cancelable: false, detail: undefined };
88 var evt = document.createEvent( 'CustomEvent' );
89 evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
90 return evt;
91 }
92
93 CustomEvent.prototype = window.Event.prototype;
94
95 window.CustomEvent = CustomEvent;
96})();
97
98// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
99// MIT license
100(function() {
101 var lastTime = 0;
102 var vendors = ['ms', 'moz', 'webkit', 'o'];
103 for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
104 window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
105 window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] ||
106 window[vendors[x]+'CancelRequestAnimationFrame'];
107 }
108
109 if (!window.requestAnimationFrame)
110 window.requestAnimationFrame = function(callback, element) {
111 var currTime = new Date().getTime();
112 var timeToCall = Math.max(0, 16 - (currTime - lastTime));
113 var id = window.setTimeout(function() { callback(currTime + timeToCall); },
114 timeToCall);
115 lastTime = currTime + timeToCall;
116 return id;
117 };
118
119 if (!window.cancelAnimationFrame)
120 window.cancelAnimationFrame = function(id) {
121 clearTimeout(id);
122 };
123}());
124
125// Array.isArray polyfill
126// From MDN
127(function() {
128 if(!Array.isArray) {
129 Array.isArray = function(arg) {
130 return Object.prototype.toString.call(arg) === '[object Array]';
131 };
132 }
133}());
134/**
135 * Taken from jQuery 2.1.3
136 *
137 * @param obj
138 * @returns {boolean}
139 */
140var $isplainobject = function( obj ) {
141 // Not plain objects:
142 // - Any object or value whose internal [[Class]] property is not "[object Object]"
143 // - DOM nodes
144 // - window
145 if (typeof obj !== "object" || obj.nodeType || (obj !== null && obj === obj.window)) {
146 return false;
147 }
148
149 if (obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
150 return false;
151 }
152
153 // If the function hasn't returned already, we're confident that
154 // |obj| is a plain object, created by {} or constructed with new Object
155 return true;
156};
157
158var $extend = function(destination) {
159 var source, i,property;
160 for(i=1; i<arguments.length; i++) {
161 source = arguments[i];
162 for (property in source) {
163 if(!source.hasOwnProperty(property)) continue;
164 if(source[property] && $isplainobject(source[property])) {
165 if(!destination.hasOwnProperty(property)) destination[property] = {};
166 $extend(destination[property], source[property]);
167 }
168 else {
169 destination[property] = source[property];
170 }
171 }
172 }
173 return destination;
174};
175
176var $each = function(obj,callback) {
177 if(!obj || typeof obj !== "object") return;
178 var i;
179 if(Array.isArray(obj) || (typeof obj.length === 'number' && obj.length > 0 && (obj.length - 1) in obj)) {
180 for(i=0; i<obj.length; i++) {
181 if(callback(i,obj[i])===false) return;
182 }
183 }
184 else {
185 if (Object.keys) {
186 var keys = Object.keys(obj);
187 for(i=0; i<keys.length; i++) {
188 if(callback(keys[i],obj[keys[i]])===false) return;
189 }
190 }
191 else {
192 for(i in obj) {
193 if(!obj.hasOwnProperty(i)) continue;
194 if(callback(i,obj[i])===false) return;
195 }
196 }
197 }
198};
199
200var $trigger = function(el,event) {
201 var e = document.createEvent('HTMLEvents');
202 e.initEvent(event, true, true);
203 el.dispatchEvent(e);
204};
205var $triggerc = function(el,event) {
206 var e = new CustomEvent(event,{
207 bubbles: true,
208 cancelable: true
209 });
210
211 el.dispatchEvent(e);
212};
213
214var JSONEditor = function(element,options) {
215 if (!(element instanceof Element)) {
216 throw new Error('element should be an instance of Element');
217 }
218 options = $extend({},JSONEditor.defaults.options,options||{});
219 this.element = element;
220 this.options = options;
221 this.init();
222};
223JSONEditor.prototype = {
224 // necessary since we remove the ctor property by doing a literal assignment. Without this
225 // the $isplainobject function will think that this is a plain object.
226 constructor: JSONEditor,
227 init: function() {
228 var self = this;
229
230 this.ready = false;
231
232 var theme_class = JSONEditor.defaults.themes[this.options.theme || JSONEditor.defaults.theme];
233 if(!theme_class) throw "Unknown theme " + (this.options.theme || JSONEditor.defaults.theme);
234
235 this.schema = this.options.schema;
236 this.theme = new theme_class();
237 this.template = this.options.template;
238 this.refs = this.options.refs || {};
239 this.uuid = 0;
240 this.__data = {};
241
242 var icon_class = JSONEditor.defaults.iconlibs[this.options.iconlib || JSONEditor.defaults.iconlib];
243 if(icon_class) this.iconlib = new icon_class();
244
245 this.root_container = this.theme.getContainer();
246 this.element.appendChild(this.root_container);
247
248 this.translate = this.options.translate || JSONEditor.defaults.translate;
249
250 // Fetch all external refs via ajax
251 this._loadExternalRefs(this.schema, function() {
252 self._getDefinitions(self.schema);
253
254 // Validator options
255 var validator_options = {};
256 if(self.options.custom_validators) {
257 validator_options.custom_validators = self.options.custom_validators;
258 }
259 self.validator = new JSONEditor.Validator(self,null,validator_options);
260
261 // Create the root editor
262 var editor_class = self.getEditorClass(self.schema);
263 self.root = self.createEditor(editor_class, {
264 jsoneditor: self,
265 schema: self.schema,
266 required: true,
267 container: self.root_container
268 });
269
270 self.root.preBuild();
271 self.root.build();
272 self.root.postBuild();
273
274 // Starting data
275 if(self.options.startval) self.root.setValue(self.options.startval);
276
277 self.validation_results = self.validator.validate(self.root.getValue());
278 self.root.showValidationErrors(self.validation_results);
279 self.ready = true;
280
281 // Fire ready event asynchronously
282 window.requestAnimationFrame(function() {
283 if(!self.ready) return;
284 self.validation_results = self.validator.validate(self.root.getValue());
285 self.root.showValidationErrors(self.validation_results);
286 self.trigger('ready');
287 self.trigger('change');
288 });
289 });
290 },
291 getValue: function() {
292 if(!this.ready) throw "JSON Editor not ready yet. Listen for 'ready' event before getting the value";
293
294 return this.root.getValue();
295 },
296 setValue: function(value) {
297 if(!this.ready) throw "JSON Editor not ready yet. Listen for 'ready' event before setting the value";
298
299 this.root.setValue(value);
300 return this;
301 },
302 validate: function(value) {
303 if(!this.ready) throw "JSON Editor not ready yet. Listen for 'ready' event before validating";
304
305 // Custom value
306 if(arguments.length === 1) {
307 return this.validator.validate(value);
308 }
309 // Current value (use cached result)
310 else {
311 return this.validation_results;
312 }
313 },
314 destroy: function() {
315 if(this.destroyed) return;
316 if(!this.ready) return;
317
318 this.schema = null;
319 this.options = null;
320 this.root.destroy();
321 this.root = null;
322 this.root_container = null;
323 this.validator = null;
324 this.validation_results = null;
325 this.theme = null;
326 this.iconlib = null;
327 this.template = null;
328 this.__data = null;
329 this.ready = false;
330 this.element.innerHTML = '';
331
332 this.destroyed = true;
333 },
334 on: function(event, callback) {
335 this.callbacks = this.callbacks || {};
336 this.callbacks[event] = this.callbacks[event] || [];
337 this.callbacks[event].push(callback);
338
339 return this;
340 },
341 off: function(event, callback) {
342 // Specific callback
343 if(event && callback) {
344 this.callbacks = this.callbacks || {};
345 this.callbacks[event] = this.callbacks[event] || [];
346 var newcallbacks = [];
347 for(var i=0; i<this.callbacks[event].length; i++) {
348 if(this.callbacks[event][i]===callback) continue;
349 newcallbacks.push(this.callbacks[event][i]);
350 }
351 this.callbacks[event] = newcallbacks;
352 }
353 // All callbacks for a specific event
354 else if(event) {
355 this.callbacks = this.callbacks || {};
356 this.callbacks[event] = [];
357 }
358 // All callbacks for all events
359 else {
360 this.callbacks = {};
361 }
362
363 return this;
364 },
365 trigger: function(event) {
366 if(this.callbacks && this.callbacks[event] && this.callbacks[event].length) {
367 for(var i=0; i<this.callbacks[event].length; i++) {
368 this.callbacks[event][i]();
369 }
370 }
371
372 return this;
373 },
374 setOption: function(option, value) {
375 if(option === "show_errors") {
376 this.options.show_errors = value;
377 this.onChange();
378 }
379 // Only the `show_errors` option is supported for now
380 else {
381 throw "Option "+option+" must be set during instantiation and cannot be changed later";
382 }
383
384 return this;
385 },
386 getEditorClass: function(schema) {
387 var classname;
388
389 schema = this.expandSchema(schema);
390
391 $each(JSONEditor.defaults.resolvers,function(i,resolver) {
392 var tmp = resolver(schema);
393 if(tmp) {
394 if(JSONEditor.defaults.editors[tmp]) {
395 classname = tmp;
396 return false;
397 }
398 }
399 });
400
401 if(!classname) throw "Unknown editor for schema "+JSON.stringify(schema);
402 if(!JSONEditor.defaults.editors[classname]) throw "Unknown editor "+classname;
403
404 return JSONEditor.defaults.editors[classname];
405 },
406 createEditor: function(editor_class, options) {
407 options = $extend({},editor_class.options||{},options);
408 return new editor_class(options);
409 },
410 onChange: function() {
411 if(!this.ready) return;
412
413 if(this.firing_change) return;
414 this.firing_change = true;
415
416 var self = this;
417
418 window.requestAnimationFrame(function() {
419 self.firing_change = false;
420 if(!self.ready) return;
421
422 // Validate and cache results
423 self.validation_results = self.validator.validate(self.root.getValue());
424
425 if(self.options.show_errors !== "never") {
426 self.root.showValidationErrors(self.validation_results);
427 }
428 else {
429 self.root.showValidationErrors([]);
430 }
431
432 // Fire change event
433 self.trigger('change');
434 });
435
436 return this;
437 },
438 compileTemplate: function(template, name) {
439 name = name || JSONEditor.defaults.template;
440
441 var engine;
442
443 // Specifying a preset engine
444 if(typeof name === 'string') {
445 if(!JSONEditor.defaults.templates[name]) throw "Unknown template engine "+name;
446 engine = JSONEditor.defaults.templates[name]();
447
448 if(!engine) throw "Template engine "+name+" missing required library.";
449 }
450 // Specifying a custom engine
451 else {
452 engine = name;
453 }
454
455 if(!engine) throw "No template engine set";
456 if(!engine.compile) throw "Invalid template engine set";
457
458 return engine.compile(template);
459 },
460 _data: function(el,key,value) {
461 // Setting data
462 if(arguments.length === 3) {
463 var uuid;
464 if(el.hasAttribute('data-jsoneditor-'+key)) {
465 uuid = el.getAttribute('data-jsoneditor-'+key);
466 }
467 else {
468 uuid = this.uuid++;
469 el.setAttribute('data-jsoneditor-'+key,uuid);
470 }
471
472 this.__data[uuid] = value;
473 }
474 // Getting data
475 else {
476 // No data stored
477 if(!el.hasAttribute('data-jsoneditor-'+key)) return null;
478
479 return this.__data[el.getAttribute('data-jsoneditor-'+key)];
480 }
481 },
482 registerEditor: function(editor) {
483 this.editors = this.editors || {};
484 this.editors[editor.path] = editor;
485 return this;
486 },
487 unregisterEditor: function(editor) {
488 this.editors = this.editors || {};
489 this.editors[editor.path] = null;
490 return this;
491 },
492 getEditor: function(path) {
493 if(!this.editors) return;
494 return this.editors[path];
495 },
496 watch: function(path,callback) {
497 this.watchlist = this.watchlist || {};
498 this.watchlist[path] = this.watchlist[path] || [];
499 this.watchlist[path].push(callback);
500
501 return this;
502 },
503 unwatch: function(path,callback) {
504 if(!this.watchlist || !this.watchlist[path]) return this;
505 // If removing all callbacks for a path
506 if(!callback) {
507 this.watchlist[path] = null;
508 return this;
509 }
510
511 var newlist = [];
512 for(var i=0; i<this.watchlist[path].length; i++) {
513 if(this.watchlist[path][i] === callback) continue;
514 else newlist.push(this.watchlist[path][i]);
515 }
516 this.watchlist[path] = newlist.length? newlist : null;
517 return this;
518 },
519 notifyWatchers: function(path) {
520 if(!this.watchlist || !this.watchlist[path]) return this;
521 for(var i=0; i<this.watchlist[path].length; i++) {
522 this.watchlist[path][i]();
523 }
524 },
525 isEnabled: function() {
526 return !this.root || this.root.isEnabled();
527 },
528 enable: function() {
529 this.root.enable();
530 },
531 disable: function() {
532 this.root.disable();
533 },
534 _getDefinitions: function(schema,path) {
535 path = path || '#/definitions/';
536 if(schema.definitions) {
537 for(var i in schema.definitions) {
538 if(!schema.definitions.hasOwnProperty(i)) continue;
539 this.refs[path+i] = schema.definitions[i];
540 if(schema.definitions[i].definitions) {
541 this._getDefinitions(schema.definitions[i],path+i+'/definitions/');
542 }
543 }
544 }
545 },
546 _getExternalRefs: function(schema) {
547 var refs = {};
548 var merge_refs = function(newrefs) {
549 for(var i in newrefs) {
550 if(newrefs.hasOwnProperty(i)) {
551 refs[i] = true;
552 }
553 }
554 };
555
556 if(schema.$ref && typeof schema.$ref !== "object" && schema.$ref.substr(0,1) !== "#" && !this.refs[schema.$ref]) {
557 refs[schema.$ref] = true;
558 }
559
560 for(var i in schema) {
561 if(!schema.hasOwnProperty(i)) continue;
562 if(schema[i] && typeof schema[i] === "object" && Array.isArray(schema[i])) {
563 for(var j=0; j<schema[i].length; j++) {
564 if(typeof schema[i][j]==="object") {
565 merge_refs(this._getExternalRefs(schema[i][j]));
566 }
567 }
568 }
569 else if(schema[i] && typeof schema[i] === "object") {
570 merge_refs(this._getExternalRefs(schema[i]));
571 }
572 }
573
574 return refs;
575 },
576 _loadExternalRefs: function(schema, callback) {
577 var self = this;
578 var refs = this._getExternalRefs(schema);
579
580 var done = 0, waiting = 0, callback_fired = false;
581
582 $each(refs,function(url) {
583 if(self.refs[url]) return;
584 if(!self.options.ajax) throw "Must set ajax option to true to load external ref "+url;
585 self.refs[url] = 'loading';
586 waiting++;
587
588 var r = new XMLHttpRequest();
589 r.open("GET", url, true);
590 r.onreadystatechange = function () {
591 if (r.readyState != 4) return;
592 // Request succeeded
593 if(r.status === 200) {
594 var response;
595 try {
596 response = JSON.parse(r.responseText);
597 }
598 catch(e) {
599 window.console.log(e);
600 throw "Failed to parse external ref "+url;
601 }
602 if(!response || typeof response !== "object") throw "External ref does not contain a valid schema - "+url;
603
604 self.refs[url] = response;
605 self._loadExternalRefs(response,function() {
606 done++;
607 if(done >= waiting && !callback_fired) {
608 callback_fired = true;
609 callback();
610 }
611 });
612 }
613 // Request failed
614 else {
615 window.console.log(r);
616 throw "Failed to fetch ref via ajax- "+url;
617 }
618 };
619 r.send();
620 });
621
622 if(!waiting) {
623 callback();
624 }
625 },
626 expandRefs: function(schema) {
627 schema = $extend({},schema);
628
629 while (schema.$ref) {
630 var ref = schema.$ref;
631 delete schema.$ref;
632
633 if(!this.refs[ref]) ref = decodeURIComponent(ref);
634
635 schema = this.extendSchemas(schema,this.refs[ref]);
636 }
637 return schema;
638 },
639 expandSchema: function(schema) {
640 var self = this;
641 var extended = $extend({},schema);
642 var i;
643
644 // Version 3 `type`
645 if(typeof schema.type === 'object') {
646 // Array of types
647 if(Array.isArray(schema.type)) {
648 $each(schema.type, function(key,value) {
649 // Schema
650 if(typeof value === 'object') {
651 schema.type[key] = self.expandSchema(value);
652 }
653 });
654 }
655 // Schema
656 else {
657 schema.type = self.expandSchema(schema.type);
658 }
659 }
660 // Version 3 `disallow`
661 if(typeof schema.disallow === 'object') {
662 // Array of types
663 if(Array.isArray(schema.disallow)) {
664 $each(schema.disallow, function(key,value) {
665 // Schema
666 if(typeof value === 'object') {
667 schema.disallow[key] = self.expandSchema(value);
668 }
669 });
670 }
671 // Schema
672 else {
673 schema.disallow = self.expandSchema(schema.disallow);
674 }
675 }
676 // Version 4 `anyOf`
677 if(schema.anyOf) {
678 $each(schema.anyOf, function(key,value) {
679 schema.anyOf[key] = self.expandSchema(value);
680 });
681 }
682 // Version 4 `dependencies` (schema dependencies)
683 if(schema.dependencies) {
684 $each(schema.dependencies,function(key,value) {
685 if(typeof value === "object" && !(Array.isArray(value))) {
686 schema.dependencies[key] = self.expandSchema(value);
687 }
688 });
689 }
690 // Version 4 `not`
691 if(schema.not) {
692 schema.not = this.expandSchema(schema.not);
693 }
694
695 // allOf schemas should be merged into the parent
696 if(schema.allOf) {
697 for(i=0; i<schema.allOf.length; i++) {
698 extended = this.extendSchemas(extended,this.expandSchema(schema.allOf[i]));
699 }
700 delete extended.allOf;
701 }
702 // extends schemas should be merged into parent
703 if(schema["extends"]) {
704 // If extends is a schema
705 if(!(Array.isArray(schema["extends"]))) {
706 extended = this.extendSchemas(extended,this.expandSchema(schema["extends"]));
707 }
708 // If extends is an array of schemas
709 else {
710 for(i=0; i<schema["extends"].length; i++) {
711 extended = this.extendSchemas(extended,this.expandSchema(schema["extends"][i]));
712 }
713 }
714 delete extended["extends"];
715 }
716 // parent should be merged into oneOf schemas
717 if(schema.oneOf) {
718 var tmp = $extend({},extended);
719 delete tmp.oneOf;
720 for(i=0; i<schema.oneOf.length; i++) {
721 extended.oneOf[i] = this.extendSchemas(this.expandSchema(schema.oneOf[i]),tmp);
722 }
723 }
724
725 return this.expandRefs(extended);
726 },
727 extendSchemas: function(obj1, obj2) {
728 obj1 = $extend({},obj1);
729 obj2 = $extend({},obj2);
730
731 var self = this;
732 var extended = {};
733 $each(obj1, function(prop,val) {
734 // If this key is also defined in obj2, merge them
735 if(typeof obj2[prop] !== "undefined") {
736 // Required arrays should be unioned together
737 if(prop === 'required' && typeof val === "object" && Array.isArray(val)) {
738 // Union arrays and unique
739 extended.required = val.concat(obj2[prop]).reduce(function(p, c) {
740 if (p.indexOf(c) < 0) p.push(c);
741 return p;
742 }, []);
743 }
744 // Type should be intersected and is either an array or string
745 else if(prop === 'type' && (typeof val === "string" || Array.isArray(val))) {
746 // Make sure we're dealing with arrays
747 if(typeof val === "string") val = [val];
748 if(typeof obj2.type === "string") obj2.type = [obj2.type];
749
750
751 extended.type = val.filter(function(n) {
752 return obj2.type.indexOf(n) !== -1;
753 });
754
755 // If there's only 1 type and it's a primitive, use a string instead of array
756 if(extended.type.length === 1 && typeof extended.type[0] === "string") {
757 extended.type = extended.type[0];
758 }
759 }
760 // All other arrays should be intersected (enum, etc.)
761 else if(typeof val === "object" && Array.isArray(val)){
762 extended[prop] = val.filter(function(n) {
763 return obj2[prop].indexOf(n) !== -1;
764 });
765 }
766 // Objects should be recursively merged
767 else if(typeof val === "object" && val !== null) {
768 extended[prop] = self.extendSchemas(val,obj2[prop]);
769 }
770 // Otherwise, use the first value
771 else {
772 extended[prop] = val;
773 }
774 }
775 // Otherwise, just use the one in obj1
776 else {
777 extended[prop] = val;
778 }
779 });
780 // Properties in obj2 that aren't in obj1
781 $each(obj2, function(prop,val) {
782 if(typeof obj1[prop] === "undefined") {
783 extended[prop] = val;
784 }
785 });
786
787 return extended;
788 }
789};
790
791JSONEditor.defaults = {
792 themes: {},
793 templates: {},
794 iconlibs: {},
795 editors: {},
796 languages: {},
797 resolvers: [],
798 custom_validators: []
799};
800
801JSONEditor.Validator = Class.extend({
802 init: function(jsoneditor,schema,options) {
803 this.jsoneditor = jsoneditor;
804 this.schema = schema || this.jsoneditor.schema;
805 this.options = options || {};
806 this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
807 },
808 validate: function(value) {
809 return this._validateSchema(this.schema, value);
810 },
811 _validateSchema: function(schema,value,path) {
812 var self = this;
813 var errors = [];
814 var valid, i, j;
815 var stringified = JSON.stringify(value);
816
817 path = path || 'root';
818
819 // Work on a copy of the schema
820 schema = $extend({},this.jsoneditor.expandRefs(schema));
821
822 /*
823 * Type Agnostic Validation
824 */
825
826 // Version 3 `required`
827 if(schema.required && schema.required === true) {
828 if(typeof value === "undefined") {
829 errors.push({
830 path: path,
831 property: 'required',
832 message: this.translate("error_notset")
833 });
834
835 // Can't do any more validation at this point
836 return errors;
837 }
838 }
839 // Value not defined
840 else if(typeof value === "undefined") {
841 // If required_by_default is set, all fields are required
842 if(this.jsoneditor.options.required_by_default) {
843 errors.push({
844 path: path,
845 property: 'required',
846 message: this.translate("error_notset")
847 });
848 }
849 // Not required, no further validation needed
850 else {
851 return errors;
852 }
853 }
854
855 // `enum`
856 if(schema["enum"]) {
857 valid = false;
858 for(i=0; i<schema["enum"].length; i++) {
859 if(stringified === JSON.stringify(schema["enum"][i])) valid = true;
860 }
861 if(!valid) {
862 errors.push({
863 path: path,
864 property: 'enum',
865 message: this.translate("error_enum")
866 });
867 }
868 }
869
870 // `extends` (version 3)
871 if(schema["extends"]) {
872 for(i=0; i<schema["extends"].length; i++) {
873 errors = errors.concat(this._validateSchema(schema["extends"][i],value,path));
874 }
875 }
876
877 // `allOf`
878 if(schema.allOf) {
879 for(i=0; i<schema.allOf.length; i++) {
880 errors = errors.concat(this._validateSchema(schema.allOf[i],value,path));
881 }
882 }
883
884 // `anyOf`
885 if(schema.anyOf) {
886 valid = false;
887 for(i=0; i<schema.anyOf.length; i++) {
888 if(!this._validateSchema(schema.anyOf[i],value,path).length) {
889 valid = true;
890 break;
891 }
892 }
893 if(!valid) {
894 errors.push({
895 path: path,
896 property: 'anyOf',
897 message: this.translate('error_anyOf')
898 });
899 }
900 }
901
902 // `oneOf`
903 if(schema.oneOf) {
904 valid = 0;
905 var oneof_errors = [];
906 for(i=0; i<schema.oneOf.length; i++) {
907 // Set the error paths to be path.oneOf[i].rest.of.path
908 var tmp = this._validateSchema(schema.oneOf[i],value,path);
909 if(!tmp.length) {
910 valid++;
911 }
912
913 for(j=0; j<tmp.length; j++) {
914 tmp[j].path = path+'.oneOf['+i+']'+tmp[j].path.substr(path.length);
915 }
916 oneof_errors = oneof_errors.concat(tmp);
917
918 }
919 if(valid !== 1) {
920 errors.push({
921 path: path,
922 property: 'oneOf',
923 message: this.translate('error_oneOf', [valid])
924 });
925 errors = errors.concat(oneof_errors);
926 }
927 }
928
929 // `not`
930 if(schema.not) {
931 if(!this._validateSchema(schema.not,value,path).length) {
932 errors.push({
933 path: path,
934 property: 'not',
935 message: this.translate('error_not')
936 });
937 }
938 }
939
940 // `type` (both Version 3 and Version 4 support)
941 if(schema.type) {
942 // Union type
943 if(Array.isArray(schema.type)) {
944 valid = false;
945 for(i=0;i<schema.type.length;i++) {
946 if(this._checkType(schema.type[i], value)) {
947 valid = true;
948 break;
949 }
950 }
951 if(!valid) {
952 errors.push({
953 path: path,
954 property: 'type',
955 message: this.translate('error_type_union')
956 });
957 }
958 }
959 // Simple type
960 else {
961 if(!this._checkType(schema.type, value)) {
962 errors.push({
963 path: path,
964 property: 'type',
965 message: this.translate('error_type', [schema.type])
966 });
967 }
968 }
969 }
970
971
972 // `disallow` (version 3)
973 if(schema.disallow) {
974 // Union type
975 if(Array.isArray(schema.disallow)) {
976 valid = true;
977 for(i=0;i<schema.disallow.length;i++) {
978 if(this._checkType(schema.disallow[i], value)) {
979 valid = false;
980 break;
981 }
982 }
983 if(!valid) {
984 errors.push({
985 path: path,
986 property: 'disallow',
987 message: this.translate('error_disallow_union')
988 });
989 }
990 }
991 // Simple type
992 else {
993 if(this._checkType(schema.disallow, value)) {
994 errors.push({
995 path: path,
996 property: 'disallow',
997 message: this.translate('error_disallow', [schema.disallow])
998 });
999 }
1000 }
1001 }
1002
1003 /*
1004 * Type Specific Validation
1005 */
1006
1007 // Number Specific Validation
1008 if(typeof value === "number") {
1009 // `multipleOf` and `divisibleBy`
1010 if(schema.multipleOf || schema.divisibleBy) {
1011 var divisor = schema.multipleOf || schema.divisibleBy;
1012 // Vanilla JS, prone to floating point rounding errors (e.g. 1.14 / .01 == 113.99999)
1013 valid = (value/divisor === Math.floor(value/divisor));
1014
1015 // Use math.js is available
1016 if(window.math) {
1017 valid = window.math.mod(window.math.bignumber(value), window.math.bignumber(divisor)).equals(0);
1018 }
1019 // Use decimal.js is available
1020 else if(window.Decimal) {
1021 valid = (new window.Decimal(value)).mod(new window.Decimal(divisor)).equals(0);
1022 }
1023
1024 if(!valid) {
1025 errors.push({
1026 path: path,
1027 property: schema.multipleOf? 'multipleOf' : 'divisibleBy',
1028 message: this.translate('error_multipleOf', [divisor])
1029 });
1030 }
1031 }
1032
1033 // `maximum`
1034 if(schema.hasOwnProperty('maximum')) {
1035 // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
1036 valid = schema.exclusiveMaximum? (value < schema.maximum) : (value <= schema.maximum);
1037
1038 // Use math.js is available
1039 if(window.math) {
1040 valid = window.math[schema.exclusiveMaximum?'smaller':'smallerEq'](
1041 window.math.bignumber(value),
1042 window.math.bignumber(schema.maximum)
1043 );
1044 }
1045 // Use Decimal.js if available
1046 else if(window.Decimal) {
1047 valid = (new window.Decimal(value))[schema.exclusiveMaximum?'lt':'lte'](new window.Decimal(schema.maximum));
1048 }
1049
1050 if(!valid) {
1051 errors.push({
1052 path: path,
1053 property: 'maximum',
1054 message: this.translate(
1055 (schema.exclusiveMaximum?'error_maximum_excl':'error_maximum_incl'),
1056 [schema.maximum]
1057 )
1058 });
1059 }
1060 }
1061
1062 // `minimum`
1063 if(schema.hasOwnProperty('minimum')) {
1064 // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
1065 valid = schema.exclusiveMinimum? (value > schema.minimum) : (value >= schema.minimum);
1066
1067 // Use math.js is available
1068 if(window.math) {
1069 valid = window.math[schema.exclusiveMinimum?'larger':'largerEq'](
1070 window.math.bignumber(value),
1071 window.math.bignumber(schema.minimum)
1072 );
1073 }
1074 // Use Decimal.js if available
1075 else if(window.Decimal) {
1076 valid = (new window.Decimal(value))[schema.exclusiveMinimum?'gt':'gte'](new window.Decimal(schema.minimum));
1077 }
1078
1079 if(!valid) {
1080 errors.push({
1081 path: path,
1082 property: 'minimum',
1083 message: this.translate(
1084 (schema.exclusiveMinimum?'error_minimum_excl':'error_minimum_incl'),
1085 [schema.minimum]
1086 )
1087 });
1088 }
1089 }
1090 }
1091 // String specific validation
1092 else if(typeof value === "string") {
1093 // `maxLength`
1094 if(schema.maxLength) {
1095 if((value+"").length > schema.maxLength) {
1096 errors.push({
1097 path: path,
1098 property: 'maxLength',
1099 message: this.translate('error_maxLength', [schema.maxLength])
1100 });
1101 }
1102 }
1103
1104 // `minLength`
1105 if(schema.minLength) {
1106 if((value+"").length < schema.minLength) {
1107 errors.push({
1108 path: path,
1109 property: 'minLength',
1110 message: this.translate((schema.minLength===1?'error_notempty':'error_minLength'), [schema.minLength])
1111 });
1112 }
1113 }
1114
1115 // `pattern`
1116 if(schema.pattern) {
1117 if(!(new RegExp(schema.pattern)).test(value)) {
1118 errors.push({
1119 path: path,
1120 property: 'pattern',
1121 message: this.translate('error_pattern', [schema.pattern])
1122 });
1123 }
1124 }
1125 }
1126 // Array specific validation
1127 else if(typeof value === "object" && value !== null && Array.isArray(value)) {
1128 // `items` and `additionalItems`
1129 if(schema.items) {
1130 // `items` is an array
1131 if(Array.isArray(schema.items)) {
1132 for(i=0; i<value.length; i++) {
1133 // If this item has a specific schema tied to it
1134 // Validate against it
1135 if(schema.items[i]) {
1136 errors = errors.concat(this._validateSchema(schema.items[i],value[i],path+'.'+i));
1137 }
1138 // If all additional items are allowed
1139 else if(schema.additionalItems === true) {
1140 break;
1141 }
1142 // If additional items is a schema
1143 // TODO: Incompatibility between version 3 and 4 of the spec
1144 else if(schema.additionalItems) {
1145 errors = errors.concat(this._validateSchema(schema.additionalItems,value[i],path+'.'+i));
1146 }
1147 // If no additional items are allowed
1148 else if(schema.additionalItems === false) {
1149 errors.push({
1150 path: path,
1151 property: 'additionalItems',
1152 message: this.translate('error_additionalItems')
1153 });
1154 break;
1155 }
1156 // Default for `additionalItems` is an empty schema
1157 else {
1158 break;
1159 }
1160 }
1161 }
1162 // `items` is a schema
1163 else {
1164 // Each item in the array must validate against the schema
1165 for(i=0; i<value.length; i++) {
1166 errors = errors.concat(this._validateSchema(schema.items,value[i],path+'.'+i));
1167 }
1168 }
1169 }
1170
1171 // `maxItems`
1172 if(schema.maxItems) {
1173 if(value.length > schema.maxItems) {
1174 errors.push({
1175 path: path,
1176 property: 'maxItems',
1177 message: this.translate('error_maxItems', [schema.maxItems])
1178 });
1179 }
1180 }
1181
1182 // `minItems`
1183 if(schema.minItems) {
1184 if(value.length < schema.minItems) {
1185 errors.push({
1186 path: path,
1187 property: 'minItems',
1188 message: this.translate('error_minItems', [schema.minItems])
1189 });
1190 }
1191 }
1192
1193 // `uniqueItems`
1194 if(schema.uniqueItems) {
1195 var seen = {};
1196 for(i=0; i<value.length; i++) {
1197 valid = JSON.stringify(value[i]);
1198 if(seen[valid]) {
1199 errors.push({
1200 path: path,
1201 property: 'uniqueItems',
1202 message: this.translate('error_uniqueItems')
1203 });
1204 break;
1205 }
1206 seen[valid] = true;
1207 }
1208 }
1209 }
1210 // Object specific validation
1211 else if(typeof value === "object" && value !== null) {
1212 // `maxProperties`
1213 if(schema.maxProperties) {
1214 valid = 0;
1215 for(i in value) {
1216 if(!value.hasOwnProperty(i)) continue;
1217 valid++;
1218 }
1219 if(valid > schema.maxProperties) {
1220 errors.push({
1221 path: path,
1222 property: 'maxProperties',
1223 message: this.translate('error_maxProperties', [schema.maxProperties])
1224 });
1225 }
1226 }
1227
1228 // `minProperties`
1229 if(schema.minProperties) {
1230 valid = 0;
1231 for(i in value) {
1232 if(!value.hasOwnProperty(i)) continue;
1233 valid++;
1234 }
1235 if(valid < schema.minProperties) {
1236 errors.push({
1237 path: path,
1238 property: 'minProperties',
1239 message: this.translate('error_minProperties', [schema.minProperties])
1240 });
1241 }
1242 }
1243
1244 // Version 4 `required`
1245 if(schema.required && Array.isArray(schema.required)) {
1246 for(i=0; i<schema.required.length; i++) {
1247 if(typeof value[schema.required[i]] === "undefined") {
1248 errors.push({
1249 path: path,
1250 property: 'required',
1251 message: this.translate('error_required', [schema.required[i]])
1252 });
1253 }
1254 }
1255 }
1256
1257 // `properties`
1258 var validated_properties = {};
1259 if(schema.properties) {
1260 for(i in schema.properties) {
1261 if(!schema.properties.hasOwnProperty(i)) continue;
1262 validated_properties[i] = true;
1263 errors = errors.concat(this._validateSchema(schema.properties[i],value[i],path+'.'+i));
1264 }
1265 }
1266
1267 // `patternProperties`
1268 if(schema.patternProperties) {
1269 for(i in schema.patternProperties) {
1270 if(!schema.patternProperties.hasOwnProperty(i)) continue;
1271
1272 var regex = new RegExp(i);
1273
1274 // Check which properties match
1275 for(j in value) {
1276 if(!value.hasOwnProperty(j)) continue;
1277 if(regex.test(j)) {
1278 validated_properties[j] = true;
1279 errors = errors.concat(this._validateSchema(schema.patternProperties[i],value[j],path+'.'+j));
1280 }
1281 }
1282 }
1283 }
1284
1285 // The no_additional_properties option currently doesn't work with extended schemas that use oneOf or anyOf
1286 if(typeof schema.additionalProperties === "undefined" && this.jsoneditor.options.no_additional_properties && !schema.oneOf && !schema.anyOf) {
1287 schema.additionalProperties = false;
1288 }
1289
1290 // `additionalProperties`
1291 if(typeof schema.additionalProperties !== "undefined") {
1292 for(i in value) {
1293 if(!value.hasOwnProperty(i)) continue;
1294 if(!validated_properties[i]) {
1295 // No extra properties allowed
1296 if(!schema.additionalProperties) {
1297 errors.push({
1298 path: path,
1299 property: 'additionalProperties',
1300 message: this.translate('error_additional_properties', [i])
1301 });
1302 break;
1303 }
1304 // Allowed
1305 else if(schema.additionalProperties === true) {
1306 break;
1307 }
1308 // Must match schema
1309 // TODO: incompatibility between version 3 and 4 of the spec
1310 else {
1311 errors = errors.concat(this._validateSchema(schema.additionalProperties,value[i],path+'.'+i));
1312 }
1313 }
1314 }
1315 }
1316
1317 // `dependencies`
1318 if(schema.dependencies) {
1319 for(i in schema.dependencies) {
1320 if(!schema.dependencies.hasOwnProperty(i)) continue;
1321
1322 // Doesn't need to meet the dependency
1323 if(typeof value[i] === "undefined") continue;
1324
1325 // Property dependency
1326 if(Array.isArray(schema.dependencies[i])) {
1327 for(j=0; j<schema.dependencies[i].length; j++) {
1328 if(typeof value[schema.dependencies[i][j]] === "undefined") {
1329 errors.push({
1330 path: path,
1331 property: 'dependencies',
1332 message: this.translate('error_dependency', [schema.dependencies[i][j]])
1333 });
1334 }
1335 }
1336 }
1337 // Schema dependency
1338 else {
1339 errors = errors.concat(this._validateSchema(schema.dependencies[i],value,path));
1340 }
1341 }
1342 }
1343 }
1344
1345 // Custom type validation (global)
1346 $each(JSONEditor.defaults.custom_validators,function(i,validator) {
1347 errors = errors.concat(validator.call(self,schema,value,path));
1348 });
1349 // Custom type validation (instance specific)
1350 if(this.options.custom_validators) {
1351 $each(this.options.custom_validators,function(i,validator) {
1352 errors = errors.concat(validator.call(self,schema,value,path));
1353 });
1354 }
1355
1356 return errors;
1357 },
1358 _checkType: function(type, value) {
1359 // Simple types
1360 if(typeof type === "string") {
1361 if(type==="string") return typeof value === "string";
1362 else if(type==="number") return typeof value === "number";
1363 else if(type==="integer") return typeof value === "number" && value === Math.floor(value);
1364 else if(type==="boolean") return typeof value === "boolean";
1365 else if(type==="array") return Array.isArray(value);
1366 else if(type === "object") return value !== null && !(Array.isArray(value)) && typeof value === "object";
1367 else if(type === "null") return value === null;
1368 else return true;
1369 }
1370 // Schema
1371 else {
1372 return !this._validateSchema(type,value).length;
1373 }
1374 }
1375});
1376
1377/**
1378 * All editors should extend from this class
1379 */
1380JSONEditor.AbstractEditor = Class.extend({
1381 onChildEditorChange: function(editor) {
1382 this.onChange(true);
1383 },
1384 notify: function() {
1385 this.jsoneditor.notifyWatchers(this.path);
1386 },
1387 change: function() {
1388 if(this.parent) this.parent.onChildEditorChange(this);
1389 else this.jsoneditor.onChange();
1390 },
1391 onChange: function(bubble) {
1392 this.notify();
1393 if(this.watch_listener) this.watch_listener();
1394 if(bubble) this.change();
1395 },
1396 register: function() {
1397 this.jsoneditor.registerEditor(this);
1398 this.onChange();
1399 },
1400 unregister: function() {
1401 if(!this.jsoneditor) return;
1402 this.jsoneditor.unregisterEditor(this);
1403 },
1404 getNumColumns: function() {
1405 return 12;
1406 },
1407 init: function(options) {
1408 this.jsoneditor = options.jsoneditor;
1409
1410 this.theme = this.jsoneditor.theme;
1411 this.template_engine = this.jsoneditor.template;
1412 this.iconlib = this.jsoneditor.iconlib;
1413
1414 this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
1415
1416 this.original_schema = options.schema;
1417 this.schema = this.jsoneditor.expandSchema(this.original_schema);
1418
1419 this.options = $extend({}, (this.options || {}), (options.schema.options || {}), options);
1420
1421 if(!options.path && !this.schema.id) this.schema.id = 'root';
1422 this.path = options.path || 'root';
1423 this.formname = options.formname || this.path.replace(/\.([^.]+)/g,'[$1]');
1424 if(this.jsoneditor.options.form_name_root) this.formname = this.formname.replace(/^root\[/,this.jsoneditor.options.form_name_root+'[');
1425 this.key = this.path.split('.').pop();
1426 this.parent = options.parent;
1427
1428 this.link_watchers = [];
1429
1430 if(options.container) this.setContainer(options.container);
1431 },
1432 setContainer: function(container) {
1433 this.container = container;
1434 if(this.schema.id) this.container.setAttribute('data-schemaid',this.schema.id);
1435 if(this.schema.type && typeof this.schema.type === "string") this.container.setAttribute('data-schematype',this.schema.type);
1436 this.container.setAttribute('data-schemapath',this.path);
1437 },
1438
1439 preBuild: function() {
1440
1441 },
1442 build: function() {
1443
1444 },
1445 postBuild: function() {
1446 this.setupWatchListeners();
1447 this.addLinks();
1448 this.setValue(this.getDefault(), true);
1449 this.updateHeaderText();
1450 this.register();
1451 this.onWatchedFieldChange();
1452 },
1453
1454 setupWatchListeners: function() {
1455 var self = this;
1456
1457 // Watched fields
1458 this.watched = {};
1459 if(this.schema.vars) this.schema.watch = this.schema.vars;
1460 this.watched_values = {};
1461 this.watch_listener = function() {
1462 if(self.refreshWatchedFieldValues()) {
1463 self.onWatchedFieldChange();
1464 }
1465 };
1466
1467 this.register();
1468 if(this.schema.hasOwnProperty('watch')) {
1469 var path,path_parts,first,root,adjusted_path;
1470
1471 for(var name in this.schema.watch) {
1472 if(!this.schema.watch.hasOwnProperty(name)) continue;
1473 path = this.schema.watch[name];
1474
1475 if(Array.isArray(path)) {
1476 path_parts = [path[0]].concat(path[1].split('.'));
1477 }
1478 else {
1479 path_parts = path.split('.');
1480 if(!self.theme.closest(self.container,'[data-schemaid="'+path_parts[0]+'"]')) path_parts.unshift('#');
1481 }
1482 first = path_parts.shift();
1483
1484 if(first === '#') first = self.jsoneditor.schema.id || 'root';
1485
1486 // Find the root node for this template variable
1487 root = self.theme.closest(self.container,'[data-schemaid="'+first+'"]');
1488 if(!root) throw "Could not find ancestor node with id "+first;
1489
1490 // Keep track of the root node and path for use when rendering the template
1491 adjusted_path = root.getAttribute('data-schemapath') + '.' + path_parts.join('.');
1492
1493 self.jsoneditor.watch(adjusted_path,self.watch_listener);
1494
1495 self.watched[name] = adjusted_path;
1496 }
1497 }
1498
1499 // Dynamic header
1500 if(this.schema.headerTemplate) {
1501 this.header_template = this.jsoneditor.compileTemplate(this.schema.headerTemplate, this.template_engine);
1502 }
1503 },
1504
1505 addLinks: function() {
1506 // Add links
1507 if(!this.no_link_holder) {
1508 this.link_holder = this.theme.getLinksHolder();
1509 this.container.appendChild(this.link_holder);
1510 if(this.schema.links) {
1511 for(var i=0; i<this.schema.links.length; i++) {
1512 this.addLink(this.getLink(this.schema.links[i]));
1513 }
1514 }
1515 }
1516 },
1517
1518
1519 getButton: function(text, icon, title) {
1520 var btnClass = 'json-editor-btn-'+icon;
1521 if(!this.iconlib) icon = null;
1522 else icon = this.iconlib.getIcon(icon);
1523
1524 if(!icon && title) {
1525 text = title;
1526 title = null;
1527 }
1528
1529 var btn = this.theme.getButton(text, icon, title);
1530 btn.className += ' ' + btnClass + ' ';
1531 return btn;
1532 },
1533 setButtonText: function(button, text, icon, title) {
1534 if(!this.iconlib) icon = null;
1535 else icon = this.iconlib.getIcon(icon);
1536
1537 if(!icon && title) {
1538 text = title;
1539 title = null;
1540 }
1541
1542 return this.theme.setButtonText(button, text, icon, title);
1543 },
1544 addLink: function(link) {
1545 if(this.link_holder) this.link_holder.appendChild(link);
1546 },
1547 getLink: function(data) {
1548 var holder, link;
1549
1550 // Get mime type of the link
1551 var mime = data.mediaType || 'application/javascript';
1552 var type = mime.split('/')[0];
1553
1554 // Template to generate the link href
1555 var href = this.jsoneditor.compileTemplate(data.href,this.template_engine);
1556
1557 // Template to generate the link's download attribute
1558 var download = null;
1559 if(data.download) download = data.download;
1560
1561 if(download && download !== true) {
1562 download = this.jsoneditor.compileTemplate(download, this.template_engine);
1563 }
1564
1565 // Image links
1566 if(type === 'image') {
1567 holder = this.theme.getBlockLinkHolder();
1568 link = document.createElement('a');
1569 link.setAttribute('target','_blank');
1570 var image = document.createElement('img');
1571
1572 this.theme.createImageLink(holder,link,image);
1573
1574 // When a watched field changes, update the url
1575 this.link_watchers.push(function(vars) {
1576 var url = href(vars);
1577 link.setAttribute('href',url);
1578 link.setAttribute('title',data.rel || url);
1579 image.setAttribute('src',url);
1580 });
1581 }
1582 // Audio/Video links
1583 else if(['audio','video'].indexOf(type) >=0) {
1584 holder = this.theme.getBlockLinkHolder();
1585
1586 link = this.theme.getBlockLink();
1587 link.setAttribute('target','_blank');
1588
1589 var media = document.createElement(type);
1590 media.setAttribute('controls','controls');
1591
1592 this.theme.createMediaLink(holder,link,media);
1593
1594 // When a watched field changes, update the url
1595 this.link_watchers.push(function(vars) {
1596 var url = href(vars);
1597 link.setAttribute('href',url);
1598 link.textContent = data.rel || url;
1599 media.setAttribute('src',url);
1600 });
1601 }
1602 // Text links
1603 else {
1604 link = holder = this.theme.getBlockLink();
1605 holder.setAttribute('target','_blank');
1606 holder.textContent = data.rel;
1607
1608 // When a watched field changes, update the url
1609 this.link_watchers.push(function(vars) {
1610 var url = href(vars);
1611 holder.setAttribute('href',url);
1612 holder.textContent = data.rel || url;
1613 });
1614 }
1615
1616 if(download && link) {
1617 if(download === true) {
1618 link.setAttribute('download','');
1619 }
1620 else {
1621 this.link_watchers.push(function(vars) {
1622 link.setAttribute('download',download(vars));
1623 });
1624 }
1625 }
1626
1627 return holder;
1628 },
1629 refreshWatchedFieldValues: function() {
1630 if(!this.watched_values) return;
1631 var watched = {};
1632 var changed = false;
1633 var self = this;
1634
1635 if(this.watched) {
1636 var val,editor;
1637 for(var name in this.watched) {
1638 if(!this.watched.hasOwnProperty(name)) continue;
1639 editor = self.jsoneditor.getEditor(this.watched[name]);
1640 val = editor? editor.getValue() : null;
1641 if(self.watched_values[name] !== val) changed = true;
1642 watched[name] = val;
1643 }
1644 }
1645
1646 watched.self = this.getValue();
1647 if(this.watched_values.self !== watched.self) changed = true;
1648
1649 this.watched_values = watched;
1650
1651 return changed;
1652 },
1653 getWatchedFieldValues: function() {
1654 return this.watched_values;
1655 },
1656 updateHeaderText: function() {
1657 if(this.header) {
1658 // If the header has children, only update the text node's value
1659 if(this.header.children.length) {
1660 for(var i=0; i<this.header.childNodes.length; i++) {
1661 if(this.header.childNodes[i].nodeType===3) {
1662 this.header.childNodes[i].nodeValue = this.getHeaderText();
1663 break;
1664 }
1665 }
1666 }
1667 // Otherwise, just update the entire node
1668 else {
1669 this.header.textContent = this.getHeaderText();
1670 }
1671 }
1672 },
1673 getHeaderText: function(title_only) {
1674 if(this.header_text) return this.header_text;
1675 else if(title_only) return this.schema.title;
1676 else return this.getTitle();
1677 },
1678 onWatchedFieldChange: function() {
1679 var vars;
1680 if(this.header_template) {
1681 vars = $extend(this.getWatchedFieldValues(),{
1682 key: this.key,
1683 i: this.key,
1684 i0: (this.key*1),
1685 i1: (this.key*1+1),
1686 title: this.getTitle()
1687 });
1688 var header_text = this.header_template(vars);
1689
1690 if(header_text !== this.header_text) {
1691 this.header_text = header_text;
1692 this.updateHeaderText();
1693 this.notify();
1694 //this.fireChangeHeaderEvent();
1695 }
1696 }
1697 if(this.link_watchers.length) {
1698 vars = this.getWatchedFieldValues();
1699 for(var i=0; i<this.link_watchers.length; i++) {
1700 this.link_watchers[i](vars);
1701 }
1702 }
1703 },
1704 setValue: function(value) {
1705 this.value = value;
1706 },
1707 getValue: function() {
1708 return this.value;
1709 },
1710 refreshValue: function() {
1711
1712 },
1713 getChildEditors: function() {
1714 return false;
1715 },
1716 destroy: function() {
1717 var self = this;
1718 this.unregister(this);
1719 $each(this.watched,function(name,adjusted_path) {
1720 self.jsoneditor.unwatch(adjusted_path,self.watch_listener);
1721 });
1722 this.watched = null;
1723 this.watched_values = null;
1724 this.watch_listener = null;
1725 this.header_text = null;
1726 this.header_template = null;
1727 this.value = null;
1728 if(this.container && this.container.parentNode) this.container.parentNode.removeChild(this.container);
1729 this.container = null;
1730 this.jsoneditor = null;
1731 this.schema = null;
1732 this.path = null;
1733 this.key = null;
1734 this.parent = null;
1735 },
1736 getDefault: function() {
1737 if(this.schema["default"]) return this.schema["default"];
1738 if(this.schema["enum"]) return this.schema["enum"][0];
1739
1740 var type = this.schema.type || this.schema.oneOf;
1741 if(type && Array.isArray(type)) type = type[0];
1742 if(type && typeof type === "object") type = type.type;
1743 if(type && Array.isArray(type)) type = type[0];
1744
1745 if(typeof type === "string") {
1746 if(type === "number") return 0.0;
1747 if(type === "boolean") return false;
1748 if(type === "integer") return 0;
1749 if(type === "string") return "";
1750 if(type === "object") return {};
1751 if(type === "array") return [];
1752 }
1753
1754 return null;
1755 },
1756 getTitle: function() {
1757 return this.schema.title || this.key;
1758 },
1759 enable: function() {
1760 this.disabled = false;
1761 },
1762 disable: function() {
1763 this.disabled = true;
1764 },
1765 isEnabled: function() {
1766 return !this.disabled;
1767 },
1768 isRequired: function() {
1769 if(typeof this.schema.required === "boolean") return this.schema.required;
1770 else if(this.parent && this.parent.schema && Array.isArray(this.parent.schema.required)) return this.parent.schema.required.indexOf(this.key) > -1;
1771 else if(this.jsoneditor.options.required_by_default) return true;
1772 else return false;
1773 },
1774 getDisplayText: function(arr) {
1775 var disp = [];
1776 var used = {};
1777
1778 // Determine how many times each attribute name is used.
1779 // This helps us pick the most distinct display text for the schemas.
1780 $each(arr,function(i,el) {
1781 if(el.title) {
1782 used[el.title] = used[el.title] || 0;
1783 used[el.title]++;
1784 }
1785 if(el.description) {
1786 used[el.description] = used[el.description] || 0;
1787 used[el.description]++;
1788 }
1789 if(el.format) {
1790 used[el.format] = used[el.format] || 0;
1791 used[el.format]++;
1792 }
1793 if(el.type) {
1794 used[el.type] = used[el.type] || 0;
1795 used[el.type]++;
1796 }
1797 });
1798
1799 // Determine display text for each element of the array
1800 $each(arr,function(i,el) {
1801 var name;
1802
1803 // If it's a simple string
1804 if(typeof el === "string") name = el;
1805 // Object
1806 else if(el.title && used[el.title]<=1) name = el.title;
1807 else if(el.format && used[el.format]<=1) name = el.format;
1808 else if(el.type && used[el.type]<=1) name = el.type;
1809 else if(el.description && used[el.description]<=1) name = el.descripton;
1810 else if(el.title) name = el.title;
1811 else if(el.format) name = el.format;
1812 else if(el.type) name = el.type;
1813 else if(el.description) name = el.description;
1814 else if(JSON.stringify(el).length < 50) name = JSON.stringify(el);
1815 else name = "type";
1816
1817 disp.push(name);
1818 });
1819
1820 // Replace identical display text with "text 1", "text 2", etc.
1821 var inc = {};
1822 $each(disp,function(i,name) {
1823 inc[name] = inc[name] || 0;
1824 inc[name]++;
1825
1826 if(used[name] > 1) disp[i] = name + " " + inc[name];
1827 });
1828
1829 return disp;
1830 },
1831 getOption: function(key) {
1832 try {
1833 throw "getOption is deprecated";
1834 }
1835 catch(e) {
1836 window.console.error(e);
1837 }
1838
1839 return this.options[key];
1840 },
1841 showValidationErrors: function(errors) {
1842
1843 }
1844});
1845
1846JSONEditor.defaults.editors["null"] = JSONEditor.AbstractEditor.extend({
1847 getValue: function() {
1848 return null;
1849 },
1850 setValue: function() {
1851 this.onChange();
1852 },
1853 getNumColumns: function() {
1854 return 2;
1855 }
1856});
1857
1858JSONEditor.defaults.editors.string = JSONEditor.AbstractEditor.extend({
1859 register: function() {
1860 this._super();
1861 if(!this.input) return;
1862 this.input.setAttribute('name',this.formname);
1863 },
1864 unregister: function() {
1865 this._super();
1866 if(!this.input) return;
1867 this.input.removeAttribute('name');
1868 },
1869 setValue: function(value,initial,from_template) {
1870 var self = this;
1871
1872 if(this.template && !from_template) {
1873 return;
1874 }
1875
1876 if(value === null || typeof value === 'undefined') value = "";
1877 else if(typeof value === "object") value = JSON.stringify(value);
1878 else if(typeof value !== "string") value = ""+value;
1879
1880 if(value === this.serialized) return;
1881
1882 // Sanitize value before setting it
1883 var sanitized = this.sanitize(value);
1884
1885 if(this.input.value === sanitized) {
1886 return;
1887 }
1888
1889 this.input.value = sanitized;
1890
1891 // If using SCEditor, update the WYSIWYG
1892 if(this.sceditor_instance) {
1893 this.sceditor_instance.val(sanitized);
1894 }
1895 else if(this.epiceditor) {
1896 this.epiceditor.importFile(null,sanitized);
1897 }
1898 else if(this.ace_editor) {
1899 this.ace_editor.setValue(sanitized);
1900 }
1901
1902 var changed = from_template || this.getValue() !== value;
1903
1904 this.refreshValue();
1905
1906 if(initial) this.is_dirty = false;
1907 else if(this.jsoneditor.options.show_errors === "change") this.is_dirty = true;
1908
1909 if(this.adjust_height) this.adjust_height(this.input);
1910
1911 // Bubble this setValue to parents if the value changed
1912 this.onChange(changed);
1913 },
1914 getNumColumns: function() {
1915 var min = Math.ceil(Math.max(this.getTitle().length,this.schema.maxLength||0,this.schema.minLength||0)/5);
1916 var num;
1917
1918 if(this.input_type === 'textarea') num = 6;
1919 else if(['text','email'].indexOf(this.input_type) >= 0) num = 4;
1920 else num = 2;
1921
1922 return Math.min(12,Math.max(min,num));
1923 },
1924 build: function() {
1925 var self = this, i;
1926 if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
1927 if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
1928
1929 this.format = this.schema.format;
1930 if(!this.format && this.schema.media && this.schema.media.type) {
1931 this.format = this.schema.media.type.replace(/(^(application|text)\/(x-)?(script\.)?)|(-source$)/g,'');
1932 }
1933 if(!this.format && this.options.default_format) {
1934 this.format = this.options.default_format;
1935 }
1936 if(this.options.format) {
1937 this.format = this.options.format;
1938 }
1939
1940 // Specific format
1941 if(this.format) {
1942 // Text Area
1943 if(this.format === 'textarea') {
1944 this.input_type = 'textarea';
1945 this.input = this.theme.getTextareaInput();
1946 }
1947 // Range Input
1948 else if(this.format === 'range') {
1949 this.input_type = 'range';
1950 var min = this.schema.minimum || 0;
1951 var max = this.schema.maximum || Math.max(100,min+1);
1952 var step = 1;
1953 if(this.schema.multipleOf) {
1954 if(min%this.schema.multipleOf) min = Math.ceil(min/this.schema.multipleOf)*this.schema.multipleOf;
1955 if(max%this.schema.multipleOf) max = Math.floor(max/this.schema.multipleOf)*this.schema.multipleOf;
1956 step = this.schema.multipleOf;
1957 }
1958
1959 this.input = this.theme.getRangeInput(min,max,step);
1960 }
1961 // Source Code
1962 else if([
1963 'actionscript',
1964 'batchfile',
1965 'bbcode',
1966 'c',
1967 'c++',
1968 'cpp',
1969 'coffee',
1970 'csharp',
1971 'css',
1972 'dart',
1973 'django',
1974 'ejs',
1975 'erlang',
1976 'golang',
1977 'groovy',
1978 'handlebars',
1979 'haskell',
1980 'haxe',
1981 'html',
1982 'ini',
1983 'jade',
1984 'java',
1985 'javascript',
1986 'json',
1987 'less',
1988 'lisp',
1989 'lua',
1990 'makefile',
1991 'markdown',
1992 'matlab',
1993 'mysql',
1994 'objectivec',
1995 'pascal',
1996 'perl',
1997 'pgsql',
1998 'php',
1999 'python',
2000 'r',
2001 'ruby',
2002 'sass',
2003 'scala',
2004 'scss',
2005 'smarty',
2006 'sql',
2007 'stylus',
2008 'svg',
2009 'twig',
2010 'vbscript',
2011 'xml',
2012 'yaml'
2013 ].indexOf(this.format) >= 0
2014 ) {
2015 this.input_type = this.format;
2016 this.source_code = true;
2017
2018 this.input = this.theme.getTextareaInput();
2019 }
2020 // HTML5 Input type
2021 else {
2022 this.input_type = this.format;
2023 this.input = this.theme.getFormInputField(this.input_type);
2024 }
2025 }
2026 // Normal text input
2027 else {
2028 this.input_type = 'text';
2029 this.input = this.theme.getFormInputField(this.input_type);
2030 }
2031
2032 // minLength, maxLength, and pattern
2033 if(typeof this.schema.maxLength !== "undefined") this.input.setAttribute('maxlength',this.schema.maxLength);
2034 if(typeof this.schema.pattern !== "undefined") this.input.setAttribute('pattern',this.schema.pattern);
2035 else if(typeof this.schema.minLength !== "undefined") this.input.setAttribute('pattern','.{'+this.schema.minLength+',}');
2036
2037 if(this.options.compact) {
2038 this.container.className += ' compact';
2039 }
2040 else {
2041 if(this.options.input_width) this.input.style.width = this.options.input_width;
2042 }
2043
2044 if(this.schema.readOnly || this.schema.readonly || this.schema.template) {
2045 this.always_disabled = true;
2046 this.input.disabled = true;
2047 }
2048
2049 this.input
2050 .addEventListener('change',function(e) {
2051 e.preventDefault();
2052 e.stopPropagation();
2053
2054 // Don't allow changing if this field is a template
2055 if(self.schema.template) {
2056 this.value = self.value;
2057 return;
2058 }
2059
2060 var val = this.value;
2061
2062 // sanitize value
2063 var sanitized = self.sanitize(val);
2064 if(val !== sanitized) {
2065 this.value = sanitized;
2066 }
2067
2068 self.is_dirty = true;
2069
2070 self.refreshValue();
2071 self.onChange(true);
2072 });
2073
2074 if(this.options.input_height) this.input.style.height = this.options.input_height;
2075 if(this.options.expand_height) {
2076 this.adjust_height = function(el) {
2077 if(!el) return;
2078 var i, ch=el.offsetHeight;
2079 // Input too short
2080 if(el.offsetHeight < el.scrollHeight) {
2081 i=0;
2082 while(el.offsetHeight < el.scrollHeight+3) {
2083 if(i>100) break;
2084 i++;
2085 ch++;
2086 el.style.height = ch+'px';
2087 }
2088 }
2089 else {
2090 i=0;
2091 while(el.offsetHeight >= el.scrollHeight+3) {
2092 if(i>100) break;
2093 i++;
2094 ch--;
2095 el.style.height = ch+'px';
2096 }
2097 el.style.height = (ch+1)+'px';
2098 }
2099 };
2100
2101 this.input.addEventListener('keyup',function(e) {
2102 self.adjust_height(this);
2103 });
2104 this.input.addEventListener('change',function(e) {
2105 self.adjust_height(this);
2106 });
2107 this.adjust_height();
2108 }
2109
2110 if(this.format) this.input.setAttribute('data-schemaformat',this.format);
2111
2112 this.control = this.theme.getFormControl(this.label, this.input, this.description);
2113 this.container.appendChild(this.control);
2114
2115 // Any special formatting that needs to happen after the input is added to the dom
2116 window.requestAnimationFrame(function() {
2117 // Skip in case the input is only a temporary editor,
2118 // otherwise, in the case of an ace_editor creation,
2119 // it will generate an error trying to append it to the missing parentNode
2120 if(self.input.parentNode) self.afterInputReady();
2121 if(self.adjust_height) self.adjust_height(self.input);
2122 });
2123
2124 // Compile and store the template
2125 if(this.schema.template) {
2126 this.template = this.jsoneditor.compileTemplate(this.schema.template, this.template_engine);
2127 this.refreshValue();
2128 }
2129 else {
2130 this.refreshValue();
2131 }
2132 },
2133 enable: function() {
2134 if(!this.always_disabled) {
2135 this.input.disabled = false;
2136 // TODO: WYSIWYG and Markdown editors
2137 }
2138 this._super();
2139 },
2140 disable: function() {
2141 this.input.disabled = true;
2142 // TODO: WYSIWYG and Markdown editors
2143 this._super();
2144 },
2145 afterInputReady: function() {
2146 var self = this, options;
2147
2148 // Code editor
2149 if(this.source_code) {
2150 // WYSIWYG html and bbcode editor
2151 if(this.options.wysiwyg &&
2152 ['html','bbcode'].indexOf(this.input_type) >= 0 &&
2153 window.jQuery && window.jQuery.fn && window.jQuery.fn.sceditor
2154 ) {
2155 options = $extend({},{
2156 plugins: self.input_type==='html'? 'xhtml' : 'bbcode',
2157 emoticonsEnabled: false,
2158 width: '100%',
2159 height: 300
2160 },JSONEditor.plugins.sceditor,self.options.sceditor_options||{});
2161
2162 window.jQuery(self.input).sceditor(options);
2163
2164 self.sceditor_instance = window.jQuery(self.input).sceditor('instance');
2165
2166 self.sceditor_instance.blur(function() {
2167 // Get editor's value
2168 var val = window.jQuery("<div>"+self.sceditor_instance.val()+"</div>");
2169 // Remove sceditor spans/divs
2170 window.jQuery('#sceditor-start-marker,#sceditor-end-marker,.sceditor-nlf',val).remove();
2171 // Set the value and update
2172 self.input.value = val.html();
2173 self.value = self.input.value;
2174 self.is_dirty = true;
2175 self.onChange(true);
2176 });
2177 }
2178 // EpicEditor for markdown (if it's loaded)
2179 else if (this.input_type === 'markdown' && window.EpicEditor) {
2180 this.epiceditor_container = document.createElement('div');
2181 this.input.parentNode.insertBefore(this.epiceditor_container,this.input);
2182 this.input.style.display = 'none';
2183
2184 options = $extend({},JSONEditor.plugins.epiceditor,{
2185 container: this.epiceditor_container,
2186 clientSideStorage: false
2187 });
2188
2189 this.epiceditor = new window.EpicEditor(options).load();
2190
2191 this.epiceditor.importFile(null,this.getValue());
2192
2193 this.epiceditor.on('update',function() {
2194 var val = self.epiceditor.exportFile();
2195 self.input.value = val;
2196 self.value = val;
2197 self.is_dirty = true;
2198 self.onChange(true);
2199 });
2200 }
2201 // ACE editor for everything else
2202 else if(window.ace) {
2203 var mode = this.input_type;
2204 // aliases for c/cpp
2205 if(mode === 'cpp' || mode === 'c++' || mode === 'c') {
2206 mode = 'c_cpp';
2207 }
2208
2209 this.ace_container = document.createElement('div');
2210 this.ace_container.style.width = '100%';
2211 this.ace_container.style.position = 'relative';
2212 this.ace_container.style.height = '400px';
2213 this.input.parentNode.insertBefore(this.ace_container,this.input);
2214 this.input.style.display = 'none';
2215 this.ace_editor = window.ace.edit(this.ace_container);
2216
2217 this.ace_editor.setValue(this.getValue());
2218
2219 // The theme
2220 if(JSONEditor.plugins.ace.theme) this.ace_editor.setTheme('ace/theme/'+JSONEditor.plugins.ace.theme);
2221 // The mode
2222 mode = window.ace.require("ace/mode/"+mode);
2223 if(mode) this.ace_editor.getSession().setMode(new mode.Mode());
2224
2225 // Listen for changes
2226 this.ace_editor.on('change',function() {
2227 var val = self.ace_editor.getValue();
2228 self.input.value = val;
2229 self.refreshValue();
2230 self.is_dirty = true;
2231 self.onChange(true);
2232 });
2233 }
2234 }
2235
2236 self.theme.afterInputReady(self.input);
2237 },
2238 refreshValue: function() {
2239 this.value = this.input.value;
2240 if(typeof this.value !== "string") this.value = '';
2241 this.serialized = this.value;
2242 },
2243 destroy: function() {
2244 // If using SCEditor, destroy the editor instance
2245 if(this.sceditor_instance) {
2246 this.sceditor_instance.destroy();
2247 }
2248 else if(this.epiceditor) {
2249 this.epiceditor.unload();
2250 }
2251 else if(this.ace_editor) {
2252 this.ace_editor.destroy();
2253 }
2254
2255
2256 this.template = null;
2257 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
2258 if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
2259 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
2260
2261 this._super();
2262 },
2263 /**
2264 * This is overridden in derivative editors
2265 */
2266 sanitize: function(value) {
2267 return value;
2268 },
2269 /**
2270 * Re-calculates the value if needed
2271 */
2272 onWatchedFieldChange: function() {
2273 var self = this, vars, j;
2274
2275 // If this editor needs to be rendered by a macro template
2276 if(this.template) {
2277 vars = this.getWatchedFieldValues();
2278 this.setValue(this.template(vars),false,true);
2279 }
2280
2281 this._super();
2282 },
2283 showValidationErrors: function(errors) {
2284 var self = this;
2285
2286 if(this.jsoneditor.options.show_errors === "always") {}
2287 else if(!this.is_dirty && this.previous_error_setting===this.jsoneditor.options.show_errors) return;
2288
2289 this.previous_error_setting = this.jsoneditor.options.show_errors;
2290
2291 var messages = [];
2292 $each(errors,function(i,error) {
2293 if(error.path === self.path) {
2294 messages.push(error.message);
2295 }
2296 });
2297
2298 if(messages.length) {
2299 this.theme.addInputError(this.input, messages.join('. ')+'.');
2300 }
2301 else {
2302 this.theme.removeInputError(this.input);
2303 }
2304 }
2305});
2306
2307JSONEditor.defaults.editors.number = JSONEditor.defaults.editors.string.extend({
2308 sanitize: function(value) {
2309 return (value+"").replace(/[^0-9\.\-eE]/g,'');
2310 },
2311 getNumColumns: function() {
2312 return 2;
2313 },
2314 getValue: function() {
2315 return this.value*1;
2316 }
2317});
2318
2319JSONEditor.defaults.editors.integer = JSONEditor.defaults.editors.number.extend({
2320 sanitize: function(value) {
2321 value = value + "";
2322 return value.replace(/[^0-9\-]/g,'');
2323 },
2324 getNumColumns: function() {
2325 return 2;
2326 }
2327});
2328
2329JSONEditor.defaults.editors.object = JSONEditor.AbstractEditor.extend({
2330 getDefault: function() {
2331 return $extend({},this.schema["default"] || {});
2332 },
2333 getChildEditors: function() {
2334 return this.editors;
2335 },
2336 register: function() {
2337 this._super();
2338 if(this.editors) {
2339 for(var i in this.editors) {
2340 if(!this.editors.hasOwnProperty(i)) continue;
2341 this.editors[i].register();
2342 }
2343 }
2344 },
2345 unregister: function() {
2346 this._super();
2347 if(this.editors) {
2348 for(var i in this.editors) {
2349 if(!this.editors.hasOwnProperty(i)) continue;
2350 this.editors[i].unregister();
2351 }
2352 }
2353 },
2354 getNumColumns: function() {
2355 return Math.max(Math.min(12,this.maxwidth),3);
2356 },
2357 enable: function() {
2358 if(this.editjson_button) this.editjson_button.disabled = false;
2359 if(this.addproperty_button) this.addproperty_button.disabled = false;
2360
2361 this._super();
2362 if(this.editors) {
2363 for(var i in this.editors) {
2364 if(!this.editors.hasOwnProperty(i)) continue;
2365 this.editors[i].enable();
2366 }
2367 }
2368 },
2369 disable: function() {
2370 if(this.editjson_button) this.editjson_button.disabled = true;
2371 if(this.addproperty_button) this.addproperty_button.disabled = true;
2372 this.hideEditJSON();
2373
2374 this._super();
2375 if(this.editors) {
2376 for(var i in this.editors) {
2377 if(!this.editors.hasOwnProperty(i)) continue;
2378 this.editors[i].disable();
2379 }
2380 }
2381 },
2382 layoutEditors: function() {
2383 var self = this, i, j;
2384
2385 if(!this.row_container) return;
2386
2387 // Sort editors by propertyOrder
2388 this.property_order = Object.keys(this.editors);
2389 this.property_order = this.property_order.sort(function(a,b) {
2390 var ordera = self.editors[a].schema.propertyOrder;
2391 var orderb = self.editors[b].schema.propertyOrder;
2392 if(typeof ordera !== "number") ordera = 1000;
2393 if(typeof orderb !== "number") orderb = 1000;
2394
2395 return ordera - orderb;
2396 });
2397
2398 var container;
2399
2400 if(this.format === 'grid') {
2401 var rows = [];
2402 $each(this.property_order, function(j,key) {
2403 var editor = self.editors[key];
2404 if(editor.property_removed) return;
2405 var found = false;
2406 var width = editor.options.hidden? 0 : (editor.options.grid_columns || editor.getNumColumns());
2407 var height = editor.options.hidden? 0 : editor.container.offsetHeight;
2408 // See if the editor will fit in any of the existing rows first
2409 for(var i=0; i<rows.length; i++) {
2410 // If the editor will fit in the row horizontally
2411 if(rows[i].width + width <= 12) {
2412 // If the editor is close to the other elements in height
2413 // i.e. Don't put a really tall editor in an otherwise short row or vice versa
2414 if(!height || (rows[i].minh*0.5 < height && rows[i].maxh*2 > height)) {
2415 found = i;
2416 }
2417 }
2418 }
2419
2420 // If there isn't a spot in any of the existing rows, start a new row
2421 if(found === false) {
2422 rows.push({
2423 width: 0,
2424 minh: 999999,
2425 maxh: 0,
2426 editors: []
2427 });
2428 found = rows.length-1;
2429 }
2430
2431 rows[found].editors.push({
2432 key: key,
2433 //editor: editor,
2434 width: width,
2435 height: height
2436 });
2437 rows[found].width += width;
2438 rows[found].minh = Math.min(rows[found].minh,height);
2439 rows[found].maxh = Math.max(rows[found].maxh,height);
2440 });
2441
2442 // Make almost full rows width 12
2443 // Do this by increasing all editors' sizes proprotionately
2444 // Any left over space goes to the biggest editor
2445 // Don't touch rows with a width of 6 or less
2446 for(i=0; i<rows.length; i++) {
2447 if(rows[i].width < 12) {
2448 var biggest = false;
2449 var new_width = 0;
2450 for(j=0; j<rows[i].editors.length; j++) {
2451 if(biggest === false) biggest = j;
2452 else if(rows[i].editors[j].width > rows[i].editors[biggest].width) biggest = j;
2453 rows[i].editors[j].width *= 12/rows[i].width;
2454 rows[i].editors[j].width = Math.floor(rows[i].editors[j].width);
2455 new_width += rows[i].editors[j].width;
2456 }
2457 if(new_width < 12) rows[i].editors[biggest].width += 12-new_width;
2458 rows[i].width = 12;
2459 }
2460 }
2461
2462 // layout hasn't changed
2463 if(this.layout === JSON.stringify(rows)) return false;
2464 this.layout = JSON.stringify(rows);
2465
2466 // Layout the form
2467 container = document.createElement('div');
2468 for(i=0; i<rows.length; i++) {
2469 var row = this.theme.getGridRow();
2470 container.appendChild(row);
2471 for(j=0; j<rows[i].editors.length; j++) {
2472 var key = rows[i].editors[j].key;
2473 var editor = this.editors[key];
2474
2475 if(editor.options.hidden) editor.container.style.display = 'none';
2476 else this.theme.setGridColumnSize(editor.container,rows[i].editors[j].width);
2477 row.appendChild(editor.container);
2478 }
2479 }
2480 }
2481 // Normal layout
2482 else {
2483 container = document.createElement('div');
2484 $each(this.property_order, function(i,key) {
2485 var editor = self.editors[key];
2486 if(editor.property_removed) return;
2487 var row = self.theme.getGridRow();
2488 container.appendChild(row);
2489
2490 if(editor.options.hidden) editor.container.style.display = 'none';
2491 else self.theme.setGridColumnSize(editor.container,12);
2492 row.appendChild(editor.container);
2493 });
2494 }
2495 this.row_container.innerHTML = '';
2496 this.row_container.appendChild(container);
2497 },
2498 getPropertySchema: function(key) {
2499 // Schema declared directly in properties
2500 var schema = this.schema.properties[key] || {};
2501 schema = $extend({},schema);
2502 var matched = this.schema.properties[key]? true : false;
2503
2504 // Any matching patternProperties should be merged in
2505 if(this.schema.patternProperties) {
2506 for(var i in this.schema.patternProperties) {
2507 if(!this.schema.patternProperties.hasOwnProperty(i)) continue;
2508 var regex = new RegExp(i);
2509 if(regex.test(key)) {
2510 schema.allOf = schema.allOf || [];
2511 schema.allOf.push(this.schema.patternProperties[i]);
2512 matched = true;
2513 }
2514 }
2515 }
2516
2517 // Hasn't matched other rules, use additionalProperties schema
2518 if(!matched && this.schema.additionalProperties && typeof this.schema.additionalProperties === "object") {
2519 schema = $extend({},this.schema.additionalProperties);
2520 }
2521
2522 return schema;
2523 },
2524 preBuild: function() {
2525 this._super();
2526
2527 this.editors = {};
2528 this.cached_editors = {};
2529 var self = this;
2530
2531 this.format = this.options.layout || this.options.object_layout || this.schema.format || this.jsoneditor.options.object_layout || 'normal';
2532
2533 this.schema.properties = this.schema.properties || {};
2534
2535 this.minwidth = 0;
2536 this.maxwidth = 0;
2537
2538 // If the object should be rendered as a table row
2539 if(this.options.table_row) {
2540 $each(this.schema.properties, function(key,schema) {
2541 var editor = self.jsoneditor.getEditorClass(schema);
2542 self.editors[key] = self.jsoneditor.createEditor(editor,{
2543 jsoneditor: self.jsoneditor,
2544 schema: schema,
2545 path: self.path+'.'+key,
2546 parent: self,
2547 compact: true,
2548 required: true
2549 });
2550 self.editors[key].preBuild();
2551
2552 var width = self.editors[key].options.hidden? 0 : (self.editors[key].options.grid_columns || self.editors[key].getNumColumns());
2553
2554 self.minwidth += width;
2555 self.maxwidth += width;
2556 });
2557 this.no_link_holder = true;
2558 }
2559 // If the object should be rendered as a table
2560 else if(this.options.table) {
2561 // TODO: table display format
2562 throw "Not supported yet";
2563 }
2564 // If the object should be rendered as a div
2565 else {
2566 this.defaultProperties = this.schema.defaultProperties || Object.keys(this.schema.properties);
2567
2568 // Increase the grid width to account for padding
2569 self.maxwidth += 1;
2570
2571 $each(this.defaultProperties, function(i,key) {
2572 self.addObjectProperty(key, true);
2573
2574 if(self.editors[key]) {
2575 self.minwidth = Math.max(self.minwidth,(self.editors[key].options.grid_columns || self.editors[key].getNumColumns()));
2576 self.maxwidth += (self.editors[key].options.grid_columns || self.editors[key].getNumColumns());
2577 }
2578 });
2579 }
2580
2581 // Sort editors by propertyOrder
2582 this.property_order = Object.keys(this.editors);
2583 this.property_order = this.property_order.sort(function(a,b) {
2584 var ordera = self.editors[a].schema.propertyOrder;
2585 var orderb = self.editors[b].schema.propertyOrder;
2586 if(typeof ordera !== "number") ordera = 1000;
2587 if(typeof orderb !== "number") orderb = 1000;
2588
2589 return ordera - orderb;
2590 });
2591 },
2592 build: function() {
2593 var self = this;
2594
2595 // If the object should be rendered as a table row
2596 if(this.options.table_row) {
2597 this.editor_holder = this.container;
2598 $each(this.editors, function(key,editor) {
2599 var holder = self.theme.getTableCell();
2600 self.editor_holder.appendChild(holder);
2601
2602 editor.setContainer(holder);
2603 editor.build();
2604 editor.postBuild();
2605
2606 if(self.editors[key].options.hidden) {
2607 holder.style.display = 'none';
2608 }
2609 if(self.editors[key].options.input_width) {
2610 holder.style.width = self.editors[key].options.input_width;
2611 }
2612 });
2613 }
2614 // If the object should be rendered as a table
2615 else if(this.options.table) {
2616 // TODO: table display format
2617 throw "Not supported yet";
2618 }
2619 // If the object should be rendered as a div
2620 else {
2621 this.header = document.createElement('span');
2622 this.header.textContent = this.getTitle();
2623 this.title = this.theme.getHeader(this.header);
2624 this.container.appendChild(this.title);
2625 this.container.style.position = 'relative';
2626
2627 // Edit JSON modal
2628 this.editjson_holder = this.theme.getModal();
2629 this.editjson_textarea = this.theme.getTextareaInput();
2630 this.editjson_textarea.style.height = '170px';
2631 this.editjson_textarea.style.width = '300px';
2632 this.editjson_textarea.style.display = 'block';
2633 this.editjson_save = this.getButton('Save','save','Save');
2634 this.editjson_save.addEventListener('click',function(e) {
2635 e.preventDefault();
2636 e.stopPropagation();
2637 self.saveJSON();
2638 });
2639 this.editjson_cancel = this.getButton('Cancel','cancel','Cancel');
2640 this.editjson_cancel.addEventListener('click',function(e) {
2641 e.preventDefault();
2642 e.stopPropagation();
2643 self.hideEditJSON();
2644 });
2645 this.editjson_holder.appendChild(this.editjson_textarea);
2646 this.editjson_holder.appendChild(this.editjson_save);
2647 this.editjson_holder.appendChild(this.editjson_cancel);
2648
2649 // Manage Properties modal
2650 this.addproperty_holder = this.theme.getModal();
2651 this.addproperty_list = document.createElement('div');
2652 this.addproperty_list.style.width = '295px';
2653 this.addproperty_list.style.maxHeight = '160px';
2654 this.addproperty_list.style.padding = '5px 0';
2655 this.addproperty_list.style.overflowY = 'auto';
2656 this.addproperty_list.style.overflowX = 'hidden';
2657 this.addproperty_list.style.paddingLeft = '5px';
2658 this.addproperty_list.setAttribute('class', 'property-selector');
2659 this.addproperty_add = this.getButton('add','add','add');
2660 this.addproperty_input = this.theme.getFormInputField('text');
2661 this.addproperty_input.setAttribute('placeholder','Property name...');
2662 this.addproperty_input.style.width = '220px';
2663 this.addproperty_input.style.marginBottom = '0';
2664 this.addproperty_input.style.display = 'inline-block';
2665 this.addproperty_add.addEventListener('click',function(e) {
2666 e.preventDefault();
2667 e.stopPropagation();
2668 if(self.addproperty_input.value) {
2669 if(self.editors[self.addproperty_input.value]) {
2670 window.alert('there is already a property with that name');
2671 return;
2672 }
2673
2674 self.addObjectProperty(self.addproperty_input.value);
2675 if(self.editors[self.addproperty_input.value]) {
2676 self.editors[self.addproperty_input.value].disable();
2677 }
2678 self.onChange(true);
2679 }
2680 });
2681 this.addproperty_holder.appendChild(this.addproperty_list);
2682 this.addproperty_holder.appendChild(this.addproperty_input);
2683 this.addproperty_holder.appendChild(this.addproperty_add);
2684 var spacer = document.createElement('div');
2685 spacer.style.clear = 'both';
2686 this.addproperty_holder.appendChild(spacer);
2687
2688
2689 // Description
2690 if(this.schema.description) {
2691 this.description = this.theme.getDescription(this.schema.description);
2692 this.container.appendChild(this.description);
2693 }
2694
2695 // Validation error placeholder area
2696 this.error_holder = document.createElement('div');
2697 this.container.appendChild(this.error_holder);
2698
2699 // Container for child editor area
2700 this.editor_holder = this.theme.getIndentedPanel();
2701 this.container.appendChild(this.editor_holder);
2702
2703 // Container for rows of child editors
2704 this.row_container = this.theme.getGridContainer();
2705 this.editor_holder.appendChild(this.row_container);
2706
2707 $each(this.editors, function(key,editor) {
2708 var holder = self.theme.getGridColumn();
2709 self.row_container.appendChild(holder);
2710
2711 editor.setContainer(holder);
2712 editor.build();
2713 editor.postBuild();
2714 });
2715
2716 // Control buttons
2717 this.title_controls = this.theme.getHeaderButtonHolder();
2718 this.editjson_controls = this.theme.getHeaderButtonHolder();
2719 this.addproperty_controls = this.theme.getHeaderButtonHolder();
2720 this.title.appendChild(this.title_controls);
2721 this.title.appendChild(this.editjson_controls);
2722 this.title.appendChild(this.addproperty_controls);
2723
2724 // Show/Hide button
2725 this.collapsed = false;
2726 this.toggle_button = this.getButton('','collapse','Collapse');
2727 this.title_controls.appendChild(this.toggle_button);
2728 this.toggle_button.addEventListener('click',function(e) {
2729 e.preventDefault();
2730 e.stopPropagation();
2731 if(self.collapsed) {
2732 self.editor_holder.style.display = '';
2733 self.collapsed = false;
2734 self.setButtonText(self.toggle_button,'','collapse','Collapse');
2735 }
2736 else {
2737 self.editor_holder.style.display = 'none';
2738 self.collapsed = true;
2739 self.setButtonText(self.toggle_button,'','expand','Expand');
2740 }
2741 });
2742
2743 // If it should start collapsed
2744 if(this.options.collapsed) {
2745 $trigger(this.toggle_button,'click');
2746 }
2747
2748 // Collapse button disabled
2749 if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
2750 if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
2751 }
2752 else if(this.jsoneditor.options.disable_collapse) {
2753 this.toggle_button.style.display = 'none';
2754 }
2755
2756 // Edit JSON Button
2757 this.editjson_button = this.getButton('JSON','edit','Edit JSON');
2758 this.editjson_button.addEventListener('click',function(e) {
2759 e.preventDefault();
2760 e.stopPropagation();
2761 self.toggleEditJSON();
2762 });
2763 this.editjson_controls.appendChild(this.editjson_button);
2764 this.editjson_controls.appendChild(this.editjson_holder);
2765
2766 // Edit JSON Buttton disabled
2767 if(this.schema.options && typeof this.schema.options.disable_edit_json !== "undefined") {
2768 if(this.schema.options.disable_edit_json) this.editjson_button.style.display = 'none';
2769 }
2770 else if(this.jsoneditor.options.disable_edit_json) {
2771 this.editjson_button.style.display = 'none';
2772 }
2773
2774 // Object Properties Button
2775 this.addproperty_button = this.getButton('Properties','edit','Object Properties');
2776 this.addproperty_button.addEventListener('click',function(e) {
2777 e.preventDefault();
2778 e.stopPropagation();
2779 self.toggleAddProperty();
2780 });
2781 this.addproperty_controls.appendChild(this.addproperty_button);
2782 this.addproperty_controls.appendChild(this.addproperty_holder);
2783 this.refreshAddProperties();
2784 }
2785
2786 // Fix table cell ordering
2787 if(this.options.table_row) {
2788 this.editor_holder = this.container;
2789 $each(this.property_order,function(i,key) {
2790 self.editor_holder.appendChild(self.editors[key].container);
2791 });
2792 }
2793 // Layout object editors in grid if needed
2794 else {
2795 // Initial layout
2796 this.layoutEditors();
2797 // Do it again now that we know the approximate heights of elements
2798 this.layoutEditors();
2799 }
2800 },
2801 showEditJSON: function() {
2802 if(!this.editjson_holder) return;
2803 this.hideAddProperty();
2804
2805 // Position the form directly beneath the button
2806 // TODO: edge detection
2807 this.editjson_holder.style.left = this.editjson_button.offsetLeft+"px";
2808 this.editjson_holder.style.top = this.editjson_button.offsetTop + this.editjson_button.offsetHeight+"px";
2809
2810 // Start the textarea with the current value
2811 this.editjson_textarea.value = JSON.stringify(this.getValue(),null,2);
2812
2813 // Disable the rest of the form while editing JSON
2814 this.disable();
2815
2816 this.editjson_holder.style.display = '';
2817 this.editjson_button.disabled = false;
2818 this.editing_json = true;
2819 },
2820 hideEditJSON: function() {
2821 if(!this.editjson_holder) return;
2822 if(!this.editing_json) return;
2823
2824 this.editjson_holder.style.display = 'none';
2825 this.enable();
2826 this.editing_json = false;
2827 },
2828 saveJSON: function() {
2829 if(!this.editjson_holder) return;
2830
2831 try {
2832 var json = JSON.parse(this.editjson_textarea.value);
2833 this.setValue(json);
2834 this.hideEditJSON();
2835 }
2836 catch(e) {
2837 window.alert('invalid JSON');
2838 throw e;
2839 }
2840 },
2841 toggleEditJSON: function() {
2842 if(this.editing_json) this.hideEditJSON();
2843 else this.showEditJSON();
2844 },
2845 insertPropertyControlUsingPropertyOrder: function (property, control, container) {
2846 var propertyOrder;
2847 if (this.schema.properties[property])
2848 propertyOrder = this.schema.properties[property].propertyOrder;
2849 if (typeof propertyOrder !== "number") propertyOrder = 1000;
2850 control.propertyOrder = propertyOrder;
2851
2852 for (var i = 0; i < container.childNodes.length; i++) {
2853 var child = container.childNodes[i];
2854 if (control.propertyOrder < child.propertyOrder) {
2855 this.addproperty_list.insertBefore(control, child);
2856 control = null;
2857 break;
2858 }
2859 }
2860 if (control) {
2861 this.addproperty_list.appendChild(control);
2862 }
2863 },
2864 addPropertyCheckbox: function(key) {
2865 var self = this;
2866 var checkbox, label, labelText, control;
2867
2868 checkbox = self.theme.getCheckbox();
2869 checkbox.style.width = 'auto';
2870
2871 if (this.schema.properties[key] && this.schema.properties[key].title)
2872 labelText = this.schema.properties[key].title;
2873 else
2874 labelText = key;
2875
2876 label = self.theme.getCheckboxLabel(labelText);
2877
2878 control = self.theme.getFormControl(label,checkbox);
2879 control.style.paddingBottom = control.style.marginBottom = control.style.paddingTop = control.style.marginTop = 0;
2880 control.style.height = 'auto';
2881 //control.style.overflowY = 'hidden';
2882
2883 this.insertPropertyControlUsingPropertyOrder(key, control, this.addproperty_list);
2884
2885 checkbox.checked = key in this.editors;
2886 checkbox.addEventListener('change',function() {
2887 if(checkbox.checked) {
2888 self.addObjectProperty(key);
2889 }
2890 else {
2891 self.removeObjectProperty(key);
2892 }
2893 self.onChange(true);
2894 });
2895 self.addproperty_checkboxes[key] = checkbox;
2896
2897 return checkbox;
2898 },
2899 showAddProperty: function() {
2900 if(!this.addproperty_holder) return;
2901 this.hideEditJSON();
2902
2903 // Position the form directly beneath the button
2904 // TODO: edge detection
2905 this.addproperty_holder.style.left = this.addproperty_button.offsetLeft+"px";
2906 this.addproperty_holder.style.top = this.addproperty_button.offsetTop + this.addproperty_button.offsetHeight+"px";
2907
2908 // Disable the rest of the form while editing JSON
2909 this.disable();
2910
2911 this.adding_property = true;
2912 this.addproperty_button.disabled = false;
2913 this.addproperty_holder.style.display = '';
2914 this.refreshAddProperties();
2915 },
2916 hideAddProperty: function() {
2917 if(!this.addproperty_holder) return;
2918 if(!this.adding_property) return;
2919
2920 this.addproperty_holder.style.display = 'none';
2921 this.enable();
2922
2923 this.adding_property = false;
2924 },
2925 toggleAddProperty: function() {
2926 if(this.adding_property) this.hideAddProperty();
2927 else this.showAddProperty();
2928 },
2929 removeObjectProperty: function(property) {
2930 if(this.editors[property]) {
2931 this.editors[property].unregister();
2932 delete this.editors[property];
2933
2934 this.refreshValue();
2935 this.layoutEditors();
2936 }
2937 },
2938 addObjectProperty: function(name, prebuild_only) {
2939 var self = this;
2940
2941 // Property is already added
2942 if(this.editors[name]) return;
2943
2944 // Property was added before and is cached
2945 if(this.cached_editors[name]) {
2946 this.editors[name] = this.cached_editors[name];
2947 if(prebuild_only) return;
2948 this.editors[name].register();
2949 }
2950 // New property
2951 else {
2952 if(!this.canHaveAdditionalProperties() && (!this.schema.properties || !this.schema.properties[name])) {
2953 return;
2954 }
2955
2956 var schema = self.getPropertySchema(name);
2957
2958
2959 // Add the property
2960 var editor = self.jsoneditor.getEditorClass(schema);
2961
2962 self.editors[name] = self.jsoneditor.createEditor(editor,{
2963 jsoneditor: self.jsoneditor,
2964 schema: schema,
2965 path: self.path+'.'+name,
2966 parent: self
2967 });
2968 self.editors[name].preBuild();
2969
2970 if(!prebuild_only) {
2971 var holder = self.theme.getChildEditorHolder();
2972 self.editor_holder.appendChild(holder);
2973 self.editors[name].setContainer(holder);
2974 self.editors[name].build();
2975 self.editors[name].postBuild();
2976 }
2977
2978 self.cached_editors[name] = self.editors[name];
2979 }
2980
2981 // If we're only prebuilding the editors, don't refresh values
2982 if(!prebuild_only) {
2983 self.refreshValue();
2984 self.layoutEditors();
2985 }
2986 },
2987 onChildEditorChange: function(editor) {
2988 this.refreshValue();
2989 this._super(editor);
2990 },
2991 canHaveAdditionalProperties: function() {
2992 if (typeof this.schema.additionalProperties === "boolean") {
2993 return this.schema.additionalProperties;
2994 }
2995 return !this.jsoneditor.options.no_additional_properties;
2996 },
2997 destroy: function() {
2998 $each(this.cached_editors, function(i,el) {
2999 el.destroy();
3000 });
3001 if(this.editor_holder) this.editor_holder.innerHTML = '';
3002 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
3003 if(this.error_holder && this.error_holder.parentNode) this.error_holder.parentNode.removeChild(this.error_holder);
3004
3005 this.editors = null;
3006 this.cached_editors = null;
3007 if(this.editor_holder && this.editor_holder.parentNode) this.editor_holder.parentNode.removeChild(this.editor_holder);
3008 this.editor_holder = null;
3009
3010 this._super();
3011 },
3012 getValue: function() {
3013 var result = this._super();
3014 if(this.jsoneditor.options.remove_empty_properties || this.options.remove_empty_properties) {
3015 for(var i in result) {
3016 if(result.hasOwnProperty(i)) {
3017 if(!result[i]) delete result[i];
3018 }
3019 }
3020 }
3021 return result;
3022 },
3023 refreshValue: function() {
3024 this.value = {};
3025 var self = this;
3026
3027 for(var i in this.editors) {
3028 if(!this.editors.hasOwnProperty(i)) continue;
3029 this.value[i] = this.editors[i].getValue();
3030 }
3031
3032 if(this.adding_property) this.refreshAddProperties();
3033 },
3034 refreshAddProperties: function() {
3035 if(this.options.disable_properties || (this.options.disable_properties !== false && this.jsoneditor.options.disable_properties)) {
3036 this.addproperty_controls.style.display = 'none';
3037 return;
3038 }
3039
3040 var can_add = false, can_remove = false, num_props = 0, i, show_modal = false;
3041
3042 // Get number of editors
3043 for(i in this.editors) {
3044 if(!this.editors.hasOwnProperty(i)) continue;
3045 num_props++;
3046 }
3047
3048 // Determine if we can add back removed properties
3049 can_add = this.canHaveAdditionalProperties() && !(typeof this.schema.maxProperties !== "undefined" && num_props >= this.schema.maxProperties);
3050
3051 if(this.addproperty_checkboxes) {
3052 this.addproperty_list.innerHTML = '';
3053 }
3054 this.addproperty_checkboxes = {};
3055
3056 // Check for which editors can't be removed or added back
3057 for(i in this.cached_editors) {
3058 if(!this.cached_editors.hasOwnProperty(i)) continue;
3059
3060 this.addPropertyCheckbox(i);
3061
3062 if(this.isRequired(this.cached_editors[i]) && i in this.editors) {
3063 this.addproperty_checkboxes[i].disabled = true;
3064 }
3065
3066 if(typeof this.schema.minProperties !== "undefined" && num_props <= this.schema.minProperties) {
3067 this.addproperty_checkboxes[i].disabled = this.addproperty_checkboxes[i].checked;
3068 if(!this.addproperty_checkboxes[i].checked) show_modal = true;
3069 }
3070 else if(!(i in this.editors)) {
3071 if(!can_add && !this.schema.properties.hasOwnProperty(i)) {
3072 this.addproperty_checkboxes[i].disabled = true;
3073 }
3074 else {
3075 this.addproperty_checkboxes[i].disabled = false;
3076 show_modal = true;
3077 }
3078 }
3079 else {
3080 show_modal = true;
3081 can_remove = true;
3082 }
3083 }
3084
3085 if(this.canHaveAdditionalProperties()) {
3086 show_modal = true;
3087 }
3088
3089 // Additional addproperty checkboxes not tied to a current editor
3090 for(i in this.schema.properties) {
3091 if(!this.schema.properties.hasOwnProperty(i)) continue;
3092 if(this.cached_editors[i]) continue;
3093 show_modal = true;
3094 this.addPropertyCheckbox(i);
3095 }
3096
3097 // If no editors can be added or removed, hide the modal button
3098 if(!show_modal) {
3099 this.hideAddProperty();
3100 this.addproperty_controls.style.display = 'none';
3101 }
3102 // If additional properties are disabled
3103 else if(!this.canHaveAdditionalProperties()) {
3104 this.addproperty_add.style.display = 'none';
3105 this.addproperty_input.style.display = 'none';
3106 }
3107 // If no new properties can be added
3108 else if(!can_add) {
3109 this.addproperty_add.disabled = true;
3110 }
3111 // If new properties can be added
3112 else {
3113 this.addproperty_add.disabled = false;
3114 }
3115 },
3116 isRequired: function(editor) {
3117 if(typeof editor.schema.required === "boolean") return editor.schema.required;
3118 else if(Array.isArray(this.schema.required)) return this.schema.required.indexOf(editor.key) > -1;
3119 else if(this.jsoneditor.options.required_by_default) return true;
3120 else return false;
3121 },
3122 setValue: function(value, initial) {
3123 var self = this;
3124 value = value || {};
3125
3126 if(typeof value !== "object" || Array.isArray(value)) value = {};
3127
3128 // First, set the values for all of the defined properties
3129 $each(this.cached_editors, function(i,editor) {
3130 // Value explicitly set
3131 if(typeof value[i] !== "undefined") {
3132 self.addObjectProperty(i);
3133 editor.setValue(value[i],initial);
3134 }
3135 // Otherwise, remove value unless this is the initial set or it's required
3136 else if(!initial && !self.isRequired(editor)) {
3137 self.removeObjectProperty(i);
3138 }
3139 // Otherwise, set the value to the default
3140 else {
3141 editor.setValue(editor.getDefault(),initial);
3142 }
3143 });
3144
3145 $each(value, function(i,val) {
3146 if(!self.cached_editors[i]) {
3147 self.addObjectProperty(i);
3148 if(self.editors[i]) self.editors[i].setValue(val,initial);
3149 }
3150 });
3151
3152 this.refreshValue();
3153 this.layoutEditors();
3154 this.onChange();
3155 },
3156 showValidationErrors: function(errors) {
3157 var self = this;
3158
3159 // Get all the errors that pertain to this editor
3160 var my_errors = [];
3161 var other_errors = [];
3162 $each(errors, function(i,error) {
3163 if(error.path === self.path) {
3164 my_errors.push(error);
3165 }
3166 else {
3167 other_errors.push(error);
3168 }
3169 });
3170
3171 // Show errors for this editor
3172 if(this.error_holder) {
3173 if(my_errors.length) {
3174 var message = [];
3175 this.error_holder.innerHTML = '';
3176 this.error_holder.style.display = '';
3177 $each(my_errors, function(i,error) {
3178 self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
3179 });
3180 }
3181 // Hide error area
3182 else {
3183 this.error_holder.style.display = 'none';
3184 }
3185 }
3186
3187 // Show error for the table row if this is inside a table
3188 if(this.options.table_row) {
3189 if(my_errors.length) {
3190 this.theme.addTableRowError(this.container);
3191 }
3192 else {
3193 this.theme.removeTableRowError(this.container);
3194 }
3195 }
3196
3197 // Show errors for child editors
3198 $each(this.editors, function(i,editor) {
3199 editor.showValidationErrors(other_errors);
3200 });
3201 }
3202});
3203
3204JSONEditor.defaults.editors.array = JSONEditor.AbstractEditor.extend({
3205 getDefault: function() {
3206 return this.schema["default"] || [];
3207 },
3208 register: function() {
3209 this._super();
3210 if(this.rows) {
3211 for(var i=0; i<this.rows.length; i++) {
3212 this.rows[i].register();
3213 }
3214 }
3215 },
3216 unregister: function() {
3217 this._super();
3218 if(this.rows) {
3219 for(var i=0; i<this.rows.length; i++) {
3220 this.rows[i].unregister();
3221 }
3222 }
3223 },
3224 getNumColumns: function() {
3225 var info = this.getItemInfo(0);
3226 // Tabs require extra horizontal space
3227 if(this.tabs_holder) {
3228 return Math.max(Math.min(12,info.width+2),4);
3229 }
3230 else {
3231 return info.width;
3232 }
3233 },
3234 enable: function() {
3235 if(this.add_row_button) this.add_row_button.disabled = false;
3236 if(this.remove_all_rows_button) this.remove_all_rows_button.disabled = false;
3237 if(this.delete_last_row_button) this.delete_last_row_button.disabled = false;
3238
3239 if(this.rows) {
3240 for(var i=0; i<this.rows.length; i++) {
3241 this.rows[i].enable();
3242
3243 if(this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = false;
3244 if(this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = false;
3245 if(this.rows[i].delete_button) this.rows[i].delete_button.disabled = false;
3246 }
3247 }
3248 this._super();
3249 },
3250 disable: function() {
3251 if(this.add_row_button) this.add_row_button.disabled = true;
3252 if(this.remove_all_rows_button) this.remove_all_rows_button.disabled = true;
3253 if(this.delete_last_row_button) this.delete_last_row_button.disabled = true;
3254
3255 if(this.rows) {
3256 for(var i=0; i<this.rows.length; i++) {
3257 this.rows[i].disable();
3258
3259 if(this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = true;
3260 if(this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = true;
3261 if(this.rows[i].delete_button) this.rows[i].delete_button.disabled = true;
3262 }
3263 }
3264 this._super();
3265 },
3266 preBuild: function() {
3267 this._super();
3268
3269 this.rows = [];
3270 this.row_cache = [];
3271
3272 this.hide_delete_buttons = this.options.disable_array_delete || this.jsoneditor.options.disable_array_delete;
3273 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;
3274 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;
3275 this.hide_move_buttons = this.options.disable_array_reorder || this.jsoneditor.options.disable_array_reorder;
3276 this.hide_add_button = this.options.disable_array_add || this.jsoneditor.options.disable_array_add;
3277 },
3278 build: function() {
3279 var self = this;
3280
3281 if(!this.options.compact) {
3282 this.header = document.createElement('span');
3283 this.header.textContent = this.getTitle();
3284 this.title = this.theme.getHeader(this.header);
3285 this.container.appendChild(this.title);
3286 this.title_controls = this.theme.getHeaderButtonHolder();
3287 this.title.appendChild(this.title_controls);
3288 if(this.schema.description) {
3289 this.description = this.theme.getDescription(this.schema.description);
3290 this.container.appendChild(this.description);
3291 }
3292 this.error_holder = document.createElement('div');
3293 this.container.appendChild(this.error_holder);
3294
3295 if(this.schema.format === 'tabs') {
3296 this.controls = this.theme.getHeaderButtonHolder();
3297 this.title.appendChild(this.controls);
3298 this.tabs_holder = this.theme.getTabHolder();
3299 this.container.appendChild(this.tabs_holder);
3300 this.row_holder = this.theme.getTabContentHolder(this.tabs_holder);
3301
3302 this.active_tab = null;
3303 }
3304 else {
3305 this.panel = this.theme.getIndentedPanel();
3306 this.container.appendChild(this.panel);
3307 this.row_holder = document.createElement('div');
3308 this.panel.appendChild(this.row_holder);
3309 this.controls = this.theme.getButtonHolder();
3310 this.panel.appendChild(this.controls);
3311 }
3312 }
3313 else {
3314 this.panel = this.theme.getIndentedPanel();
3315 this.container.appendChild(this.panel);
3316 this.controls = this.theme.getButtonHolder();
3317 this.panel.appendChild(this.controls);
3318 this.row_holder = document.createElement('div');
3319 this.panel.appendChild(this.row_holder);
3320 }
3321
3322 // Add controls
3323 this.addControls();
3324 },
3325 onChildEditorChange: function(editor) {
3326 this.refreshValue();
3327 this.refreshTabs(true);
3328 this._super(editor);
3329 },
3330 getItemTitle: function() {
3331 if(!this.item_title) {
3332 if(this.schema.items && !Array.isArray(this.schema.items)) {
3333 var tmp = this.jsoneditor.expandRefs(this.schema.items);
3334 this.item_title = tmp.title || 'item';
3335 }
3336 else {
3337 this.item_title = 'item';
3338 }
3339 }
3340 return this.item_title;
3341 },
3342 getItemSchema: function(i) {
3343 if(Array.isArray(this.schema.items)) {
3344 if(i >= this.schema.items.length) {
3345 if(this.schema.additionalItems===true) {
3346 return {};
3347 }
3348 else if(this.schema.additionalItems) {
3349 return $extend({},this.schema.additionalItems);
3350 }
3351 }
3352 else {
3353 return $extend({},this.schema.items[i]);
3354 }
3355 }
3356 else if(this.schema.items) {
3357 return $extend({},this.schema.items);
3358 }
3359 else {
3360 return {};
3361 }
3362 },
3363 getItemInfo: function(i) {
3364 var schema = this.getItemSchema(i);
3365
3366 // Check if it's cached
3367 this.item_info = this.item_info || {};
3368 var stringified = JSON.stringify(schema);
3369 if(typeof this.item_info[stringified] !== "undefined") return this.item_info[stringified];
3370
3371 // Get the schema for this item
3372 schema = this.jsoneditor.expandRefs(schema);
3373
3374 this.item_info[stringified] = {
3375 title: schema.title || "item",
3376 'default': schema["default"],
3377 width: 12,
3378 child_editors: schema.properties || schema.items
3379 };
3380
3381 return this.item_info[stringified];
3382 },
3383 getElementEditor: function(i) {
3384 var item_info = this.getItemInfo(i);
3385 var schema = this.getItemSchema(i);
3386 schema = this.jsoneditor.expandRefs(schema);
3387 schema.title = item_info.title+' '+(i+1);
3388
3389 var editor = this.jsoneditor.getEditorClass(schema);
3390
3391 var holder;
3392 if(this.tabs_holder) {
3393 holder = this.theme.getTabContent();
3394 }
3395 else if(item_info.child_editors) {
3396 holder = this.theme.getChildEditorHolder();
3397 }
3398 else {
3399 holder = this.theme.getIndentedPanel();
3400 }
3401
3402 this.row_holder.appendChild(holder);
3403
3404 var ret = this.jsoneditor.createEditor(editor,{
3405 jsoneditor: this.jsoneditor,
3406 schema: schema,
3407 container: holder,
3408 path: this.path+'.'+i,
3409 parent: this,
3410 required: true
3411 });
3412 ret.preBuild();
3413 ret.build();
3414 ret.postBuild();
3415
3416 if(!ret.title_controls) {
3417 ret.array_controls = this.theme.getButtonHolder();
3418 holder.appendChild(ret.array_controls);
3419 }
3420
3421 return ret;
3422 },
3423 destroy: function() {
3424 this.empty(true);
3425 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
3426 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
3427 if(this.row_holder && this.row_holder.parentNode) this.row_holder.parentNode.removeChild(this.row_holder);
3428 if(this.controls && this.controls.parentNode) this.controls.parentNode.removeChild(this.controls);
3429 if(this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
3430
3431 this.rows = this.row_cache = this.title = this.description = this.row_holder = this.panel = this.controls = null;
3432
3433 this._super();
3434 },
3435 empty: function(hard) {
3436 if(!this.rows) return;
3437 var self = this;
3438 $each(this.rows,function(i,row) {
3439 if(hard) {
3440 if(row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
3441 self.destroyRow(row,true);
3442 self.row_cache[i] = null;
3443 }
3444 self.rows[i] = null;
3445 });
3446 self.rows = [];
3447 if(hard) self.row_cache = [];
3448 },
3449 destroyRow: function(row,hard) {
3450 var holder = row.container;
3451 if(hard) {
3452 row.destroy();
3453 if(holder.parentNode) holder.parentNode.removeChild(holder);
3454 if(row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
3455 }
3456 else {
3457 if(row.tab) row.tab.style.display = 'none';
3458 holder.style.display = 'none';
3459 row.unregister();
3460 }
3461 },
3462 getMax: function() {
3463 if((Array.isArray(this.schema.items)) && this.schema.additionalItems === false) {
3464 return Math.min(this.schema.items.length,this.schema.maxItems || Infinity);
3465 }
3466 else {
3467 return this.schema.maxItems || Infinity;
3468 }
3469 },
3470 refreshTabs: function(refresh_headers) {
3471 var self = this;
3472 $each(this.rows, function(i,row) {
3473 if(!row.tab) return;
3474
3475 if(refresh_headers) {
3476 row.tab_text.textContent = row.getHeaderText();
3477 }
3478 else {
3479 if(row.tab === self.active_tab) {
3480 self.theme.markTabActive(row.tab);
3481 row.container.style.display = '';
3482 }
3483 else {
3484 self.theme.markTabInactive(row.tab);
3485 row.container.style.display = 'none';
3486 }
3487 }
3488 });
3489 },
3490 setValue: function(value, initial) {
3491 // Update the array's value, adding/removing rows when necessary
3492 value = value || [];
3493
3494 if(!(Array.isArray(value))) value = [value];
3495
3496 var serialized = JSON.stringify(value);
3497 if(serialized === this.serialized) return;
3498
3499 // Make sure value has between minItems and maxItems items in it
3500 if(this.schema.minItems) {
3501 while(value.length < this.schema.minItems) {
3502 value.push(this.getItemInfo(value.length)["default"]);
3503 }
3504 }
3505 if(this.getMax() && value.length > this.getMax()) {
3506 value = value.slice(0,this.getMax());
3507 }
3508
3509 var self = this;
3510 $each(value,function(i,val) {
3511 if(self.rows[i]) {
3512 // TODO: don't set the row's value if it hasn't changed
3513 self.rows[i].setValue(val,initial);
3514 }
3515 else if(self.row_cache[i]) {
3516 self.rows[i] = self.row_cache[i];
3517 self.rows[i].setValue(val,initial);
3518 self.rows[i].container.style.display = '';
3519 if(self.rows[i].tab) self.rows[i].tab.style.display = '';
3520 self.rows[i].register();
3521 }
3522 else {
3523 self.addRow(val,initial);
3524 }
3525 });
3526
3527 for(var j=value.length; j<self.rows.length; j++) {
3528 self.destroyRow(self.rows[j]);
3529 self.rows[j] = null;
3530 }
3531 self.rows = self.rows.slice(0,value.length);
3532
3533 // Set the active tab
3534 var new_active_tab = null;
3535 $each(self.rows, function(i,row) {
3536 if(row.tab === self.active_tab) {
3537 new_active_tab = row.tab;
3538 return false;
3539 }
3540 });
3541 if(!new_active_tab && self.rows.length) new_active_tab = self.rows[0].tab;
3542
3543 self.active_tab = new_active_tab;
3544
3545 self.refreshValue(initial);
3546 self.refreshTabs(true);
3547 self.refreshTabs();
3548
3549 self.onChange();
3550
3551 // TODO: sortable
3552 },
3553 refreshValue: function(force) {
3554 var self = this;
3555 var oldi = this.value? this.value.length : 0;
3556 this.value = [];
3557
3558 $each(this.rows,function(i,editor) {
3559 // Get the value for this editor
3560 self.value[i] = editor.getValue();
3561 });
3562
3563 if(oldi !== this.value.length || force) {
3564 // If we currently have minItems items in the array
3565 var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
3566
3567 $each(this.rows,function(i,editor) {
3568 // Hide the move down button for the last row
3569 if(editor.movedown_button) {
3570 if(i === self.rows.length - 1) {
3571 editor.movedown_button.style.display = 'none';
3572 }
3573 else {
3574 editor.movedown_button.style.display = '';
3575 }
3576 }
3577
3578 // Hide the delete button if we have minItems items
3579 if(editor.delete_button) {
3580 if(minItems) {
3581 editor.delete_button.style.display = 'none';
3582 }
3583 else {
3584 editor.delete_button.style.display = '';
3585 }
3586 }
3587
3588 // Get the value for this editor
3589 self.value[i] = editor.getValue();
3590 });
3591
3592 var controls_needed = false;
3593
3594 if(!this.value.length) {
3595 this.delete_last_row_button.style.display = 'none';
3596 this.remove_all_rows_button.style.display = 'none';
3597 }
3598 else if(this.value.length === 1) {
3599 this.remove_all_rows_button.style.display = 'none';
3600
3601 // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
3602 if(minItems || this.hide_delete_last_row_buttons) {
3603 this.delete_last_row_button.style.display = 'none';
3604 }
3605 else {
3606 this.delete_last_row_button.style.display = '';
3607 controls_needed = true;
3608 }
3609 }
3610 else {
3611 if(minItems || this.hide_delete_last_row_buttons) {
3612 this.delete_last_row_button.style.display = 'none';
3613 }
3614 else {
3615 this.delete_last_row_button.style.display = '';
3616 controls_needed = true;
3617 }
3618
3619 if(minItems || this.hide_delete_all_rows_buttons) {
3620 this.remove_all_rows_button.style.display = 'none';
3621 }
3622 else {
3623 this.remove_all_rows_button.style.display = '';
3624 controls_needed = true;
3625 }
3626 }
3627
3628 // If there are maxItems in the array, hide the add button beneath the rows
3629 if((this.getMax() && this.getMax() <= this.rows.length) || this.hide_add_button){
3630 this.add_row_button.style.display = 'none';
3631 }
3632 else {
3633 this.add_row_button.style.display = '';
3634 controls_needed = true;
3635 }
3636
3637 if(!this.collapsed && controls_needed) {
3638 this.controls.style.display = 'inline-block';
3639 }
3640 else {
3641 this.controls.style.display = 'none';
3642 }
3643 }
3644 },
3645 addRow: function(value, initial) {
3646 var self = this;
3647 var i = this.rows.length;
3648
3649 self.rows[i] = this.getElementEditor(i);
3650 self.row_cache[i] = self.rows[i];
3651
3652 if(self.tabs_holder) {
3653 self.rows[i].tab_text = document.createElement('span');
3654 self.rows[i].tab_text.textContent = self.rows[i].getHeaderText();
3655 self.rows[i].tab = self.theme.getTab(self.rows[i].tab_text);
3656 self.rows[i].tab.addEventListener('click', function(e) {
3657 self.active_tab = self.rows[i].tab;
3658 self.refreshTabs();
3659 e.preventDefault();
3660 e.stopPropagation();
3661 });
3662
3663 self.theme.addTab(self.tabs_holder, self.rows[i].tab);
3664 }
3665
3666 var controls_holder = self.rows[i].title_controls || self.rows[i].array_controls;
3667
3668 // Buttons to delete row, move row up, and move row down
3669 if(!self.hide_delete_buttons) {
3670 self.rows[i].delete_button = this.getButton(self.getItemTitle(),'delete',this.translate('button_delete_row_title',[self.getItemTitle()]));
3671 self.rows[i].delete_button.className += ' delete';
3672 self.rows[i].delete_button.setAttribute('data-i',i);
3673 self.rows[i].delete_button.addEventListener('click',function(e) {
3674 e.preventDefault();
3675 e.stopPropagation();
3676 var i = this.getAttribute('data-i')*1;
3677
3678 var value = self.getValue();
3679
3680 var newval = [];
3681 var new_active_tab = null;
3682 $each(value,function(j,row) {
3683 if(j===i) {
3684 // If the one we're deleting is the active tab
3685 if(self.rows[j].tab === self.active_tab) {
3686 // Make the next tab active if there is one
3687 // Note: the next tab is going to be the current tab after deletion
3688 if(self.rows[j+1]) new_active_tab = self.rows[j].tab;
3689 // Otherwise, make the previous tab active if there is one
3690 else if(j) new_active_tab = self.rows[j-1].tab;
3691 }
3692
3693 return; // If this is the one we're deleting
3694 }
3695 newval.push(row);
3696 });
3697 self.setValue(newval);
3698 if(new_active_tab) {
3699 self.active_tab = new_active_tab;
3700 self.refreshTabs();
3701 }
3702
3703 self.onChange(true);
3704 });
3705
3706 if(controls_holder) {
3707 controls_holder.appendChild(self.rows[i].delete_button);
3708 }
3709 }
3710
3711 if(i && !self.hide_move_buttons) {
3712 self.rows[i].moveup_button = this.getButton('','moveup',this.translate('button_move_up_title'));
3713 self.rows[i].moveup_button.className += ' moveup';
3714 self.rows[i].moveup_button.setAttribute('data-i',i);
3715 self.rows[i].moveup_button.addEventListener('click',function(e) {
3716 e.preventDefault();
3717 e.stopPropagation();
3718 var i = this.getAttribute('data-i')*1;
3719
3720 if(i<=0) return;
3721 var rows = self.getValue();
3722 var tmp = rows[i-1];
3723 rows[i-1] = rows[i];
3724 rows[i] = tmp;
3725
3726 self.setValue(rows);
3727 self.active_tab = self.rows[i-1].tab;
3728 self.refreshTabs();
3729
3730 self.onChange(true);
3731 });
3732
3733 if(controls_holder) {
3734 controls_holder.appendChild(self.rows[i].moveup_button);
3735 }
3736 }
3737
3738 if(!self.hide_move_buttons) {
3739 self.rows[i].movedown_button = this.getButton('','movedown',this.translate('button_move_down_title'));
3740 self.rows[i].movedown_button.className += ' movedown';
3741 self.rows[i].movedown_button.setAttribute('data-i',i);
3742 self.rows[i].movedown_button.addEventListener('click',function(e) {
3743 e.preventDefault();
3744 e.stopPropagation();
3745 var i = this.getAttribute('data-i')*1;
3746
3747 var rows = self.getValue();
3748 if(i>=rows.length-1) return;
3749 var tmp = rows[i+1];
3750 rows[i+1] = rows[i];
3751 rows[i] = tmp;
3752
3753 self.setValue(rows);
3754 self.active_tab = self.rows[i+1].tab;
3755 self.refreshTabs();
3756 self.onChange(true);
3757 });
3758
3759 if(controls_holder) {
3760 controls_holder.appendChild(self.rows[i].movedown_button);
3761 }
3762 }
3763
3764 if(value) self.rows[i].setValue(value, initial);
3765 self.refreshTabs();
3766 },
3767 addControls: function() {
3768 var self = this;
3769
3770 this.collapsed = false;
3771 this.toggle_button = this.getButton('','collapse',this.translate('button_collapse'));
3772 this.title_controls.appendChild(this.toggle_button);
3773 var row_holder_display = self.row_holder.style.display;
3774 var controls_display = self.controls.style.display;
3775 this.toggle_button.addEventListener('click',function(e) {
3776 e.preventDefault();
3777 e.stopPropagation();
3778 if(self.collapsed) {
3779 self.collapsed = false;
3780 if(self.panel) self.panel.style.display = '';
3781 self.row_holder.style.display = row_holder_display;
3782 if(self.tabs_holder) self.tabs_holder.style.display = '';
3783 self.controls.style.display = controls_display;
3784 self.setButtonText(this,'','collapse','Collapse');
3785 }
3786 else {
3787 self.collapsed = true;
3788 self.row_holder.style.display = 'none';
3789 if(self.tabs_holder) self.tabs_holder.style.display = 'none';
3790 self.controls.style.display = 'none';
3791 if(self.panel) self.panel.style.display = 'none';
3792 self.setButtonText(this,'','expand','Expand');
3793 }
3794 });
3795
3796 // If it should start collapsed
3797 if(this.options.collapsed) {
3798 $trigger(this.toggle_button,'click');
3799 }
3800
3801 // Collapse button disabled
3802 if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
3803 if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
3804 }
3805 else if(this.jsoneditor.options.disable_collapse) {
3806 this.toggle_button.style.display = 'none';
3807 }
3808
3809 // Add "new row" and "delete last" buttons below editor
3810 this.add_row_button = this.getButton(this.getItemTitle(),'add',this.translate('button_add_row_title',[this.getItemTitle()]));
3811
3812 this.add_row_button.addEventListener('click',function(e) {
3813 e.preventDefault();
3814 e.stopPropagation();
3815 var i = self.rows.length;
3816 if(self.row_cache[i]) {
3817 self.rows[i] = self.row_cache[i];
3818 self.rows[i].setValue(self.rows[i].getDefault());
3819 self.rows[i].container.style.display = '';
3820 if(self.rows[i].tab) self.rows[i].tab.style.display = '';
3821 self.rows[i].register();
3822 }
3823 else {
3824 self.addRow();
3825 }
3826 self.active_tab = self.rows[i].tab;
3827 self.refreshTabs();
3828 self.refreshValue();
3829 self.onChange(true);
3830 });
3831 self.controls.appendChild(this.add_row_button);
3832
3833 this.delete_last_row_button = this.getButton(this.translate('button_delete_last',[this.getItemTitle()]),'delete',this.translate('button_delete_last_title',[this.getItemTitle()]));
3834 this.delete_last_row_button.addEventListener('click',function(e) {
3835 e.preventDefault();
3836 e.stopPropagation();
3837 var rows = self.getValue();
3838
3839 var new_active_tab = null;
3840 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;
3841
3842 rows.pop();
3843 self.setValue(rows);
3844 if(new_active_tab) {
3845 self.active_tab = new_active_tab;
3846 self.refreshTabs();
3847 }
3848 self.onChange(true);
3849 });
3850 self.controls.appendChild(this.delete_last_row_button);
3851
3852 this.remove_all_rows_button = this.getButton(this.translate('button_delete_all'),'delete',this.translate('button_delete_all_title'));
3853 this.remove_all_rows_button.addEventListener('click',function(e) {
3854 e.preventDefault();
3855 e.stopPropagation();
3856 self.setValue([]);
3857 self.onChange(true);
3858 });
3859 self.controls.appendChild(this.remove_all_rows_button);
3860
3861 if(self.tabs) {
3862 this.add_row_button.style.width = '100%';
3863 this.add_row_button.style.textAlign = 'left';
3864 this.add_row_button.style.marginBottom = '3px';
3865
3866 this.delete_last_row_button.style.width = '100%';
3867 this.delete_last_row_button.style.textAlign = 'left';
3868 this.delete_last_row_button.style.marginBottom = '3px';
3869
3870 this.remove_all_rows_button.style.width = '100%';
3871 this.remove_all_rows_button.style.textAlign = 'left';
3872 this.remove_all_rows_button.style.marginBottom = '3px';
3873 }
3874 },
3875 showValidationErrors: function(errors) {
3876 var self = this;
3877
3878 // Get all the errors that pertain to this editor
3879 var my_errors = [];
3880 var other_errors = [];
3881 $each(errors, function(i,error) {
3882 if(error.path === self.path) {
3883 my_errors.push(error);
3884 }
3885 else {
3886 other_errors.push(error);
3887 }
3888 });
3889
3890 // Show errors for this editor
3891 if(this.error_holder) {
3892 if(my_errors.length) {
3893 var message = [];
3894 this.error_holder.innerHTML = '';
3895 this.error_holder.style.display = '';
3896 $each(my_errors, function(i,error) {
3897 self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
3898 });
3899 }
3900 // Hide error area
3901 else {
3902 this.error_holder.style.display = 'none';
3903 }
3904 }
3905
3906 // Show errors for child editors
3907 $each(this.rows, function(i,row) {
3908 row.showValidationErrors(other_errors);
3909 });
3910 }
3911});
3912
3913JSONEditor.defaults.editors.table = JSONEditor.defaults.editors.array.extend({
3914 register: function() {
3915 this._super();
3916 if(this.rows) {
3917 for(var i=0; i<this.rows.length; i++) {
3918 this.rows[i].register();
3919 }
3920 }
3921 },
3922 unregister: function() {
3923 this._super();
3924 if(this.rows) {
3925 for(var i=0; i<this.rows.length; i++) {
3926 this.rows[i].unregister();
3927 }
3928 }
3929 },
3930 getNumColumns: function() {
3931 return Math.max(Math.min(12,this.width),3);
3932 },
3933 preBuild: function() {
3934 var item_schema = this.jsoneditor.expandRefs(this.schema.items || {});
3935
3936 this.item_title = item_schema.title || 'row';
3937 this.item_default = item_schema["default"] || null;
3938 this.item_has_child_editors = item_schema.properties || item_schema.items;
3939 this.width = 12;
3940 this._super();
3941 },
3942 build: function() {
3943 var self = this;
3944 this.table = this.theme.getTable();
3945 this.container.appendChild(this.table);
3946 this.thead = this.theme.getTableHead();
3947 this.table.appendChild(this.thead);
3948 this.header_row = this.theme.getTableRow();
3949 this.thead.appendChild(this.header_row);
3950 this.row_holder = this.theme.getTableBody();
3951 this.table.appendChild(this.row_holder);
3952
3953 // Determine the default value of array element
3954 var tmp = this.getElementEditor(0,true);
3955 this.item_default = tmp.getDefault();
3956 this.width = tmp.getNumColumns() + 2;
3957
3958 if(!this.options.compact) {
3959 this.title = this.theme.getHeader(this.getTitle());
3960 this.container.appendChild(this.title);
3961 this.title_controls = this.theme.getHeaderButtonHolder();
3962 this.title.appendChild(this.title_controls);
3963 if(this.schema.description) {
3964 this.description = this.theme.getDescription(this.schema.description);
3965 this.container.appendChild(this.description);
3966 }
3967 this.panel = this.theme.getIndentedPanel();
3968 this.container.appendChild(this.panel);
3969 this.error_holder = document.createElement('div');
3970 this.panel.appendChild(this.error_holder);
3971 }
3972 else {
3973 this.panel = document.createElement('div');
3974 this.container.appendChild(this.panel);
3975 }
3976
3977 this.panel.appendChild(this.table);
3978 this.controls = this.theme.getButtonHolder();
3979 this.panel.appendChild(this.controls);
3980
3981 if(this.item_has_child_editors) {
3982 var ce = tmp.getChildEditors();
3983 var order = tmp.property_order || Object.keys(ce);
3984 for(var i=0; i<order.length; i++) {
3985 var th = self.theme.getTableHeaderCell(ce[order[i]].getTitle());
3986 if(ce[order[i]].options.hidden) th.style.display = 'none';
3987 self.header_row.appendChild(th);
3988 }
3989 }
3990 else {
3991 self.header_row.appendChild(self.theme.getTableHeaderCell(this.item_title));
3992 }
3993
3994 tmp.destroy();
3995 this.row_holder.innerHTML = '';
3996
3997 // Row Controls column
3998 this.controls_header_cell = self.theme.getTableHeaderCell(" ");
3999 self.header_row.appendChild(this.controls_header_cell);
4000
4001 // Add controls
4002 this.addControls();
4003 },
4004 onChildEditorChange: function(editor) {
4005 this.refreshValue();
4006 this._super();
4007 },
4008 getItemDefault: function() {
4009 return $extend({},{"default":this.item_default})["default"];
4010 },
4011 getItemTitle: function() {
4012 return this.item_title;
4013 },
4014 getElementEditor: function(i,ignore) {
4015 var schema_copy = $extend({},this.schema.items);
4016 var editor = this.jsoneditor.getEditorClass(schema_copy, this.jsoneditor);
4017 var row = this.row_holder.appendChild(this.theme.getTableRow());
4018 var holder = row;
4019 if(!this.item_has_child_editors) {
4020 holder = this.theme.getTableCell();
4021 row.appendChild(holder);
4022 }
4023
4024 var ret = this.jsoneditor.createEditor(editor,{
4025 jsoneditor: this.jsoneditor,
4026 schema: schema_copy,
4027 container: holder,
4028 path: this.path+'.'+i,
4029 parent: this,
4030 compact: true,
4031 table_row: true
4032 });
4033
4034 ret.preBuild();
4035 if(!ignore) {
4036 ret.build();
4037 ret.postBuild();
4038
4039 ret.controls_cell = row.appendChild(this.theme.getTableCell());
4040 ret.row = row;
4041 ret.table_controls = this.theme.getButtonHolder();
4042 ret.controls_cell.appendChild(ret.table_controls);
4043 ret.table_controls.style.margin = 0;
4044 ret.table_controls.style.padding = 0;
4045 }
4046
4047 return ret;
4048 },
4049 destroy: function() {
4050 this.innerHTML = '';
4051 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
4052 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
4053 if(this.row_holder && this.row_holder.parentNode) this.row_holder.parentNode.removeChild(this.row_holder);
4054 if(this.table && this.table.parentNode) this.table.parentNode.removeChild(this.table);
4055 if(this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
4056
4057 this.rows = this.title = this.description = this.row_holder = this.table = this.panel = null;
4058
4059 this._super();
4060 },
4061 setValue: function(value, initial) {
4062 // Update the array's value, adding/removing rows when necessary
4063 value = value || [];
4064
4065 // Make sure value has between minItems and maxItems items in it
4066 if(this.schema.minItems) {
4067 while(value.length < this.schema.minItems) {
4068 value.push(this.getItemDefault());
4069 }
4070 }
4071 if(this.schema.maxItems && value.length > this.schema.maxItems) {
4072 value = value.slice(0,this.schema.maxItems);
4073 }
4074
4075 var serialized = JSON.stringify(value);
4076 if(serialized === this.serialized) return;
4077
4078 var numrows_changed = false;
4079
4080 var self = this;
4081 $each(value,function(i,val) {
4082 if(self.rows[i]) {
4083 // TODO: don't set the row's value if it hasn't changed
4084 self.rows[i].setValue(val);
4085 }
4086 else {
4087 self.addRow(val);
4088 numrows_changed = true;
4089 }
4090 });
4091
4092 for(var j=value.length; j<self.rows.length; j++) {
4093 var holder = self.rows[j].container;
4094 if(!self.item_has_child_editors) {
4095 self.rows[j].row.parentNode.removeChild(self.rows[j].row);
4096 }
4097 self.rows[j].destroy();
4098 if(holder.parentNode) holder.parentNode.removeChild(holder);
4099 self.rows[j] = null;
4100 numrows_changed = true;
4101 }
4102 self.rows = self.rows.slice(0,value.length);
4103
4104 self.refreshValue();
4105 if(numrows_changed || initial) self.refreshRowButtons();
4106
4107 self.onChange();
4108
4109 // TODO: sortable
4110 },
4111 refreshRowButtons: function() {
4112 var self = this;
4113
4114 // If we currently have minItems items in the array
4115 var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
4116
4117 var need_row_buttons = false;
4118 $each(this.rows,function(i,editor) {
4119 // Hide the move down button for the last row
4120 if(editor.movedown_button) {
4121 if(i === self.rows.length - 1) {
4122 editor.movedown_button.style.display = 'none';
4123 }
4124 else {
4125 need_row_buttons = true;
4126 editor.movedown_button.style.display = '';
4127 }
4128 }
4129
4130 // Hide the delete button if we have minItems items
4131 if(editor.delete_button) {
4132 if(minItems) {
4133 editor.delete_button.style.display = 'none';
4134 }
4135 else {
4136 need_row_buttons = true;
4137 editor.delete_button.style.display = '';
4138 }
4139 }
4140
4141 if(editor.moveup_button) {
4142 need_row_buttons = true;
4143 }
4144 });
4145
4146 // Show/hide controls column in table
4147 $each(this.rows,function(i,editor) {
4148 if(need_row_buttons) {
4149 editor.controls_cell.style.display = '';
4150 }
4151 else {
4152 editor.controls_cell.style.display = 'none';
4153 }
4154 });
4155 if(need_row_buttons) {
4156 this.controls_header_cell.style.display = '';
4157 }
4158 else {
4159 this.controls_header_cell.style.display = 'none';
4160 }
4161
4162 var controls_needed = false;
4163
4164 if(!this.value.length) {
4165 this.delete_last_row_button.style.display = 'none';
4166 this.remove_all_rows_button.style.display = 'none';
4167 this.table.style.display = 'none';
4168 }
4169 else if(this.value.length === 1) {
4170 this.table.style.display = '';
4171 this.remove_all_rows_button.style.display = 'none';
4172
4173 // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
4174 if(minItems || this.hide_delete_last_row_buttons) {
4175 this.delete_last_row_button.style.display = 'none';
4176 }
4177 else {
4178 this.delete_last_row_button.style.display = '';
4179 controls_needed = true;
4180 }
4181 }
4182 else {
4183 this.table.style.display = '';
4184
4185 if(minItems || this.hide_delete_last_row_buttons) {
4186 this.delete_last_row_button.style.display = 'none';
4187 }
4188 else {
4189 this.delete_last_row_button.style.display = '';
4190 controls_needed = true;
4191 }
4192
4193 if(minItems || this.hide_delete_all_rows_buttons) {
4194 this.remove_all_rows_button.style.display = 'none';
4195 }
4196 else {
4197 this.remove_all_rows_button.style.display = '';
4198 controls_needed = true;
4199 }
4200 }
4201
4202 // If there are maxItems in the array, hide the add button beneath the rows
4203 if((this.schema.maxItems && this.schema.maxItems <= this.rows.length) || this.hide_add_button) {
4204 this.add_row_button.style.display = 'none';
4205 }
4206 else {
4207 this.add_row_button.style.display = '';
4208 controls_needed = true;
4209 }
4210
4211 if(!controls_needed) {
4212 this.controls.style.display = 'none';
4213 }
4214 else {
4215 this.controls.style.display = '';
4216 }
4217 },
4218 refreshValue: function() {
4219 var self = this;
4220 this.value = [];
4221
4222 $each(this.rows,function(i,editor) {
4223 // Get the value for this editor
4224 self.value[i] = editor.getValue();
4225 });
4226 this.serialized = JSON.stringify(this.value);
4227 },
4228 addRow: function(value) {
4229 var self = this;
4230 var i = this.rows.length;
4231
4232 self.rows[i] = this.getElementEditor(i);
4233
4234 var controls_holder = self.rows[i].table_controls;
4235
4236 // Buttons to delete row, move row up, and move row down
4237 if(!this.hide_delete_buttons) {
4238 self.rows[i].delete_button = this.getButton('','delete',this.translate('button_delete_row_title_short'));
4239 self.rows[i].delete_button.className += ' delete';
4240 self.rows[i].delete_button.setAttribute('data-i',i);
4241 self.rows[i].delete_button.addEventListener('click',function(e) {
4242 e.preventDefault();
4243 e.stopPropagation();
4244 var i = this.getAttribute('data-i')*1;
4245
4246 var value = self.getValue();
4247
4248 var newval = [];
4249 $each(value,function(j,row) {
4250 if(j===i) return; // If this is the one we're deleting
4251 newval.push(row);
4252 });
4253 self.setValue(newval);
4254 self.onChange(true);
4255 });
4256 controls_holder.appendChild(self.rows[i].delete_button);
4257 }
4258
4259
4260 if(i && !this.hide_move_buttons) {
4261 self.rows[i].moveup_button = this.getButton('','moveup',this.translate('button_move_up_title'));
4262 self.rows[i].moveup_button.className += ' moveup';
4263 self.rows[i].moveup_button.setAttribute('data-i',i);
4264 self.rows[i].moveup_button.addEventListener('click',function(e) {
4265 e.preventDefault();
4266 e.stopPropagation();
4267 var i = this.getAttribute('data-i')*1;
4268
4269 if(i<=0) return;
4270 var rows = self.getValue();
4271 var tmp = rows[i-1];
4272 rows[i-1] = rows[i];
4273 rows[i] = tmp;
4274
4275 self.setValue(rows);
4276 self.onChange(true);
4277 });
4278 controls_holder.appendChild(self.rows[i].moveup_button);
4279 }
4280
4281 if(!this.hide_move_buttons) {
4282 self.rows[i].movedown_button = this.getButton('','movedown',this.translate('button_move_down_title'));
4283 self.rows[i].movedown_button.className += ' movedown';
4284 self.rows[i].movedown_button.setAttribute('data-i',i);
4285 self.rows[i].movedown_button.addEventListener('click',function(e) {
4286 e.preventDefault();
4287 e.stopPropagation();
4288 var i = this.getAttribute('data-i')*1;
4289 var rows = self.getValue();
4290 if(i>=rows.length-1) return;
4291 var tmp = rows[i+1];
4292 rows[i+1] = rows[i];
4293 rows[i] = tmp;
4294
4295 self.setValue(rows);
4296 self.onChange(true);
4297 });
4298 controls_holder.appendChild(self.rows[i].movedown_button);
4299 }
4300
4301 if(value) self.rows[i].setValue(value);
4302 },
4303 addControls: function() {
4304 var self = this;
4305
4306 this.collapsed = false;
4307 this.toggle_button = this.getButton('','collapse',this.translate('button_collapse'));
4308 if(this.title_controls) {
4309 this.title_controls.appendChild(this.toggle_button);
4310 this.toggle_button.addEventListener('click',function(e) {
4311 e.preventDefault();
4312 e.stopPropagation();
4313
4314 if(self.collapsed) {
4315 self.collapsed = false;
4316 self.panel.style.display = '';
4317 self.setButtonText(this,'','collapse','Collapse');
4318 }
4319 else {
4320 self.collapsed = true;
4321 self.panel.style.display = 'none';
4322 self.setButtonText(this,'','expand','Expand');
4323 }
4324 });
4325
4326 // If it should start collapsed
4327 if(this.options.collapsed) {
4328 $trigger(this.toggle_button,'click');
4329 }
4330
4331 // Collapse button disabled
4332 if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
4333 if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
4334 }
4335 else if(this.jsoneditor.options.disable_collapse) {
4336 this.toggle_button.style.display = 'none';
4337 }
4338 }
4339
4340 // Add "new row" and "delete last" buttons below editor
4341 this.add_row_button = this.getButton(this.getItemTitle(),'add',this.translate('button_add_row_title',[this.getItemTitle()]));
4342 this.add_row_button.addEventListener('click',function(e) {
4343 e.preventDefault();
4344 e.stopPropagation();
4345
4346 self.addRow();
4347 self.refreshValue();
4348 self.refreshRowButtons();
4349 self.onChange(true);
4350 });
4351 self.controls.appendChild(this.add_row_button);
4352
4353 this.delete_last_row_button = this.getButton(this.translate('button_delete_last',[this.getItemTitle()]),'delete',this.translate('button_delete_last_title',[this.getItemTitle()]));
4354 this.delete_last_row_button.addEventListener('click',function(e) {
4355 e.preventDefault();
4356 e.stopPropagation();
4357
4358 var rows = self.getValue();
4359 rows.pop();
4360 self.setValue(rows);
4361 self.onChange(true);
4362 });
4363 self.controls.appendChild(this.delete_last_row_button);
4364
4365 this.remove_all_rows_button = this.getButton(this.translate('button_delete_all'),'delete',this.translate('button_delete_all_title'));
4366 this.remove_all_rows_button.addEventListener('click',function(e) {
4367 e.preventDefault();
4368 e.stopPropagation();
4369
4370 self.setValue([]);
4371 self.onChange(true);
4372 });
4373 self.controls.appendChild(this.remove_all_rows_button);
4374 }
4375});
4376
4377// Multiple Editor (for when `type` is an array)
4378JSONEditor.defaults.editors.multiple = JSONEditor.AbstractEditor.extend({
4379 register: function() {
4380 if(this.editors) {
4381 for(var i=0; i<this.editors.length; i++) {
4382 if(!this.editors[i]) continue;
4383 this.editors[i].unregister();
4384 }
4385 if(this.editors[this.type]) this.editors[this.type].register();
4386 }
4387 this._super();
4388 },
4389 unregister: function() {
4390 this._super();
4391 if(this.editors) {
4392 for(var i=0; i<this.editors.length; i++) {
4393 if(!this.editors[i]) continue;
4394 this.editors[i].unregister();
4395 }
4396 }
4397 },
4398 getNumColumns: function() {
4399 if(!this.editors[this.type]) return 4;
4400 return Math.max(this.editors[this.type].getNumColumns(),4);
4401 },
4402 enable: function() {
4403 if(this.editors) {
4404 for(var i=0; i<this.editors.length; i++) {
4405 if(!this.editors[i]) continue;
4406 this.editors[i].enable();
4407 }
4408 }
4409 this.switcher.disabled = false;
4410 this._super();
4411 },
4412 disable: function() {
4413 if(this.editors) {
4414 for(var i=0; i<this.editors.length; i++) {
4415 if(!this.editors[i]) continue;
4416 this.editors[i].disable();
4417 }
4418 }
4419 this.switcher.disabled = true;
4420 this._super();
4421 },
4422 switchEditor: function(i) {
4423 var self = this;
4424
4425 if(!this.editors[i]) {
4426 this.buildChildEditor(i);
4427 }
4428
4429 self.type = i;
4430
4431 self.register();
4432
4433 var current_value = self.getValue();
4434
4435 $each(self.editors,function(type,editor) {
4436 if(!editor) return;
4437 if(self.type === type) {
4438 if(self.keep_values) editor.setValue(current_value,true);
4439 editor.container.style.display = '';
4440 }
4441 else editor.container.style.display = 'none';
4442 });
4443 self.refreshValue();
4444 self.refreshHeaderText();
4445 },
4446 buildChildEditor: function(i) {
4447 var self = this;
4448 var type = this.types[i];
4449 var holder = self.theme.getChildEditorHolder();
4450 self.editor_holder.appendChild(holder);
4451
4452 var schema;
4453
4454 if(typeof type === "string") {
4455 schema = $extend({},self.schema);
4456 schema.type = type;
4457 }
4458 else {
4459 schema = $extend({},self.schema,type);
4460 schema = self.jsoneditor.expandRefs(schema);
4461
4462 // If we need to merge `required` arrays
4463 if(type.required && Array.isArray(type.required) && self.schema.required && Array.isArray(self.schema.required)) {
4464 schema.required = self.schema.required.concat(type.required);
4465 }
4466 }
4467
4468 var editor = self.jsoneditor.getEditorClass(schema);
4469
4470 self.editors[i] = self.jsoneditor.createEditor(editor,{
4471 jsoneditor: self.jsoneditor,
4472 schema: schema,
4473 container: holder,
4474 path: self.path,
4475 parent: self,
4476 required: true
4477 });
4478 self.editors[i].preBuild();
4479 self.editors[i].build();
4480 self.editors[i].postBuild();
4481
4482 if(self.editors[i].header) self.editors[i].header.style.display = 'none';
4483
4484 self.editors[i].option = self.switcher_options[i];
4485
4486 holder.addEventListener('change_header_text',function() {
4487 self.refreshHeaderText();
4488 });
4489
4490 if(i !== self.type) holder.style.display = 'none';
4491 },
4492 preBuild: function() {
4493 var self = this;
4494
4495 this.types = [];
4496 this.type = 0;
4497 this.editors = [];
4498 this.validators = [];
4499
4500 this.keep_values = true;
4501 if(typeof this.jsoneditor.options.keep_oneof_values !== "undefined") this.keep_values = this.jsoneditor.options.keep_oneof_values;
4502 if(typeof this.options.keep_oneof_values !== "undefined") this.keep_values = this.options.keep_oneof_values;
4503
4504 if(this.schema.oneOf) {
4505 this.oneOf = true;
4506 this.types = this.schema.oneOf;
4507 $each(this.types,function(i,oneof) {
4508 //self.types[i] = self.jsoneditor.expandSchema(oneof);
4509 });
4510 delete this.schema.oneOf;
4511 }
4512 else {
4513 if(!this.schema.type || this.schema.type === "any") {
4514 this.types = ['string','number','integer','boolean','object','array','null'];
4515
4516 // If any of these primitive types are disallowed
4517 if(this.schema.disallow) {
4518 var disallow = this.schema.disallow;
4519 if(typeof disallow !== 'object' || !(Array.isArray(disallow))) {
4520 disallow = [disallow];
4521 }
4522 var allowed_types = [];
4523 $each(this.types,function(i,type) {
4524 if(disallow.indexOf(type) === -1) allowed_types.push(type);
4525 });
4526 this.types = allowed_types;
4527 }
4528 }
4529 else if(Array.isArray(this.schema.type)) {
4530 this.types = this.schema.type;
4531 }
4532 else {
4533 this.types = [this.schema.type];
4534 }
4535 delete this.schema.type;
4536 }
4537
4538 this.display_text = this.getDisplayText(this.types);
4539 },
4540 build: function() {
4541 var self = this;
4542 var container = this.container;
4543
4544 this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
4545 this.container.appendChild(this.header);
4546
4547 this.switcher = this.theme.getSwitcher(this.display_text);
4548 container.appendChild(this.switcher);
4549 this.switcher.addEventListener('change',function(e) {
4550 e.preventDefault();
4551 e.stopPropagation();
4552
4553 self.switchEditor(self.display_text.indexOf(this.value));
4554 self.onChange(true);
4555 });
4556
4557 this.editor_holder = document.createElement('div');
4558 container.appendChild(this.editor_holder);
4559
4560
4561 var validator_options = {};
4562 if(self.jsoneditor.options.custom_validators) {
4563 validator_options.custom_validators = self.jsoneditor.options.custom_validators;
4564 }
4565
4566 this.switcher_options = this.theme.getSwitcherOptions(this.switcher);
4567 $each(this.types,function(i,type) {
4568 self.editors[i] = false;
4569
4570 var schema;
4571
4572 if(typeof type === "string") {
4573 schema = $extend({},self.schema);
4574 schema.type = type;
4575 }
4576 else {
4577 schema = $extend({},self.schema,type);
4578
4579 // If we need to merge `required` arrays
4580 if(type.required && Array.isArray(type.required) && self.schema.required && Array.isArray(self.schema.required)) {
4581 schema.required = self.schema.required.concat(type.required);
4582 }
4583 }
4584
4585 self.validators[i] = new JSONEditor.Validator(self.jsoneditor,schema,validator_options);
4586 });
4587
4588 this.switchEditor(0);
4589 },
4590 onChildEditorChange: function(editor) {
4591 if(this.editors[this.type]) {
4592 this.refreshValue();
4593 this.refreshHeaderText();
4594 }
4595
4596 this._super();
4597 },
4598 refreshHeaderText: function() {
4599 var display_text = this.getDisplayText(this.types);
4600 $each(this.switcher_options, function(i,option) {
4601 option.textContent = display_text[i];
4602 });
4603 },
4604 refreshValue: function() {
4605 this.value = this.editors[this.type].getValue();
4606 },
4607 setValue: function(val,initial) {
4608 // Determine type by getting the first one that validates
4609 var self = this;
4610 $each(this.validators, function(i,validator) {
4611 if(!validator.validate(val).length) {
4612 self.type = i;
4613 self.switcher.value = self.display_text[i];
4614 return false;
4615 }
4616 });
4617
4618 this.switchEditor(this.type);
4619
4620 this.editors[this.type].setValue(val,initial);
4621
4622 this.refreshValue();
4623 self.onChange();
4624 },
4625 destroy: function() {
4626 $each(this.editors, function(type,editor) {
4627 if(editor) editor.destroy();
4628 });
4629 if(this.editor_holder && this.editor_holder.parentNode) this.editor_holder.parentNode.removeChild(this.editor_holder);
4630 if(this.switcher && this.switcher.parentNode) this.switcher.parentNode.removeChild(this.switcher);
4631 this._super();
4632 },
4633 showValidationErrors: function(errors) {
4634 var self = this;
4635
4636 // oneOf error paths need to remove the oneOf[i] part before passing to child editors
4637 if(this.oneOf) {
4638 $each(this.editors,function(i,editor) {
4639 if(!editor) return;
4640 var check = self.path+'.oneOf['+i+']';
4641 var new_errors = [];
4642 $each(errors, function(j,error) {
4643 if(error.path.substr(0,check.length)===check) {
4644 var new_error = $extend({},error);
4645 new_error.path = self.path+new_error.path.substr(check.length);
4646 new_errors.push(new_error);
4647 }
4648 });
4649
4650 editor.showValidationErrors(new_errors);
4651 });
4652 }
4653 else {
4654 $each(this.editors,function(type,editor) {
4655 if(!editor) return;
4656 editor.showValidationErrors(errors);
4657 });
4658 }
4659 }
4660});
4661
4662// Enum Editor (used for objects and arrays with enumerated values)
4663JSONEditor.defaults.editors["enum"] = JSONEditor.AbstractEditor.extend({
4664 getNumColumns: function() {
4665 return 4;
4666 },
4667 build: function() {
4668 var container = this.container;
4669 this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
4670 this.container.appendChild(this.title);
4671
4672 this.options.enum_titles = this.options.enum_titles || [];
4673
4674 this["enum"] = this.schema["enum"];
4675 this.selected = 0;
4676 this.select_options = [];
4677 this.html_values = [];
4678
4679 var self = this;
4680 for(var i=0; i<this["enum"].length; i++) {
4681 this.select_options[i] = this.options.enum_titles[i] || "Value "+(i+1);
4682 this.html_values[i] = this.getHTML(this["enum"][i]);
4683 }
4684
4685 // Switcher
4686 this.switcher = this.theme.getSwitcher(this.select_options);
4687 this.container.appendChild(this.switcher);
4688
4689 // Display area
4690 this.display_area = this.theme.getIndentedPanel();
4691 this.container.appendChild(this.display_area);
4692
4693 if(this.options.hide_display) this.display_area.style.display = "none";
4694
4695 this.switcher.addEventListener('change',function() {
4696 self.selected = self.select_options.indexOf(this.value);
4697 self.value = self["enum"][self.selected];
4698 self.refreshValue();
4699 self.onChange(true);
4700 });
4701 this.value = this["enum"][0];
4702 this.refreshValue();
4703
4704 if(this["enum"].length === 1) this.switcher.style.display = 'none';
4705 },
4706 refreshValue: function() {
4707 var self = this;
4708 self.selected = -1;
4709 var stringified = JSON.stringify(this.value);
4710 $each(this["enum"], function(i, el) {
4711 if(stringified === JSON.stringify(el)) {
4712 self.selected = i;
4713 return false;
4714 }
4715 });
4716
4717 if(self.selected<0) {
4718 self.setValue(self["enum"][0]);
4719 return;
4720 }
4721
4722 this.switcher.value = this.select_options[this.selected];
4723 this.display_area.innerHTML = this.html_values[this.selected];
4724 },
4725 enable: function() {
4726 if(!this.always_disabled) this.switcher.disabled = false;
4727 this._super();
4728 },
4729 disable: function() {
4730 this.switcher.disabled = true;
4731 this._super();
4732 },
4733 getHTML: function(el) {
4734 var self = this;
4735
4736 if(el === null) {
4737 return '<em>null</em>';
4738 }
4739 // Array or Object
4740 else if(typeof el === "object") {
4741 // TODO: use theme
4742 var ret = '';
4743
4744 $each(el,function(i,child) {
4745 var html = self.getHTML(child);
4746
4747 // Add the keys to object children
4748 if(!(Array.isArray(el))) {
4749 // TODO: use theme
4750 html = '<div><em>'+i+'</em>: '+html+'</div>';
4751 }
4752
4753 // TODO: use theme
4754 ret += '<li>'+html+'</li>';
4755 });
4756
4757 if(Array.isArray(el)) ret = '<ol>'+ret+'</ol>';
4758 else ret = "<ul style='margin-top:0;margin-bottom:0;padding-top:0;padding-bottom:0;'>"+ret+'</ul>';
4759
4760 return ret;
4761 }
4762 // Boolean
4763 else if(typeof el === "boolean") {
4764 return el? 'true' : 'false';
4765 }
4766 // String
4767 else if(typeof el === "string") {
4768 return el.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
4769 }
4770 // Number
4771 else {
4772 return el;
4773 }
4774 },
4775 setValue: function(val) {
4776 if(this.value !== val) {
4777 this.value = val;
4778 this.refreshValue();
4779 this.onChange();
4780 }
4781 },
4782 destroy: function() {
4783 if(this.display_area && this.display_area.parentNode) this.display_area.parentNode.removeChild(this.display_area);
4784 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
4785 if(this.switcher && this.switcher.parentNode) this.switcher.parentNode.removeChild(this.switcher);
4786
4787 this._super();
4788 }
4789});
4790
4791JSONEditor.defaults.editors.select = JSONEditor.AbstractEditor.extend({
4792 setValue: function(value,initial) {
4793 value = this.typecast(value||'');
4794
4795 // Sanitize value before setting it
4796 var sanitized = value;
4797 if(this.enum_values.indexOf(sanitized) < 0) {
4798 sanitized = this.enum_values[0];
4799 }
4800
4801 if(this.value === sanitized) {
4802 return;
4803 }
4804
4805 this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
4806 if(this.select2) this.select2.select2('val',this.input.value);
4807 this.value = sanitized;
4808 this.onChange();
4809 },
4810 register: function() {
4811 this._super();
4812 if(!this.input) return;
4813 this.input.setAttribute('name',this.formname);
4814 },
4815 unregister: function() {
4816 this._super();
4817 if(!this.input) return;
4818 this.input.removeAttribute('name');
4819 },
4820 getNumColumns: function() {
4821 if(!this.enum_options) return 3;
4822 var longest_text = this.getTitle().length;
4823 for(var i=0; i<this.enum_options.length; i++) {
4824 longest_text = Math.max(longest_text,this.enum_options[i].length+4);
4825 }
4826 return Math.min(12,Math.max(longest_text/7,2));
4827 },
4828 typecast: function(value) {
4829 if(this.schema.type === "boolean") {
4830 return !!value;
4831 }
4832 else if(this.schema.type === "number") {
4833 return 1*value;
4834 }
4835 else if(this.schema.type === "integer") {
4836 return Math.floor(value*1);
4837 }
4838 else {
4839 return ""+value;
4840 }
4841 },
4842 getValue: function() {
4843 return this.value;
4844 },
4845 preBuild: function() {
4846 var self = this;
4847 this.input_type = 'select';
4848 this.enum_options = [];
4849 this.enum_values = [];
4850 this.enum_display = [];
4851
4852 // Enum options enumerated
4853 if(this.schema["enum"]) {
4854 var display = this.schema.options && this.schema.options.enum_titles || [];
4855
4856 $each(this.schema["enum"],function(i,option) {
4857 self.enum_options[i] = ""+option;
4858 self.enum_display[i] = ""+(display[i] || option);
4859 self.enum_values[i] = self.typecast(option);
4860 });
4861
4862 if(!this.isRequired()){
4863 self.enum_display.unshift(' ');
4864 self.enum_options.unshift('undefined');
4865 self.enum_values.unshift(undefined);
4866 }
4867
4868 }
4869 // Boolean
4870 else if(this.schema.type === "boolean") {
4871 self.enum_display = this.schema.options && this.schema.options.enum_titles || ['true','false'];
4872 self.enum_options = ['1',''];
4873 self.enum_values = [true,false];
4874
4875 if(!this.isRequired()){
4876 self.enum_display.unshift(' ');
4877 self.enum_options.unshift('undefined');
4878 self.enum_values.unshift(undefined);
4879 }
4880
4881 }
4882 // Dynamic Enum
4883 else if(this.schema.enumSource) {
4884 this.enumSource = [];
4885 this.enum_display = [];
4886 this.enum_options = [];
4887 this.enum_values = [];
4888
4889 // Shortcut declaration for using a single array
4890 if(!(Array.isArray(this.schema.enumSource))) {
4891 if(this.schema.enumValue) {
4892 this.enumSource = [
4893 {
4894 source: this.schema.enumSource,
4895 value: this.schema.enumValue
4896 }
4897 ];
4898 }
4899 else {
4900 this.enumSource = [
4901 {
4902 source: this.schema.enumSource
4903 }
4904 ];
4905 }
4906 }
4907 else {
4908 for(i=0; i<this.schema.enumSource.length; i++) {
4909 // Shorthand for watched variable
4910 if(typeof this.schema.enumSource[i] === "string") {
4911 this.enumSource[i] = {
4912 source: this.schema.enumSource[i]
4913 };
4914 }
4915 // Make a copy of the schema
4916 else if(!(Array.isArray(this.schema.enumSource[i]))) {
4917 this.enumSource[i] = $extend({},this.schema.enumSource[i]);
4918 }
4919 else {
4920 this.enumSource[i] = this.schema.enumSource[i];
4921 }
4922 }
4923 }
4924
4925 // Now, enumSource is an array of sources
4926 // Walk through this array and fix up the values
4927 for(i=0; i<this.enumSource.length; i++) {
4928 if(this.enumSource[i].value) {
4929 this.enumSource[i].value = this.jsoneditor.compileTemplate(this.enumSource[i].value, this.template_engine);
4930 }
4931 if(this.enumSource[i].title) {
4932 this.enumSource[i].title = this.jsoneditor.compileTemplate(this.enumSource[i].title, this.template_engine);
4933 }
4934 if(this.enumSource[i].filter) {
4935 this.enumSource[i].filter = this.jsoneditor.compileTemplate(this.enumSource[i].filter, this.template_engine);
4936 }
4937 }
4938 }
4939 // Other, not supported
4940 else {
4941 throw "'select' editor requires the enum property to be set.";
4942 }
4943 },
4944 build: function() {
4945 var self = this;
4946 if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
4947 if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
4948
4949 if(this.options.compact) this.container.className += ' compact';
4950
4951 this.input = this.theme.getSelectInput(this.enum_options);
4952 this.theme.setSelectOptions(this.input,this.enum_options,this.enum_display);
4953
4954 if(this.schema.readOnly || this.schema.readonly) {
4955 this.always_disabled = true;
4956 this.input.disabled = true;
4957 }
4958
4959 this.input.addEventListener('change',function(e) {
4960 e.preventDefault();
4961 e.stopPropagation();
4962 self.onInputChange();
4963 });
4964
4965 this.control = this.theme.getFormControl(this.label, this.input, this.description);
4966 this.container.appendChild(this.control);
4967
4968 this.value = this.enum_values[0];
4969 },
4970 onInputChange: function() {
4971 var val = this.input.value;
4972
4973 var new_val;
4974 // Invalid option, use first option instead
4975 if(this.enum_options.indexOf(val) === -1) {
4976 new_val = this.enum_values[0];
4977 }
4978 else {
4979 new_val = this.enum_values[this.enum_options.indexOf(val)];
4980 }
4981
4982 // If valid hasn't changed
4983 if(new_val === this.value) return;
4984
4985 // Store new value and propogate change event
4986 this.value = new_val;
4987 this.onChange(true);
4988 },
4989 setupSelect2: function() {
4990 // If the Select2 library is loaded use it when we have lots of items
4991 if(window.jQuery && window.jQuery.fn && window.jQuery.fn.select2 && (this.enum_options.length > 2 || (this.enum_options.length && this.enumSource))) {
4992 var options = $extend({},JSONEditor.plugins.select2);
4993 if(this.schema.options && this.schema.options.select2_options) options = $extend(options,this.schema.options.select2_options);
4994 this.select2 = window.jQuery(this.input).select2(options);
4995 var self = this;
4996 this.select2.on('select2-blur',function() {
4997 self.input.value = self.select2.select2('val');
4998 self.onInputChange();
4999 });
5000 this.select2.on('change',function() {
5001 self.input.value = self.select2.select2('val');
5002 self.onInputChange();
5003 });
5004 }
5005 else {
5006 this.select2 = null;
5007 }
5008 },
5009 postBuild: function() {
5010 this._super();
5011 this.theme.afterInputReady(this.input);
5012 this.setupSelect2();
5013 },
5014 onWatchedFieldChange: function() {
5015 var self = this, vars, j;
5016
5017 // If this editor uses a dynamic select box
5018 if(this.enumSource) {
5019 vars = this.getWatchedFieldValues();
5020 var select_options = [];
5021 var select_titles = [];
5022
5023 for(var i=0; i<this.enumSource.length; i++) {
5024 // Constant values
5025 if(Array.isArray(this.enumSource[i])) {
5026 select_options = select_options.concat(this.enumSource[i]);
5027 select_titles = select_titles.concat(this.enumSource[i]);
5028 }
5029 else {
5030 var items = [];
5031 // Static list of items
5032 if(Array.isArray(this.enumSource[i].source)) {
5033 items = this.enumSource[i].source;
5034 // A watched field
5035 } else {
5036 items = vars[this.enumSource[i].source];
5037 }
5038
5039 if(items) {
5040 // Only use a predefined part of the array
5041 if(this.enumSource[i].slice) {
5042 items = Array.prototype.slice.apply(items,this.enumSource[i].slice);
5043 }
5044 // Filter the items
5045 if(this.enumSource[i].filter) {
5046 var new_items = [];
5047 for(j=0; j<items.length; j++) {
5048 if(this.enumSource[i].filter({i:j,item:items[j],watched:vars})) new_items.push(items[j]);
5049 }
5050 items = new_items;
5051 }
5052
5053 var item_titles = [];
5054 var item_values = [];
5055 for(j=0; j<items.length; j++) {
5056 var item = items[j];
5057
5058 // Rendered value
5059 if(this.enumSource[i].value) {
5060 item_values[j] = this.enumSource[i].value({
5061 i: j,
5062 item: item
5063 });
5064 }
5065 // Use value directly
5066 else {
5067 item_values[j] = items[j];
5068 }
5069
5070 // Rendered title
5071 if(this.enumSource[i].title) {
5072 item_titles[j] = this.enumSource[i].title({
5073 i: j,
5074 item: item
5075 });
5076 }
5077 // Use value as the title also
5078 else {
5079 item_titles[j] = item_values[j];
5080 }
5081 }
5082
5083 // TODO: sort
5084
5085 select_options = select_options.concat(item_values);
5086 select_titles = select_titles.concat(item_titles);
5087 }
5088 }
5089 }
5090
5091 var prev_value = this.value;
5092
5093 this.theme.setSelectOptions(this.input, select_options, select_titles);
5094 this.enum_options = select_options;
5095 this.enum_display = select_titles;
5096 this.enum_values = select_options;
5097
5098 if(this.select2) {
5099 this.select2.select2('destroy');
5100 }
5101
5102 // If the previous value is still in the new select options, stick with it
5103 if(select_options.indexOf(prev_value) !== -1) {
5104 this.input.value = prev_value;
5105 this.value = prev_value;
5106 }
5107 // Otherwise, set the value to the first select option
5108 else {
5109 this.input.value = select_options[0];
5110 this.value = select_options[0] || "";
5111 if(this.parent) this.parent.onChildEditorChange(this);
5112 else this.jsoneditor.onChange();
5113 this.jsoneditor.notifyWatchers(this.path);
5114 }
5115
5116 this.setupSelect2();
5117 }
5118
5119 this._super();
5120 },
5121 enable: function() {
5122 if(!this.always_disabled) {
5123 this.input.disabled = false;
5124 if(this.select2) this.select2.select2("enable",true);
5125 }
5126 this._super();
5127 },
5128 disable: function() {
5129 this.input.disabled = true;
5130 if(this.select2) this.select2.select2("enable",false);
5131 this._super();
5132 },
5133 destroy: function() {
5134 if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
5135 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
5136 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
5137 if(this.select2) {
5138 this.select2.select2('destroy');
5139 this.select2 = null;
5140 }
5141
5142 this._super();
5143 }
5144});
5145
5146JSONEditor.defaults.editors.selectize = JSONEditor.AbstractEditor.extend({
5147 setValue: function(value,initial) {
5148 value = this.typecast(value||'');
5149
5150 // Sanitize value before setting it
5151 var sanitized = value;
5152 if(this.enum_values.indexOf(sanitized) < 0) {
5153 sanitized = this.enum_values[0];
5154 }
5155
5156 if(this.value === sanitized) {
5157 return;
5158 }
5159
5160 this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
5161
5162 if(this.selectize) {
5163 this.selectize[0].selectize.addItem(sanitized);
5164 }
5165
5166 this.value = sanitized;
5167 this.onChange();
5168 },
5169 register: function() {
5170 this._super();
5171 if(!this.input) return;
5172 this.input.setAttribute('name',this.formname);
5173 },
5174 unregister: function() {
5175 this._super();
5176 if(!this.input) return;
5177 this.input.removeAttribute('name');
5178 },
5179 getNumColumns: function() {
5180 if(!this.enum_options) return 3;
5181 var longest_text = this.getTitle().length;
5182 for(var i=0; i<this.enum_options.length; i++) {
5183 longest_text = Math.max(longest_text,this.enum_options[i].length+4);
5184 }
5185 return Math.min(12,Math.max(longest_text/7,2));
5186 },
5187 typecast: function(value) {
5188 if(this.schema.type === "boolean") {
5189 return !!value;
5190 }
5191 else if(this.schema.type === "number") {
5192 return 1*value;
5193 }
5194 else if(this.schema.type === "integer") {
5195 return Math.floor(value*1);
5196 }
5197 else {
5198 return ""+value;
5199 }
5200 },
5201 getValue: function() {
5202 return this.value;
5203 },
5204 preBuild: function() {
5205 var self = this;
5206 this.input_type = 'select';
5207 this.enum_options = [];
5208 this.enum_values = [];
5209 this.enum_display = [];
5210
5211 // Enum options enumerated
5212 if(this.schema.enum) {
5213 var display = this.schema.options && this.schema.options.enum_titles || [];
5214
5215 $each(this.schema.enum,function(i,option) {
5216 self.enum_options[i] = ""+option;
5217 self.enum_display[i] = ""+(display[i] || option);
5218 self.enum_values[i] = self.typecast(option);
5219 });
5220 }
5221 // Boolean
5222 else if(this.schema.type === "boolean") {
5223 self.enum_display = this.schema.options && this.schema.options.enum_titles || ['true','false'];
5224 self.enum_options = ['1','0'];
5225 self.enum_values = [true,false];
5226 }
5227 // Dynamic Enum
5228 else if(this.schema.enumSource) {
5229 this.enumSource = [];
5230 this.enum_display = [];
5231 this.enum_options = [];
5232 this.enum_values = [];
5233
5234 // Shortcut declaration for using a single array
5235 if(!(Array.isArray(this.schema.enumSource))) {
5236 if(this.schema.enumValue) {
5237 this.enumSource = [
5238 {
5239 source: this.schema.enumSource,
5240 value: this.schema.enumValue
5241 }
5242 ];
5243 }
5244 else {
5245 this.enumSource = [
5246 {
5247 source: this.schema.enumSource
5248 }
5249 ];
5250 }
5251 }
5252 else {
5253 for(i=0; i<this.schema.enumSource.length; i++) {
5254 // Shorthand for watched variable
5255 if(typeof this.schema.enumSource[i] === "string") {
5256 this.enumSource[i] = {
5257 source: this.schema.enumSource[i]
5258 };
5259 }
5260 // Make a copy of the schema
5261 else if(!(Array.isArray(this.schema.enumSource[i]))) {
5262 this.enumSource[i] = $extend({},this.schema.enumSource[i]);
5263 }
5264 else {
5265 this.enumSource[i] = this.schema.enumSource[i];
5266 }
5267 }
5268 }
5269
5270 // Now, enumSource is an array of sources
5271 // Walk through this array and fix up the values
5272 for(i=0; i<this.enumSource.length; i++) {
5273 if(this.enumSource[i].value) {
5274 this.enumSource[i].value = this.jsoneditor.compileTemplate(this.enumSource[i].value, this.template_engine);
5275 }
5276 if(this.enumSource[i].title) {
5277 this.enumSource[i].title = this.jsoneditor.compileTemplate(this.enumSource[i].title, this.template_engine);
5278 }
5279 if(this.enumSource[i].filter) {
5280 this.enumSource[i].filter = this.jsoneditor.compileTemplate(this.enumSource[i].filter, this.template_engine);
5281 }
5282 }
5283 }
5284 // Other, not supported
5285 else {
5286 throw "'select' editor requires the enum property to be set.";
5287 }
5288 },
5289 build: function() {
5290 var self = this;
5291 if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5292 if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
5293
5294 if(this.options.compact) this.container.className += ' compact';
5295
5296 this.input = this.theme.getSelectInput(this.enum_options);
5297 this.theme.setSelectOptions(this.input,this.enum_options,this.enum_display);
5298
5299 if(this.schema.readOnly || this.schema.readonly) {
5300 this.always_disabled = true;
5301 this.input.disabled = true;
5302 }
5303
5304 this.input.addEventListener('change',function(e) {
5305 e.preventDefault();
5306 e.stopPropagation();
5307 self.onInputChange();
5308 });
5309
5310 this.control = this.theme.getFormControl(this.label, this.input, this.description);
5311 this.container.appendChild(this.control);
5312
5313 this.value = this.enum_values[0];
5314 },
5315 onInputChange: function() {
5316 var val = this.input.value;
5317
5318 var sanitized = val;
5319 if(this.enum_options.indexOf(val) === -1) {
5320 sanitized = this.enum_options[0];
5321 }
5322
5323 this.value = this.enum_values[this.enum_options.indexOf(val)];
5324 this.onChange(true);
5325 },
5326 setupSelectize: function() {
5327 // If the Selectize library is loaded use it when we have lots of items
5328 var self = this;
5329 if(window.jQuery && window.jQuery.fn && window.jQuery.fn.selectize && (this.enum_options.length >= 2 || (this.enum_options.length && this.enumSource))) {
5330 var options = $extend({},JSONEditor.plugins.selectize);
5331 if(this.schema.options && this.schema.options.selectize_options) options = $extend(options,this.schema.options.selectize_options);
5332 this.selectize = window.jQuery(this.input).selectize($extend(options,
5333 {
5334 create: true,
5335 onChange : function() {
5336 self.onInputChange();
5337 }
5338 }));
5339 }
5340 else {
5341 this.selectize = null;
5342 }
5343 },
5344 postBuild: function() {
5345 this._super();
5346 this.theme.afterInputReady(this.input);
5347 this.setupSelectize();
5348 },
5349 onWatchedFieldChange: function() {
5350 var self = this, vars, j;
5351
5352 // If this editor uses a dynamic select box
5353 if(this.enumSource) {
5354 vars = this.getWatchedFieldValues();
5355 var select_options = [];
5356 var select_titles = [];
5357
5358 for(var i=0; i<this.enumSource.length; i++) {
5359 // Constant values
5360 if(Array.isArray(this.enumSource[i])) {
5361 select_options = select_options.concat(this.enumSource[i]);
5362 select_titles = select_titles.concat(this.enumSource[i]);
5363 }
5364 // A watched field
5365 else if(vars[this.enumSource[i].source]) {
5366 var items = vars[this.enumSource[i].source];
5367
5368 // Only use a predefined part of the array
5369 if(this.enumSource[i].slice) {
5370 items = Array.prototype.slice.apply(items,this.enumSource[i].slice);
5371 }
5372 // Filter the items
5373 if(this.enumSource[i].filter) {
5374 var new_items = [];
5375 for(j=0; j<items.length; j++) {
5376 if(this.enumSource[i].filter({i:j,item:items[j]})) new_items.push(items[j]);
5377 }
5378 items = new_items;
5379 }
5380
5381 var item_titles = [];
5382 var item_values = [];
5383 for(j=0; j<items.length; j++) {
5384 var item = items[j];
5385
5386 // Rendered value
5387 if(this.enumSource[i].value) {
5388 item_values[j] = this.enumSource[i].value({
5389 i: j,
5390 item: item
5391 });
5392 }
5393 // Use value directly
5394 else {
5395 item_values[j] = items[j];
5396 }
5397
5398 // Rendered title
5399 if(this.enumSource[i].title) {
5400 item_titles[j] = this.enumSource[i].title({
5401 i: j,
5402 item: item
5403 });
5404 }
5405 // Use value as the title also
5406 else {
5407 item_titles[j] = item_values[j];
5408 }
5409 }
5410
5411 // TODO: sort
5412
5413 select_options = select_options.concat(item_values);
5414 select_titles = select_titles.concat(item_titles);
5415 }
5416 }
5417
5418 var prev_value = this.value;
5419
5420 this.theme.setSelectOptions(this.input, select_options, select_titles);
5421 this.enum_options = select_options;
5422 this.enum_display = select_titles;
5423 this.enum_values = select_options;
5424
5425 // If the previous value is still in the new select options, stick with it
5426 if(select_options.indexOf(prev_value) !== -1) {
5427 this.input.value = prev_value;
5428 this.value = prev_value;
5429 }
5430
5431 // Otherwise, set the value to the first select option
5432 else {
5433 this.input.value = select_options[0];
5434 this.value = select_options[0] || "";
5435 if(this.parent) this.parent.onChildEditorChange(this);
5436 else this.jsoneditor.onChange();
5437 this.jsoneditor.notifyWatchers(this.path);
5438 }
5439
5440 if(this.selectize) {
5441 // Update the Selectize options
5442 this.updateSelectizeOptions(select_options);
5443 }
5444 else {
5445 this.setupSelectize();
5446 }
5447
5448 this._super();
5449 }
5450 },
5451 updateSelectizeOptions: function(select_options) {
5452 var selectized = this.selectize[0].selectize,
5453 self = this;
5454
5455 selectized.off();
5456 selectized.clearOptions();
5457 for(var n in select_options) {
5458 selectized.addOption({value:select_options[n],text:select_options[n]});
5459 }
5460 selectized.addItem(this.value);
5461 selectized.on('change',function() {
5462 self.onInputChange();
5463 });
5464 },
5465 enable: function() {
5466 if(!this.always_disabled) {
5467 this.input.disabled = false;
5468 if(this.selectize) {
5469 this.selectize[0].selectize.unlock();
5470 }
5471 }
5472 this._super();
5473 },
5474 disable: function() {
5475 this.input.disabled = true;
5476 if(this.selectize) {
5477 this.selectize[0].selectize.lock();
5478 }
5479 this._super();
5480 },
5481 destroy: function() {
5482 if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
5483 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
5484 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
5485 if(this.selectize) {
5486 this.selectize[0].selectize.destroy();
5487 this.selectize = null;
5488 }
5489 this._super();
5490 }
5491});
5492
5493JSONEditor.defaults.editors.multiselect = JSONEditor.AbstractEditor.extend({
5494 preBuild: function() {
5495 this._super();
5496
5497 this.select_options = {};
5498 this.select_values = {};
5499
5500 var items_schema = this.jsoneditor.expandRefs(this.schema.items || {});
5501
5502 var e = items_schema["enum"] || [];
5503 this.option_keys = [];
5504 for(i=0; i<e.length; i++) {
5505 // If the sanitized value is different from the enum value, don't include it
5506 if(this.sanitize(e[i]) !== e[i]) continue;
5507
5508 this.option_keys.push(e[i]+"");
5509 this.select_values[e[i]+""] = e[i];
5510 }
5511 },
5512 build: function() {
5513 var self = this, i;
5514 if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5515 if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
5516
5517 if((!this.schema.format && this.option_keys.length < 8) || this.schema.format === "checkbox") {
5518 this.input_type = 'checkboxes';
5519
5520 this.inputs = {};
5521 this.controls = {};
5522 for(i=0; i<this.option_keys.length; i++) {
5523 this.inputs[this.option_keys[i]] = this.theme.getCheckbox();
5524 this.select_options[this.option_keys[i]] = this.inputs[this.option_keys[i]];
5525 var label = this.theme.getCheckboxLabel(this.option_keys[i]);
5526 this.controls[this.option_keys[i]] = this.theme.getFormControl(label, this.inputs[this.option_keys[i]]);
5527 }
5528
5529 this.control = this.theme.getMultiCheckboxHolder(this.controls,this.label,this.description);
5530 }
5531 else {
5532 this.input_type = 'select';
5533 this.input = this.theme.getSelectInput(this.option_keys);
5534 this.input.multiple = true;
5535 this.input.size = Math.min(10,this.option_keys.length);
5536
5537 for(i=0; i<this.option_keys.length; i++) {
5538 this.select_options[this.option_keys[i]] = this.input.children[i];
5539 }
5540
5541 if(this.schema.readOnly || this.schema.readonly) {
5542 this.always_disabled = true;
5543 this.input.disabled = true;
5544 }
5545
5546 this.control = this.theme.getFormControl(this.label, this.input, this.description);
5547 }
5548
5549 this.container.appendChild(this.control);
5550 this.control.addEventListener('change',function(e) {
5551 e.preventDefault();
5552 e.stopPropagation();
5553
5554 var new_value = [];
5555 for(i = 0; i<self.option_keys.length; i++) {
5556 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]]);
5557 }
5558
5559 self.updateValue(new_value);
5560 self.onChange(true);
5561 });
5562 },
5563 setValue: function(value, initial) {
5564 var i;
5565 value = value || [];
5566 if(typeof value !== "object") value = [value];
5567 else if(!(Array.isArray(value))) value = [];
5568
5569 // Make sure we are dealing with an array of strings so we can check for strict equality
5570 for(i=0; i<value.length; i++) {
5571 if(typeof value[i] !== "string") value[i] += "";
5572 }
5573
5574 // Update selected status of options
5575 for(i in this.select_options) {
5576 if(!this.select_options.hasOwnProperty(i)) continue;
5577
5578 this.select_options[i][this.input_type === "select"? "selected" : "checked"] = (value.indexOf(i) !== -1);
5579 }
5580
5581 this.updateValue(value);
5582 this.onChange();
5583 },
5584 setupSelect2: function() {
5585 if(window.jQuery && window.jQuery.fn && window.jQuery.fn.select2) {
5586 var options = window.jQuery.extend({},JSONEditor.plugins.select2);
5587 if(this.schema.options && this.schema.options.select2_options) options = $extend(options,this.schema.options.select2_options);
5588 this.select2 = window.jQuery(this.input).select2(options);
5589 var self = this;
5590 this.select2.on('select2-blur',function() {
5591 var val =self.select2.select2('val');
5592 self.value = val;
5593 self.onChange(true);
5594 });
5595 }
5596 else {
5597 this.select2 = null;
5598 }
5599 },
5600 onInputChange: function() {
5601 this.value = this.input.value;
5602 this.onChange(true);
5603 },
5604 postBuild: function() {
5605 this._super();
5606 this.setupSelect2();
5607 },
5608 register: function() {
5609 this._super();
5610 if(!this.input) return;
5611 this.input.setAttribute('name',this.formname);
5612 },
5613 unregister: function() {
5614 this._super();
5615 if(!this.input) return;
5616 this.input.removeAttribute('name');
5617 },
5618 getNumColumns: function() {
5619 var longest_text = this.getTitle().length;
5620 for(var i in this.select_values) {
5621 if(!this.select_values.hasOwnProperty(i)) continue;
5622 longest_text = Math.max(longest_text,(this.select_values[i]+"").length+4);
5623 }
5624
5625 return Math.min(12,Math.max(longest_text/7,2));
5626 },
5627 updateValue: function(value) {
5628 var changed = false;
5629 var new_value = [];
5630 for(var i=0; i<value.length; i++) {
5631 if(!this.select_options[value[i]+""]) {
5632 changed = true;
5633 continue;
5634 }
5635 var sanitized = this.sanitize(this.select_values[value[i]]);
5636 new_value.push(sanitized);
5637 if(sanitized !== value[i]) changed = true;
5638 }
5639 this.value = new_value;
5640 if(this.select2) this.select2.select2('val',this.value);
5641 return changed;
5642 },
5643 sanitize: function(value) {
5644 if(this.schema.items.type === "number") {
5645 return 1*value;
5646 }
5647 else if(this.schema.items.type === "integer") {
5648 return Math.floor(value*1);
5649 }
5650 else {
5651 return ""+value;
5652 }
5653 },
5654 enable: function() {
5655 if(!this.always_disabled) {
5656 if(this.input) {
5657 this.input.disabled = false;
5658 }
5659 else if(this.inputs) {
5660 for(var i in this.inputs) {
5661 if(!this.inputs.hasOwnProperty(i)) continue;
5662 this.inputs[i].disabled = false;
5663 }
5664 }
5665 if(this.select2) this.select2.select2("enable",true);
5666 }
5667 this._super();
5668 },
5669 disable: function() {
5670 if(this.input) {
5671 this.input.disabled = true;
5672 }
5673 else if(this.inputs) {
5674 for(var i in this.inputs) {
5675 if(!this.inputs.hasOwnProperty(i)) continue;
5676 this.inputs[i].disabled = true;
5677 }
5678 }
5679 if(this.select2) this.select2.select2("enable",false);
5680 this._super();
5681 },
5682 destroy: function() {
5683 if(this.select2) {
5684 this.select2.select2('destroy');
5685 this.select2 = null;
5686 }
5687 this._super();
5688 }
5689});
5690
5691JSONEditor.defaults.editors.base64 = JSONEditor.AbstractEditor.extend({
5692 getNumColumns: function() {
5693 return 4;
5694 },
5695 build: function() {
5696 var self = this;
5697 this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5698
5699 // Input that holds the base64 string
5700 this.input = this.theme.getFormInputField('hidden');
5701 this.container.appendChild(this.input);
5702
5703 // Don't show uploader if this is readonly
5704 if(!this.schema.readOnly && !this.schema.readonly) {
5705 if(!window.FileReader) throw "FileReader required for base64 editor";
5706
5707 // File uploader
5708 this.uploader = this.theme.getFormInputField('file');
5709
5710 this.uploader.addEventListener('change',function(e) {
5711 e.preventDefault();
5712 e.stopPropagation();
5713
5714 if(this.files && this.files.length) {
5715 var fr = new FileReader();
5716 fr.onload = function(evt) {
5717 self.value = evt.target.result;
5718 self.refreshPreview();
5719 self.onChange(true);
5720 fr = null;
5721 };
5722 fr.readAsDataURL(this.files[0]);
5723 }
5724 });
5725 }
5726
5727 this.preview = this.theme.getFormInputDescription(this.schema.description);
5728 this.container.appendChild(this.preview);
5729
5730 this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview);
5731 this.container.appendChild(this.control);
5732 },
5733 refreshPreview: function() {
5734 if(this.last_preview === this.value) return;
5735 this.last_preview = this.value;
5736
5737 this.preview.innerHTML = '';
5738
5739 if(!this.value) return;
5740
5741 var mime = this.value.match(/^data:([^;,]+)[;,]/);
5742 if(mime) mime = mime[1];
5743
5744 if(!mime) {
5745 this.preview.innerHTML = '<em>Invalid data URI</em>';
5746 }
5747 else {
5748 this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+Math.floor((this.value.length-this.value.split(',')[0].length-1)/1.33333)+' bytes';
5749 if(mime.substr(0,5)==="image") {
5750 this.preview.innerHTML += '<br>';
5751 var img = document.createElement('img');
5752 img.style.maxWidth = '100%';
5753 img.style.maxHeight = '100px';
5754 img.src = this.value;
5755 this.preview.appendChild(img);
5756 }
5757 }
5758 },
5759 enable: function() {
5760 if(this.uploader) this.uploader.disabled = false;
5761 this._super();
5762 },
5763 disable: function() {
5764 if(this.uploader) this.uploader.disabled = true;
5765 this._super();
5766 },
5767 setValue: function(val) {
5768 if(this.value !== val) {
5769 this.value = val;
5770 this.input.value = this.value;
5771 this.refreshPreview();
5772 this.onChange();
5773 }
5774 },
5775 destroy: function() {
5776 if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
5777 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
5778 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
5779 if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
5780
5781 this._super();
5782 }
5783});
5784
5785JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({
5786 getNumColumns: function() {
5787 return 4;
5788 },
5789 build: function() {
5790 var self = this;
5791 this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
5792
5793 // Input that holds the base64 string
5794 this.input = this.theme.getFormInputField('hidden');
5795 this.container.appendChild(this.input);
5796
5797 // Don't show uploader if this is readonly
5798 if(!this.schema.readOnly && !this.schema.readonly) {
5799
5800 if(!this.jsoneditor.options.upload) throw "Upload handler required for upload editor";
5801
5802 // File uploader
5803 this.uploader = this.theme.getFormInputField('file');
5804
5805 this.uploader.addEventListener('change',function(e) {
5806 e.preventDefault();
5807 e.stopPropagation();
5808
5809 if(this.files && this.files.length) {
5810 var fr = new FileReader();
5811 fr.onload = function(evt) {
5812 self.preview_value = evt.target.result;
5813 self.refreshPreview();
5814 self.onChange(true);
5815 fr = null;
5816 };
5817 fr.readAsDataURL(this.files[0]);
5818 }
5819 });
5820 }
5821
5822 var description = this.schema.description;
5823 if (!description) description = '';
5824
5825 this.preview = this.theme.getFormInputDescription(description);
5826 this.container.appendChild(this.preview);
5827
5828 this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview);
5829 this.container.appendChild(this.control);
5830 },
5831 refreshPreview: function() {
5832 if(this.last_preview === this.preview_value) return;
5833 this.last_preview = this.preview_value;
5834
5835 this.preview.innerHTML = '';
5836
5837 if(!this.preview_value) return;
5838
5839 var self = this;
5840
5841 var mime = this.preview_value.match(/^data:([^;,]+)[;,]/);
5842 if(mime) mime = mime[1];
5843 if(!mime) mime = 'unknown';
5844
5845 var file = this.uploader.files[0];
5846
5847 this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+file.size+' bytes';
5848 if(mime.substr(0,5)==="image") {
5849 this.preview.innerHTML += '<br>';
5850 var img = document.createElement('img');
5851 img.style.maxWidth = '100%';
5852 img.style.maxHeight = '100px';
5853 img.src = this.preview_value;
5854 this.preview.appendChild(img);
5855 }
5856
5857 this.preview.innerHTML += '<br>';
5858 var uploadButton = this.getButton('Upload', 'upload', 'Upload');
5859 this.preview.appendChild(uploadButton);
5860 uploadButton.addEventListener('click',function(event) {
5861 event.preventDefault();
5862
5863 uploadButton.setAttribute("disabled", "disabled");
5864 self.theme.removeInputError(self.uploader);
5865
5866 if (self.theme.getProgressBar) {
5867 self.progressBar = self.theme.getProgressBar();
5868 self.preview.appendChild(self.progressBar);
5869 }
5870
5871 self.jsoneditor.options.upload(self.path, file, {
5872 success: function(url) {
5873 self.setValue(url);
5874
5875 if(self.parent) self.parent.onChildEditorChange(self);
5876 else self.jsoneditor.onChange();
5877
5878 if (self.progressBar) self.preview.removeChild(self.progressBar);
5879 uploadButton.removeAttribute("disabled");
5880 },
5881 failure: function(error) {
5882 self.theme.addInputError(self.uploader, error);
5883 if (self.progressBar) self.preview.removeChild(self.progressBar);
5884 uploadButton.removeAttribute("disabled");
5885 },
5886 updateProgress: function(progress) {
5887 if (self.progressBar) {
5888 if (progress) self.theme.updateProgressBar(self.progressBar, progress);
5889 else self.theme.updateProgressBarUnknown(self.progressBar);
5890 }
5891 }
5892 });
5893 });
5894 },
5895 enable: function() {
5896 if(this.uploader) this.uploader.disabled = false;
5897 this._super();
5898 },
5899 disable: function() {
5900 if(this.uploader) this.uploader.disabled = true;
5901 this._super();
5902 },
5903 setValue: function(val) {
5904 if(this.value !== val) {
5905 this.value = val;
5906 this.input.value = this.value;
5907 this.onChange();
5908 }
5909 },
5910 destroy: function() {
5911 if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
5912 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
5913 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
5914 if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
5915
5916 this._super();
5917 }
5918});
5919
5920JSONEditor.defaults.editors.checkbox = JSONEditor.AbstractEditor.extend({
5921 setValue: function(value,initial) {
5922 this.value = !!value;
5923 this.input.checked = this.value;
5924 this.onChange();
5925 },
5926 register: function() {
5927 this._super();
5928 if(!this.input) return;
5929 this.input.setAttribute('name',this.formname);
5930 },
5931 unregister: function() {
5932 this._super();
5933 if(!this.input) return;
5934 this.input.removeAttribute('name');
5935 },
5936 getNumColumns: function() {
5937 return Math.min(12,Math.max(this.getTitle().length/7,2));
5938 },
5939 build: function() {
5940 var self = this;
5941 if(!this.options.compact) {
5942 this.label = this.header = this.theme.getCheckboxLabel(this.getTitle());
5943 }
5944 if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
5945 if(this.options.compact) this.container.className += ' compact';
5946
5947 this.input = this.theme.getCheckbox();
5948 this.control = this.theme.getFormControl(this.label, this.input, this.description);
5949
5950 if(this.schema.readOnly || this.schema.readonly) {
5951 this.always_disabled = true;
5952 this.input.disabled = true;
5953 }
5954
5955 this.input.addEventListener('change',function(e) {
5956 e.preventDefault();
5957 e.stopPropagation();
5958 self.value = this.checked;
5959 self.onChange(true);
5960 });
5961
5962 this.container.appendChild(this.control);
5963 },
5964 enable: function() {
5965 if(!this.always_disabled) {
5966 this.input.disabled = false;
5967 }
5968 this._super();
5969 },
5970 disable: function() {
5971 this.input.disabled = true;
5972 this._super();
5973 },
5974 destroy: function() {
5975 if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
5976 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
5977 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
5978 this._super();
5979 }
5980});
5981
5982JSONEditor.defaults.editors.arraySelectize = JSONEditor.AbstractEditor.extend({
5983 build: function() {
5984 this.title = this.theme.getFormInputLabel(this.getTitle());
5985
5986 this.title_controls = this.theme.getHeaderButtonHolder();
5987 this.title.appendChild(this.title_controls);
5988 this.error_holder = document.createElement('div');
5989
5990 if(this.schema.description) {
5991 this.description = this.theme.getDescription(this.schema.description);
5992 }
5993
5994 this.input = document.createElement('select');
5995 this.input.setAttribute('multiple', 'multiple');
5996
5997 var group = this.theme.getFormControl(this.title, this.input, this.description);
5998
5999 this.container.appendChild(group);
6000 this.container.appendChild(this.error_holder);
6001
6002 window.jQuery(this.input).selectize({
6003 delimiter: false,
6004 createOnBlur: true,
6005 create: true
6006 });
6007 },
6008 postBuild: function() {
6009 var self = this;
6010 this.input.selectize.on('change', function(event) {
6011 self.refreshValue();
6012 self.onChange(true);
6013 });
6014 },
6015 destroy: function() {
6016 this.empty(true);
6017 if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
6018 if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
6019 if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
6020
6021 this._super();
6022 },
6023 empty: function(hard) {},
6024 setValue: function(value, initial) {
6025 var self = this;
6026 // Update the array's value, adding/removing rows when necessary
6027 value = value || [];
6028 if(!(Array.isArray(value))) value = [value];
6029
6030 this.input.selectize.clearOptions();
6031 this.input.selectize.clear(true);
6032
6033 value.forEach(function(item) {
6034 self.input.selectize.addOption({text: item, value: item});
6035 });
6036 this.input.selectize.setValue(value);
6037
6038 this.refreshValue(initial);
6039 },
6040 refreshValue: function(force) {
6041 this.value = this.input.selectize.getValue();
6042 },
6043 showValidationErrors: function(errors) {
6044 var self = this;
6045
6046 // Get all the errors that pertain to this editor
6047 var my_errors = [];
6048 var other_errors = [];
6049 $each(errors, function(i,error) {
6050 if(error.path === self.path) {
6051 my_errors.push(error);
6052 }
6053 else {
6054 other_errors.push(error);
6055 }
6056 });
6057
6058 // Show errors for this editor
6059 if(this.error_holder) {
6060
6061 if(my_errors.length) {
6062 var message = [];
6063 this.error_holder.innerHTML = '';
6064 this.error_holder.style.display = '';
6065 $each(my_errors, function(i,error) {
6066 self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
6067 });
6068 }
6069 // Hide error area
6070 else {
6071 this.error_holder.style.display = 'none';
6072 }
6073 }
6074 }
6075});
6076
6077var matchKey = (function () {
6078 var elem = document.documentElement;
6079
6080 if (elem.matches) return 'matches';
6081 else if (elem.webkitMatchesSelector) return 'webkitMatchesSelector';
6082 else if (elem.mozMatchesSelector) return 'mozMatchesSelector';
6083 else if (elem.msMatchesSelector) return 'msMatchesSelector';
6084 else if (elem.oMatchesSelector) return 'oMatchesSelector';
6085})();
6086
6087JSONEditor.AbstractTheme = Class.extend({
6088 getContainer: function() {
6089 return document.createElement('div');
6090 },
6091 getFloatRightLinkHolder: function() {
6092 var el = document.createElement('div');
6093 el.style = el.style || {};
6094 el.style.cssFloat = 'right';
6095 el.style.marginLeft = '10px';
6096 return el;
6097 },
6098 getModal: function() {
6099 var el = document.createElement('div');
6100 el.style.backgroundColor = 'white';
6101 el.style.border = '1px solid black';
6102 el.style.boxShadow = '3px 3px black';
6103 el.style.position = 'absolute';
6104 el.style.zIndex = '10';
6105 el.style.display = 'none';
6106 return el;
6107 },
6108 getGridContainer: function() {
6109 var el = document.createElement('div');
6110 return el;
6111 },
6112 getGridRow: function() {
6113 var el = document.createElement('div');
6114 el.className = 'row';
6115 return el;
6116 },
6117 getGridColumn: function() {
6118 var el = document.createElement('div');
6119 return el;
6120 },
6121 setGridColumnSize: function(el,size) {
6122
6123 },
6124 getLink: function(text) {
6125 var el = document.createElement('a');
6126 el.setAttribute('href','#');
6127 el.appendChild(document.createTextNode(text));
6128 return el;
6129 },
6130 disableHeader: function(header) {
6131 header.style.color = '#ccc';
6132 },
6133 disableLabel: function(label) {
6134 label.style.color = '#ccc';
6135 },
6136 enableHeader: function(header) {
6137 header.style.color = '';
6138 },
6139 enableLabel: function(label) {
6140 label.style.color = '';
6141 },
6142 getFormInputLabel: function(text) {
6143 var el = document.createElement('label');
6144 el.appendChild(document.createTextNode(text));
6145 return el;
6146 },
6147 getCheckboxLabel: function(text) {
6148 var el = this.getFormInputLabel(text);
6149 el.style.fontWeight = 'normal';
6150 return el;
6151 },
6152 getHeader: function(text) {
6153 var el = document.createElement('h3');
6154 if(typeof text === "string") {
6155 el.textContent = text;
6156 }
6157 else {
6158 el.appendChild(text);
6159 }
6160
6161 return el;
6162 },
6163 getCheckbox: function() {
6164 var el = this.getFormInputField('checkbox');
6165 el.style.display = 'inline-block';
6166 el.style.width = 'auto';
6167 return el;
6168 },
6169 getMultiCheckboxHolder: function(controls,label,description) {
6170 var el = document.createElement('div');
6171
6172 if(label) {
6173 label.style.display = 'block';
6174 el.appendChild(label);
6175 }
6176
6177 for(var i in controls) {
6178 if(!controls.hasOwnProperty(i)) continue;
6179 controls[i].style.display = 'inline-block';
6180 controls[i].style.marginRight = '20px';
6181 el.appendChild(controls[i]);
6182 }
6183
6184 if(description) el.appendChild(description);
6185
6186 return el;
6187 },
6188 getSelectInput: function(options) {
6189 var select = document.createElement('select');
6190 if(options) this.setSelectOptions(select, options);
6191 return select;
6192 },
6193 getSwitcher: function(options) {
6194 var switcher = this.getSelectInput(options);
6195 switcher.style.backgroundColor = 'transparent';
6196 switcher.style.display = 'inline-block';
6197 switcher.style.fontStyle = 'italic';
6198 switcher.style.fontWeight = 'normal';
6199 switcher.style.height = 'auto';
6200 switcher.style.marginBottom = 0;
6201 switcher.style.marginLeft = '5px';
6202 switcher.style.padding = '0 0 0 3px';
6203 switcher.style.width = 'auto';
6204 return switcher;
6205 },
6206 getSwitcherOptions: function(switcher) {
6207 return switcher.getElementsByTagName('option');
6208 },
6209 setSwitcherOptions: function(switcher, options, titles) {
6210 this.setSelectOptions(switcher, options, titles);
6211 },
6212 setSelectOptions: function(select, options, titles) {
6213 titles = titles || [];
6214 select.innerHTML = '';
6215 for(var i=0; i<options.length; i++) {
6216 var option = document.createElement('option');
6217 option.setAttribute('value',options[i]);
6218 option.textContent = titles[i] || options[i];
6219 select.appendChild(option);
6220 }
6221 },
6222 getTextareaInput: function() {
6223 var el = document.createElement('textarea');
6224 el.style = el.style || {};
6225 el.style.width = '100%';
6226 el.style.height = '300px';
6227 el.style.boxSizing = 'border-box';
6228 return el;
6229 },
6230 getRangeInput: function(min,max,step) {
6231 var el = this.getFormInputField('range');
6232 el.setAttribute('min',min);
6233 el.setAttribute('max',max);
6234 el.setAttribute('step',step);
6235 return el;
6236 },
6237 getFormInputField: function(type) {
6238 var el = document.createElement('input');
6239 el.setAttribute('type',type);
6240 return el;
6241 },
6242 afterInputReady: function(input) {
6243
6244 },
6245 getFormControl: function(label, input, description) {
6246 var el = document.createElement('div');
6247 el.className = 'form-control';
6248 if(label) el.appendChild(label);
6249 if(input.type === 'checkbox') {
6250 label.insertBefore(input,label.firstChild);
6251 }
6252 else {
6253 el.appendChild(input);
6254 }
6255
6256 if(description) el.appendChild(description);
6257 return el;
6258 },
6259 getIndentedPanel: function() {
6260 var el = document.createElement('div');
6261 el.style = el.style || {};
6262 el.style.paddingLeft = '10px';
6263 el.style.marginLeft = '10px';
6264 el.style.borderLeft = '1px solid #ccc';
6265 return el;
6266 },
6267 getChildEditorHolder: function() {
6268 return document.createElement('div');
6269 },
6270 getDescription: function(text) {
6271 var el = document.createElement('p');
6272 el.innerHTML = text;
6273 return el;
6274 },
6275 getCheckboxDescription: function(text) {
6276 return this.getDescription(text);
6277 },
6278 getFormInputDescription: function(text) {
6279 return this.getDescription(text);
6280 },
6281 getHeaderButtonHolder: function() {
6282 return this.getButtonHolder();
6283 },
6284 getButtonHolder: function() {
6285 return document.createElement('div');
6286 },
6287 getButton: function(text, icon, title) {
6288 var el = document.createElement('button');
6289 el.type = 'button';
6290 this.setButtonText(el,text,icon,title);
6291 return el;
6292 },
6293 setButtonText: function(button, text, icon, title) {
6294 button.innerHTML = '';
6295 if(icon) {
6296 button.appendChild(icon);
6297 button.innerHTML += ' ';
6298 }
6299 button.appendChild(document.createTextNode(text));
6300 if(title) button.setAttribute('title',title);
6301 },
6302 getTable: function() {
6303 return document.createElement('table');
6304 },
6305 getTableRow: function() {
6306 return document.createElement('tr');
6307 },
6308 getTableHead: function() {
6309 return document.createElement('thead');
6310 },
6311 getTableBody: function() {
6312 return document.createElement('tbody');
6313 },
6314 getTableHeaderCell: function(text) {
6315 var el = document.createElement('th');
6316 el.textContent = text;
6317 return el;
6318 },
6319 getTableCell: function() {
6320 var el = document.createElement('td');
6321 return el;
6322 },
6323 getErrorMessage: function(text) {
6324 var el = document.createElement('p');
6325 el.style = el.style || {};
6326 el.style.color = 'red';
6327 el.appendChild(document.createTextNode(text));
6328 return el;
6329 },
6330 addInputError: function(input, text) {
6331 },
6332 removeInputError: function(input) {
6333 },
6334 addTableRowError: function(row) {
6335 },
6336 removeTableRowError: function(row) {
6337 },
6338 getTabHolder: function() {
6339 var el = document.createElement('div');
6340 el.innerHTML = "<div style='float: left; width: 130px;' class='tabs'></div><div class='content' style='margin-left: 130px;'></div><div style='clear:both;'></div>";
6341 return el;
6342 },
6343 applyStyles: function(el,styles) {
6344 el.style = el.style || {};
6345 for(var i in styles) {
6346 if(!styles.hasOwnProperty(i)) continue;
6347 el.style[i] = styles[i];
6348 }
6349 },
6350 closest: function(elem, selector) {
6351 while (elem && elem !== document) {
6352 if (matchKey) {
6353 if (elem[matchKey](selector)) {
6354 return elem;
6355 } else {
6356 elem = elem.parentNode;
6357 }
6358 }
6359 else {
6360 return false;
6361 }
6362 }
6363 return false;
6364 },
6365 getTab: function(span) {
6366 var el = document.createElement('div');
6367 el.appendChild(span);
6368 el.style = el.style || {};
6369 this.applyStyles(el,{
6370 border: '1px solid #ccc',
6371 borderWidth: '1px 0 1px 1px',
6372 textAlign: 'center',
6373 lineHeight: '30px',
6374 borderRadius: '5px',
6375 borderBottomRightRadius: 0,
6376 borderTopRightRadius: 0,
6377 fontWeight: 'bold',
6378 cursor: 'pointer'
6379 });
6380 return el;
6381 },
6382 getTabContentHolder: function(tab_holder) {
6383 return tab_holder.children[1];
6384 },
6385 getTabContent: function() {
6386 return this.getIndentedPanel();
6387 },
6388 markTabActive: function(tab) {
6389 this.applyStyles(tab,{
6390 opacity: 1,
6391 background: 'white'
6392 });
6393 },
6394 markTabInactive: function(tab) {
6395 this.applyStyles(tab,{
6396 opacity:0.5,
6397 background: ''
6398 });
6399 },
6400 addTab: function(holder, tab) {
6401 holder.children[0].appendChild(tab);
6402 },
6403 getBlockLink: function() {
6404 var link = document.createElement('a');
6405 link.style.display = 'block';
6406 return link;
6407 },
6408 getBlockLinkHolder: function() {
6409 var el = document.createElement('div');
6410 return el;
6411 },
6412 getLinksHolder: function() {
6413 var el = document.createElement('div');
6414 return el;
6415 },
6416 createMediaLink: function(holder,link,media) {
6417 holder.appendChild(link);
6418 media.style.width='100%';
6419 holder.appendChild(media);
6420 },
6421 createImageLink: function(holder,link,image) {
6422 holder.appendChild(link);
6423 link.appendChild(image);
6424 }
6425});
6426
6427JSONEditor.defaults.themes.bootstrap2 = JSONEditor.AbstractTheme.extend({
6428 getRangeInput: function(min, max, step) {
6429 // TODO: use bootstrap slider
6430 return this._super(min, max, step);
6431 },
6432 getGridContainer: function() {
6433 var el = document.createElement('div');
6434 el.className = 'container-fluid';
6435 return el;
6436 },
6437 getGridRow: function() {
6438 var el = document.createElement('div');
6439 el.className = 'row-fluid';
6440 return el;
6441 },
6442 getFormInputLabel: function(text) {
6443 var el = this._super(text);
6444 el.style.display = 'inline-block';
6445 el.style.fontWeight = 'bold';
6446 return el;
6447 },
6448 setGridColumnSize: function(el,size) {
6449 el.className = 'span'+size;
6450 },
6451 getSelectInput: function(options) {
6452 var input = this._super(options);
6453 input.style.width = 'auto';
6454 input.style.maxWidth = '98%';
6455 return input;
6456 },
6457 getFormInputField: function(type) {
6458 var el = this._super(type);
6459 el.style.width = '98%';
6460 return el;
6461 },
6462 afterInputReady: function(input) {
6463 if(input.controlgroup) return;
6464 input.controlgroup = this.closest(input,'.control-group');
6465 input.controls = this.closest(input,'.controls');
6466 if(this.closest(input,'.compact')) {
6467 input.controlgroup.className = input.controlgroup.className.replace(/control-group/g,'').replace(/[ ]{2,}/g,' ');
6468 input.controls.className = input.controlgroup.className.replace(/controls/g,'').replace(/[ ]{2,}/g,' ');
6469 input.style.marginBottom = 0;
6470 }
6471
6472 // TODO: use bootstrap slider
6473 },
6474 getIndentedPanel: function() {
6475 var el = document.createElement('div');
6476 el.className = 'well well-small';
6477 el.style.paddingBottom = 0;
6478 return el;
6479 },
6480 getFormInputDescription: function(text) {
6481 var el = document.createElement('p');
6482 el.className = 'help-inline';
6483 el.textContent = text;
6484 return el;
6485 },
6486 getFormControl: function(label, input, description) {
6487 var ret = document.createElement('div');
6488 ret.className = 'control-group';
6489
6490 var controls = document.createElement('div');
6491 controls.className = 'controls';
6492
6493 if(label && input.getAttribute('type') === 'checkbox') {
6494 ret.appendChild(controls);
6495 label.className += ' checkbox';
6496 label.appendChild(input);
6497 controls.appendChild(label);
6498 controls.style.height = '30px';
6499 }
6500 else {
6501 if(label) {
6502 label.className += ' control-label';
6503 ret.appendChild(label);
6504 }
6505 controls.appendChild(input);
6506 ret.appendChild(controls);
6507 }
6508
6509 if(description) controls.appendChild(description);
6510
6511 return ret;
6512 },
6513 getHeaderButtonHolder: function() {
6514 var el = this.getButtonHolder();
6515 el.style.marginLeft = '10px';
6516 return el;
6517 },
6518 getButtonHolder: function() {
6519 var el = document.createElement('div');
6520 el.className = 'btn-group';
6521 return el;
6522 },
6523 getButton: function(text, icon, title) {
6524 var el = this._super(text, icon, title);
6525 el.className += ' btn btn-default';
6526 return el;
6527 },
6528 getTable: function() {
6529 var el = document.createElement('table');
6530 el.className = 'table table-bordered';
6531 el.style.width = 'auto';
6532 el.style.maxWidth = 'none';
6533 return el;
6534 },
6535 addInputError: function(input,text) {
6536 if(!input.controlgroup || !input.controls) return;
6537 input.controlgroup.className += ' error';
6538 if(!input.errmsg) {
6539 input.errmsg = document.createElement('p');
6540 input.errmsg.className = 'help-block errormsg';
6541 input.controls.appendChild(input.errmsg);
6542 }
6543 else {
6544 input.errmsg.style.display = '';
6545 }
6546
6547 input.errmsg.textContent = text;
6548 },
6549 removeInputError: function(input) {
6550 if(!input.errmsg) return;
6551 input.errmsg.style.display = 'none';
6552 input.controlgroup.className = input.controlgroup.className.replace(/\s?error/g,'');
6553 },
6554 getTabHolder: function() {
6555 var el = document.createElement('div');
6556 el.className = 'tabbable tabs-left';
6557 el.innerHTML = "<ul class='nav nav-tabs span2' style='margin-right: 0;'></ul><div class='tab-content span10' style='overflow:visible;'></div>";
6558 return el;
6559 },
6560 getTab: function(text) {
6561 var el = document.createElement('li');
6562 var a = document.createElement('a');
6563 a.setAttribute('href','#');
6564 a.appendChild(text);
6565 el.appendChild(a);
6566 return el;
6567 },
6568 getTabContentHolder: function(tab_holder) {
6569 return tab_holder.children[1];
6570 },
6571 getTabContent: function() {
6572 var el = document.createElement('div');
6573 el.className = 'tab-pane active';
6574 return el;
6575 },
6576 markTabActive: function(tab) {
6577 tab.className += ' active';
6578 },
6579 markTabInactive: function(tab) {
6580 tab.className = tab.className.replace(/\s?active/g,'');
6581 },
6582 addTab: function(holder, tab) {
6583 holder.children[0].appendChild(tab);
6584 },
6585 getProgressBar: function() {
6586 var container = document.createElement('div');
6587 container.className = 'progress';
6588
6589 var bar = document.createElement('div');
6590 bar.className = 'bar';
6591 bar.style.width = '0%';
6592 container.appendChild(bar);
6593
6594 return container;
6595 },
6596 updateProgressBar: function(progressBar, progress) {
6597 if (!progressBar) return;
6598
6599 progressBar.firstChild.style.width = progress + "%";
6600 },
6601 updateProgressBarUnknown: function(progressBar) {
6602 if (!progressBar) return;
6603
6604 progressBar.className = 'progress progress-striped active';
6605 progressBar.firstChild.style.width = '100%';
6606 }
6607});
6608
6609JSONEditor.defaults.themes.bootstrap3 = JSONEditor.AbstractTheme.extend({
6610 getSelectInput: function(options) {
6611 var el = this._super(options);
6612 el.className += 'form-control';
6613 //el.style.width = 'auto';
6614 return el;
6615 },
6616 setGridColumnSize: function(el,size) {
6617 el.className = 'col-md-'+size;
6618 },
6619 afterInputReady: function(input) {
6620 if(input.controlgroup) return;
6621 input.controlgroup = this.closest(input,'.form-group');
6622 if(this.closest(input,'.compact')) {
6623 input.controlgroup.style.marginBottom = 0;
6624 }
6625
6626 // TODO: use bootstrap slider
6627 },
6628 getTextareaInput: function() {
6629 var el = document.createElement('textarea');
6630 el.className = 'form-control';
6631 return el;
6632 },
6633 getRangeInput: function(min, max, step) {
6634 // TODO: use better slider
6635 return this._super(min, max, step);
6636 },
6637 getFormInputField: function(type) {
6638 var el = this._super(type);
6639 if(type !== 'checkbox') {
6640 el.className += 'form-control';
6641 }
6642 return el;
6643 },
6644 getFormControl: function(label, input, description) {
6645 var group = document.createElement('div');
6646
6647 if(label && input.type === 'checkbox') {
6648 group.className += ' checkbox';
6649 label.appendChild(input);
6650 label.style.fontSize = '14px';
6651 group.style.marginTop = '0';
6652 group.appendChild(label);
6653 input.style.position = 'relative';
6654 input.style.cssFloat = 'left';
6655 }
6656 else {
6657 group.className += ' form-group';
6658 if(label) {
6659 label.className += ' control-label';
6660 group.appendChild(label);
6661 }
6662 group.appendChild(input);
6663 }
6664
6665 if(description) group.appendChild(description);
6666
6667 return group;
6668 },
6669 getIndentedPanel: function() {
6670 var el = document.createElement('div');
6671 el.className = 'well well-sm';
6672 el.style.paddingBottom = 0;
6673 return el;
6674 },
6675 getFormInputDescription: function(text) {
6676 var el = document.createElement('p');
6677 el.className = 'help-block';
6678 el.innerHTML = text;
6679 return el;
6680 },
6681 getHeaderButtonHolder: function() {
6682 var el = this.getButtonHolder();
6683 el.style.marginLeft = '10px';
6684 return el;
6685 },
6686 getButtonHolder: function() {
6687 var el = document.createElement('div');
6688 el.className = 'btn-group';
6689 return el;
6690 },
6691 getButton: function(text, icon, title) {
6692 var el = this._super(text, icon, title);
6693 el.className += 'btn btn-default';
6694 return el;
6695 },
6696 getTable: function() {
6697 var el = document.createElement('table');
6698 el.className = 'table table-bordered';
6699 el.style.width = 'auto';
6700 el.style.maxWidth = 'none';
6701 return el;
6702 },
6703
6704 addInputError: function(input,text) {
6705 if(!input.controlgroup) return;
6706 input.controlgroup.className += ' has-error';
6707 if(!input.errmsg) {
6708 input.errmsg = document.createElement('p');
6709 input.errmsg.className = 'help-block errormsg';
6710 input.controlgroup.appendChild(input.errmsg);
6711 }
6712 else {
6713 input.errmsg.style.display = '';
6714 }
6715
6716 input.errmsg.textContent = text;
6717 },
6718 removeInputError: function(input) {
6719 if(!input.errmsg) return;
6720 input.errmsg.style.display = 'none';
6721 input.controlgroup.className = input.controlgroup.className.replace(/\s?has-error/g,'');
6722 },
6723 getTabHolder: function() {
6724 var el = document.createElement('div');
6725 el.innerHTML = "<div class='tabs list-group col-md-2'></div><div class='col-md-10'></div>";
6726 el.className = 'rows';
6727 return el;
6728 },
6729 getTab: function(text) {
6730 var el = document.createElement('a');
6731 el.className = 'list-group-item';
6732 el.setAttribute('href','#');
6733 el.appendChild(text);
6734 return el;
6735 },
6736 markTabActive: function(tab) {
6737 tab.className += ' active';
6738 },
6739 markTabInactive: function(tab) {
6740 tab.className = tab.className.replace(/\s?active/g,'');
6741 },
6742 getProgressBar: function() {
6743 var min = 0, max = 100, start = 0;
6744
6745 var container = document.createElement('div');
6746 container.className = 'progress';
6747
6748 var bar = document.createElement('div');
6749 bar.className = 'progress-bar';
6750 bar.setAttribute('role', 'progressbar');
6751 bar.setAttribute('aria-valuenow', start);
6752 bar.setAttribute('aria-valuemin', min);
6753 bar.setAttribute('aria-valuenax', max);
6754 bar.innerHTML = start + "%";
6755 container.appendChild(bar);
6756
6757 return container;
6758 },
6759 updateProgressBar: function(progressBar, progress) {
6760 if (!progressBar) return;
6761
6762 var bar = progressBar.firstChild;
6763 var percentage = progress + "%";
6764 bar.setAttribute('aria-valuenow', progress);
6765 bar.style.width = percentage;
6766 bar.innerHTML = percentage;
6767 },
6768 updateProgressBarUnknown: function(progressBar) {
6769 if (!progressBar) return;
6770
6771 var bar = progressBar.firstChild;
6772 progressBar.className = 'progress progress-striped active';
6773 bar.removeAttribute('aria-valuenow');
6774 bar.style.width = '100%';
6775 bar.innerHTML = '';
6776 }
6777});
6778
6779// Base Foundation theme
6780JSONEditor.defaults.themes.foundation = JSONEditor.AbstractTheme.extend({
6781 getChildEditorHolder: function() {
6782 var el = document.createElement('div');
6783 el.style.marginBottom = '15px';
6784 return el;
6785 },
6786 getSelectInput: function(options) {
6787 var el = this._super(options);
6788 el.style.minWidth = 'none';
6789 el.style.padding = '5px';
6790 el.style.marginTop = '3px';
6791 return el;
6792 },
6793 getSwitcher: function(options) {
6794 var el = this._super(options);
6795 el.style.paddingRight = '8px';
6796 return el;
6797 },
6798 afterInputReady: function(input) {
6799 if(this.closest(input,'.compact')) {
6800 input.style.marginBottom = 0;
6801 }
6802 input.group = this.closest(input,'.form-control');
6803 },
6804 getFormInputLabel: function(text) {
6805 var el = this._super(text);
6806 el.style.display = 'inline-block';
6807 return el;
6808 },
6809 getFormInputField: function(type) {
6810 var el = this._super(type);
6811 el.style.width = '100%';
6812 el.style.marginBottom = type==='checkbox'? '0' : '12px';
6813 return el;
6814 },
6815 getFormInputDescription: function(text) {
6816 var el = document.createElement('p');
6817 el.textContent = text;
6818 el.style.marginTop = '-10px';
6819 el.style.fontStyle = 'italic';
6820 return el;
6821 },
6822 getIndentedPanel: function() {
6823 var el = document.createElement('div');
6824 el.className = 'panel';
6825 el.style.paddingBottom = 0;
6826 return el;
6827 },
6828 getHeaderButtonHolder: function() {
6829 var el = this.getButtonHolder();
6830 el.style.display = 'inline-block';
6831 el.style.marginLeft = '10px';
6832 el.style.verticalAlign = 'middle';
6833 return el;
6834 },
6835 getButtonHolder: function() {
6836 var el = document.createElement('div');
6837 el.className = 'button-group';
6838 return el;
6839 },
6840 getButton: function(text, icon, title) {
6841 var el = this._super(text, icon, title);
6842 el.className += ' small button';
6843 return el;
6844 },
6845 addInputError: function(input,text) {
6846 if(!input.group) return;
6847 input.group.className += ' error';
6848
6849 if(!input.errmsg) {
6850 input.insertAdjacentHTML('afterend','<small class="error"></small>');
6851 input.errmsg = input.parentNode.getElementsByClassName('error')[0];
6852 }
6853 else {
6854 input.errmsg.style.display = '';
6855 }
6856
6857 input.errmsg.textContent = text;
6858 },
6859 removeInputError: function(input) {
6860 if(!input.errmsg) return;
6861 input.group.className = input.group.className.replace(/ error/g,'');
6862 input.errmsg.style.display = 'none';
6863 },
6864 getProgressBar: function() {
6865 var progressBar = document.createElement('div');
6866 progressBar.className = 'progress';
6867
6868 var meter = document.createElement('span');
6869 meter.className = 'meter';
6870 meter.style.width = '0%';
6871 progressBar.appendChild(meter);
6872 return progressBar;
6873 },
6874 updateProgressBar: function(progressBar, progress) {
6875 if (!progressBar) return;
6876 progressBar.firstChild.style.width = progress + '%';
6877 },
6878 updateProgressBarUnknown: function(progressBar) {
6879 if (!progressBar) return;
6880 progressBar.firstChild.style.width = '100%';
6881 }
6882});
6883
6884// Foundation 3 Specific Theme
6885JSONEditor.defaults.themes.foundation3 = JSONEditor.defaults.themes.foundation.extend({
6886 getHeaderButtonHolder: function() {
6887 var el = this._super();
6888 el.style.fontSize = '.6em';
6889 return el;
6890 },
6891 getFormInputLabel: function(text) {
6892 var el = this._super(text);
6893 el.style.fontWeight = 'bold';
6894 return el;
6895 },
6896 getTabHolder: function() {
6897 var el = document.createElement('div');
6898 el.className = 'row';
6899 el.innerHTML = "<dl class='tabs vertical two columns'></dl><div class='tabs-content ten columns'></div>";
6900 return el;
6901 },
6902 setGridColumnSize: function(el,size) {
6903 var sizes = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve'];
6904 el.className = 'columns '+sizes[size];
6905 },
6906 getTab: function(text) {
6907 var el = document.createElement('dd');
6908 var a = document.createElement('a');
6909 a.setAttribute('href','#');
6910 a.appendChild(text);
6911 el.appendChild(a);
6912 return el;
6913 },
6914 getTabContentHolder: function(tab_holder) {
6915 return tab_holder.children[1];
6916 },
6917 getTabContent: function() {
6918 var el = document.createElement('div');
6919 el.className = 'content active';
6920 el.style.paddingLeft = '5px';
6921 return el;
6922 },
6923 markTabActive: function(tab) {
6924 tab.className += ' active';
6925 },
6926 markTabInactive: function(tab) {
6927 tab.className = tab.className.replace(/\s*active/g,'');
6928 },
6929 addTab: function(holder, tab) {
6930 holder.children[0].appendChild(tab);
6931 }
6932});
6933
6934// Foundation 4 Specific Theme
6935JSONEditor.defaults.themes.foundation4 = JSONEditor.defaults.themes.foundation.extend({
6936 getHeaderButtonHolder: function() {
6937 var el = this._super();
6938 el.style.fontSize = '.6em';
6939 return el;
6940 },
6941 setGridColumnSize: function(el,size) {
6942 el.className = 'columns large-'+size;
6943 },
6944 getFormInputDescription: function(text) {
6945 var el = this._super(text);
6946 el.style.fontSize = '.8rem';
6947 return el;
6948 },
6949 getFormInputLabel: function(text) {
6950 var el = this._super(text);
6951 el.style.fontWeight = 'bold';
6952 return el;
6953 }
6954});
6955
6956// Foundation 5 Specific Theme
6957JSONEditor.defaults.themes.foundation5 = JSONEditor.defaults.themes.foundation.extend({
6958 getFormInputDescription: function(text) {
6959 var el = this._super(text);
6960 el.style.fontSize = '.8rem';
6961 return el;
6962 },
6963 setGridColumnSize: function(el,size) {
6964 el.className = 'columns medium-'+size;
6965 },
6966 getButton: function(text, icon, title) {
6967 var el = this._super(text,icon,title);
6968 el.className = el.className.replace(/\s*small/g,'') + ' tiny';
6969 return el;
6970 },
6971 getTabHolder: function() {
6972 var el = document.createElement('div');
6973 el.innerHTML = "<dl class='tabs vertical'></dl><div class='tabs-content vertical'></div>";
6974 return el;
6975 },
6976 getTab: function(text) {
6977 var el = document.createElement('dd');
6978 var a = document.createElement('a');
6979 a.setAttribute('href','#');
6980 a.appendChild(text);
6981 el.appendChild(a);
6982 return el;
6983 },
6984 getTabContentHolder: function(tab_holder) {
6985 return tab_holder.children[1];
6986 },
6987 getTabContent: function() {
6988 var el = document.createElement('div');
6989 el.className = 'content active';
6990 el.style.paddingLeft = '5px';
6991 return el;
6992 },
6993 markTabActive: function(tab) {
6994 tab.className += ' active';
6995 },
6996 markTabInactive: function(tab) {
6997 tab.className = tab.className.replace(/\s*active/g,'');
6998 },
6999 addTab: function(holder, tab) {
7000 holder.children[0].appendChild(tab);
7001 }
7002});
7003
7004JSONEditor.defaults.themes.foundation6 = JSONEditor.defaults.themes.foundation5.extend({
7005 getIndentedPanel: function() {
7006 var el = document.createElement('div');
7007 el.className = 'callout secondary';
7008 return el;
7009 },
7010 getButtonHolder: function() {
7011 var el = document.createElement('div');
7012 el.className = 'button-group tiny';
7013 el.style.marginBottom = 0;
7014 return el;
7015 },
7016 getFormInputLabel: function(text) {
7017 var el = this._super(text);
7018 el.style.display = 'block';
7019 return el;
7020 },
7021 getFormControl: function(label, input, description) {
7022 var el = document.createElement('div');
7023 el.className = 'form-control';
7024 if(label) el.appendChild(label);
7025 if(input.type === 'checkbox') {
7026 label.insertBefore(input,label.firstChild);
7027 }
7028 else if (label) {
7029 label.appendChild(input);
7030 } else {
7031 el.appendChild(input);
7032 }
7033
7034 if(description) label.appendChild(description);
7035 return el;
7036 },
7037 addInputError: function(input,text) {
7038 if(!input.group) return;
7039 input.group.className += ' error';
7040
7041 if(!input.errmsg) {
7042 var errorEl = document.createElement('span');
7043 errorEl.className = 'form-error is-visible';
7044 input.group.getElementsByTagName('label')[0].appendChild(errorEl);
7045
7046 input.className = input.className + ' is-invalid-input';
7047
7048 input.errmsg = errorEl;
7049 }
7050 else {
7051 input.errmsg.style.display = '';
7052 input.className = '';
7053 }
7054
7055 input.errmsg.textContent = text;
7056 },
7057 removeInputError: function(input) {
7058 if(!input.errmsg) return;
7059 input.className = input.className.replace(/ is-invalid-input/g,'');
7060 if(input.errmsg.parentNode) {
7061 input.errmsg.parentNode.removeChild(input.errmsg);
7062 }
7063 },
7064});
7065
7066JSONEditor.defaults.themes.html = JSONEditor.AbstractTheme.extend({
7067 getFormInputLabel: function(text) {
7068 var el = this._super(text);
7069 el.style.display = 'block';
7070 el.style.marginBottom = '3px';
7071 el.style.fontWeight = 'bold';
7072 return el;
7073 },
7074 getFormInputDescription: function(text) {
7075 var el = this._super(text);
7076 el.style.fontSize = '.8em';
7077 el.style.margin = 0;
7078 el.style.display = 'inline-block';
7079 el.style.fontStyle = 'italic';
7080 return el;
7081 },
7082 getIndentedPanel: function() {
7083 var el = this._super();
7084 el.style.border = '1px solid #ddd';
7085 el.style.padding = '5px';
7086 el.style.margin = '5px';
7087 el.style.borderRadius = '3px';
7088 return el;
7089 },
7090 getChildEditorHolder: function() {
7091 var el = this._super();
7092 el.style.marginBottom = '8px';
7093 return el;
7094 },
7095 getHeaderButtonHolder: function() {
7096 var el = this.getButtonHolder();
7097 el.style.display = 'inline-block';
7098 el.style.marginLeft = '10px';
7099 el.style.fontSize = '.8em';
7100 el.style.verticalAlign = 'middle';
7101 return el;
7102 },
7103 getTable: function() {
7104 var el = this._super();
7105 el.style.borderBottom = '1px solid #ccc';
7106 el.style.marginBottom = '5px';
7107 return el;
7108 },
7109 addInputError: function(input, text) {
7110 input.style.borderColor = 'red';
7111
7112 if(!input.errmsg) {
7113 var group = this.closest(input,'.form-control');
7114 input.errmsg = document.createElement('div');
7115 input.errmsg.setAttribute('class','errmsg');
7116 input.errmsg.style = input.errmsg.style || {};
7117 input.errmsg.style.color = 'red';
7118 group.appendChild(input.errmsg);
7119 }
7120 else {
7121 input.errmsg.style.display = 'block';
7122 }
7123
7124 input.errmsg.innerHTML = '';
7125 input.errmsg.appendChild(document.createTextNode(text));
7126 },
7127 removeInputError: function(input) {
7128 input.style.borderColor = '';
7129 if(input.errmsg) input.errmsg.style.display = 'none';
7130 },
7131 getProgressBar: function() {
7132 var max = 100, start = 0;
7133
7134 var progressBar = document.createElement('progress');
7135 progressBar.setAttribute('max', max);
7136 progressBar.setAttribute('value', start);
7137 return progressBar;
7138 },
7139 updateProgressBar: function(progressBar, progress) {
7140 if (!progressBar) return;
7141 progressBar.setAttribute('value', progress);
7142 },
7143 updateProgressBarUnknown: function(progressBar) {
7144 if (!progressBar) return;
7145 progressBar.removeAttribute('value');
7146 }
7147});
7148
7149JSONEditor.defaults.themes.jqueryui = JSONEditor.AbstractTheme.extend({
7150 getTable: function() {
7151 var el = this._super();
7152 el.setAttribute('cellpadding',5);
7153 el.setAttribute('cellspacing',0);
7154 return el;
7155 },
7156 getTableHeaderCell: function(text) {
7157 var el = this._super(text);
7158 el.className = 'ui-state-active';
7159 el.style.fontWeight = 'bold';
7160 return el;
7161 },
7162 getTableCell: function() {
7163 var el = this._super();
7164 el.className = 'ui-widget-content';
7165 return el;
7166 },
7167 getHeaderButtonHolder: function() {
7168 var el = this.getButtonHolder();
7169 el.style.marginLeft = '10px';
7170 el.style.fontSize = '.6em';
7171 el.style.display = 'inline-block';
7172 return el;
7173 },
7174 getFormInputDescription: function(text) {
7175 var el = this.getDescription(text);
7176 el.style.marginLeft = '10px';
7177 el.style.display = 'inline-block';
7178 return el;
7179 },
7180 getFormControl: function(label, input, description) {
7181 var el = this._super(label,input,description);
7182 if(input.type === 'checkbox') {
7183 el.style.lineHeight = '25px';
7184
7185 el.style.padding = '3px 0';
7186 }
7187 else {
7188 el.style.padding = '4px 0 8px 0';
7189 }
7190 return el;
7191 },
7192 getDescription: function(text) {
7193 var el = document.createElement('span');
7194 el.style.fontSize = '.8em';
7195 el.style.fontStyle = 'italic';
7196 el.textContent = text;
7197 return el;
7198 },
7199 getButtonHolder: function() {
7200 var el = document.createElement('div');
7201 el.className = 'ui-buttonset';
7202 el.style.fontSize = '.7em';
7203 return el;
7204 },
7205 getFormInputLabel: function(text) {
7206 var el = document.createElement('label');
7207 el.style.fontWeight = 'bold';
7208 el.style.display = 'block';
7209 el.textContent = text;
7210 return el;
7211 },
7212 getButton: function(text, icon, title) {
7213 var button = document.createElement("button");
7214 button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
7215
7216 // Icon only
7217 if(icon && !text) {
7218 button.className += ' ui-button-icon-only';
7219 icon.className += ' ui-button-icon-primary ui-icon-primary';
7220 button.appendChild(icon);
7221 }
7222 // Icon and Text
7223 else if(icon) {
7224 button.className += ' ui-button-text-icon-primary';
7225 icon.className += ' ui-button-icon-primary ui-icon-primary';
7226 button.appendChild(icon);
7227 }
7228 // Text only
7229 else {
7230 button.className += ' ui-button-text-only';
7231 }
7232
7233 var el = document.createElement('span');
7234 el.className = 'ui-button-text';
7235 el.textContent = text||title||".";
7236 button.appendChild(el);
7237
7238 button.setAttribute('title',title);
7239
7240 return button;
7241 },
7242 setButtonText: function(button,text, icon, title) {
7243 button.innerHTML = '';
7244 button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
7245
7246 // Icon only
7247 if(icon && !text) {
7248 button.className += ' ui-button-icon-only';
7249 icon.className += ' ui-button-icon-primary ui-icon-primary';
7250 button.appendChild(icon);
7251 }
7252 // Icon and Text
7253 else if(icon) {
7254 button.className += ' ui-button-text-icon-primary';
7255 icon.className += ' ui-button-icon-primary ui-icon-primary';
7256 button.appendChild(icon);
7257 }
7258 // Text only
7259 else {
7260 button.className += ' ui-button-text-only';
7261 }
7262
7263 var el = document.createElement('span');
7264 el.className = 'ui-button-text';
7265 el.textContent = text||title||".";
7266 button.appendChild(el);
7267
7268 button.setAttribute('title',title);
7269 },
7270 getIndentedPanel: function() {
7271 var el = document.createElement('div');
7272 el.className = 'ui-widget-content ui-corner-all';
7273 el.style.padding = '1em 1.4em';
7274 el.style.marginBottom = '20px';
7275 return el;
7276 },
7277 afterInputReady: function(input) {
7278 if(input.controls) return;
7279 input.controls = this.closest(input,'.form-control');
7280 },
7281 addInputError: function(input,text) {
7282 if(!input.controls) return;
7283 if(!input.errmsg) {
7284 input.errmsg = document.createElement('div');
7285 input.errmsg.className = 'ui-state-error';
7286 input.controls.appendChild(input.errmsg);
7287 }
7288 else {
7289 input.errmsg.style.display = '';
7290 }
7291
7292 input.errmsg.textContent = text;
7293 },
7294 removeInputError: function(input) {
7295 if(!input.errmsg) return;
7296 input.errmsg.style.display = 'none';
7297 },
7298 markTabActive: function(tab) {
7299 tab.className = tab.className.replace(/\s*ui-widget-header/g,'')+' ui-state-active';
7300 },
7301 markTabInactive: function(tab) {
7302 tab.className = tab.className.replace(/\s*ui-state-active/g,'')+' ui-widget-header';
7303 }
7304});
7305
7306JSONEditor.defaults.themes.barebones = JSONEditor.AbstractTheme.extend({
7307 getFormInputLabel: function (text) {
7308 var el = this._super(text);
7309 return el;
7310 },
7311 getFormInputDescription: function (text) {
7312 var el = this._super(text);
7313 return el;
7314 },
7315 getIndentedPanel: function () {
7316 var el = this._super();
7317 return el;
7318 },
7319 getChildEditorHolder: function () {
7320 var el = this._super();
7321 return el;
7322 },
7323 getHeaderButtonHolder: function () {
7324 var el = this.getButtonHolder();
7325 return el;
7326 },
7327 getTable: function () {
7328 var el = this._super();
7329 return el;
7330 },
7331 addInputError: function (input, text) {
7332 if (!input.errmsg) {
7333 var group = this.closest(input, '.form-control');
7334 input.errmsg = document.createElement('div');
7335 input.errmsg.setAttribute('class', 'errmsg');
7336 group.appendChild(input.errmsg);
7337 }
7338 else {
7339 input.errmsg.style.display = 'block';
7340 }
7341
7342 input.errmsg.innerHTML = '';
7343 input.errmsg.appendChild(document.createTextNode(text));
7344 },
7345 removeInputError: function (input) {
7346 input.style.borderColor = '';
7347 if (input.errmsg) input.errmsg.style.display = 'none';
7348 },
7349 getProgressBar: function () {
7350 var max = 100, start = 0;
7351
7352 var progressBar = document.createElement('progress');
7353 progressBar.setAttribute('max', max);
7354 progressBar.setAttribute('value', start);
7355 return progressBar;
7356 },
7357 updateProgressBar: function (progressBar, progress) {
7358 if (!progressBar) return;
7359 progressBar.setAttribute('value', progress);
7360 },
7361 updateProgressBarUnknown: function (progressBar) {
7362 if (!progressBar) return;
7363 progressBar.removeAttribute('value');
7364 }
7365});
7366
7367JSONEditor.AbstractIconLib = Class.extend({
7368 mapping: {
7369 collapse: '',
7370 expand: '',
7371 "delete": '',
7372 edit: '',
7373 add: '',
7374 cancel: '',
7375 save: '',
7376 moveup: '',
7377 movedown: ''
7378 },
7379 icon_prefix: '',
7380 getIconClass: function(key) {
7381 if(this.mapping[key]) return this.icon_prefix+this.mapping[key];
7382 else return null;
7383 },
7384 getIcon: function(key) {
7385 var iconclass = this.getIconClass(key);
7386
7387 if(!iconclass) return null;
7388
7389 var i = document.createElement('i');
7390 i.className = iconclass;
7391 return i;
7392 }
7393});
7394
7395JSONEditor.defaults.iconlibs.bootstrap2 = JSONEditor.AbstractIconLib.extend({
7396 mapping: {
7397 collapse: 'chevron-down',
7398 expand: 'chevron-up',
7399 "delete": 'trash',
7400 edit: 'pencil',
7401 add: 'plus',
7402 cancel: 'ban-circle',
7403 save: 'ok',
7404 moveup: 'arrow-up',
7405 movedown: 'arrow-down'
7406 },
7407 icon_prefix: 'icon-'
7408});
7409
7410JSONEditor.defaults.iconlibs.bootstrap3 = JSONEditor.AbstractIconLib.extend({
7411 mapping: {
7412 collapse: 'chevron-down',
7413 expand: 'chevron-right',
7414 "delete": 'remove',
7415 edit: 'pencil',
7416 add: 'plus',
7417 cancel: 'floppy-remove',
7418 save: 'floppy-saved',
7419 moveup: 'arrow-up',
7420 movedown: 'arrow-down'
7421 },
7422 icon_prefix: 'glyphicon glyphicon-'
7423});
7424
7425JSONEditor.defaults.iconlibs.fontawesome3 = JSONEditor.AbstractIconLib.extend({
7426 mapping: {
7427 collapse: 'chevron-down',
7428 expand: 'chevron-right',
7429 "delete": 'remove',
7430 edit: 'pencil',
7431 add: 'plus',
7432 cancel: 'ban-circle',
7433 save: 'save',
7434 moveup: 'arrow-up',
7435 movedown: 'arrow-down'
7436 },
7437 icon_prefix: 'icon-'
7438});
7439
7440JSONEditor.defaults.iconlibs.fontawesome4 = JSONEditor.AbstractIconLib.extend({
7441 mapping: {
7442 collapse: 'caret-square-o-down',
7443 expand: 'caret-square-o-right',
7444 "delete": 'times',
7445 edit: 'pencil',
7446 add: 'plus',
7447 cancel: 'ban',
7448 save: 'save',
7449 moveup: 'arrow-up',
7450 movedown: 'arrow-down'
7451 },
7452 icon_prefix: 'fa fa-'
7453});
7454
7455JSONEditor.defaults.iconlibs.foundation2 = JSONEditor.AbstractIconLib.extend({
7456 mapping: {
7457 collapse: 'minus',
7458 expand: 'plus',
7459 "delete": 'remove',
7460 edit: 'edit',
7461 add: 'add-doc',
7462 cancel: 'error',
7463 save: 'checkmark',
7464 moveup: 'up-arrow',
7465 movedown: 'down-arrow'
7466 },
7467 icon_prefix: 'foundicon-'
7468});
7469
7470JSONEditor.defaults.iconlibs.foundation3 = JSONEditor.AbstractIconLib.extend({
7471 mapping: {
7472 collapse: 'minus',
7473 expand: 'plus',
7474 "delete": 'x',
7475 edit: 'pencil',
7476 add: 'page-add',
7477 cancel: 'x-circle',
7478 save: 'save',
7479 moveup: 'arrow-up',
7480 movedown: 'arrow-down'
7481 },
7482 icon_prefix: 'fi-'
7483});
7484
7485JSONEditor.defaults.iconlibs.jqueryui = JSONEditor.AbstractIconLib.extend({
7486 mapping: {
7487 collapse: 'triangle-1-s',
7488 expand: 'triangle-1-e',
7489 "delete": 'trash',
7490 edit: 'pencil',
7491 add: 'plusthick',
7492 cancel: 'closethick',
7493 save: 'disk',
7494 moveup: 'arrowthick-1-n',
7495 movedown: 'arrowthick-1-s'
7496 },
7497 icon_prefix: 'ui-icon ui-icon-'
7498});
7499
7500JSONEditor.defaults.templates["default"] = function() {
7501 return {
7502 compile: function(template) {
7503 var matches = template.match(/{{\s*([a-zA-Z0-9\-_ \.]+)\s*}}/g);
7504 var l = matches && matches.length;
7505
7506 // Shortcut if the template contains no variables
7507 if(!l) return function() { return template; };
7508
7509 // Pre-compute the search/replace functions
7510 // This drastically speeds up template execution
7511 var replacements = [];
7512 var get_replacement = function(i) {
7513 var p = matches[i].replace(/[{}]+/g,'').trim().split('.');
7514 var n = p.length;
7515 var func;
7516
7517 if(n > 1) {
7518 var cur;
7519 func = function(vars) {
7520 cur = vars;
7521 for(i=0; i<n; i++) {
7522 cur = cur[p[i]];
7523 if(!cur) break;
7524 }
7525 return cur;
7526 };
7527 }
7528 else {
7529 p = p[0];
7530 func = function(vars) {
7531 return vars[p];
7532 };
7533 }
7534
7535 replacements.push({
7536 s: matches[i],
7537 r: func
7538 });
7539 };
7540 for(var i=0; i<l; i++) {
7541 get_replacement(i);
7542 }
7543
7544 // The compiled function
7545 return function(vars) {
7546 var ret = template+"";
7547 var r;
7548 for(i=0; i<l; i++) {
7549 r = replacements[i];
7550 ret = ret.replace(r.s, r.r(vars));
7551 }
7552 return ret;
7553 };
7554 }
7555 };
7556};
7557
7558JSONEditor.defaults.templates.ejs = function() {
7559 if(!window.EJS) return false;
7560
7561 return {
7562 compile: function(template) {
7563 var compiled = new window.EJS({
7564 text: template
7565 });
7566
7567 return function(context) {
7568 return compiled.render(context);
7569 };
7570 }
7571 };
7572};
7573
7574JSONEditor.defaults.templates.handlebars = function() {
7575 return window.Handlebars;
7576};
7577
7578JSONEditor.defaults.templates.hogan = function() {
7579 if(!window.Hogan) return false;
7580
7581 return {
7582 compile: function(template) {
7583 var compiled = window.Hogan.compile(template);
7584 return function(context) {
7585 return compiled.render(context);
7586 };
7587 }
7588 };
7589};
7590
7591JSONEditor.defaults.templates.markup = function() {
7592 if(!window.Mark || !window.Mark.up) return false;
7593
7594 return {
7595 compile: function(template) {
7596 return function(context) {
7597 return window.Mark.up(template,context);
7598 };
7599 }
7600 };
7601};
7602
7603JSONEditor.defaults.templates.mustache = function() {
7604 if(!window.Mustache) return false;
7605
7606 return {
7607 compile: function(template) {
7608 return function(view) {
7609 return window.Mustache.render(template, view);
7610 };
7611 }
7612 };
7613};
7614
7615JSONEditor.defaults.templates.swig = function() {
7616 return window.swig;
7617};
7618
7619JSONEditor.defaults.templates.underscore = function() {
7620 if(!window._) return false;
7621
7622 return {
7623 compile: function(template) {
7624 return function(context) {
7625 return window._.template(template, context);
7626 };
7627 }
7628 };
7629};
7630
7631// Set the default theme
7632JSONEditor.defaults.theme = 'html';
7633
7634// Set the default template engine
7635JSONEditor.defaults.template = 'default';
7636
7637// Default options when initializing JSON Editor
7638JSONEditor.defaults.options = {};
7639
7640// String translate function
7641JSONEditor.defaults.translate = function(key, variables) {
7642 var lang = JSONEditor.defaults.languages[JSONEditor.defaults.language];
7643 if(!lang) throw "Unknown language "+JSONEditor.defaults.language;
7644
7645 var string = lang[key] || JSONEditor.defaults.languages[JSONEditor.defaults.default_language][key];
7646
7647 if(typeof string === "undefined") throw "Unknown translate string "+key;
7648
7649 if(variables) {
7650 for(var i=0; i<variables.length; i++) {
7651 string = string.replace(new RegExp('\\{\\{'+i+'}}','g'),variables[i]);
7652 }
7653 }
7654
7655 return string;
7656};
7657
7658// Translation strings and default languages
7659JSONEditor.defaults.default_language = 'en';
7660JSONEditor.defaults.language = JSONEditor.defaults.default_language;
7661JSONEditor.defaults.languages.en = {
7662 /**
7663 * When a property is not set
7664 */
7665 error_notset: "Property must be set",
7666 /**
7667 * When a string must not be empty
7668 */
7669 error_notempty: "Value required",
7670 /**
7671 * When a value is not one of the enumerated values
7672 */
7673 error_enum: "Value must be one of the enumerated values",
7674 /**
7675 * When a value doesn't validate any schema of a 'anyOf' combination
7676 */
7677 error_anyOf: "Value must validate against at least one of the provided schemas",
7678 /**
7679 * When a value doesn't validate
7680 * @variables This key takes one variable: The number of schemas the value does not validate
7681 */
7682 error_oneOf: 'Value must validate against exactly one of the provided schemas. It currently validates against {{0}} of the schemas.',
7683 /**
7684 * When a value does not validate a 'not' schema
7685 */
7686 error_not: "Value must not validate against the provided schema",
7687 /**
7688 * When a value does not match any of the provided types
7689 */
7690 error_type_union: "Value must be one of the provided types",
7691 /**
7692 * When a value does not match the given type
7693 * @variables This key takes one variable: The type the value should be of
7694 */
7695 error_type: "Value must be of type {{0}}",
7696 /**
7697 * When the value validates one of the disallowed types
7698 */
7699 error_disallow_union: "Value must not be one of the provided disallowed types",
7700 /**
7701 * When the value validates a disallowed type
7702 * @variables This key takes one variable: The type the value should not be of
7703 */
7704 error_disallow: "Value must not be of type {{0}}",
7705 /**
7706 * When a value is not a multiple of or divisible by a given number
7707 * @variables This key takes one variable: The number mentioned above
7708 */
7709 error_multipleOf: "Value must be a multiple of {{0}}",
7710 /**
7711 * When a value is greater than it's supposed to be (exclusive)
7712 * @variables This key takes one variable: The maximum
7713 */
7714 error_maximum_excl: "Value must be less than {{0}}",
7715 /**
7716 * When a value is greater than it's supposed to be (inclusive
7717 * @variables This key takes one variable: The maximum
7718 */
7719 error_maximum_incl: "Value must be at most {{0}}",
7720 /**
7721 * When a value is lesser than it's supposed to be (exclusive)
7722 * @variables This key takes one variable: The minimum
7723 */
7724 error_minimum_excl: "Value must be greater than {{0}}",
7725 /**
7726 * When a value is lesser than it's supposed to be (inclusive)
7727 * @variables This key takes one variable: The minimum
7728 */
7729 error_minimum_incl: "Value must be at least {{0}}",
7730 /**
7731 * When a value have too many characters
7732 * @variables This key takes one variable: The maximum character count
7733 */
7734 error_maxLength: "Value must be at most {{0}} characters long",
7735 /**
7736 * When a value does not have enough characters
7737 * @variables This key takes one variable: The minimum character count
7738 */
7739 error_minLength: "Value must be at least {{0}} characters long",
7740 /**
7741 * When a value does not match a given pattern
7742 */
7743 error_pattern: "Value must match the pattern {{0}}",
7744 /**
7745 * When an array has additional items whereas it is not supposed to
7746 */
7747 error_additionalItems: "No additional items allowed in this array",
7748 /**
7749 * When there are to many items in an array
7750 * @variables This key takes one variable: The maximum item count
7751 */
7752 error_maxItems: "Value must have at most {{0}} items",
7753 /**
7754 * When there are not enough items in an array
7755 * @variables This key takes one variable: The minimum item count
7756 */
7757 error_minItems: "Value must have at least {{0}} items",
7758 /**
7759 * When an array is supposed to have unique items but has duplicates
7760 */
7761 error_uniqueItems: "Array must have unique items",
7762 /**
7763 * When there are too many properties in an object
7764 * @variables This key takes one variable: The maximum property count
7765 */
7766 error_maxProperties: "Object must have at most {{0}} properties",
7767 /**
7768 * When there are not enough properties in an object
7769 * @variables This key takes one variable: The minimum property count
7770 */
7771 error_minProperties: "Object must have at least {{0}} properties",
7772 /**
7773 * When a required property is not defined
7774 * @variables This key takes one variable: The name of the missing property
7775 */
7776 error_required: "Object is missing the required property '{{0}}'",
7777 /**
7778 * When there is an additional property is set whereas there should be none
7779 * @variables This key takes one variable: The name of the additional property
7780 */
7781 error_additional_properties: "No additional properties allowed, but property {{0}} is set",
7782 /**
7783 * When a dependency is not resolved
7784 * @variables This key takes one variable: The name of the missing property for the dependency
7785 */
7786 error_dependency: "Must have property {{0}}",
7787 /**
7788 * Text on Delete All buttons
7789 */
7790 button_delete_all: "All",
7791 /**
7792 * Title on Delete All buttons
7793 */
7794 button_delete_all_title: "Delete All",
7795 /**
7796 * Text on Delete Last buttons
7797 * @variable This key takes one variable: The title of object to delete
7798 */
7799 button_delete_last: "Last {{0}}",
7800 /**
7801 * Title on Delete Last buttons
7802 * @variable This key takes one variable: The title of object to delete
7803 */
7804 button_delete_last_title: "Delete Last {{0}}",
7805 /**
7806 * Title on Add Row buttons
7807 * @variable This key takes one variable: The title of object to add
7808 */
7809 button_add_row_title: "Add {{0}}",
7810 /**
7811 * Title on Move Down buttons
7812 */
7813 button_move_down_title: "Move down",
7814 /**
7815 * Title on Move Up buttons
7816 */
7817 button_move_up_title: "Move up",
7818 /**
7819 * Title on Delete Row buttons
7820 * @variable This key takes one variable: The title of object to delete
7821 */
7822 button_delete_row_title: "Delete {{0}}",
7823 /**
7824 * Title on Delete Row buttons, short version (no parameter with the object title)
7825 */
7826 button_delete_row_title_short: "Delete",
7827 /**
7828 * Title on Collapse buttons
7829 */
7830 button_collapse: "Collapse"
7831};
7832
7833// Miscellaneous Plugin Settings
7834JSONEditor.plugins = {
7835 ace: {
7836 theme: ''
7837 },
7838 epiceditor: {
7839
7840 },
7841 sceditor: {
7842
7843 },
7844 select2: {
7845
7846 },
7847 selectize: {
7848 }
7849};
7850
7851// Default per-editor options
7852for(var i in JSONEditor.defaults.editors) {
7853 if(!JSONEditor.defaults.editors.hasOwnProperty(i)) continue;
7854 JSONEditor.defaults.editors[i].options = JSONEditor.defaults.editors.options || {};
7855}
7856
7857// Set the default resolvers
7858// Use "multiple" as a fall back for everything
7859JSONEditor.defaults.resolvers.unshift(function(schema) {
7860 if(typeof schema.type !== "string") return "multiple";
7861});
7862// If the type is not set but properties are defined, we can infer the type is actually object
7863JSONEditor.defaults.resolvers.unshift(function(schema) {
7864 // If the schema is a simple type
7865 if(!schema.type && schema.properties ) return "object";
7866});
7867// If the type is set and it's a basic type, use the primitive editor
7868JSONEditor.defaults.resolvers.unshift(function(schema) {
7869 // If the schema is a simple type
7870 if(typeof schema.type === "string") return schema.type;
7871});
7872// Boolean editors
7873JSONEditor.defaults.resolvers.unshift(function(schema) {
7874 if(schema.type === 'boolean') {
7875 // If explicitly set to 'checkbox', use that
7876 if(schema.format === "checkbox" || (schema.options && schema.options.checkbox)) {
7877 return "checkbox";
7878 }
7879 // Otherwise, default to select menu
7880 return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
7881 }
7882});
7883// Use the multiple editor for schemas where the `type` is set to "any"
7884JSONEditor.defaults.resolvers.unshift(function(schema) {
7885 // If the schema can be of any type
7886 if(schema.type === "any") return "multiple";
7887});
7888// Editor for base64 encoded files
7889JSONEditor.defaults.resolvers.unshift(function(schema) {
7890 // If the schema can be of any type
7891 if(schema.type === "string" && schema.media && schema.media.binaryEncoding==="base64") {
7892 return "base64";
7893 }
7894});
7895// Editor for uploading files
7896JSONEditor.defaults.resolvers.unshift(function(schema) {
7897 if(schema.type === "string" && schema.format === "url" && schema.options && schema.options.upload === true) {
7898 if(window.FileReader) return "upload";
7899 }
7900});
7901// Use the table editor for arrays with the format set to `table`
7902JSONEditor.defaults.resolvers.unshift(function(schema) {
7903 // Type `array` with format set to `table`
7904 if(schema.type == "array" && schema.format == "table") {
7905 return "table";
7906 }
7907});
7908// Use the `select` editor for dynamic enumSource enums
7909JSONEditor.defaults.resolvers.unshift(function(schema) {
7910 if(schema.enumSource) return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
7911});
7912// Use the `enum` or `select` editors for schemas with enumerated properties
7913JSONEditor.defaults.resolvers.unshift(function(schema) {
7914 if(schema["enum"]) {
7915 if(schema.type === "array" || schema.type === "object") {
7916 return "enum";
7917 }
7918 else if(schema.type === "number" || schema.type === "integer" || schema.type === "string") {
7919 return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
7920 }
7921 }
7922});
7923// Specialized editors for arrays of strings
7924JSONEditor.defaults.resolvers.unshift(function(schema) {
7925 if(schema.type === "array" && schema.items && !(Array.isArray(schema.items)) && schema.uniqueItems && ['string','number','integer'].indexOf(schema.items.type) >= 0) {
7926 // For enumerated strings, number, or integers
7927 if(schema.items.enum) {
7928 return 'multiselect';
7929 }
7930 // For non-enumerated strings (tag editor)
7931 else if(JSONEditor.plugins.selectize.enable && schema.items.type === "string") {
7932 return 'arraySelectize';
7933 }
7934 }
7935});
7936// Use the multiple editor for schemas with `oneOf` set
7937JSONEditor.defaults.resolvers.unshift(function(schema) {
7938 // If this schema uses `oneOf`
7939 if(schema.oneOf) return "multiple";
7940});
7941
7942/**
7943 * This is a small wrapper for using JSON Editor like a typical jQuery plugin.
7944 */
7945(function() {
7946 if(window.jQuery || window.Zepto) {
7947 var $ = window.jQuery || window.Zepto;
7948 $.jsoneditor = JSONEditor.defaults;
7949
7950 $.fn.jsoneditor = function(options) {
7951 var self = this;
7952 var editor = this.data('jsoneditor');
7953 if(options === 'value') {
7954 if(!editor) throw "Must initialize jsoneditor before getting/setting the value";
7955
7956 // Set value
7957 if(arguments.length > 1) {
7958 editor.setValue(arguments[1]);
7959 }
7960 // Get value
7961 else {
7962 return editor.getValue();
7963 }
7964 }
7965 else if(options === 'validate') {
7966 if(!editor) throw "Must initialize jsoneditor before validating";
7967
7968 // Validate a specific value
7969 if(arguments.length > 1) {
7970 return editor.validate(arguments[1]);
7971 }
7972 // Validate current value
7973 else {
7974 return editor.validate();
7975 }
7976 }
7977 else if(options === 'destroy') {
7978 if(editor) {
7979 editor.destroy();
7980 this.data('jsoneditor',null);
7981 }
7982 }
7983 else {
7984 // Destroy first
7985 if(editor) {
7986 editor.destroy();
7987 }
7988
7989 // Create editor
7990 editor = new JSONEditor(this.get(0),options);
7991 this.data('jsoneditor',editor);
7992
7993 // Setup event listeners
7994 editor.on('change',function() {
7995 self.trigger('change');
7996 });
7997 editor.on('ready',function() {
7998 self.trigger('ready');
7999 });
8000 }
8001
8002 return this;
8003 };
8004 }
8005})();
8006
8007 window.JSONEditor = JSONEditor;
8008})();
8009
8010//# sourceMappingURL=jsoneditor.js.map