Show validation errors inline in code mode (#560)

* util.getPositionForPath

* utils.getPositionForPath - allow multiple paths

* code mode - show validation errors on gutter

* show all validation errors with scroll indication on text mode

* import json-source-map in favor of getting validation errors location

* revert dist change

* add statusbar indication for validation errors

* reset valodation errors indication  + code clean

* change display indication for validationErrorIndication

* extend schema validatin example with additional errors to demonstrate recent changes

* minor css change
This commit is contained in:
Meir Rotstein 2018-08-12 13:41:02 +03:00 committed by Jos de Jong
parent 6a6c34fd00
commit d387de366a
6 changed files with 243 additions and 43 deletions

View File

@ -43,6 +43,9 @@
"gender": { "gender": {
"enum": ["male", "female"] "enum": ["male", "female"]
}, },
"availableToHire": {
"type": "boolean"
},
"age": { "age": {
"description": "Age in years", "description": "Age in years",
"type": "integer", "type": "integer",
@ -58,12 +61,20 @@
var job = { var job = {
"title": "Job description", "title": "Job description",
"type": "object", "type": "object",
"required": ["address"],
"properties": { "properties": {
"company": { "company": {
"type": "string" "type": "string"
}, },
"role": { "role": {
"type": "string" "type": "string"
},
"address": {
"type": "string"
},
"salary": {
"type": "number",
"minimum": 120
} }
} }
}; };
@ -72,16 +83,20 @@
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
gender: null, gender: null,
age: 28, age: "28",
availableToHire: 1,
job: { job: {
company: 'freelance', company: 'freelance',
role: 'developer' role: 'developer',
salary: 100
} }
}; };
var options = { var options = {
schema: schema, schema: schema,
schemaRefs: {"job": job} schemaRefs: {"job": job},
mode: 'tree',
modes: ['code', 'text', 'tree']
}; };
// create the editor // create the editor

View File

@ -27,6 +27,7 @@
"brace": "0.11.0", "brace": "0.11.0",
"javascript-natural-sort": "0.7.1", "javascript-natural-sort": "0.7.1",
"jmespath": "0.15.0", "jmespath": "0.15.0",
"json-source-map": "^0.4.0",
"mobius1-selectr": "2.4.1", "mobius1-selectr": "2.4.1",
"picomodal": "3.0.0" "picomodal": "3.0.0"
}, },

View File

