UNPKG

json-editor

Version:
1,660 lines (1,478 loc) 247 kB
/*! JSON Editor v0.7.28 - JSON Schema -> HTML Editor * By Jeremy Dorn - https://github.com/jdorn/json-editor/ * Released under the MIT license * * Date: 2016-08-07 */ /** * See README.md for requirements and usage info */ (function() { /*jshint loopfunc: true */ /* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. */ // Inspired by base2 and Prototype var Class; (function(){ var initializing = false, fnTest = /xyz/.test(function(){window.postMessage("xyz");}) ? /\b_super\b/ : /.*/; // The base Class implementation (does nothing) Class = function(){}; // Create a new Class that inherits from this class Class.extend = function extend(prop) { var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; var prototype = new this(); initializing = false; // Copy the properties over onto the new prototype for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn){ return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } // The dummy class constructor function Class() { // All construction is actually done in the init method if ( !initializing && this.init ) this.init.apply(this, arguments); } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.prototype.constructor = Class; // And make this class extendable Class.extend = extend; return Class; }; return Class; })(); // CustomEvent constructor polyfill // From MDN (function () { function CustomEvent ( event, params ) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var evt = document.createEvent( 'CustomEvent' ); evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); return evt; } CustomEvent.prototype = window.Event.prototype; window.CustomEvent = CustomEvent; })(); // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel // MIT license (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }()); // Array.isArray polyfill // From MDN (function() { if(!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } }()); /** * Taken from jQuery 2.1.3 * * @param obj * @returns {boolean} */ var $isplainobject = function( obj ) { // Not plain objects: // - Any object or value whose internal [[Class]] property is not "[object Object]" // - DOM nodes // - window if (typeof obj !== "object" || obj.nodeType || (obj !== null && obj === obj.window)) { return false; } if (obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) { return false; } // If the function hasn't returned already, we're confident that // |obj| is a plain object, created by {} or constructed with new Object return true; }; var $extend = function(destination) { var source, i,property; for(i=1; i<arguments.length; i++) { source = arguments[i]; for (property in source) { if(!source.hasOwnProperty(property)) continue; if(source[property] && $isplainobject(source[property])) { if(!destination.hasOwnProperty(property)) destination[property] = {}; $extend(destination[property], source[property]); } else { destination[property] = source[property]; } } } return destination; }; var $each = function(obj,callback) { if(!obj || typeof obj !== "object") return; var i; if(Array.isArray(obj) || (typeof obj.length === 'number' && obj.length > 0 && (obj.length - 1) in obj)) { for(i=0; i<obj.length; i++) { if(callback(i,obj[i])===false) return; } } else { if (Object.keys) { var keys = Object.keys(obj); for(i=0; i<keys.length; i++) { if(callback(keys[i],obj[keys[i]])===false) return; } } else { for(i in obj) { if(!obj.hasOwnProperty(i)) continue; if(callback(i,obj[i])===false) return; } } } }; var $trigger = function(el,event) { var e = document.createEvent('HTMLEvents'); e.initEvent(event, true, true); el.dispatchEvent(e); }; var $triggerc = function(el,event) { var e = new CustomEvent(event,{ bubbles: true, cancelable: true }); el.dispatchEvent(e); }; var JSONEditor = function(element,options) { if (!(element instanceof Element)) { throw new Error('element should be an instance of Element'); } options = $extend({},JSONEditor.defaults.options,options||{}); this.element = element; this.options = options; this.init(); }; JSONEditor.prototype = { // necessary since we remove the ctor property by doing a literal assignment. Without this // the $isplainobject function will think that this is a plain object. constructor: JSONEditor, init: function() { var self = this; this.ready = false; var theme_class = JSONEditor.defaults.themes[this.options.theme || JSONEditor.defaults.theme]; if(!theme_class) throw "Unknown theme " + (this.options.theme || JSONEditor.defaults.theme); this.schema = this.options.schema; this.theme = new theme_class(); this.template = this.options.template; this.refs = this.options.refs || {}; this.uuid = 0; this.__data = {}; var icon_class = JSONEditor.defaults.iconlibs[this.options.iconlib || JSONEditor.defaults.iconlib]; if(icon_class) this.iconlib = new icon_class(); this.root_container = this.theme.getContainer(); this.element.appendChild(this.root_container); this.translate = this.options.translate || JSONEditor.defaults.translate; // Fetch all external refs via ajax this._loadExternalRefs(this.schema, function() { self._getDefinitions(self.schema); // Validator options var validator_options = {}; if(self.options.custom_validators) { validator_options.custom_validators = self.options.custom_validators; } self.validator = new JSONEditor.Validator(self,null,validator_options); // Create the root editor var editor_class = self.getEditorClass(self.schema); self.root = self.createEditor(editor_class, { jsoneditor: self, schema: self.schema, required: true, container: self.root_container }); self.root.preBuild(); self.root.build(); self.root.postBuild(); // Starting data if(self.options.startval) self.root.setValue(self.options.startval); self.validation_results = self.validator.validate(self.root.getValue()); self.root.showValidationErrors(self.validation_results); self.ready = true; // Fire ready event asynchronously window.requestAnimationFrame(function() { if(!self.ready) return; self.validation_results = self.validator.validate(self.root.getValue()); self.root.showValidationErrors(self.validation_results); self.trigger('ready'); self.trigger('change'); }); }); }, getValue: function() { if(!this.ready) throw "JSON Editor not ready yet. Listen for 'ready' event before getting the value"; return this.root.getValue(); }, setValue: function(value) { if(!this.ready) throw "JSON Editor not ready yet. Listen for 'ready' event before setting the value"; this.root.setValue(value); return this; }, validate: function(value) { if(!this.ready) throw "JSON Editor not ready yet. Listen for 'ready' event before validating"; // Custom value if(arguments.length === 1) { return this.validator.validate(value); } // Current value (use cached result) else { return this.validation_results; } }, destroy: function() { if(this.destroyed) return; if(!this.ready) return; this.schema = null; this.options = null; this.root.destroy(); this.root = null; this.root_container = null; this.validator = null; this.validation_results = null; this.theme = null; this.iconlib = null; this.template = null; this.__data = null; this.ready = false; this.element.innerHTML = ''; this.destroyed = true; }, on: function(event, callback) { this.callbacks = this.callbacks || {}; this.callbacks[event] = this.callbacks[event] || []; this.callbacks[event].push(callback); return this; }, off: function(event, callback) { // Specific callback if(event && callback) { this.callbacks = this.callbacks || {}; this.callbacks[event] = this.callbacks[event] || []; var newcallbacks = []; for(var i=0; i<this.callbacks[event].length; i++) { if(this.callbacks[event][i]===callback) continue; newcallbacks.push(this.callbacks[event][i]); } this.callbacks[event] = newcallbacks; } // All callbacks for a specific event else if(event) { this.callbacks = this.callbacks || {}; this.callbacks[event] = []; } // All callbacks for all events else { this.callbacks = {}; } return this; }, trigger: function(event) { if(this.callbacks && this.callbacks[event] && this.callbacks[event].length) { for(var i=0; i<this.callbacks[event].length; i++) { this.callbacks[event][i](); } } return this; }, setOption: function(option, value) { if(option === "show_errors") { this.options.show_errors = value; this.onChange(); } // Only the `show_errors` option is supported for now else { throw "Option "+option+" must be set during instantiation and cannot be changed later"; } return this; }, getEditorClass: function(schema) { var classname; schema = this.expandSchema(schema); $each(JSONEditor.defaults.resolvers,function(i,resolver) { var tmp = resolver(schema); if(tmp) { if(JSONEditor.defaults.editors[tmp]) { classname = tmp; return false; } } }); if(!classname) throw "Unknown editor for schema "+JSON.stringify(schema); if(!JSONEditor.defaults.editors[classname]) throw "Unknown editor "+classname; return JSONEditor.defaults.editors[classname]; }, createEditor: function(editor_class, options) { options = $extend({},editor_class.options||{},options); return new editor_class(options); }, onChange: function() { if(!this.ready) return; if(this.firing_change) return; this.firing_change = true; var self = this; window.requestAnimationFrame(function() { self.firing_change = false; if(!self.ready) return; // Validate and cache results self.validation_results = self.validator.validate(self.root.getValue()); if(self.options.show_errors !== "never") { self.root.showValidationErrors(self.validation_results); } else { self.root.showValidationErrors([]); } // Fire change event self.trigger('change'); }); return this; }, compileTemplate: function(template, name) { name = name || JSONEditor.defaults.template; var engine; // Specifying a preset engine if(typeof name === 'string') { if(!JSONEditor.defaults.templates[name]) throw "Unknown template engine "+name; engine = JSONEditor.defaults.templates[name](); if(!engine) throw "Template engine "+name+" missing required library."; } // Specifying a custom engine else { engine = name; } if(!engine) throw "No template engine set"; if(!engine.compile) throw "Invalid template engine set"; return engine.compile(template); }, _data: function(el,key,value) { // Setting data if(arguments.length === 3) { var uuid; if(el.hasAttribute('data-jsoneditor-'+key)) { uuid = el.getAttribute('data-jsoneditor-'+key); } else { uuid = this.uuid++; el.setAttribute('data-jsoneditor-'+key,uuid); } this.__data[uuid] = value; } // Getting data else { // No data stored if(!el.hasAttribute('data-jsoneditor-'+key)) return null; return this.__data[el.getAttribute('data-jsoneditor-'+key)]; } }, registerEditor: function(editor) { this.editors = this.editors || {}; this.editors[editor.path] = editor; return this; }, unregisterEditor: function(editor) { this.editors = this.editors || {}; this.editors[editor.path] = null; return this; }, getEditor: function(path) { if(!this.editors) return; return this.editors[path]; }, watch: function(path,callback) { this.watchlist = this.watchlist || {}; this.watchlist[path] = this.watchlist[path] || []; this.watchlist[path].push(callback); return this; }, unwatch: function(path,callback) { if(!this.watchlist || !this.watchlist[path]) return this; // If removing all callbacks for a path if(!callback) { this.watchlist[path] = null; return this; } var newlist = []; for(var i=0; i<this.watchlist[path].length; i++) { if(this.watchlist[path][i] === callback) continue; else newlist.push(this.watchlist[path][i]); } this.watchlist[path] = newlist.length? newlist : null; return this; }, notifyWatchers: function(path) { if(!this.watchlist || !this.watchlist[path]) return this; for(var i=0; i<this.watchlist[path].length; i++) { this.watchlist[path][i](); } }, isEnabled: function() { return !this.root || this.root.isEnabled(); }, enable: function() { this.root.enable(); }, disable: function() { this.root.disable(); }, _getDefinitions: function(schema,path) { path = path || '#/definitions/'; if(schema.definitions) { for(var i in schema.definitions) { if(!schema.definitions.hasOwnProperty(i)) continue; this.refs[path+i] = schema.definitions[i]; if(schema.definitions[i].definitions) { this._getDefinitions(schema.definitions[i],path+i+'/definitions/'); } } } }, _getExternalRefs: function(schema) { var refs = {}; var merge_refs = function(newrefs) { for(var i in newrefs) { if(newrefs.hasOwnProperty(i)) { refs[i] = true; } } }; if(schema.$ref && typeof schema.$ref !== "object" && schema.$ref.substr(0,1) !== "#" && !this.refs[schema.$ref]) { refs[schema.$ref] = true; } for(var i in schema) { if(!schema.hasOwnProperty(i)) continue; if(schema[i] && typeof schema[i] === "object" && Array.isArray(schema[i])) { for(var j=0; j<schema[i].length; j++) { if(typeof schema[i][j]==="object") { merge_refs(this._getExternalRefs(schema[i][j])); } } } else if(schema[i] && typeof schema[i] === "object") { merge_refs(this._getExternalRefs(schema[i])); } } return refs; }, _loadExternalRefs: function(schema, callback) { var self = this; var refs = this._getExternalRefs(schema); var done = 0, waiting = 0, callback_fired = false; $each(refs,function(url) { if(self.refs[url]) return; if(!self.options.ajax) throw "Must set ajax option to true to load external ref "+url; self.refs[url] = 'loading'; waiting++; var r = new XMLHttpRequest(); r.open("GET", url, true); r.onreadystatechange = function () { if (r.readyState != 4) return; // Request succeeded if(r.status === 200) { var response; try { response = JSON.parse(r.responseText); } catch(e) { window.console.log(e); throw "Failed to parse external ref "+url; } if(!response || typeof response !== "object") throw "External ref does not contain a valid schema - "+url; self.refs[url] = response; self._loadExternalRefs(response,function() { done++; if(done >= waiting && !callback_fired) { callback_fired = true; callback(); } }); } // Request failed else { window.console.log(r); throw "Failed to fetch ref via ajax- "+url; } }; r.send(); }); if(!waiting) { callback(); } }, expandRefs: function(schema) { schema = $extend({},schema); while (schema.$ref) { var ref = schema.$ref; delete schema.$ref; if(!this.refs[ref]) ref = decodeURIComponent(ref); schema = this.extendSchemas(schema,this.refs[ref]); } return schema; }, expandSchema: function(schema) { var self = this; var extended = $extend({},schema); var i; // Version 3 `type` if(typeof schema.type === 'object') { // Array of types if(Array.isArray(schema.type)) { $each(schema.type, function(key,value) { // Schema if(typeof value === 'object') { schema.type[key] = self.expandSchema(value); } }); } // Schema else { schema.type = self.expandSchema(schema.type); } } // Version 3 `disallow` if(typeof schema.disallow === 'object') { // Array of types if(Array.isArray(schema.disallow)) { $each(schema.disallow, function(key,value) { // Schema if(typeof value === 'object') { schema.disallow[key] = self.expandSchema(value); } }); } // Schema else { schema.disallow = self.expandSchema(schema.disallow); } } // Version 4 `anyOf` if(schema.anyOf) { $each(schema.anyOf, function(key,value) { schema.anyOf[key] = self.expandSchema(value); }); } // Version 4 `dependencies` (schema dependencies) if(schema.dependencies) { $each(schema.dependencies,function(key,value) { if(typeof value === "object" && !(Array.isArray(value))) { schema.dependencies[key] = self.expandSchema(value); } }); } // Version 4 `not` if(schema.not) { schema.not = this.expandSchema(schema.not); } // allOf schemas should be merged into the parent if(schema.allOf) { for(i=0; i<schema.allOf.length; i++) { extended = this.extendSchemas(extended,this.expandSchema(schema.allOf[i])); } delete extended.allOf; } // extends schemas should be merged into parent if(schema["extends"]) { // If extends is a schema if(!(Array.isArray(schema["extends"]))) { extended = this.extendSchemas(extended,this.expandSchema(schema["extends"])); } // If extends is an array of schemas else { for(i=0; i<schema["extends"].length; i++) { extended = this.extendSchemas(extended,this.expandSchema(schema["extends"][i])); } } delete extended["extends"]; } // parent should be merged into oneOf schemas if(schema.oneOf) { var tmp = $extend({},extended); delete tmp.oneOf; for(i=0; i<schema.oneOf.length; i++) { extended.oneOf[i] = this.extendSchemas(this.expandSchema(schema.oneOf[i]),tmp); } } return this.expandRefs(extended); }, extendSchemas: function(obj1, obj2) { obj1 = $extend({},obj1); obj2 = $extend({},obj2); var self = this; var extended = {}; $each(obj1, function(prop,val) { // If this key is also defined in obj2, merge them if(typeof obj2[prop] !== "undefined") { // Required and defaultProperties arrays should be unioned together if((prop === 'required'||prop === 'defaultProperties') && typeof val === "object" && Array.isArray(val)) { // Union arrays and unique extended[prop] = val.concat(obj2[prop]).reduce(function(p, c) { if (p.indexOf(c) < 0) p.push(c); return p; }, []); } // Type should be intersected and is either an array or string else if(prop === 'type' && (typeof val === "string" || Array.isArray(val))) { // Make sure we're dealing with arrays if(typeof val === "string") val = [val]; if(typeof obj2.type === "string") obj2.type = [obj2.type]; // If type is only defined in the first schema, keep it if(!obj2.type || !obj2.type.length) { extended.type = val; } // If type is defined in both schemas, do an intersect else { extended.type = val.filter(function(n) { return obj2.type.indexOf(n) !== -1; }); } // If there's only 1 type and it's a primitive, use a string instead of array if(extended.type.length === 1 && typeof extended.type[0] === "string") { extended.type = extended.type[0]; } // Remove the type property if it's empty else if(extended.type.length === 0) { delete extended.type; } } // All other arrays should be intersected (enum, etc.) else if(typeof val === "object" && Array.isArray(val)){ extended[prop] = val.filter(function(n) { return obj2[prop].indexOf(n) !== -1; }); } // Objects should be recursively merged else if(typeof val === "object" && val !== null) { extended[prop] = self.extendSchemas(val,obj2[prop]); } // Otherwise, use the first value else { extended[prop] = val; } } // Otherwise, just use the one in obj1 else { extended[prop] = val; } }); // Properties in obj2 that aren't in obj1 $each(obj2, function(prop,val) { if(typeof obj1[prop] === "undefined") { extended[prop] = val; } }); return extended; } }; JSONEditor.defaults = { themes: {}, templates: {}, iconlibs: {}, editors: {}, languages: {}, resolvers: [], custom_validators: [] }; JSONEditor.Validator = Class.extend({ init: function(jsoneditor,schema,options) { this.jsoneditor = jsoneditor; this.schema = schema || this.jsoneditor.schema; this.options = options || {}; this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate; }, validate: function(value) { return this._validateSchema(this.schema, value); }, _validateSchema: function(schema,value,path) { var self = this; var errors = []; var valid, i, j; var stringified = JSON.stringify(value); path = path || 'root'; // Work on a copy of the schema schema = $extend({},this.jsoneditor.expandRefs(schema)); /* * Type Agnostic Validation */ // Version 3 `required` if(schema.required && schema.required === true) { if(typeof value === "undefined") { errors.push({ path: path, property: 'required', message: this.translate("error_notset") }); // Can't do any more validation at this point return errors; } } // Value not defined else if(typeof value === "undefined") { // If required_by_default is set, all fields are required if(this.jsoneditor.options.required_by_default) { errors.push({ path: path, property: 'required', message: this.translate("error_notset") }); } // Not required, no further validation needed else { return errors; } } // `enum` if(schema["enum"]) { valid = false; for(i=0; i<schema["enum"].length; i++) { if(stringified === JSON.stringify(schema["enum"][i])) valid = true; } if(!valid) { errors.push({ path: path, property: 'enum', message: this.translate("error_enum") }); } } // `extends` (version 3) if(schema["extends"]) { for(i=0; i<schema["extends"].length; i++) { errors = errors.concat(this._validateSchema(schema["extends"][i],value,path)); } } // `allOf` if(schema.allOf) { for(i=0; i<schema.allOf.length; i++) { errors = errors.concat(this._validateSchema(schema.allOf[i],value,path)); } } // `anyOf` if(schema.anyOf) { valid = false; for(i=0; i<schema.anyOf.length; i++) { if(!this._validateSchema(schema.anyOf[i],value,path).length) { valid = true; break; } } if(!valid) { errors.push({ path: path, property: 'anyOf', message: this.translate('error_anyOf') }); } } // `oneOf` if(schema.oneOf) { valid = 0; var oneof_errors = []; for(i=0; i<schema.oneOf.length; i++) { // Set the error paths to be path.oneOf[i].rest.of.path var tmp = this._validateSchema(schema.oneOf[i],value,path); if(!tmp.length) { valid++; } for(j=0; j<tmp.length; j++) { tmp[j].path = path+'.oneOf['+i+']'+tmp[j].path.substr(path.length); } oneof_errors = oneof_errors.concat(tmp); } if(valid !== 1) { errors.push({ path: path, property: 'oneOf', message: this.translate('error_oneOf', [valid]) }); errors = errors.concat(oneof_errors); } } // `not` if(schema.not) { if(!this._validateSchema(schema.not,value,path).length) { errors.push({ path: path, property: 'not', message: this.translate('error_not') }); } } // `type` (both Version 3 and Version 4 support) if(schema.type) { // Union type if(Array.isArray(schema.type)) { valid = false; for(i=0;i<schema.type.length;i++) { if(this._checkType(schema.type[i], value)) { valid = true; break; } } if(!valid) { errors.push({ path: path, property: 'type', message: this.translate('error_type_union') }); } } // Simple type else { if(!this._checkType(schema.type, value)) { errors.push({ path: path, property: 'type', message: this.translate('error_type', [schema.type]) }); } } } // `disallow` (version 3) if(schema.disallow) { // Union type if(Array.isArray(schema.disallow)) { valid = true; for(i=0;i<schema.disallow.length;i++) { if(this._checkType(schema.disallow[i], value)) { valid = false; break; } } if(!valid) { errors.push({ path: path, property: 'disallow', message: this.translate('error_disallow_union') }); } } // Simple type else { if(this._checkType(schema.disallow, value)) { errors.push({ path: path, property: 'disallow', message: this.translate('error_disallow', [schema.disallow]) }); } } } /* * Type Specific Validation */ // Number Specific Validation if(typeof value === "number") { // `multipleOf` and `divisibleBy` if(schema.multipleOf || schema.divisibleBy) { var divisor = schema.multipleOf || schema.divisibleBy; // Vanilla JS, prone to floating point rounding errors (e.g. 1.14 / .01 == 113.99999) valid = (value/divisor === Math.floor(value/divisor)); // Use math.js is available if(window.math) { valid = window.math.mod(window.math.bignumber(value), window.math.bignumber(divisor)).equals(0); } // Use decimal.js is available else if(window.Decimal) { valid = (new window.Decimal(value)).mod(new window.Decimal(divisor)).equals(0); } if(!valid) { errors.push({ path: path, property: schema.multipleOf? 'multipleOf' : 'divisibleBy', message: this.translate('error_multipleOf', [divisor]) }); } } // `maximum` if(schema.hasOwnProperty('maximum')) { // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1) valid = schema.exclusiveMaximum? (value < schema.maximum) : (value <= schema.maximum); // Use math.js is available if(window.math) { valid = window.math[schema.exclusiveMaximum?'smaller':'smallerEq']( window.math.bignumber(value), window.math.bignumber(schema.maximum) ); } // Use Decimal.js if available else if(window.Decimal) { valid = (new window.Decimal(value))[schema.exclusiveMaximum?'lt':'lte'](new window.Decimal(schema.maximum)); } if(!valid) { errors.push({ path: path, property: 'maximum', message: this.translate( (schema.exclusiveMaximum?'error_maximum_excl':'error_maximum_incl'), [schema.maximum] ) }); } } // `minimum` if(schema.hasOwnProperty('minimum')) { // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1) valid = schema.exclusiveMinimum? (value > schema.minimum) : (value >= schema.minimum); // Use math.js is available if(window.math) { valid = window.math[schema.exclusiveMinimum?'larger':'largerEq']( window.math.bignumber(value), window.math.bignumber(schema.minimum) ); } // Use Decimal.js if available else if(window.Decimal) { valid = (new window.Decimal(value))[schema.exclusiveMinimum?'gt':'gte'](new window.Decimal(schema.minimum)); } if(!valid) { errors.push({ path: path, property: 'minimum', message: this.translate( (schema.exclusiveMinimum?'error_minimum_excl':'error_minimum_incl'), [schema.minimum] ) }); } } } // String specific validation else if(typeof value === "string") { // `maxLength` if(schema.maxLength) { if((value+"").length > schema.maxLength) { errors.push({ path: path, property: 'maxLength', message: this.translate('error_maxLength', [schema.maxLength]) }); } } // `minLength` if(schema.minLength) { if((value+"").length < schema.minLength) { errors.push({ path: path, property: 'minLength', message: this.translate((schema.minLength===1?'error_notempty':'error_minLength'), [schema.minLength]) }); } } // `pattern` if(schema.pattern) { if(!(new RegExp(schema.pattern)).test(value)) { errors.push({ path: path, property: 'pattern', message: this.translate('error_pattern', [schema.pattern]) }); } } } // Array specific validation else if(typeof value === "object" && value !== null && Array.isArray(value)) { // `items` and `additionalItems` if(schema.items) { // `items` is an array if(Array.isArray(schema.items)) { for(i=0; i<value.length; i++) { // If this item has a specific schema tied to it // Validate against it if(schema.items[i]) { errors = errors.concat(this._validateSchema(schema.items[i],value[i],path+'.'+i)); } // If all additional items are allowed else if(schema.additionalItems === true) { break; } // If additional items is a schema // TODO: Incompatibility between version 3 and 4 of the spec else if(schema.additionalItems) { errors = errors.concat(this._validateSchema(schema.additionalItems,value[i],path+'.'+i)); } // If no additional items are allowed else if(schema.additionalItems === false) { errors.push({ path: path, property: 'additionalItems', message: this.translate('error_additionalItems') }); break; } // Default for `additionalItems` is an empty schema else { break; } } } // `items` is a schema else { // Each item in the array must validate against the schema for(i=0; i<value.length; i++) { errors = errors.concat(this._validateSchema(schema.items,value[i],path+'.'+i)); } } } // `maxItems` if(schema.maxItems) { if(value.length > schema.maxItems) { errors.push({ path: path, property: 'maxItems', message: this.translate('error_maxItems', [schema.maxItems]) }); } } // `minItems` if(schema.minItems) { if(value.length < schema.minItems) { errors.push({ path: path, property: 'minItems', message: this.translate('error_minItems', [schema.minItems]) }); } } // `uniqueItems` if(schema.uniqueItems) { var seen = {}; for(i=0; i<value.length; i++) { valid = JSON.stringify(value[i]); if(seen[valid]) { errors.push({ path: path, property: 'uniqueItems', message: this.translate('error_uniqueItems') }); break; } seen[valid] = true; } } } // Object specific validation else if(typeof value === "object" && value !== null) { // `maxProperties` if(schema.maxProperties) { valid = 0; for(i in value) { if(!value.hasOwnProperty(i)) continue; valid++; } if(valid > schema.maxProperties) { errors.push({ path: path, property: 'maxProperties', message: this.translate('error_maxProperties', [schema.maxProperties]) }); } } // `minProperties` if(schema.minProperties) { valid = 0; for(i in value) { if(!value.hasOwnProperty(i)) continue; valid++; } if(valid < schema.minProperties) { errors.push({ path: path, property: 'minProperties', message: this.translate('error_minProperties', [schema.minProperties]) }); } } // Version 4 `required` if(schema.required && Array.isArray(schema.required)) { for(i=0; i<schema.required.length; i++) { if(typeof value[schema.required[i]] === "undefined") { errors.push({ path: path, property: 'required', message: this.translate('error_required', [schema.required[i]]) }); } } } // `properties` var validated_properties = {}; if(schema.properties) { for(i in schema.properties) { if(!schema.properties.hasOwnProperty(i)) continue; validated_properties[i] = true; errors = errors.concat(this._validateSchema(schema.properties[i],value[i],path+'.'+i)); } } // `patternProperties` if(schema.patternProperties) { for(i in schema.patternProperties) { if(!schema.patternProperties.hasOwnProperty(i)) continue; var regex = new RegExp(i); // Check which properties match for(j in value) { if(!value.hasOwnProperty(j)) continue; if(regex.test(j)) { validated_properties[j] = true; errors = errors.concat(this._validateSchema(schema.patternProperties[i],value[j],path+'.'+j)); } } } } // The no_additional_properties option currently doesn't work with extended schemas that use oneOf or anyOf if(typeof schema.additionalProperties === "undefined" && this.jsoneditor.options.no_additional_properties && !schema.oneOf && !schema.anyOf) { schema.additionalProperties = false; } // `additionalProperties` if(typeof schema.additionalProperties !== "undefined") { for(i in value) { if(!value.hasOwnProperty(i)) continue; if(!validated_properties[i]) { // No extra properties allowed if(!schema.additionalProperties) { errors.push({ path: path, property: 'additionalProperties', message: this.translate('error_additional_properties', [i]) }); break; } // Allowed else if(schema.additionalProperties === true) { break; } // Must match schema // TODO: incompatibility between version 3 and 4 of the spec else { errors = errors.concat(this._validateSchema(schema.additionalProperties,value[i],path+'.'+i)); } } } } // `dependencies` if(schema.dependencies) { for(i in schema.dependencies) { if(!schema.dependencies.hasOwnProperty(i)) continue; // Doesn't need to meet the dependency if(typeof value[i] === "undefined") continue; // Property dependency if(Array.isArray(schema.dependencies[i])) { for(j=0; j<schema.dependencies[i].length; j++) { if(typeof value[schema.dependencies[i][j]] === "undefined") { errors.push({ path: path, property: 'dependencies', message: this.translate('error_dependency', [schema.dependencies[i][j]]) }); } } } // Schema dependency else { errors = errors.concat(this._validateSchema(schema.dependencies[i],value,path)); } } } } // Custom type validation (global) $each(JSONEditor.defaults.custom_validators,function(i,validator) { errors = errors.concat(validator.call(self,schema,value,path)); }); // Custom type validation (instance specific) if(this.options.custom_validators) { $each(this.options.custom_validators,function(i,validator) { errors = errors.concat(validator.call(self,schema,value,path)); }); } return errors; }, _checkType: function(type, value) { // Simple types if(typeof type === "string") { if(type==="string") return typeof value === "string"; else if(type==="number") return typeof value === "number"; else if(type==="integer") return typeof value === "number" && value === Math.floor(value); else if(type==="boolean") return typeof value === "boolean"; else if(type==="array") return Array.isArray(value); else if(type === "object") return value !== null && !(Array.isArray(value)) && typeof value === "object"; else if(type === "null") return value === null; else return true; } // Schema else { return !this._validateSchema(type,value).length; } } }); /** * All editors should extend from this class */ JSONEditor.AbstractEditor = Class.extend({ onChildEditorChange: function(editor) { this.onChange(true); }, notify: function() { this.jsoneditor.notifyWatchers(this.path); }, change: function() { if(this.parent) this.parent.onChildEditorChange(this); else this.jsoneditor.onChange(); }, onChange: function(bubble) { this.notify(); if(this.watch_listener) this.watch_listener(); if(bubble) this.change(); }, register: function() { this.jsoneditor.registerEditor(this); this.onChange(); }, unregister: function() { if(!this.jsoneditor) return; this.jsoneditor.unregisterEditor(this); }, getNumColumns: function() { return 12; }, init: function(options) { this.jsoneditor = options.jsoneditor; this.theme = this.jsoneditor.theme; this.template_engine = this.jsoneditor.template; this.iconlib = this.jsoneditor.iconlib; this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate; this.original_schema = options.schema; this.schema = this.jsoneditor.expandSchema(this.original_schema); this.options = $extend({}, (this.options || {}), (options.schema.options || {}), options); if(!options.path && !this.schema.id) this.schema.id = 'root'; this.path = options.path || 'root'; this.formname = options.formname || this.path.replace(/\.([^.]+)/g,'[$1]'); if(this.jsoneditor.options.form_name_root) this.formname = this.formname.replace(/^root\[/,this.jsoneditor.options.form_name_root+'['); this.key = this.path.split('.').pop(); this.parent = options.parent; this.link_watchers = []; if(options.container) this.setContainer(options.container); }, setContainer: function(container) { this.container = container; if(this.schema.id) this.container.setAttribute('data-schemaid',this.schema.id); if(this.schema.type && typeof this.schema.type === "string") this.container.setAttribute('data-schematype',this.schema.type); this.container.setAttribute('data-schemapath',this.path); }, preBuild: function() { }, build: function() { }, postBuild: function() { this.setupWatchListeners(); this.addLinks(); this.setValue(this.getDefault(), true); this.updateHeaderText(); this.register(); this.onWatchedFieldChange(); }, setupWatchListeners: function() { var self = this; // Watched fields this.watched = {}; if(this.schema.vars) this.schema.watch = this.schema.vars; this.watched_values = {}; this.watch_listener = function() { if(self.refreshWatchedFieldValues()) { self.onWatchedFieldChange(); } }; this.register(); if(this.schema.hasOwnProperty('watch')) { var path,path_parts,first,root,adjusted_path; for(var name in this.schema.watch) { if(!this.schema.watch.hasOwnProperty(name)) continue; path = this.schema.watch[name]; if(Array.isArray(path)) { if(path.length<2) continue; path_parts = [path[0]].concat(path[1].split('.')); } else { path_parts = path.split('.'); if(!self.theme.closest(self.container,'[data-schemaid="'+path_parts[0]+'"]')) path_parts.unshift('#'); } first = path_parts.shift(); if(first === '#') first = self.jsoneditor.schema.id || 'root'; // Find the root node for this template variable root = self.theme.closest(self.container,'[data-schemaid="'+first+'"]'); if(!root) throw "Could not find ancestor node with id "+first; // Keep track of the root node and path for use when rendering the template adjusted_path = root.getAttribute('data-schemapath') + '.' + path_parts.join('.'); self.jsoneditor.watch(adjusted_path,self.watch_listener); self.watched[name] = adjusted_path; } } // Dynamic header if(this.schema.headerTemplate) { this.header_template = this.jsoneditor.compileTemplate(this.schema.headerTemplate, this.template_engine); } }, addLinks: function() { // Add links if(!this.no_link_holder) { this.link_holder = this.theme.getLinksHolder(); this.container.appendChild(this.link_holder); if(this.schema.links) { for(var i=0; i<this.schema.links.length; i++) { this.addLink(this.getLink(this.schema.links[i])); } } } }, getButton: function(text, icon, title) { var btnClass = 'json-editor-btn-'+icon; if(!this.iconlib) icon = null; else icon = this.iconlib.getIcon(icon); if(!icon && title) { text = title; title = null; } var btn = this.theme.getButton(text, icon, title); btn.className += ' ' + btnClass + ' '; return btn; }, setButtonText: function(button, text, icon, title) { if(!this.iconlib) icon = null; else icon = this.iconlib.getIcon(icon); if(!icon && title) { text = title; title = null; } return this.theme.setButtonText(button, text, icon, title); }, addLink: function(link) { if(this.link_holder) this.link_holder.appendChild(link); }, getLink: function(data) { var holder, link; // Get mime type of the link var mime = data.mediaType || 'application/javascript'; var type = mime.split('/')[0]; // Template to generate the link href var href = this.jsoneditor.compileTemplate(data.href,this.template_engine); // Template to generate the link's download attribute var download = null; if(data.download) download = data.download; if(download && download !== true) { download = this.jsoneditor.compileTemplate(download, this.template_engine); } // Image links if(type === 'image') { holder = this.theme.getBlockLinkHolder(); link = document.createElement('a'); link.setAttribute('target','_blank'); var image = document.createElement('img'); this.theme.createImageLink(holder,link,image); // When a watched field changes, update the url this.link_watchers.push(function(vars) { var url = href(vars); link.setAttribute('href',url); link.setAttribute('title',data.rel || url); image.setAttribute('src',url); }); } // Audio/Video links else if(['audio','video'].indexOf(type) >=0) { holder = this.theme.getBlockLinkHolder(); link = this.theme.getBlockLink(); link.setAttribute('target','_blank'); var media = document.createElement(type); media.setAttribute('controls','controls'); this.theme.createMediaLink(holder,link,media); // When a watched field changes, update the url this.link_watchers.push(function(vars) { var url = href(vars); link.setAttribute('href',url); link.textContent = data.rel || url; media.setAttribute('src',url); }); } // Text links else { link = holder = this.theme.getBlockLink(); holder.setAttribute('target','_blank'); holder.textContent = data.rel; // When a watched field changes, update the url this.link_watchers.push(function(vars) { var url = href(vars); holder.setAttribute('href',url); holder.textContent = data.rel || url; }); } if(download && link) { if(download === true) { link.setAttribute('download',''); } else { this.link_watchers.push(function(vars) { link.setAttribute('download',download(vars)); }); } } if(data.class) link.className = link.className + ' ' + data.class; return holder; }, refreshWatchedFieldValues: function() { if(!this.watched_values) return; var watched = {}; var changed = false; var self = this; if(this.watched) { var val,editor; for(var name in this.watched) { if(!this.watched.hasOwnProperty(name)) continue; editor = self.jsoneditor.getEditor(this.watched[name]); val = editor? editor.getValue() : null; if(self.watched_values[name] !== val) changed = true; watched[name] = val; } } watched.self = this.getValue(); if(this.watched_values.self !== watched.self