From c79bea4eb883ebf4c76d748405cdfb25c89065dd Mon Sep 17 00:00:00 2001 From: Adam Vigneaux Date: Sun, 17 Mar 2019 10:39:00 -0400 Subject: [PATCH] Display schema defaults inline next to Nodes (#666) * Display schema defaults inline next to Nodes * Improve usability of schema default display - When value is default, make it bold and set a tooltip - When value is not default, display the default next to the value - When value is default and is a select, show "Default" next to it - Lighten the color of green used for values This increases the contrast between normal values and default values. * Remove styling when value is the same as the schema default This styling may have been confusing for some users and may not have been applicable to all situations. * Apply is-default and is-not-default classes to values This allows the user to supply custom styling for these states. To set styles for values that match the default value in the schema, use the class `.jsoneditor-is-default`. To set styles for values that _do not_ match the default value in the schema, use the class `.jsoneditor-is-not-default`. * Remove extra newline after schema examples in tooltip * Move schema default display from inline to tooltip This presents less opportunity for user confusion and is likely to be more widely applicable. * Add examples of schema metadata display * Add documentation on styling --- docs/styling.md | 24 +++++++++++++++++++++ examples/07_json_schema_validation.html | 27 +++++++++++++++++++++--- src/css/jsoneditor.css | 11 ++++++---- src/js/Node.js | 28 +++++++++++++++++++++++++ src/js/i18n.js | 4 ++++ src/js/util.js | 15 +++++++++++-- test/util.test.js | 23 ++++++++++++++++---- 7 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 docs/styling.md diff --git a/docs/styling.md b/docs/styling.md new file mode 100644 index 0000000..718ab9f --- /dev/null +++ b/docs/styling.md @@ -0,0 +1,24 @@ +# Styling Reference + +Documentation for writing custom JSON Editor styles. + +## Node +Node is the fundamental unit that makes up the hierarchical JSON display in the Form, Tree, and View modes. It can be +customized with several classes that reflect its type and state. + +- `jsoneditor-field`: the property name +- `jsoneditor-value`: the value of the property + - The value element will have one of the following classes depending on its type: + - `jsoneditor-null` + - `jsoneditor-undefined` + - `jsoneditor-number` + - `jsoneditor-string` + - `jsoneditor-boolean` + - `jsoneditor-regexp` + - `jsoneditor-array` + - `jsoneditor-object` + - `jsoneditor-url` + - `jsoneditor-is-default`: applied to the value element when the value matches the default from the schema + - `jsoneditor-is-not-default`: applied to the value element when the value does not match the default from the schema +- `jsoneditor-schema-error`: the warning icon that appears when the Node has a schema validation error + - `jsoneditor-popover`: the popover that appears when hovering over the schema validation error warning icon diff --git a/examples/07_json_schema_validation.html b/examples/07_json_schema_validation.html index b9f1f56..6969a2a 100644 --- a/examples/07_json_schema_validation.html +++ b/examples/07_json_schema_validation.html @@ -35,16 +35,28 @@ "type": "object", "properties": { "firstName": { + "title": "First Name", + "description": "The given name.", + "examples": [ + "John" + ], "type": "string" }, "lastName": { + "title": "Last Name", + "description": "The family name.", + "examples": [ + "Smith" + ], "type": "string" }, "gender": { + "title": "Gender", "enum": ["male", "female"] }, "availableToHire": { - "type": "boolean" + "type": "boolean", + "default": false }, "age": { "description": "Age in years", @@ -65,10 +77,19 @@ "required": ["address"], "properties": { "company": { - "type": "string" + "type": "string", + "examples": [ + "ACME", + "Dexter Industries" + ] }, "role": { - "type": "string" + "description": "Job title.", + "type": "string", + "examples": [ + "Human Resources Coordinator", + "Software Developer" + ] }, "address": { "type": "string" diff --git a/src/css/jsoneditor.css b/src/css/jsoneditor.css index f1af3af..fd94585 100644 --- a/src/css/jsoneditor.css +++ b/src/css/jsoneditor.css @@ -5,7 +5,8 @@ div.jsoneditor { div.jsoneditor-field, div.jsoneditor-value, -div.jsoneditor-readonly { +div.jsoneditor-readonly, +div.jsoneditor-default { border: 1px solid transparent; min-height: 16px; min-width: 32px; @@ -97,13 +98,12 @@ div.jsoneditor-value.jsoneditor-highlight-active:hover { } div.jsoneditor-value.jsoneditor-string { - color: #008000; + color: #006000; } div.jsoneditor-value.jsoneditor-object, div.jsoneditor-value.jsoneditor-array { min-width: 16px; - color: #808080; } div.jsoneditor-value.jsoneditor-number { @@ -122,7 +122,10 @@ div.jsoneditor-value.jsoneditor-invalid { color: #000000; } - +div.jsoneditor-default { + color: #808080; + padding-left: 10px; +} div.jsoneditor-tree button.jsoneditor-button { width: 24px; diff --git a/src/js/Node.js b/src/js/Node.js index 21dee0c..b03ac11 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -1822,6 +1822,8 @@ Node.prototype._updateDomValue = function () { // strip formatting from the contents of the editable div util.stripFormatting(domValue); + + this._updateDomDefault(); } }; @@ -1908,6 +1910,32 @@ Node.prototype._getDomField = function(silent) { } }; +/** + * Update the value of the schema default element in the DOM. + * @private + * @returns {undefined} + */ +Node.prototype._updateDomDefault = function () { + // Short-circuit if schema is missing, has no default, or if Node has children + if (!this.schema || !this.schema.default || this._hasChilds()) { + return; + } + + if (this.value === this.schema.default) { + if (this.dom.select) { + this.dom.value.removeAttribute('title'); + } else { + this.dom.value.title = translate('default'); + this.dom.value.classList.add('jsoneditor-is-default'); + this.dom.value.classList.remove('jsoneditor-is-not-default'); + } + } else { + this.dom.value.removeAttribute('title'); + this.dom.value.classList.remove('jsoneditor-is-default'); + this.dom.value.classList.add('jsoneditor-is-not-default'); + } +}; + /** * Validate this node and all it's childs * @return {Array.<{node: Node, error: {message: string}}>} Returns a list with duplicates diff --git a/src/js/i18n.js b/src/js/i18n.js index 0ab0a08..9c4450a 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -86,6 +86,7 @@ var _defs = { modeViewText: 'View', modeViewTitle: 'Switch to tree view', examples: 'Examples', + default: 'Default', }, 'zh-CN': { array: '数组', @@ -169,6 +170,7 @@ var _defs = { modeViewText: '视图', modeViewTitle: '切换至树视图', examples: '例子', + default: '缺省', }, 'pt-BR': { array: 'Lista', @@ -264,6 +266,7 @@ var _defs = { 'Campo do tipo nao é determinado através do seu valor, ' + 'mas sempre retornara um texto.', examples: 'Exemplos', + default: 'Revelia', }, tr: { array: 'Dizin', @@ -347,6 +350,7 @@ var _defs = { modeViewText: 'Görünüm', modeViewTitle: 'Ağaç görünümüne geç', examples: 'Örnekler', + default: 'Varsayılan', } }; diff --git a/src/js/util.js b/src/js/util.js index a0f2847..493dd15 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -1087,13 +1087,24 @@ exports.makeFieldTooltip = function (schema, locale) { tooltip += schema.description; } + if (schema.default) { + if (tooltip.length > 0) { + tooltip += '\n\n'; + } + tooltip += translate('default', undefined, locale) + '\n'; + tooltip += JSON.stringify(schema.default, null, 2); + } + if (Array.isArray(schema.examples) && schema.examples.length > 0) { if (tooltip.length > 0) { tooltip += '\n\n'; } tooltip += translate('examples', undefined, locale) + '\n'; - schema.examples.forEach(function (example) { - tooltip += JSON.stringify(example, null, 2) + '\n'; + schema.examples.forEach(function (example, index) { + tooltip += JSON.stringify(example, null, 2); + if (index !== schema.examples.length - 1) { + tooltip += '\n'; + } }); } diff --git a/test/util.test.js b/test/util.test.js index dab06a8..6a62a7d 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -160,8 +160,12 @@ describe('util', function () { assert.strictEqual(util.makeFieldTooltip({description: 'foo'}), 'foo'); }); + it('should make tooltips with only default', function () { + assert.strictEqual(util.makeFieldTooltip({default: 'foo'}), 'Default\n"foo"'); + }); + it('should make tooltips with only examples', function () { - assert.strictEqual(util.makeFieldTooltip({examples: ['foo', 'bar']}), 'Examples\n"foo"\n"bar"\n'); + assert.strictEqual(util.makeFieldTooltip({examples: ['foo', 'bar']}), 'Examples\n"foo"\n"bar"'); }); it('should make tooltips with title and description', function () { @@ -180,7 +184,14 @@ describe('util', function () { it('should make tooltips with title, description, and examples', function () { assert.strictEqual( util.makeFieldTooltip({title: 'foo', description: 'bar', examples: ['baz']}), - 'foo\nbar\n\nExamples\n"baz"\n', + 'foo\nbar\n\nExamples\n"baz"', + ); + }); + + it('should make tooltips with title, description, default, and examples', function () { + assert.strictEqual( + util.makeFieldTooltip({title: 'foo', description: 'bar', default: 'bat', examples: ['baz']}), + 'foo\nbar\n\nDefault\n"bat"\n\nExamples\n"baz"', ); }); @@ -188,11 +199,15 @@ describe('util', function () { assert.strictEqual(util.makeFieldTooltip({title: '', description: 'bar'}), 'bar'); assert.strictEqual(util.makeFieldTooltip({title: 'foo', description: ''}), 'foo'); assert.strictEqual(util.makeFieldTooltip({description: 'bar', examples: []}), 'bar'); - assert.strictEqual(util.makeFieldTooltip({description: 'bar', examples: ['']}), 'bar\n\nExamples\n""\n'); + assert.strictEqual(util.makeFieldTooltip({description: 'bar', examples: ['']}), 'bar\n\nExamples\n""'); + }); + + it('should internationalize "Defaults" correctly', function () { + assert.strictEqual(util.makeFieldTooltip({default: 'foo'}, 'pt-BR'), 'Revelia\n"foo"'); }); it('should internationalize "Examples" correctly', function () { - assert.strictEqual(util.makeFieldTooltip({examples: ['foo']}, 'pt-BR'), 'Exemplos\n"foo"\n'); + assert.strictEqual(util.makeFieldTooltip({examples: ['foo']}, 'pt-BR'), 'Exemplos\n"foo"'); }); }); // TODO: thoroughly test all util methods