@ -462,6 +462,34 @@ div.jsoneditor-tree .jsoneditor-schema-error {
/* JSON schema errors displayed at the bottom of the editor in mode text and code */ /* JSON schema errors displayed at the bottom of the editor in mode text and code */
.jsoneditor .jsoneditor-validation-errors-container {
max-height: 130px;
overflow-y: auto;
}
.jsoneditor .jsoneditor-additional-errors {
position: absolute;
margin: auto;
bottom: 31px;
left: calc(50% - 92px);
color: #808080;
background-color: #ebebeb;
padding: 7px 15px;
border-radius: 8px;
}
.jsoneditor .jsoneditor-additional-errors.visible{
visibility: visible;
opacity: 1;
transition: opacity 2s linear;
}
.jsoneditor .jsoneditor-additional-errors.hidden{
visibility: hidden;
opacity: 0;
transition: visibility 0s 2s, opacity 2s linear;
}
.jsoneditor .jsoneditor-text-errors { .jsoneditor .jsoneditor-text-errors {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@ -483,3 +511,29 @@ div.jsoneditor-tree .jsoneditor-schema-error {
background: url('./img/jsoneditor-icons.svg') -168px -48px; background: url('./img/jsoneditor-icons.svg') -168px -48px;
} }
.fadein {
-webkit-animation: fadein .3s;
animation: fadein .3s;
-moz-animation: fadein .3s;
-o-animation: fadein .3s;
}
@-webkit-keyframes fadein {
0% {opacity: 0}
100% {opacity: 1}
}
@-moz-keyframes fadein{
0% {opacity: 0}
100% {opacity: 1}
}
@keyframes fadein {
0% {opacity: 0}
100% {opacity: 1}
}
@-o-keyframes fadein {
0% {opacity: 0}
100% {opacity: 1}
}

View File

@ -22,3 +22,15 @@ div.jsoneditor-statusbar > .jsoneditor-curserinfo-val {
div.jsoneditor-statusbar > .jsoneditor-curserinfo-count { div.jsoneditor-statusbar > .jsoneditor-curserinfo-count {
margin-left: 4px; margin-left: 4px;
} }
div.jsoneditor-statusbar > .jsoneditor-validation-error-icon {
float: right;
width: 24px;
height: 24px;
padding: 0;
margin-top: 1px;
background: url("img/jsoneditor-icons.svg") -168px -48px;
}
div.jsoneditor-statusbar > .jsoneditor-validation-error-count {
float: right;
margin: 0 4px 0 0;
}

View File

@ -7,8 +7,6 @@ var util = require('./util');
// create a mixin with the functions for text mode // create a mixin with the functions for text mode
var textmode = {}; var textmode = {};
var MAX_ERRORS = 3; // maximum number of displayed errors at the bottom
var DEFAULT_THEME = 'ace/theme/jsoneditor'; var DEFAULT_THEME = 'ace/theme/jsoneditor';
/** /**
@ -92,6 +90,7 @@ 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.annotations = [];
// create a debounced validate function // create a debounced validate function
this._debouncedValidate = util.debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL); this._debouncedValidate = util.debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL);
@ -189,15 +188,23 @@ textmode.create = function (container, options) {
this.content.appendChild(this.editorDom); this.content.appendChild(this.editorDom);
var aceEditor = _ace.edit(this.editorDom); var aceEditor = _ace.edit(this.editorDom);
var aceSession = aceEditor.getSession();
aceEditor.$blockScrolling = Infinity; aceEditor.$blockScrolling = Infinity;
aceEditor.setTheme(this.theme); aceEditor.setTheme(this.theme);
aceEditor.setOptions({ readOnly: isReadOnly }); aceEditor.setOptions({ readOnly: isReadOnly });
aceEditor.setShowPrintMargin(false); aceEditor.setShowPrintMargin(false);
aceEditor.setFontSize(13); aceEditor.setFontSize(13);
aceEditor.getSession().setMode('ace/mode/json'); aceSession.setMode('ace/mode/json');
aceEditor.getSession().setTabSize(this.indentation); aceSession.setTabSize(this.indentation);
aceEditor.getSession().setUseSoftTabs(true); aceSession.setUseSoftTabs(true);
aceEditor.getSession().setUseWrapMode(true); aceSession.setUseWrapMode(true);
// replace ace setAnnotations with custom function that also covers jsoneditor annotations
var originalSetAnnotations = aceSession.setAnnotations;
aceSession.setAnnotations = function (annotations) {
originalSetAnnotations.call(this, annotations && annotations.length ? annotations : me.annotations);
};
aceEditor.commands.bindKey('Ctrl-L', null); // disable Ctrl+L (is used by the browser to select the address bar) aceEditor.commands.bindKey('Ctrl-L', null); // disable Ctrl+L (is used by the browser to select the address bar)
aceEditor.commands.bindKey('Command-L', null); // disable Ctrl+L (is used by the browser to select the address bar) aceEditor.commands.bindKey('Command-L', null); // disable Ctrl+L (is used by the browser to select the address bar)
this.aceEditor = aceEditor; this.aceEditor = aceEditor;
@ -257,10 +264,20 @@ textmode.create = function (container, options) {
} }
var validationErrorsContainer = document.createElement('div'); var validationErrorsContainer = document.createElement('div');
validationErrorsContainer.className = 'validation-errors-container'; validationErrorsContainer.className = 'jsoneditor-validation-errors-container';
this.dom.validationErrorsContainer = validationErrorsContainer; this.dom.validationErrorsContainer = validationErrorsContainer;
this.frame.appendChild(validationErrorsContainer); this.frame.appendChild(validationErrorsContainer);
var additinalErrorsIndication = document.createElement('div');
additinalErrorsIndication.style.display = 'none';
additinalErrorsIndication.className = "jsoneditor-additional-errors fadein";
additinalErrorsIndication.innerHTML = "Scroll for more ▿";
this.dom.additinalErrorsIndication = additinalErrorsIndication;
validationErrorsContainer.appendChild(additinalErrorsIndication);
validationErrorsContainer.onscroll = function () {
additinalErrorsIndication.style.display = me.dom.validationErrorsContainer.scrollTop === 0 ? 'block' : 'none';
}
if (options.statusBar) { if (options.statusBar) {
util.addClassName(this.content, 'has-status-bar'); util.addClassName(this.content, 'has-status-bar');
@ -310,6 +327,22 @@ textmode.create = function (container, options) {
statusBar.appendChild(countVal); statusBar.appendChild(countVal);
statusBar.appendChild(countLabel); statusBar.appendChild(countLabel);
var validationErrorIcon = document.createElement('span');
validationErrorIcon.className = 'jsoneditor-validation-error-icon';
validationErrorIcon.style.display = 'none';
var validationErrorCount = document.createElement('span');
validationErrorCount.className = 'jsoneditor-validation-error-count';
validationErrorCount.style.display = 'none';
this.validationErrorIndication = {
validationErrorIcon: validationErrorIcon,
validationErrorCount: validationErrorCount
};
statusBar.appendChild(validationErrorCount);
statusBar.appendChild(validationErrorIcon);
} }
this.setSchema(this.options.schema, this.options.schemaRefs); this.setSchema(this.options.schema, this.options.schemaRefs);
@ -486,6 +519,10 @@ textmode._emitSelectionChange = function () {
} }
} }
textmode._refreshAnnotations = function () {
this.aceEditor && this.aceEditor.getSession().setAnnotations();
}
/** /**
* Destroy the editor. Clean up DOM, event listeners, and web workers. * Destroy the editor. Clean up DOM, event listeners, and web workers.
*/ */
@ -637,7 +674,7 @@ textmode.setText = function(jsonText) {
this.onChangeDisabled = false; this.onChangeDisabled = false;
} }
// validate JSON schema // validate JSON schema
this.validate(); this._debouncedValidate();
}; };
/** /**
@ -660,10 +697,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 me = this;
// clear all current errors // clear all current errors
if (this.dom.validationErrors) { if (this.dom.validationErrors) {
this.dom.validationErrors.parentNode.removeChild(this.dom.validationErrors); this.dom.validationErrors.parentNode.removeChild(this.dom.validationErrors);
this.dom.validationErrors = null; this.dom.validationErrors = null;
this.dom.additinalErrorsIndication.style.display = 'none';
this.content.style.marginBottom = ''; this.content.style.marginBottom = '';
this.content.style.paddingBottom = ''; this.content.style.paddingBottom = '';
@ -690,40 +729,81 @@ textmode.validate = function () {
} }
} }
if (errors.length > 0) { if (errors.length > 0) {
// limit the number of displayed errors if (this.aceEditor) {
var limit = errors.length > MAX_ERRORS; var jsonText = this.getText();
if (limit) { var errorPaths = [];
errors = errors.slice(0, MAX_ERRORS); errors.reduce(function(acc, curr) {
var hidden = this.validateSchema.errors.length - MAX_ERRORS; if(acc.indexOf(curr.dataPath) === -1) {
errors.push('(' + hidden + ' more errors...)') acc.push(curr.dataPath);
};
return acc;
}, errorPaths);
var errorLocations = util.getPositionForPath(jsonText, errorPaths);
me.annotations = errorLocations.map(function (errLoc) {
var validationErrors = errors.filter(function(err){ return err.dataPath === errLoc.path; });
var validationError = validationErrors.reduce(function(acc, curr) { acc.message += '\n' + curr.message; return acc; });
if (validationError) {
return {
row: errLoc.line,
column: errLoc.column,
text: "Schema Validation Error: \n" + validationError.message,
type: "warning",
source: "jsoneditor",
}
}
return {};
});
me._refreshAnnotations();
} else {
var validationErrors = document.createElement('div');
validationErrors.innerHTML = '<table class="jsoneditor-text-errors">' +
'<tbody>' +
errors.map(function (error) {
var message;
if (typeof error === 'string') {
message = '<td colspan="2"><pre>' + error + '</pre></td>';
}
else {
message = '<td>' + error.dataPath + '</td>' +
'<td>' + error.message + '</td>';
}
return '<tr><td><button class="jsoneditor-schema-error"></button></td>' + message + '</tr>'
}).join('') +
'</tbody>' +
'</table>';
this.dom.validationErrors = validationErrors;
this.dom.validationErrorsContainer.appendChild(validationErrors);
this.dom.additinalErrorsIndication.title = errors.length + " errors total";
if (this.dom.validationErrorsContainer.clientHeight < this.dom.validationErrorsContainer.scrollHeight) {
this.dom.additinalErrorsIndication.style.display = 'block';
}
var height = this.dom.validationErrorsContainer.clientHeight + (this.dom.statusBar ? this.dom.statusBar.clientHeight : 0);
// var height = validationErrors.clientHeight + (this.dom.statusBar ? this.dom.statusBar.clientHeight : 0);
this.content.style.marginBottom = (-height) + 'px';
this.content.style.paddingBottom = height + 'px';
} }
} else {
if (this.aceEditor) {
me.annotations = [];
me._refreshAnnotations();
}
}
var validationErrors = document.createElement('div'); if (me.options.statusBar) {
validationErrors.innerHTML = '<table class="jsoneditor-text-errors">' + var showIndication = !!errors.length;
'<tbody>' + me.validationErrorIndication.validationErrorIcon.style.display = showIndication ? 'inline' : 'none';
errors.map(function (error) { me.validationErrorIndication.validationErrorCount.style.display = showIndication ? 'inline' : 'none';
var message; if (showIndication) {
if (typeof error === 'string') { me.validationErrorIndication.validationErrorCount.innerText = errors.length;
message = '<td colspan="2"><pre>' + error + '</pre></td>'; me.validationErrorIndication.validationErrorIcon.title = errors.length + ' schema validation error(s) found';
} }
else {
message = '<td>' + error.dataPath + '</td>' +
'<td>' + error.message + '</td>';
}
return '<tr><td><button class="jsoneditor-schema-error"></button></td>' + message + '</tr>'
}).join('') +
'</tbody>' +
'</table>';
this.dom.validationErrors = validationErrors;
this.dom.validationErrorsContainer.appendChild(validationErrors);
var height = validationErrors.clientHeight +
(this.dom.statusBar ? this.dom.statusBar.clientHeight : 0);
this.content.style.marginBottom = (-height) + 'px';
this.content.style.paddingBottom = height + 'px';
} }
// update the height of the ace editor // update the height of the ace editor

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
var jsonlint = require('./assets/jsonlint/jsonlint'); var jsonlint = require('./assets/jsonlint/jsonlint');
var jsonMap = require('json-source-map');
/** /**
* Parse JSON using the parser built-in in the browser. * Parse JSON using the parser built-in in the browser.
@ -905,6 +906,43 @@ exports.getIndexForPosition = function(el, row, column) {
return -1; return -1;
} }
/**
* Returns location of json paths in certain json string
* @param {String} text json string
* @param {Array<String>} paths array of json paths
* @returns {Array<{path: String, line: Number, row: Number}>}
*/
exports.getPositionForPath = function(text, paths) {
var me = this;
var result = [];
var jsmap;
if (!paths || !paths.length) {
return result;
}
try {
jsmap = jsonMap.parse(text);
} catch (err) {
return result;
}
paths.forEach(function (path) {
var pathArr = me.parsePath(path);
var pointerName = pathArr.length ? "/" + pathArr.join("/") : "";
var pointer = jsmap.pointers[pointerName];
if (pointer) {
result.push({
path: path,
line: pointer.key ? pointer.key.line : (pointer.value ? pointer.value.line : 0),
column: pointer.key ? pointer.key.column : (pointer.value ? pointer.value.column : 0)
});
}
});
return result;
}
if (typeof Element !== 'undefined') { if (typeof Element !== 'undefined') {
// Polyfill for array remove // Polyfill for array remove