Improvements for errors panel (#567)
* 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 * text mode: navigation from error to code * bugfix: validation errors scroll indication remains on json error * show parse errors on the editor bottom and add a status bar indication * give more helpful tooltip for parse error * errors container - set onscroll only when needed * (1) Json parse erros: replace jsonLint errors newline with breakdown (2) scroll to line on text selection (3) bugfix: 'show more errors' indication stays when there are no errors
This commit is contained in:
parent
ed28e91b9f
commit
d720a94d45
|
@ -374,6 +374,11 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error {
|
||||||
background: url('./img/jsoneditor-icons.svg') -168px -48px;
|
background: url('./img/jsoneditor-icons.svg') -168px -48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jsoneditor-text-errors tr.jump-to-line:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.jsoneditor-schema-error .jsoneditor-popover {
|
.jsoneditor-schema-error .jsoneditor-popover {
|
||||||
background-color: #4c4c4c;
|
background-color: #4c4c4c;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -517,14 +522,21 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error {
|
||||||
|
|
||||||
.jsoneditor .jsoneditor-text-errors {
|
.jsoneditor .jsoneditor-text-errors {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
background-color: #ffef8b;
|
|
||||||
border-top: 1px solid #ffd700;
|
border-top: 1px solid #ffd700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor .jsoneditor-text-errors td {
|
.jsoneditor .jsoneditor-text-errors td {
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor .jsoneditor-text-errors tr {
|
||||||
|
background-color: #ffef8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor .jsoneditor-text-errors tr.parse-error {
|
||||||
|
background-color: #ee2e2e70;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor-text-errors .jsoneditor-schema-error {
|
.jsoneditor-text-errors .jsoneditor-schema-error {
|
||||||
|
@ -533,9 +545,17 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 4px 0 0;
|
margin: 0 4px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor-text-errors tr .jsoneditor-schema-error {
|
||||||
background: url('./img/jsoneditor-icons.svg') -168px -48px;
|
background: url('./img/jsoneditor-icons.svg') -168px -48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jsoneditor-text-errors tr.parse-error .jsoneditor-schema-error {
|
||||||
|
background: url('./img/jsoneditor-icons.svg') -25px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.fadein {
|
.fadein {
|
||||||
-webkit-animation: fadein .3s;
|
-webkit-animation: fadein .3s;
|
||||||
animation: fadein .3s;
|
animation: fadein .3s;
|
||||||
|
|
|
@ -34,3 +34,11 @@ div.jsoneditor-statusbar > .jsoneditor-validation-error-count {
|
||||||
float: right;
|
float: right;
|
||||||
margin: 0 4px 0 0;
|
margin: 0 4px 0 0;
|
||||||
}
|
}
|
||||||
|
div.jsoneditor-statusbar > .jsoneditor-parse-error-icon {
|
||||||
|
float: right;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1px;
|
||||||
|
background: url("img/jsoneditor-icons.svg") -25px 0px;
|
||||||
|
}
|
||||||
|
|
|
@ -275,9 +275,6 @@ textmode.create = function (container, options) {
|
||||||
additinalErrorsIndication.innerHTML = "Scroll for more ▿";
|
additinalErrorsIndication.innerHTML = "Scroll for more ▿";
|
||||||
this.dom.additinalErrorsIndication = additinalErrorsIndication;
|
this.dom.additinalErrorsIndication = additinalErrorsIndication;
|
||||||
validationErrorsContainer.appendChild(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');
|
||||||
|
@ -344,6 +341,11 @@ textmode.create = function (container, options) {
|
||||||
|
|
||||||
statusBar.appendChild(validationErrorCount);
|
statusBar.appendChild(validationErrorCount);
|
||||||
statusBar.appendChild(validationErrorIcon);
|
statusBar.appendChild(validationErrorIcon);
|
||||||
|
|
||||||
|
this.parseErrorIndication = document.createElement('span');
|
||||||
|
this.parseErrorIndication.className = 'jsoneditor-parse-error-icon';
|
||||||
|
this.parseErrorIndication.style.display = 'none';
|
||||||
|
statusBar.appendChild(this.parseErrorIndication);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSchema(this.options.schema, this.options.schemaRefs);
|
this.setSchema(this.options.schema, this.options.schemaRefs);
|
||||||
|
@ -440,8 +442,16 @@ textmode._onMouseDown = function (event) {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
textmode._onBlur = function (event) {
|
textmode._onBlur = function (event) {
|
||||||
this._updateCursorInfo();
|
var me = this;
|
||||||
this._emitSelectionChange();
|
// this allows to avoid blur when clicking inner elements (like the errors panel)
|
||||||
|
// just make sure to set the isFocused to true on the inner element onclick callback
|
||||||
|
setTimeout(function(){
|
||||||
|
if (!me.isFocused) {
|
||||||
|
me._updateCursorInfo();
|
||||||
|
me._emitSelectionChange();
|
||||||
|
}
|
||||||
|
me.isFocused = false;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -700,13 +710,29 @@ textmode.updateText = function(jsonText) {
|
||||||
textmode.validate = function () {
|
textmode.validate = function () {
|
||||||
var doValidate = false;
|
var doValidate = false;
|
||||||
var schemaErrors = [];
|
var schemaErrors = [];
|
||||||
|
var parseErrors = [];
|
||||||
var json;
|
var json;
|
||||||
try {
|
try {
|
||||||
json = this.get(); // this can fail when there is no valid json
|
json = this.get(); // this can fail when there is no valid json
|
||||||
|
this.parseErrorIndication.style.display = 'none';
|
||||||
doValidate = true;
|
doValidate = true;
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// no valid JSON, don't validate
|
if (this.getText()) {
|
||||||
|
this.parseErrorIndication.style.display = 'block';
|
||||||
|
// try to extract the line number from the jsonlint error message
|
||||||
|
var match = /\w*line\s*(\d+)\w*/g.exec(err.message);
|
||||||
|
var line;
|
||||||
|
if (match) {
|
||||||
|
line = +match[1];
|
||||||
|
}
|
||||||
|
this.parseErrorIndication.title = !isNaN(line) ? ('parse error on line ' + line) : 'parse error - check that the json is valid';
|
||||||
|
parseErrors.push({
|
||||||
|
type: 'error',
|
||||||
|
message: err.message.replace(/\n/g, '<br>'),
|
||||||
|
line: line
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only validate the JSON when parsing the JSON succeeded
|
// only validate the JSON when parsing the JSON succeeded
|
||||||
|
@ -716,6 +742,7 @@ textmode.validate = function () {
|
||||||
var valid = this.validateSchema(json);
|
var valid = this.validateSchema(json);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
schemaErrors = this.validateSchema.errors.map(function (error) {
|
schemaErrors = this.validateSchema.errors.map(function (error) {
|
||||||
|
error.type = "validation";
|
||||||
return util.improveSchemaError(error);
|
return util.improveSchemaError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -729,8 +756,8 @@ textmode.validate = function () {
|
||||||
.then(function (customValidationErrors) {
|
.then(function (customValidationErrors) {
|
||||||
// only apply when there was no other validation started whilst resolving async results
|
// only apply when there was no other validation started whilst resolving async results
|
||||||
if (seq === me.validationSequence) {
|
if (seq === me.validationSequence) {
|
||||||
var errors = schemaErrors.concat(customValidationErrors || []);
|
var errors = schemaErrors.concat(parseErrors || []).concat(customValidationErrors || []);
|
||||||
me._renderValidationErrors(errors);
|
me._renderErrors(errors);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
|
@ -738,7 +765,7 @@ textmode.validate = function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this._renderValidationErrors([]);
|
this._renderErrors(parseErrors || []);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -791,8 +818,11 @@ textmode._validateCustom = function (json) {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
textmode._renderValidationErrors = function(errors) {
|
textmode._renderErrors = function(errors) {
|
||||||
// clear all current errors
|
// clear all current errors
|
||||||
|
var me = this;
|
||||||
|
var validationErrorsCount = 0;
|
||||||
|
|
||||||
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;
|
||||||
|
@ -802,18 +832,19 @@ textmode._renderValidationErrors = function(errors) {
|
||||||
this.content.style.paddingBottom = '';
|
this.content.style.paddingBottom = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
// render the new errors
|
// render the new errors
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
if (this.aceEditor) {
|
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);
|
|
||||||
this.annotations = errorLocations.map(function (errLoc) {
|
this.annotations = errorLocations.map(function (errLoc) {
|
||||||
var validationErrors = errors.filter(function(err){ return err.dataPath === errLoc.path; });
|
var validationErrors = errors.filter(function(err){ return err.dataPath === errLoc.path; });
|
||||||
var message = validationErrors.map(function(err) { return err.message }).join('\n');
|
var message = validationErrors.map(function(err) { return err.message }).join('\n');
|
||||||
|
@ -833,22 +864,50 @@ textmode._renderValidationErrors = function(errors) {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
var validationErrors = document.createElement('div');
|
var validationErrors = document.createElement('div');
|
||||||
validationErrors.innerHTML = '<table class="jsoneditor-text-errors">' +
|
validationErrors.innerHTML = '<table class="jsoneditor-text-errors"><tbody></tbody></table>';
|
||||||
'<tbody>' +
|
var tbody = validationErrors.getElementsByTagName('tbody')[0];
|
||||||
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>'
|
errors.forEach(function (error) {
|
||||||
}).join('') +
|
var message;
|
||||||
'</tbody>' +
|
if (typeof error === 'string') {
|
||||||
'</table>';
|
message = '<td colspan="2"><pre>' + error + '</pre></td>';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message =
|
||||||
|
'<td>' + (error.dataPath || '') + '</td>' +
|
||||||
|
'<td>' + error.message + '</td>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var line;
|
||||||
|
|
||||||
|
if (!isNaN(error.line)) {
|
||||||
|
line = error.line;
|
||||||
|
} else if (error.dataPath) {
|
||||||
|
var errLoc = errorLocations.find(function(loc) { return loc.path === error.dataPath; });
|
||||||
|
if (errLoc) {
|
||||||
|
line = errLoc.line + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var trEl = document.createElement('tr');
|
||||||
|
trEl.className = !isNaN(line) ? 'jump-to-line' : '';
|
||||||
|
if (error.type === 'error') {
|
||||||
|
trEl.className += ' parse-error';
|
||||||
|
} else {
|
||||||
|
trEl.className += ' validation-error';
|
||||||
|
++validationErrorsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
trEl.innerHTML = ('<td><button class="jsoneditor-schema-error"></button></td><td style="white-space:nowrap;">'+ (!isNaN(line) ? ('Ln ' + line) : '') +'</td>' + message);
|
||||||
|
trEl.onclick = function() {
|
||||||
|
me.isFocused = true;
|
||||||
|
if (!isNaN(line)) {
|
||||||
|
me.setTextSelection({row: line, column: 1}, {row: line, column: 1000});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tbody.appendChild(trEl);
|
||||||
|
});
|
||||||
|
|
||||||
this.dom.validationErrors = validationErrors;
|
this.dom.validationErrors = validationErrors;
|
||||||
this.dom.validationErrorsContainer.appendChild(validationErrors);
|
this.dom.validationErrorsContainer.appendChild(validationErrors);
|
||||||
|
@ -856,10 +915,15 @@ textmode._renderValidationErrors = function(errors) {
|
||||||
|
|
||||||
if (this.dom.validationErrorsContainer.clientHeight < this.dom.validationErrorsContainer.scrollHeight) {
|
if (this.dom.validationErrorsContainer.clientHeight < this.dom.validationErrorsContainer.scrollHeight) {
|
||||||
this.dom.additinalErrorsIndication.style.display = 'block';
|
this.dom.additinalErrorsIndication.style.display = 'block';
|
||||||
|
this.dom.validationErrorsContainer.onscroll = function () {
|
||||||
|
me.dom.additinalErrorsIndication.style.display =
|
||||||
|
(me.dom.validationErrorsContainer.clientHeight > 0 && me.dom.validationErrorsContainer.scrollTop === 0) ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.dom.validationErrorsContainer.onscroll = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
var height = this.dom.validationErrorsContainer.clientHeight + (this.dom.statusBar ? this.dom.statusBar.clientHeight : 0);
|
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.marginBottom = (-height) + 'px';
|
||||||
this.content.style.paddingBottom = height + 'px';
|
this.content.style.paddingBottom = height + 'px';
|
||||||
}
|
}
|
||||||
|
@ -871,12 +935,13 @@ textmode._renderValidationErrors = function(errors) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.statusBar) {
|
if (this.options.statusBar) {
|
||||||
var showIndication = !!errors.length;
|
validationErrorsCount = validationErrorsCount || this.annotations.length;
|
||||||
|
var showIndication = !!validationErrorsCount;
|
||||||
this.validationErrorIndication.validationErrorIcon.style.display = showIndication ? 'inline' : 'none';
|
this.validationErrorIndication.validationErrorIcon.style.display = showIndication ? 'inline' : 'none';
|
||||||
this.validationErrorIndication.validationErrorCount.style.display = showIndication ? 'inline' : 'none';
|
this.validationErrorIndication.validationErrorCount.style.display = showIndication ? 'inline' : 'none';
|
||||||
if (showIndication) {
|
if (showIndication) {
|
||||||
this.validationErrorIndication.validationErrorCount.innerText = errors.length;
|
this.validationErrorIndication.validationErrorCount.innerText = validationErrorsCount;
|
||||||
this.validationErrorIndication.validationErrorIcon.title = errors.length + ' schema validation error(s) found';
|
this.validationErrorIndication.validationErrorIcon.title = validationErrorsCount + ' schema validation error(s) found';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -967,7 +1032,7 @@ textmode.setTextSelection = function (startPos, endPos) {
|
||||||
var startIndex = util.getIndexForPosition(this.textarea, startPos.row, startPos.column);
|
var startIndex = util.getIndexForPosition(this.textarea, startPos.row, startPos.column);
|
||||||
var endIndex = util.getIndexForPosition(this.textarea, endPos.row, endPos.column);
|
var endIndex = util.getIndexForPosition(this.textarea, endPos.row, endPos.column);
|
||||||
if (startIndex > -1 && endIndex > -1) {
|
if (startIndex > -1 && endIndex > -1) {
|
||||||
if (this.textarea.setSelectionRange) {
|
if (this.textarea.setSelectionRange) {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
this.textarea.setSelectionRange(startIndex, endIndex);
|
this.textarea.setSelectionRange(startIndex, endIndex);
|
||||||
} else if (this.textarea.createTextRange) { // IE < 9
|
} else if (this.textarea.createTextRange) { // IE < 9
|
||||||
|
@ -977,6 +1042,10 @@ textmode.setTextSelection = function (startPos, endPos) {
|
||||||
range.moveStart('character', startIndex);
|
range.moveStart('character', startIndex);
|
||||||
range.select();
|
range.select();
|
||||||
}
|
}
|
||||||
|
var rows = (this.textarea.value.match(/\n/g) || []).length + 1;
|
||||||
|
var lineHeight = this.textarea.scrollHeight / rows;
|
||||||
|
var selectionScrollPos = (startPos.row * lineHeight);
|
||||||
|
this.textarea.scrollTop = selectionScrollPos > this.textarea.clientHeight ? (selectionScrollPos - (this.textarea.clientHeight / 2)) : 0;
|
||||||
}
|
}
|
||||||
} else if (this.aceEditor) {
|
} else if (this.aceEditor) {
|
||||||
var range = {
|
var range = {
|
||||||
|
|
Loading…
Reference in New Issue