From 1d9d9c25944247d9795412310079b131bfa6d934 Mon Sep 17 00:00:00 2001 From: jos Date: Tue, 12 Jan 2016 13:16:13 +0100 Subject: [PATCH] Implemented method `setSchema` --- HISTORY.md | 2 + docs/api.md | 41 +++++++++++---- src/js/JSONEditor.js | 53 +++++++++++++++++++ src/js/textmode.js | 8 +++ src/js/treemode.js | 111 +++++++++++++++++---------------------- test/test_build_min.html | 2 +- test/test_schema.html | 2 +- 7 files changed, 145 insertions(+), 74 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ecf375a..392b72e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,8 @@ https://github.com/josdejong/jsoneditor ## not yet released, version 5.1.0 - Implemented support for JSON schema validation, powered by `ajv`. + A JSON schema can be configured via the option `schema` or the method + `setSchema`. - Added a minimalist bundle to the `dist` folder, excluding `ace` and `ajv`. - Fixed an error throw when switching to mode "code" via the menu. diff --git a/docs/api.md b/docs/api.md index 1ac2f46..3aa1a36 100644 --- a/docs/api.md +++ b/docs/api.md @@ -10,7 +10,7 @@ Constructs a new JSONEditor. *Parameters:* -- `{Element} container` +- `{Element} container` An HTML DIV element. The JSONEditor will be created inside this container element. @@ -20,11 +20,13 @@ Constructs a new JSONEditor. [Configuration options](#configuration-options). - `{JSON} json` + Initial JSON data to be loaded into the JSONEditor. Alternatively, the method `JSONEditor.set(json)` can be used to load JSON data into the editor. *Returns:* - `{JSONEditor} editor` + New instance of a JSONEditor. ### Configuration options @@ -33,7 +35,7 @@ Constructs a new JSONEditor. Provide a custom version of the [Ace editor](http://ace.c9.io/) and use this instead of the version that comes embedded with JSONEditor. Only applicable when `mode` is `code`. -- `{function} ajv` +- `{Object} ajv` Provide a custom instance of [ajv](https://github.com/epoberezkin/ajv), the library used for JSON schema validation. Example: @@ -124,6 +126,7 @@ Set JSON data. *Parameters:* - `{JSON} json` + JSON data to be displayed in the JSONEditor. #### `JSONEditor.setMode(mode)` @@ -132,7 +135,8 @@ Switch mode. Mode `code` requires the [Ace editor](http://ace.ajax.org/). *Parameters:* -- `{String} mode` +- `{String} mode` + Available values: `tree`, `view`, `form`, `code`, `text`. #### `JSONEditor.setName(name)` @@ -141,19 +145,32 @@ Set a field name for the root node. *Parameters:* -- `{String | undefined} name` +- `{String | undefined} name` + Field name of the root node. If undefined, the current name will be removed. +#### `JSONEditor.setSchema(schema)` + +Set a JSON schema for validation of the JSON object. See also option `schema`. +See [http://json-schema.org/](http://json-schema.org/) for more information on the JSON schema definition. + +*Parameters:* + +- `{Object} schema` + + A JSON schema. + #### `JSONEditor.setText(jsonString)` Set text data in the editor. -This method throws an exception when the provided jsonString does not contain +This method throws an exception when the provided jsonString does not contain valid JSON and the editor is in mode `tree`, `view`, or `form`. *Parameters:* -- `{String} jsonString` +- `{String} jsonString` + Contents of the editor as string. #### `JSONEditor.get()` @@ -165,7 +182,8 @@ which can be the case when the editor is in mode `code` or `text`. *Returns:* -- `{JSON} json` +- `{JSON} json` + JSON data from the JSONEditor. #### `JSONEditor.getMode()` @@ -174,7 +192,8 @@ Retrieve the current mode of the editor. *Returns:* -- `{String} mode` +- `{String} mode` + Current mode of the editor for example `tree` or `code`. #### `JSONEditor.getName()` @@ -183,7 +202,8 @@ Retrieve the current field name of the root node. *Returns:* -- `{String | undefined} name` +- `{String | undefined} name` + Current field name of the root node, or undefined if not set. #### `JSONEditor.getText()` @@ -192,7 +212,8 @@ Get JSON data as string. *Returns:* -- `{String} jsonString` +- `{String} jsonString` + Contents of the editor as string. When the editor is in code `text` or `code`, the returned text is returned as-is. For the other modes, the returned text is a compacted string. In order to get the JSON formatted with a certain diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index 0323979..8b97938 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -1,3 +1,11 @@ +var Ajv; +try { + Ajv = require('ajv/dist/ajv.bundle.js'); +} +catch (err) { + // no problem... when we need Ajv we will throw a neat exception +} + var treemode = require('./treemode'); var textmode = require('./textmode'); var util = require('./util'); @@ -254,6 +262,51 @@ JSONEditor.prototype._onError = function(err) { } }; +/** + * Set a JSON schema for validation of the JSON object. + * To remove the schema, call JSONEditor.setSchema(null) + * @param {Object | null} schema + */ +JSONEditor.prototype.setSchema = function (schema) { + // compile a JSON schema validator if a JSON schema is provided + if (schema) { + var ajv; + try { + // grab ajv from options if provided, else create a new instance + ajv = this.options.ajv || Ajv({ allErrors: true }); + + } + catch (err) { + console.warn('Failed to create an instance of Ajv, JSON Schema validation is not available. Please use a JSONEditor bundle including Ajv, or pass an instance of Ajv as via the configuration option `ajv`.'); + } + + if (ajv) { + this.validateSchema = ajv.compile(schema); + + // add schema to the options, so that when switching to an other mode, + // the set schema is not lost + this.options.schema = schema; + + // validate now + this.validate(); + } + } + else { + // remove current schema + this.validateSchema = null; + this.options.schema = null; + this.validate(); // to clear current error messages + } +}; + +/** + * Validate current JSON object against the configured JSON schema + * Throws an exception when no JSON schema is configured + */ +JSONEditor.prototype.validate = function () { + // must be implemented by treemode and textmode +}; + /** * Register a plugin with one ore multiple modes for the JSON Editor. * diff --git a/src/js/textmode.js b/src/js/textmode.js index ed42d8f..9643713 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -342,6 +342,14 @@ textmode.setText = function(jsonText) { } }; +/** + * Validate current JSON object against the configured JSON schema + * Throws an exception when no JSON schema is configured + */ +textmode.validate = function () { + // TODO: implement validate for textmode +}; + // define modes module.exports = [ { diff --git a/src/js/treemode.js b/src/js/treemode.js index 37107b0..2c46861 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -1,10 +1,3 @@ -var Ajv; -try { - Ajv = require('ajv/dist/ajv.bundle.js'); -} -catch (err) { - // no problem... when we need Ajv we will throw a neat exception -} var Highlighter = require('./Highlighter'); var History = require('./History'); var SearchBox = require('./SearchBox'); @@ -47,6 +40,7 @@ treemode.create = function (container, options) { this.multiselection = { nodes: [] }; + this.validateSchema = null; // will be set in .setSchema(schema) this.errorNodes = []; @@ -95,20 +89,7 @@ treemode._setOptions = function (options) { } // compile a JSON schema validator if a JSON schema is provided - this._validate = null; - if (this.options.schema) { - var ajv; - try { - // grab ajv from options if provided, else create a new instance - ajv = options.ajv || Ajv({ allErrors: true }); - } - catch (err) { - console.warn('Failed to create an instance of Ajv, JSON Schema validation is not available. Please use a JSONEditor bundle including Ajv, or pass an instance of Ajv as via the configuration option `ajv`.'); - } - if (ajv) { - this._validate = ajv.compile(this.options.schema); - } - } + this.setSchema(this.options.schema); // create a debounced validate function var wait = this.options.debounceInterval; @@ -372,56 +353,62 @@ treemode._onChange = function () { * Throws an exception when no JSON schema is configured */ treemode.validate = function () { - if (!this._validate) { + // clear all current errors + if (this.errorNodes) { + this.errorNodes.forEach(function (node) { + node.setError(null); + }); + } + + if (!this.validateSchema) { // if no schema is configured or ajv was not loaded, skip validation return; } + var root = this.node; + if (!root) { // TODO: this should be redundant but is needed on mode switch + return; + } + //console.time('validate'); // TODO: clean up time measurement - var valid = this._validate(this.node.getValue()); + var valid = this.validateSchema(root.getValue()); //console.timeEnd('validate'); - // clear all current errors - this.errorNodes.forEach(function (node) { - node.setError(null); - }); - // apply all new errors - var root = this.node; if (!valid) { - this.errorNodes = this._validate.errors - .map(function findNode (error) { - return { - node: root.findNode(error.dataPath), - error: error - } - }) - .filter(function hasNode (entry) { - return entry.node != null - }) - .reduce(function expandParents (all, entry) { - // expand parents, then merge such that parents come first and - // original entries last - return entry.node - .findParents() - .map(function (parent) { - return { - node: parent, - child: entry.node, - error: { - message: parent.type === 'object' - ? 'Contains invalid properties' // object - : 'Contains invalid items' // array - } - }; - }) - .concat(all, [entry]); - }, []) - // TODO: dedupe the parent nodes - .map(function setError (entry) { - entry.node.setError(entry.error, entry.child); - return entry.node; - }); + this.errorNodes = this.validateSchema.errors + .map(function findNode (error) { + return { + node: root.findNode(error.dataPath), + error: error + } + }) + .filter(function hasNode (entry) { + return entry.node != null + }) + .reduce(function expandParents (all, entry) { + // expand parents, then merge such that parents come first and + // original entries last + return entry.node + .findParents() + .map(function (parent) { + return { + node: parent, + child: entry.node, + error: { + message: parent.type === 'object' + ? 'Contains invalid properties' // object + : 'Contains invalid items' // array + } + }; + }) + .concat(all, [entry]); + }, []) + // TODO: dedupe the parent nodes + .map(function setError (entry) { + entry.node.setError(entry.error, entry.child); + return entry.node; + }); } else { this.errorNodes = []; diff --git a/test/test_build_min.html b/test/test_build_min.html index 57324f1..28d777f 100644 --- a/test/test_build_min.html +++ b/test/test_build_min.html @@ -42,7 +42,7 @@ options = { mode: 'tree', modes: ['code', 'form', 'text', 'tree', 'view'], // allowed modes - error: function (err) { + onError: function (err) { alert(err.toString()); } }; diff --git a/test/test_schema.html b/test/test_schema.html index a8d95b9..e2e5e39 100644 --- a/test/test_schema.html +++ b/test/test_schema.html @@ -65,7 +65,7 @@ var options = { mode: 'text', - modes: ['text', 'form', 'tree', 'view'], // allowed modes + modes: ['code', 'form', 'text', 'tree', 'view'], // allowed modes onError: function (err) { console.error(err); },