Skip to content

Commit

Permalink
Show validation errors inline in code mode (josdejong#560)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
meirotstein authored and josdejong committed Aug 12, 2018
1 parent 6a6c34f commit d387de3
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 41 deletions.
21 changes: 18 additions & 3 deletions examples/07_json_schema_validation.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ <h1>JSON schema validation</h1>
"gender": {
"enum": ["male", "female"]
},
"availableToHire": {
"type": "boolean"
},
"age": {
"description": "Age in years",
"type": "integer",
Expand All @@ -58,12 +61,20 @@ <h1>JSON schema validation</h1>
var job = {
"title": "Job description",
"type": "object",
"required": ["address"],
"properties": {
"company": {
"type": "string"
},
"role": {
"type": "string"
},
"address": {
"type": "string"
},
"salary": {
"type": "number",
"minimum": 120
}
}
};
Expand All @@ -72,16 +83,20 @@ <h1>JSON schema validation</h1>
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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
54 changes: 54 additions & 0 deletions src/css/jsoneditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
}
12 changes: 12 additions & 0 deletions src/css/statusbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
156 changes: 118 additions & 38 deletions src/js/textmode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 &#9663;";
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');

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -637,7 +674,7 @@ textmode.setText = function(jsonText) {
this.onChangeDisabled = false;
}
// validate JSON schema
this.validate();
this._debouncedValidate();
};

/**
Expand All @@ -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 = '';
Expand All @@ -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...)')
}

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>';
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 '<tr><td><button class="jsoneditor-schema-error"></button></td>' + message + '</tr>'
}).join('') +
'</tbody>' +
'</table>';
return {};
});
me._refreshAnnotations();

this.dom.validationErrors = validationErrors;
this.dom.validationErrorsContainer.appendChild(validationErrors);
} 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 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
Expand Down
Loading

0 comments on commit d387de3

Please sign in to comment.