json-editor
Version:
JSON Schema based editor
1,660 lines (1,478 loc) • 247 kB
JavaScript
/*! 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