From 266eeec21ad9d6f8235828bd1da46bda60a80f8b Mon Sep 17 00:00:00 2001 From: Meir Rotstein Date: Sun, 1 Dec 2019 17:21:16 +0200 Subject: [PATCH] onValidationError option (#854) * provide onValidationError callback * linter fixes * docu fixes * textmode - invoke callback also when no errors This to cover situation of changes that fixes validations * fixed cautom validation example --- docs/api.md | 21 ++++ examples/22_on_validation_event.html | 151 +++++++++++++++++++++++++++ src/js/JSONEditor.js | 2 +- src/js/textmode.js | 11 +- src/js/treemode.js | 10 +- src/js/util.js | 20 ++++ test/util.test.js | 31 +++++- 7 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 examples/22_on_validation_event.html diff --git a/docs/api.md b/docs/api.md index ed2fa58..1110360 100644 --- a/docs/api.md +++ b/docs/api.md @@ -162,6 +162,27 @@ Constructs a new JSONEditor. ``` Also see the option `schema` for JSON schema validation. + +- `{function} onValidationError(errors)` + + Set a callback function for validation errors. Available in all modes. + + On validation of the json, if errors were found this callback is invoked with the validation errors data. + Between validations, the callback will be invoked only if the validation errors were changed. + + Example: + + ```js + var options = { + /** + * @param {Array} errors validation errors + */ + onValidationError: function (errors) { + ... + } + } + ``` + - `{function} onCreateMenu(items, node)` diff --git a/examples/22_on_validation_event.html b/examples/22_on_validation_event.html new file mode 100644 index 0000000..f8eac4e --- /dev/null +++ b/examples/22_on_validation_event.html @@ -0,0 +1,151 @@ + + + + JSONEditor | onValidationError + + + + + + + +

JSON schema validation

+

+ This example demonstrates onValidationError callback. +

+ +
+
+ + + + diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index a11fc51..af224f4 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -183,7 +183,7 @@ JSONEditor.VALID_OPTIONS = [ 'timestampTag', 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform', - 'maxVisibleChilds' + 'maxVisibleChilds', 'onValidationError' ] /** diff --git a/src/js/textmode.js b/src/js/textmode.js index c8f7e56..98f7c67 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -21,7 +21,8 @@ import { parse, repair, sort, - sortObjectKeys + sortObjectKeys, + isValidationErrorChanged } from './util' import { DEFAULT_MODAL_ANCHOR } from './constants' import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor' @@ -87,6 +88,7 @@ textmode.create = function (container, options = {}) { this.textarea = undefined // plain text editor (fallback when Ace is not available) this.validateSchema = null this.annotations = [] + this.lastSchemaErrors = undefined // create a debounced validate function this._debouncedValidate = debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL) @@ -790,12 +792,19 @@ textmode.validate = function () { if (seq === me.validationSequence) { const errors = schemaErrors.concat(parseErrors).concat(customValidationErrors) me._renderErrors(errors) + if (typeof this.options.onValidationError === 'function') { + if (isValidationErrorChanged(errors, this.lastSchemaErrors)) { + this.options.onValidationError.call(this, errors) + } + this.lastSchemaErrors = errors + } } }) .catch(err => { console.error('Custom validation function did throw an error', err) }) } catch (err) { + this.lastSchemaErrors = undefined if (this.getText()) { // try to extract the line number from the jsonlint error message const match = /\w*line\s*(\d+)\w*/g.exec(err.message) diff --git a/src/js/treemode.js b/src/js/treemode.js index 7529611..25bcf46 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -24,7 +24,8 @@ import { removeEventListener, repair, selectContentEditable, - setSelectionOffset + setSelectionOffset, + isValidationErrorChanged } from './util' import { autocomplete } from './autocomplete' import { setLanguage, setLanguages, translate } from './i18n' @@ -52,6 +53,7 @@ treemode.create = function (container, options) { this.validateSchema = null // will be set in .setSchema(schema) this.validationSequence = 0 this.errorNodes = [] + this.lastSchemaErrors = undefined this.node = null this.focusTarget = null @@ -581,6 +583,12 @@ treemode.validate = function () { if (seq === me.validationSequence) { const errorNodes = [].concat(schemaErrors, customValidationErrors || []) me._renderValidationErrors(errorNodes) + if (typeof this.options.onValidationError === 'function') { + if (isValidationErrorChanged(errorNodes, this.lastSchemaErrors)) { + this.options.onValidationError.call(this, errorNodes) + } + this.lastSchemaErrors = errorNodes + } } }) .catch(err => { diff --git a/src/js/util.js b/src/js/util.js index 7bee07d..0058474 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -1453,6 +1453,26 @@ export function contains (array, item) { return array.indexOf(item) !== -1 } +/** + * Checkes if validation has changed from the previous execution + * @param {Array} currErr current validation errors + * @param {Array} prevErr previous validation errors + */ +export function isValidationErrorChanged (currErr, prevErr) { + if (!prevErr && !currErr) { return false } + if ((prevErr && !currErr) || (!prevErr && currErr)) { return true } + if (prevErr.length !== currErr.length) { return true } + + for (let i = 0; i < currErr.length; ++i) { + const pErr = prevErr.find(p => p.dataPath === currErr[i].dataPath && p.schemaPath === currErr[i].schemaPath) + if (!pErr) { + return true + } + } + + return false +} + function hasOwnProperty (object, key) { return Object.prototype.hasOwnProperty.call(object, key) } diff --git a/test/util.test.js b/test/util.test.js index ad40eec..946997e 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -15,7 +15,8 @@ import { repair, sort, sortObjectKeys, - stringifyPath + stringifyPath, + isValidationErrorChanged } from '../src/js/util' describe('util', () => { @@ -200,6 +201,34 @@ describe('util', () => { }) }) + describe('isValidationErrorChanged', () => { + const err1 = { keyword: 'enum', dataPath: '.gender', schemaPath: '#/properties/gender/enum', params: { allowedValues: ['male', 'female'] }, message: 'should be equal to one of: "male", "female"', schema: ['male', 'female'], parentSchema: { title: 'Gender', enum: ['male', 'female'] }, data: null, type: 'validation' } + const err2 = { keyword: 'type', dataPath: '.age', schemaPath: '#/properties/age/type', params: { type: 'integer' }, message: 'should be integer', schema: 'integer', parentSchema: { description: 'Age in years', type: 'integer', minimum: 0, examples: [28, 32] }, data: '28', type: 'validation' } + const err3 = { dataPath: '.gender', message: 'Member must be an object with properties "name" and "age"' } + + it('empty value for both current and previoues error should return false', () => { + assert.strictEqual(isValidationErrorChanged(), false) + }) + + it('empty value for one of current and previoues error should return true', () => { + assert.strictEqual(isValidationErrorChanged([err1]), true) + assert.strictEqual(isValidationErrorChanged(undefined, [err1]), true) + }) + + it('different length of current and previoues errors should return true', () => { + assert.strictEqual(isValidationErrorChanged([err1], []), true) + assert.strictEqual(isValidationErrorChanged([err1], [err1, err2]), true) + }) + + it('same values for current and previoues errors should return false', () => { + assert.strictEqual(isValidationErrorChanged([err1, err2, err3], [err2, err3, err1]), false) + }) + + it('different values for current and previoues errors should return true', () => { + assert.strictEqual(isValidationErrorChanged([err1, err2], [err3, err1]), true) + }) + }) + describe('get', () => { it('should get a nested property from an object', () => { const obj = {