From 70810655b8eb1234abda292f01b2d62ad3851a78 Mon Sep 17 00:00:00 2001 From: jos Date: Sat, 12 Nov 2016 15:22:55 +0100 Subject: [PATCH] Implemented JSON schema support for tree/form/view mode --- src/components/JSONNode.js | 55 ++++++++++++++++-- src/components/TreeMode.js | 65 ++++++++++++++++++--- src/develop.html | 4 +- src/jsonData.js | 14 ++--- src/jsoneditor.less | 25 ++++---- src/popover.less | 116 +++++++++++++++++++++++++++++++++++++ src/utils/domUtils.js | 12 ++++ 7 files changed, 258 insertions(+), 33 deletions(-) create mode 100644 src/popover.less diff --git a/src/components/JSONNode.js b/src/components/JSONNode.js index b76903b..903845e 100644 --- a/src/components/JSONNode.js +++ b/src/components/JSONNode.js @@ -3,7 +3,7 @@ import { h, Component } from 'preact' import ActionButton from './menu/ActionButton' import AppendActionButton from './menu/AppendActionButton' import { escapeHTML, unescapeHTML } from '../utils/stringUtils' -import { getInnerText } from '../utils/domUtils' +import { getInnerText, insideRect } from '../utils/domUtils' import { stringConvert, valueType, isUrl } from '../utils/typeUtils' /** @@ -43,7 +43,8 @@ export default class JSONNode extends Component { this.renderExpandButton(), this.renderActionMenuButton(), this.renderProperty(prop, data, options), - this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`) + this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`), + this.renderError(data.error) ]) ] @@ -79,7 +80,8 @@ export default class JSONNode extends Component { this.renderExpandButton(), this.renderActionMenuButton(), this.renderProperty(prop, data, options), - this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`) + this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`), + this.renderError(data.error) ]) ] @@ -114,7 +116,8 @@ export default class JSONNode extends Component { this.renderActionMenuButton(), this.renderProperty(prop, data, options), this.renderSeparator(), - this.renderValue(data.value, options) + this.renderValue(data.value, options), + this.renderError(data.error) ]) ]) } @@ -206,6 +209,50 @@ export default class JSONNode extends Component { } } + renderError (error) { + if (error) { + return h('button', { + type: 'button', + class: 'jsoneditor-schema-error', + onFocus: this.updatePopoverDirection, + onMouseOver: this.updatePopoverDirection + }, + h('div', {class: 'jsoneditor-popover jsoneditor-right'}, error.message) + ) + } + else { + return null + } + } + + /** + * Find the best position for the popover: right, above, below, or left + * from the warning icon. + * @param event + */ + updatePopoverDirection = (event) => { + if (event.target.nodeName === 'BUTTON') { + const popover = event.target.firstChild + + const directions = ['right', 'above', 'below', 'left'] + for (let i = 0; i < directions.length; i++) { + const direction = directions[i] + popover.className = 'jsoneditor-popover jsoneditor-' + direction + + // FIXME: the contentRect is that of the whole contents, not the visible window + const contents = this.base.parentNode.parentNode + const contentRect = contents.getBoundingClientRect() + const popoverRect = popover.getBoundingClientRect() + const margin = 20 // account for a scroll bar + + if (insideRect(contentRect, popoverRect, margin)) { + // we found a location that fits, stop here + break + } + } + } + } + /** * Note: this function manipulates the className and title of the editable div * outside of Preact, so the user gets immediate feedback diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index 6967c72..69e1530 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -1,16 +1,28 @@ import { h, Component } from 'preact' +import Ajv from 'ajv' import { updateIn, getIn } from '../utils/immutabilityHelpers' -import { expand, jsonToData, dataToJson, toDataPath, patchData, pathExists } from '../jsonData' import { parseJSON } from '../utils/jsonUtils' +import { enrichSchemaError } from '../utils/schemaUtils' import { - duplicate, insert, append, remove, changeType, changeValue, changeProperty, sort + jsonToData, dataToJson, toDataPath, patchData, pathExists, + expand, addErrors +} from '../jsonData' +import { + duplicate, insert, append, remove, + changeType, changeValue, changeProperty, sort } from '../actions' import JSONNode from './JSONNode' import JSONNodeView from './JSONNodeView' import JSONNodeForm from './JSONNodeForm' import ModeButton from './menu/ModeButton' +const AJV_OPTIONS = { + allErrors: true, + verbose: true, + jsonPointers: true +} + const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept in memory export default class TreeMode extends Component { @@ -49,23 +61,25 @@ export default class TreeMode extends Component { ? JSONNodeForm : JSONNode + const data = addErrors(state.data, this.getErrors()) + return h('div', { class: `jsoneditor jsoneditor-mode-${props.mode}`, 'data-jsoneditor': 'true' }, [ this.renderMenu(), - h('div', {class: 'jsoneditor-contents jsoneditor-tree-contents', onClick: this.handleHideMenus}, [ - h('ul', {class: 'jsoneditor-list jsoneditor-root'}, [ + h('div', {class: 'jsoneditor-contents jsoneditor-tree-contents', onClick: this.handleHideMenus}, + h('ul', {class: 'jsoneditor-list jsoneditor-root'}, h(Node, { - data: state.data, + data, events: state.events, options: props.options, parent: null, prop: null }) - ]) - ]) + ) + ) ]) } @@ -120,6 +134,23 @@ export default class TreeMode extends Component { return h('div', {class: 'jsoneditor-menu'}, items) } + /** + * Validate the JSON against the configured JSON schema + * Returns an array with the errors when not valid, returns an empty array + * when valid. + * @return {Array.} + */ + getErrors () { + if (this.state.compiledSchema) { + const valid = this.state.compiledSchema(dataToJson(this.state.data)) + if (!valid) { + return this.state.compiledSchema.errors.map(enrichSchemaError) + } + } + + return [] + } + /** @private */ handleHideMenus = () => { JSONNode.hideActionMenu() @@ -367,9 +398,25 @@ export default class TreeMode extends Component { * To remove the schema, call JSONEditor.setSchema(null) * @param {Object | null} schema */ + // TODO: deduplicate this function, it's also implemented in TextMode setSchema (schema) { - // TODO: implement setSchema for TreeMode - console.error('setSchema not yet implemented for TreeMode') + if (schema) { + const ajv = this.props.options.ajv || Ajv && Ajv(AJV_OPTIONS) + + if (!ajv) { + throw new Error('Cannot validate JSON: ajv not available. ' + + 'Provide ajv via options or use a JSONEditor bundle including ajv.') + } + + this.setState({ + compiledSchema: ajv.compile(schema) + }) + } + else { + this.setState({ + compiledSchema: null + }) + } } /** diff --git a/src/develop.html b/src/develop.html index 07e9560..74104cc 100644 --- a/src/develop.html +++ b/src/develop.html @@ -29,8 +29,8 @@