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
This commit is contained in:
Adam Vigneaux 2019-03-17 10:39:00 -04:00 committed by Jos de Jong
parent 7a2e89c329
commit c79bea4eb8
7 changed files with 119 additions and 13 deletions

24
docs/styling.md Normal file
View File

@ -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

View File

@ -35,16 +35,28 @@
"type": "object", "type": "object",
"properties": { "properties": {
"firstName": { "firstName": {
"title": "First Name",
"description": "The given name.",
"examples": [
"John"
],
"type": "string" "type": "string"
}, },
"lastName": { "lastName": {
"title": "Last Name",
"description": "The family name.",
"examples": [
"Smith"
],
"type": "string" "type": "string"
}, },
"gender": { "gender": {
"title": "Gender",
"enum": ["male", "female"] "enum": ["male", "female"]
}, },
"availableToHire": { "availableToHire": {
"type": "boolean" "type": "boolean",
"default": false
}, },
"age": { "age": {
"description": "Age in years", "description": "Age in years",
@ -65,10 +77,19 @@
"required": ["address"], "required": ["address"],
"properties": { "properties": {
"company": { "company": {
"type": "string" "type": "string",
"examples": [
"ACME",
"Dexter Industries"
]
}, },
"role": { "role": {
"type": "string" "description": "Job title.",
"type": "string",
"examples": [
"Human Resources Coordinator",
"Software Developer"
]
}, },
"address": { "address": {
"type": "string" "type": "string"

View File

@ -5,7 +5,8 @@ div.jsoneditor {
div.jsoneditor-field, div.jsoneditor-field,
div.jsoneditor-value, div.jsoneditor-value,
div.jsoneditor-readonly { div.jsoneditor-readonly,
div.jsoneditor-default {
border: 1px solid transparent; border: 1px solid transparent;
min-height: 16px; min-height: 16px;
min-width: 32px; min-width: 32px;
@ -97,13 +98,12 @@ div.jsoneditor-value.jsoneditor-highlight-active:hover {
} }
div.jsoneditor-value.jsoneditor-string { div.jsoneditor-value.jsoneditor-string {
color: #008000; color: #006000;
} }
div.jsoneditor-value.jsoneditor-object, div.jsoneditor-value.jsoneditor-object,
div.jsoneditor-value.jsoneditor-array { div.jsoneditor-value.jsoneditor-array {
min-width: 16px; min-width: 16px;
color: #808080;
} }
div.jsoneditor-value.jsoneditor-number { div.jsoneditor-value.jsoneditor-number {
@ -122,7 +122,10 @@ div.jsoneditor-value.jsoneditor-invalid {
color: #000000; color: #000000;
} }
div.jsoneditor-default {
color: #808080;
padding-left: 10px;
}
div.jsoneditor-tree button.jsoneditor-button { div.jsoneditor-tree button.jsoneditor-button {
width: 24px; width: 24px;

View File

@ -1822,6 +1822,8 @@ Node.prototype._updateDomValue = function () {
// strip formatting from the contents of the editable div // strip formatting from the contents of the editable div
util.stripFormatting(domValue); 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 * Validate this node and all it's childs
* @return {Array.<{node: Node, error: {message: string}}>} Returns a list with duplicates * @return {Array.<{node: Node, error: {message: string}}>} Returns a list with duplicates

View File

@ -86,6 +86,7 @@ var _defs = {
modeViewText: 'View', modeViewText: 'View',
modeViewTitle: 'Switch to tree view', modeViewTitle: 'Switch to tree view',
examples: 'Examples', examples: 'Examples',
default: 'Default',
}, },
'zh-CN': { 'zh-CN': {
array: '数组', array: '数组',
@ -169,6 +170,7 @@ var _defs = {
modeViewText: '视图', modeViewText: '视图',
modeViewTitle: '切换至树视图', modeViewTitle: '切换至树视图',
examples: '例子', examples: '例子',
default: '缺省',
}, },
'pt-BR': { 'pt-BR': {
array: 'Lista', array: 'Lista',
@ -264,6 +266,7 @@ var _defs = {
'Campo do tipo nao é determinado através do seu valor, ' + 'Campo do tipo nao é determinado através do seu valor, ' +
'mas sempre retornara um texto.', 'mas sempre retornara um texto.',
examples: 'Exemplos', examples: 'Exemplos',
default: 'Revelia',
}, },
tr: { tr: {
array: 'Dizin', array: 'Dizin',
@ -347,6 +350,7 @@ var _defs = {
modeViewText: 'Görünüm', modeViewText: 'Görünüm',
modeViewTitle: 'Ağaç görünümüne geç', modeViewTitle: 'Ağaç görünümüne geç',
examples: 'Örnekler', examples: 'Örnekler',
default: 'Varsayılan',
} }
}; };

View File

@ -1087,13 +1087,24 @@ exports.makeFieldTooltip = function (schema, locale) {
tooltip += schema.description; 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 (Array.isArray(schema.examples) && schema.examples.length > 0) {
if (tooltip.length > 0) { if (tooltip.length > 0) {
tooltip += '\n\n'; tooltip += '\n\n';
} }
tooltip += translate('examples', undefined, locale) + '\n'; tooltip += translate('examples', undefined, locale) + '\n';
schema.examples.forEach(function (example) { schema.examples.forEach(function (example, index) {
tooltip += JSON.stringify(example, null, 2) + '\n'; tooltip += JSON.stringify(example, null, 2);
if (index !== schema.examples.length - 1) {
tooltip += '\n';
}
}); });
} }

View File

@ -160,8 +160,12 @@ describe('util', function () {
assert.strictEqual(util.makeFieldTooltip({description: 'foo'}), 'foo'); 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 () { 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 () { 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 () { it('should make tooltips with title, description, and examples', function () {
assert.strictEqual( assert.strictEqual(
util.makeFieldTooltip({title: 'foo', description: 'bar', examples: ['baz']}), 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: '', description: 'bar'}), 'bar');
assert.strictEqual(util.makeFieldTooltip({title: 'foo', description: ''}), 'foo'); assert.strictEqual(util.makeFieldTooltip({title: 'foo', description: ''}), 'foo');
assert.strictEqual(util.makeFieldTooltip({description: 'bar', examples: []}), 'bar'); 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 () { 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 // TODO: thoroughly test all util methods