Implement validation in mode `"preview"`

This commit is contained in:
jos 2019-07-24 13:30:44 +02:00
parent 3ed0318624
commit 745a4f3665
8 changed files with 61 additions and 43 deletions

View File

@ -125,7 +125,7 @@
schema: schema, schema: schema,
schemaRefs: {"job": job}, schemaRefs: {"job": job},
mode: 'tree', mode: 'tree',
modes: ['code', 'text', 'tree'] modes: ['code', 'text', 'tree', 'preview']
}; };
// create the editor // create the editor

View File

@ -46,7 +46,7 @@
var options = { var options = {
mode: 'tree', mode: 'tree',
modes: ['code', 'text', 'tree'], modes: ['code', 'text', 'tree', 'preview'],
onValidate: function (json) { onValidate: function (json) {
// rules: // rules:
// - team, names, and ages must be filled in and be of correct type // - team, names, and ages must be filled in and be of correct type

View File

@ -39,7 +39,7 @@
var options = { var options = {
mode: 'tree', mode: 'tree',
modes: ['code', 'text', 'tree'], modes: ['code', 'text', 'tree', 'preview'],
onValidate: function (json) { onValidate: function (json) {
// in this validation function we fake sending a request to a server // in this validation function we fake sending a request to a server
// to validate the existence of customers // to validate the existence of customers

View File

@ -549,6 +549,11 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error {
overflow-y: auto; overflow-y: auto;
} }
.jsoneditor .jsoneditor-validation-errors {
width: 100%;
overflow: hidden;
}
.jsoneditor .jsoneditor-additional-errors { .jsoneditor .jsoneditor-additional-errors {
position: absolute; position: absolute;
margin: auto; margin: auto;
@ -585,6 +590,7 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error {
.jsoneditor .jsoneditor-text-errors td pre { .jsoneditor .jsoneditor-text-errors td pre {
margin: 0; margin: 0;
white-space: normal;
} }
.jsoneditor .jsoneditor-text-errors tr { .jsoneditor .jsoneditor-text-errors tr {

View File

@ -3,14 +3,14 @@
* @param {Object} config * @param {Object} config
* @property {boolean} errorTableVisible * @property {boolean} errorTableVisible
* @property {function (boolean) : void} onToggleVisibility * @property {function (boolean) : void} onToggleVisibility
* @property {function (number) } onFocusLine * @property {function (number)} [onFocusLine]
* @property {function (number)} onChangeHeight * @property {function (number)} onChangeHeight
* @constructor * @constructor
*/ */
function ErrorTable (config) { function ErrorTable (config) {
this.errorTableVisible = config.errorTableVisible; this.errorTableVisible = config.errorTableVisible;
this.onToggleVisibility = config.onToggleVisibility; this.onToggleVisibility = config.onToggleVisibility;
this.onFocusLine = config.onFocusLine; this.onFocusLine = config.onFocusLine || function () {};
this.onChangeHeight = config.onChangeHeight; this.onChangeHeight = config.onChangeHeight;
this.dom = {}; this.dom = {};
@ -76,6 +76,7 @@ ErrorTable.prototype.setErrors = function (errors, errorLocations) {
// keep default behavior for parse errors // keep default behavior for parse errors
if (this.errorTableVisible && errors.length > 0) { if (this.errorTableVisible && errors.length > 0) {
var validationErrors = document.createElement('div'); var validationErrors = document.createElement('div');
validationErrors.className = 'jsoneditor-validation-errors';
validationErrors.innerHTML = '<table class="jsoneditor-text-errors"><tbody></tbody></table>'; validationErrors.innerHTML = '<table class="jsoneditor-text-errors"><tbody></tbody></table>';
var tbody = validationErrors.getElementsByTagName('tbody')[0]; var tbody = validationErrors.getElementsByTagName('tbody')[0];
@ -140,9 +141,9 @@ ErrorTable.prototype.setErrors = function (errors, errorLocations) {
} }
// update the status bar // update the status bar
var validationErrorsCount = errors.reduce(function (acc, curr) { var validationErrorsCount = errors.filter(function (error) {
return (curr.type === 'validation' ? ++acc: acc) return error.type !== 'error'
}, 0); }).length;
if (validationErrorsCount > 0) { if (validationErrorsCount > 0) {
this.dom.validationErrorCount.style.display = 'inline'; this.dom.validationErrorCount.style.display = 'inline';
this.dom.validationErrorCount.innerText = validationErrorsCount; this.dom.validationErrorCount.innerText = validationErrorsCount;

View File

@ -3,6 +3,8 @@
var jmespath = require('jmespath'); var jmespath = require('jmespath');
var translate = require('./i18n').translate; var translate = require('./i18n').translate;
var ModeSwitcher = require('./ModeSwitcher'); var ModeSwitcher = require('./ModeSwitcher');
var ErrorTable = require('./ErrorTable');
var textmode = require('./textmode')[0].mixin;
var showSortModal = require('./showSortModal'); var showSortModal = require('./showSortModal');
var showTransformModal = require('./showTransformModal'); var showTransformModal = require('./showTransformModal');
var MAX_PREVIEW_CHARACTERS = require('./constants').MAX_PREVIEW_CHARACTERS; var MAX_PREVIEW_CHARACTERS = require('./constants').MAX_PREVIEW_CHARACTERS;
@ -225,7 +227,22 @@ previewmode.create = function (container, options) {
} }
} }
this.errorTable = new ErrorTable({
errorTableVisible: true,
onToggleVisibility: function () {
me.validate();
},
onFocusLine: null,
onChangeHeight: function (height) {
// TODO: change CSS to using flex box, remove setting height using JavaScript
var totalHeight = height + me.dom.statusBar.clientHeight + 1;
me.content.style.marginBottom = (-totalHeight) + 'px';
me.content.style.paddingBottom = totalHeight + 'px';
}
});
this.frame.appendChild(this.content); this.frame.appendChild(this.content);
this.frame.appendChild(this.errorTable.getErrorTable());
this.container.appendChild(this.frame); this.container.appendChild(this.frame);
if (options.statusBar) { if (options.statusBar) {
@ -245,6 +262,10 @@ previewmode.create = function (container, options) {
this.dom.arrayInfo.className = 'jsoneditor-size-info'; this.dom.arrayInfo.className = 'jsoneditor-size-info';
this.dom.arrayInfo.innerText = ''; this.dom.arrayInfo.innerText = '';
statusBar.appendChild(this.dom.arrayInfo); statusBar.appendChild(this.dom.arrayInfo);
statusBar.appendChild(this.errorTable.getErrorCounter());
statusBar.appendChild(this.errorTable.getWarningIcon());
statusBar.appendChild(this.errorTable.getErrorIcon());
} }
this._renderPreview(); this._renderPreview();
@ -656,13 +677,9 @@ previewmode.executeWithBusyMessage = function (fn, message) {
} }
}; };
/** // TODO: refactor into composable functions instead of this shaky mixin-like structure
* Validate current JSON object against the configured JSON schema previewmode.validate = textmode.validate
* Throws an exception when no JSON schema is configured previewmode._renderErrors = textmode._renderErrors
*/
previewmode.validate = function () {
// FIXME: implement validate (also support custom validation)
};
// define modes // define modes
module.exports = [ module.exports = [

View File

@ -80,7 +80,6 @@ textmode.create = function (container, options) {
this.aceEditor = undefined; // ace code editor this.aceEditor = undefined; // ace code editor
this.textarea = undefined; // plain text editor (fallback when Ace is not available) this.textarea = undefined; // plain text editor (fallback when Ace is not available)
this.validateSchema = null; this.validateSchema = null;
this.validationSequence = 0;
this.annotations = []; this.annotations = [];
// create a debounced validate function // create a debounced validate function
@ -287,7 +286,6 @@ textmode.create = function (container, options) {
this.errorTable = new ErrorTable({ this.errorTable = new ErrorTable({
errorTableVisible: this.mode === 'text', errorTableVisible: this.mode === 'text',
onToggleVisibility: function () { onToggleVisibility: function () {
console.log('toggle')
me.validate(); me.validate();
}, },
onFocusLine: function (line) { onFocusLine: function (line) {
@ -787,32 +785,12 @@ textmode.updateText = function(jsonText) {
* Throws an exception when no JSON schema is configured * Throws an exception when no JSON schema is configured
*/ */
textmode.validate = function () { textmode.validate = function () {
var doValidate = false;
var schemaErrors = []; var schemaErrors = [];
var parseErrors = []; var parseErrors = [];
var json; var json;
try { try {
json = this.get(); // this can fail when there is no valid json json = this.get(); // this can fail when there is no valid json
doValidate = true;
}
catch (err) {
if (this.getText()) {
// try to extract the line number from the jsonlint error message
var match = /\w*line\s*(\d+)\w*/g.exec(err.message);
var line;
if (match) {
line = +match[1];
}
parseErrors.push({
type: 'error',
message: err.message.replace(/\n/g, '<br>'),
line: line
});
}
}
// only validate the JSON when parsing the JSON succeeded
if (doValidate) {
// execute JSON schema validation (ajv) // execute JSON schema validation (ajv)
if (this.validateSchema) { if (this.validateSchema) {
var valid = this.validateSchema(json); var valid = this.validateSchema(json);
@ -826,7 +804,7 @@ textmode.validate = function () {
// execute custom validation and after than merge and render all errors // execute custom validation and after than merge and render all errors
// TODO: implement a better mechanism for only using the last validation action // TODO: implement a better mechanism for only using the last validation action
this.validationSequence++; this.validationSequence = (this.validationSequence || 0) + 1;
var me = this; var me = this;
var seq = this.validationSequence; var seq = this.validationSequence;
validateCustom(json, this.options.onValidate) validateCustom(json, this.options.onValidate)
@ -841,8 +819,22 @@ textmode.validate = function () {
console.error('Custom validation function did throw an error', err); console.error('Custom validation function did throw an error', err);
}); });
} }
else { catch (err) {
this._renderErrors([]); if (this.getText()) {
// try to extract the line number from the jsonlint error message
var match = /\w*line\s*(\d+)\w*/g.exec(err.message);
var line;
if (match) {
line = +match[1];
}
parseErrors = [{
type: 'error',
message: err.message.replace(/\n/g, '<br>'),
line: line
}];
}
this._renderErrors(parseErrors);
} }
}; };

View File

@ -1,4 +1,6 @@
var isPromise = require('./util').isPromise; var isPromise = require('./util').isPromise;
var isValidValidationError = require('./util').isValidValidationError;
var stringifyPath = require('./util').stringifyPath;
/** /**
* Execute custom validation if configured. * Execute custom validation if configured.
@ -21,7 +23,7 @@ function validateCustom (json, onValidate) {
if (Array.isArray(customValidationPathErrors)) { if (Array.isArray(customValidationPathErrors)) {
return customValidationPathErrors return customValidationPathErrors
.filter(function (error) { .filter(function (error) {
var valid = util.isValidValidationError(error); var valid = isValidValidationError(error);
if (!valid) { if (!valid) {
console.warn('Ignoring a custom validation error with invalid structure. ' + console.warn('Ignoring a custom validation error with invalid structure. ' +
@ -34,7 +36,7 @@ function validateCustom (json, onValidate) {
.map(function (error) { .map(function (error) {
// change data structure into the structure matching the JSON schema errors // change data structure into the structure matching the JSON schema errors
return { return {
dataPath: util.stringifyPath(error.path), dataPath: stringifyPath(error.path),
message: error.message message: error.message
} }
}); });