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:
parent
6a6c34fd00
commit
d387de366a
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue