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

View File

@ -27,6 +27,7 @@
"brace": "0.11.0",
"javascript-natural-sort": "0.7.1",
"jmespath": "0.15.0",
"json-source-map": "^0.4.0",
"mobius1-selectr": "2.4.1",
"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 */
.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 {
width: 100%;
border-collapse: collapse;
@ -483,3 +511,29 @@ div.jsoneditor-tree .jsoneditor-schema-error {
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 {
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
var textmode = {};
var MAX_ERRORS = 3; // maximum number of displayed errors at the bottom
var DEFAULT_THEME = 'ace/theme/jsoneditor';
/**
@ -92,6 +90,7 @@ textmode.create = function (container, options) {
this.aceEditor = undefined; // ace code editor
this.textarea = undefined; // plain text editor (fallback when Ace is not available)
this.validateSchema = null;
this.annotations = [];
// create a debounced validate function
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);
var aceEditor = _ace.edit(this.editorDom);
var aceSession = aceEditor.getSession();
aceEditor.$blockScrolling = Infinity;
aceEditor.setTheme(this.theme);
aceEditor.setOptions({ readOnly: isReadOnly });
aceEditor.setShowPrintMargin(false);
aceEditor.setFontSize(13);
aceEditor.getSession().setMode('ace/mode/json');
aceEditor.getSession().setTabSize(this.indentation);
aceEditor.getSession().setUseSoftTabs(true);
aceEditor.getSession().setUseWrapMode(true);
aceSession.setMode('ace/mode/json');
aceSession.setTabSize(this.indentation);
aceSession.setUseSoftTabs(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('Command-L', null); // disable Ctrl+L (is used by the browser to select the address bar)
this.aceEditor = aceEditor;
@ -257,10 +264,20 @@ textmode.create = function (container, options) {
}
var validationErrorsContainer = document.createElement('div');
validationErrorsContainer.className = 'validation-errors-container';
validationErrorsContainer.className = 'jsoneditor-validation-errors-container';
this.dom.validationErrorsContainer = 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) {
util.addClassName(this.content, 'has-status-bar');
@ -310,6 +327,22 @@ textmode.create = function (container, options) {
statusBar.appendChild(countVal);
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);
@ -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.
*/
@ -637,7 +674,7 @@ textmode.setText = function(jsonText) {
this.onChangeDisabled = false;
}
// validate JSON schema
this.validate();
this._debouncedValidate();
};
/**
@ -660,10 +697,12 @@ textmode.updateText = function(jsonText) {
* Throws an exception when no JSON schema is configured
*/
textmode.validate = function () {
var me = this;
// clear all current errors
if (this.dom.validationErrors) {
this.dom.validationErrors.parentNode.removeChild(this.dom.validationErrors);
this.dom.validationErrors = null;
this.dom.additinalErrorsIndication.style.display = 'none';
this.content.style.marginBottom = '';
this.content.style.paddingBottom = '';
@ -690,40 +729,81 @@ textmode.validate = function () {
}
}
if (errors.length > 0) {
// limit the number of displayed errors
var limit = errors.length > MAX_ERRORS;
if (limit) {
errors = errors.slice(0, MAX_ERRORS);
var hidden = this.validateSchema.errors.length - MAX_ERRORS;
errors.push('(' + hidden + ' more errors...)')
if (errors.length > 0) {
if (this.aceEditor) {
var jsonText = this.getText();
var errorPaths = [];
errors.reduce(function(acc, curr) {
if(acc.indexOf(curr.dataPath) === -1) {
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');
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);
var height = validationErrors.clientHeight +
(this.dom.statusBar ? this.dom.statusBar.clientHeight : 0);
this.content.style.marginBottom = (-height) + 'px';
this.content.style.paddingBottom = height + 'px';
if (me.options.statusBar) {
var showIndication = !!errors.length;
me.validationErrorIndication.validationErrorIcon.style.display = showIndication ? 'inline' : 'none';
me.validationErrorIndication.validationErrorCount.style.display = showIndication ? 'inline' : 'none';
if (showIndication) {
me.validationErrorIndication.validationErrorCount.innerText = errors.length;
me.validationErrorIndication.validationErrorIcon.title = errors.length + ' schema validation error(s) found';
}
}
// update the height of the ace editor

View File

@ -1,6 +1,7 @@
'use strict';
var jsonlint = require('./assets/jsonlint/jsonlint');
var jsonMap = require('json-source-map');
/**
* Parse JSON using the parser built-in in the browser.
@ -905,6 +906,43 @@ exports.getIndexForPosition = function(el, row, column) {
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') {
// Polyfill for array remove