From d720a94d451a4119ea041b1ac2103fe339466b25 Mon Sep 17 00:00:00 2001 From: Meir Rotstein Date: Mon, 27 Aug 2018 20:47:22 +0300 Subject: [PATCH] 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 --- src/css/jsoneditor.css | 26 +++++++- src/css/statusbar.css | 8 +++ src/js/textmode.js | 147 ++++++++++++++++++++++++++++++----------- 3 files changed, 139 insertions(+), 42 deletions(-) diff --git a/src/css/jsoneditor.css b/src/css/jsoneditor.css index 56f675c..dd537f5 100644 --- a/src/css/jsoneditor.css +++ b/src/css/jsoneditor.css @@ -374,6 +374,11 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error { 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 { background-color: #4c4c4c; border-radius: 3px; @@ -517,14 +522,21 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error { .jsoneditor .jsoneditor-text-errors { width: 100%; - border-collapse: collapse; - background-color: #ffef8b; + border-collapse: collapse; border-top: 1px solid #ffd700; } .jsoneditor .jsoneditor-text-errors td { 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 { @@ -533,9 +545,17 @@ div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error { height: 24px; padding: 0; margin: 0 4px 0 0; + cursor: pointer; +} + +.jsoneditor-text-errors tr .jsoneditor-schema-error { 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 { -webkit-animation: fadein .3s; animation: fadein .3s; diff --git a/src/css/statusbar.css b/src/css/statusbar.css index 73e097b..57ea374 100644 --- a/src/css/statusbar.css +++ b/src/css/statusbar.css @@ -34,3 +34,11 @@ div.jsoneditor-statusbar > .jsoneditor-validation-error-count { float: right; 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; +} diff --git a/src/js/textmode.js b/src/js/textmode.js index 7f39df2..db605c5 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -275,9 +275,6 @@ textmode.create = function (container, options) { 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'); @@ -344,6 +341,11 @@ textmode.create = function (container, options) { statusBar.appendChild(validationErrorCount); 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); @@ -440,8 +442,16 @@ textmode._onMouseDown = function (event) { * @private */ textmode._onBlur = function (event) { - this._updateCursorInfo(); - this._emitSelectionChange(); + var me = this; + // 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 () { var doValidate = false; var schemaErrors = []; + var parseErrors = []; var json; try { json = this.get(); // this can fail when there is no valid json + this.parseErrorIndication.style.display = 'none'; doValidate = true; } 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, '
'), + line: line + }); + } } // only validate the JSON when parsing the JSON succeeded @@ -716,6 +742,7 @@ textmode.validate = function () { var valid = this.validateSchema(json); if (!valid) { schemaErrors = this.validateSchema.errors.map(function (error) { + error.type = "validation"; return util.improveSchemaError(error); }); } @@ -729,8 +756,8 @@ textmode.validate = function () { .then(function (customValidationErrors) { // only apply when there was no other validation started whilst resolving async results if (seq === me.validationSequence) { - var errors = schemaErrors.concat(customValidationErrors || []); - me._renderValidationErrors(errors); + var errors = schemaErrors.concat(parseErrors || []).concat(customValidationErrors || []); + me._renderErrors(errors); } }) .catch(function (err) { @@ -738,7 +765,7 @@ textmode.validate = function () { }); } else { - this._renderValidationErrors([]); + this._renderErrors(parseErrors || []); } }; @@ -791,8 +818,11 @@ textmode._validateCustom = function (json) { return Promise.resolve(null); }; -textmode._renderValidationErrors = function(errors) { +textmode._renderErrors = function(errors) { // clear all current errors + var me = this; + var validationErrorsCount = 0; + if (this.dom.validationErrors) { this.dom.validationErrors.parentNode.removeChild(this.dom.validationErrors); this.dom.validationErrors = null; @@ -802,18 +832,19 @@ textmode._renderValidationErrors = function(errors) { 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 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); this.annotations = errorLocations.map(function (errLoc) { var validationErrors = errors.filter(function(err){ return err.dataPath === errLoc.path; }); var message = validationErrors.map(function(err) { return err.message }).join('\n'); @@ -833,22 +864,50 @@ textmode._renderValidationErrors = function(errors) { } else { var validationErrors = document.createElement('div'); - validationErrors.innerHTML = '' + - '' + - errors.map(function (error) { - var message; - if (typeof error === 'string') { - message = ''; - } - else { - message = '' + - ''; - } + validationErrors.innerHTML = '
' + error + '
' + error.dataPath + '' + error.message + '
'; + var tbody = validationErrors.getElementsByTagName('tbody')[0]; - return '' + message + '' - }).join('') + - '' + - ''; + errors.forEach(function (error) { + var message; + if (typeof error === 'string') { + message = '
' + error + '
'; + } + else { + message = + '' + (error.dataPath || '') + '' + + '' + error.message + ''; + } + + 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 = (''+ (!isNaN(line) ? ('Ln ' + line) : '') +'' + 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.validationErrorsContainer.appendChild(validationErrors); @@ -856,10 +915,15 @@ textmode._renderValidationErrors = function(errors) { if (this.dom.validationErrorsContainer.clientHeight < this.dom.validationErrorsContainer.scrollHeight) { 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 = validationErrors.clientHeight + (this.dom.statusBar ? this.dom.statusBar.clientHeight : 0); this.content.style.marginBottom = (-height) + 'px'; this.content.style.paddingBottom = height + 'px'; } @@ -871,12 +935,13 @@ textmode._renderValidationErrors = function(errors) { } 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.validationErrorCount.style.display = showIndication ? 'inline' : 'none'; if (showIndication) { - this.validationErrorIndication.validationErrorCount.innerText = errors.length; - this.validationErrorIndication.validationErrorIcon.title = errors.length + ' schema validation error(s) found'; + this.validationErrorIndication.validationErrorCount.innerText = validationErrorsCount; + 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 endIndex = util.getIndexForPosition(this.textarea, endPos.row, endPos.column); if (startIndex > -1 && endIndex > -1) { - if (this.textarea.setSelectionRange) { + if (this.textarea.setSelectionRange) { this.textarea.focus(); this.textarea.setSelectionRange(startIndex, endIndex); } else if (this.textarea.createTextRange) { // IE < 9 @@ -977,6 +1042,10 @@ textmode.setTextSelection = function (startPos, endPos) { range.moveStart('character', startIndex); 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) { var range = {