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": {
|
||||
"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
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = '';
|
||||
|
@ -691,14 +730,34 @@ 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 (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>' +
|
||||
|
@ -719,12 +778,33 @@ textmode.validate = function () {
|
|||
|
||||
this.dom.validationErrors = validationErrors;
|
||||
this.dom.validationErrorsContainer.appendChild(validationErrors);
|
||||
this.dom.additinalErrorsIndication.title = errors.length + " errors total";
|
||||
|
||||
var height = validationErrors.clientHeight +
|
||||
(this.dom.statusBar ? this.dom.statusBar.clientHeight : 0);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (this.aceEditor) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue