From f011e3f107126a58113eec8a5fe1b54ce0ed275e Mon Sep 17 00:00:00 2001 From: jos Date: Tue, 12 Jul 2016 21:07:50 +0200 Subject: [PATCH] coloring fields, urls, and a refactoring --- public/jsoneditor.css | 36 +++++++++- src/JSONNode.js | 142 +++++++++++++++++++++++++++++----------- src/utils/escapeHTML.js | 13 ++-- src/utils/valueType.js | 42 ++++++++++++ 4 files changed, 185 insertions(+), 48 deletions(-) create mode 100644 src/utils/valueType.js diff --git a/public/jsoneditor.css b/public/jsoneditor.css index 794a1d8..af7c835 100644 --- a/public/jsoneditor.css +++ b/public/jsoneditor.css @@ -41,15 +41,47 @@ ul.jsoneditor-list { } .jsoneditor-separator { - color: gray; + color: #808080; } .jsoneditor-readonly { - color: gray; + color: #808080; } .jsoneditor-readonly:focus, .jsoneditor-readonly:hover { border-color: transparent; background-color: inherit; +} + + +.jsoneditor-value.jsoneditor-string { + color: #008000; +} + +.jsoneditor-value.jsoneditor-object, +.jsoneditor-value.jsoneditor-array { + min-width: 16px; + color: #808080; +} + +.jsoneditor-value.jsoneditor-number { + color: #ee422e; +} + +.jsoneditor-value.jsoneditor-boolean { + color: #ff8c00; +} + +.jsoneditor-value.jsoneditor-null { + color: #004ED0; +} + +.jsoneditor-value.jsoneditor-invalid { + color: #000000; +} + +div.jsoneditor-value.jsoneditor-url { + color: green; + text-decoration: underline; } \ No newline at end of file diff --git a/src/JSONNode.js b/src/JSONNode.js index 477bb9c..b5d3552 100644 --- a/src/JSONNode.js +++ b/src/JSONNode.js @@ -4,83 +4,131 @@ import escapeHTML from './utils/escapeHTML' import unescapeHTML from './utils/unescapeHTML' import getInnerText from './utils/getInnerText' import stringConvert from './utils/stringConvert' +import valueType, {isUrl} from './utils/valueType' export default class JSONNode extends Component { constructor (props) { super(props) - this.onBlurField = this.onBlurField.bind(this) - this.onBlurValue = this.onBlurValue.bind(this) + this.onChangeField = this.onChangeField.bind(this) + this.onChangeValue = this.onChangeValue.bind(this) + this.onClickUrl = this.onClickUrl.bind(this) + this.onKeyDownUrl = this.onKeyDownUrl.bind(this) } render (props) { if (Array.isArray(props.value)) { - return this.renderArray(props) + return this.renderJSONArray(props) } else if (isObject(props.value)) { - return this.renderObject(props) + return this.renderJSONObject(props) } else { - return this.renderValue(props) + return this.renderJSONValue(props) } } // TODO: reorganize the render methods, they are too large now - renderObject ({parent, field, value, onChangeValue, onChangeField}) { + renderJSONObject ({parent, field, value, onChangeValue, onChangeField}) { //console.log('JSONObject', field,value) - const hasParent = parent !== null + + const childs = Object.keys(value).map(f => { + return h(JSONNode, { + parent: this, + field: f, + value: value[f], + onChangeValue, + onChangeField + }) + }) return h('li', {}, [ h('div', {class: 'jsoneditor-node jsoneditor-object'}, [ - h('div', {class: 'jsoneditor-field' + (hasParent ? '' : ' jsoneditor-readonly'), contentEditable: hasParent, onBlur: this.onBlurField}, hasParent ? escapeHTML(field) : 'object'), - h('div', {class: 'jsoneditor-separator'}, ':'), - h('div', {class: 'jsoneditor-readonly', contentEditable: false}, '{' + Object.keys(value).length + '}') + this.renderField(field, parent, this.onChangeField), + this.renderSeparator(), + this.renderReadonly('{' + Object.keys(value).length + '}') ]), - h('ul', - {class: 'jsoneditor-list'}, - Object - .keys(value) - .map(f => h(JSONNode, {parent: this, field: f, value: value[f], onChangeValue, onChangeField}))) + h('ul', {class: 'jsoneditor-list'}, childs) ]) } - renderArray ({parent, field, value, onChangeValue, onChangeField}) { - const hasParent = parent !== null + renderJSONArray ({parent, field, value, onChangeValue, onChangeField}) { + const childs = value.map((v, i) => { + return h(JSONNode, { + parent: this, + index: i, + value: v, + onChangeValue, + onChangeField + }) + }) return h('li', {}, [ h('div', {class: 'jsoneditor-node jsoneditor-array'}, [ - h('div', {class: 'jsoneditor-field' + (hasParent ? '' : ' jsoneditor-readonly'), contentEditable: hasParent, onBlur: this.onBlurField}, hasParent ? escapeHTML(field) : 'array'), - h('div', {class: 'jsoneditor-separator'}, ':'), - h('div', {class: 'jsoneditor-readonly', contentEditable: false}, '{' + value.length + '}') + this.renderField(field, parent, this.onChangeField), + this.renderSeparator(), + this.renderReadonly('{' + value.length + '}') ]), - h('ul', - {class: 'jsoneditor-list'}, - value - .map((v, i) => h(JSONNode, {parent: this, index: i, value: v, onChangeValue, onChangeField}))) + h('ul', {class: 'jsoneditor-list'}, childs) ]) } - renderValue ({parent, index, field, value}) { - const hasParent = parent !== null + renderJSONValue ({parent, index, field, value}) { //console.log('JSONValue', field, value) return h('li', {}, [ h('div', {class: 'jsoneditor-node'}, [ index !== undefined - ? h('div', {class: 'jsoneditor-readonly', contentEditable: false}, index) - : h('div', {class: 'jsoneditor-field' + (hasParent ? '' : ' jsoneditor-readonly'), contentEditable: hasParent, onBlur: this.onBlurField}, hasParent ? escapeHTML(field) : 'value'), - h('div', {class: 'jsoneditor-separator'}, ':'), - h('div', {class: 'jsoneditor-value', contentEditable: true, onBlur: this.onBlurValue}, escapeHTML(value)) + ? this.renderReadonly(index) + : this.renderField(field, parent, this.onChangeField), + this.renderSeparator(), + this.renderValue(value, this.onChangeValue) ]) ]) } + renderReadonly (text) { + return h('div', {class: 'jsoneditor-readonly', contentEditable: false}, text) + } + + renderField (field, parent, onChangeField) { + const hasParent = parent !== null + const content = hasParent ? escapeHTML(field) : 'value' + + return h('div', { + class: 'jsoneditor-field' + (hasParent ? '' : ' jsoneditor-readonly'), + contentEditable: hasParent, + spellCheck: "false", // FIXME: turning off spellcheck doesn't work + onBlur: onChangeField + }, content) + } + + renderSeparator() { + return h('div', {class: 'jsoneditor-separator'}, ':') + } + + renderValue (value, onChangeValue) { + const type = valueType (value) + const _isUrl = isUrl(value) + const valueClass = 'jsoneditor-value jsoneditor-' + type + (_isUrl ? ' jsoneditor-url' : '') + + return h('div', { + class: valueClass, + contentEditable: true, + spellCheck: "false", // FIXME: turning off spellcheck doesn't work + onInput: onChangeValue, + onClick: _isUrl ? this.onClickUrl : null, + onKeyDown: _isUrl ? this.onKeyDownUrl: null, + title: _isUrl ? 'Ctrl+Click or ctrl+Enter to open url' : null + }, escapeHTML(value)) + } + shouldComponentUpdate(nextProps, nextState) { return nextProps.field !== this.props.field || nextProps.value !== this.props.value } - onBlurField (event) { + onChangeField (event) { const path = this.props.parent.getPath() const newField = unescapeHTML(getInnerText(event.target)) const oldField = this.props.field @@ -89,7 +137,7 @@ export default class JSONNode extends Component { } } - onBlurValue (event) { + onChangeValue (event) { const path = this.getPath() const value = stringConvert(unescapeHTML(getInnerText(event.target))) if (value !== this.props.value) { @@ -97,6 +145,28 @@ export default class JSONNode extends Component { } } + onClickUrl (event) { + if (event.ctrlKey && event.button === 0) { // Ctrl+Left click + event.preventDefault() + event.stopPropagation() + + this.openUrl() + } + } + + onKeyDownUrl (event) { + if (event.ctrlKey && event.which === 13) { // Ctrl+Enter + event.preventDefault() + event.stopPropagation() + + this.openUrl() + } + } + + openUrl () { + window.open(this.props.value, '_blank') + } + getPath () { const path = [] @@ -111,12 +181,4 @@ export default class JSONNode extends Component { return path } - - getRoot () { - let node = this - while (node && node.props.parent) { - node = node.props.parent - } - return node - } } diff --git a/src/utils/escapeHTML.js b/src/utils/escapeHTML.js index 04f5d92..0ce7dec 100644 --- a/src/utils/escapeHTML.js +++ b/src/utils/escapeHTML.js @@ -10,12 +10,13 @@ export default function escapeHTML (text, escapeUnicode = false) { } else { var htmlEscaped = String(text) - .replace(/&/g, '&') // must be replaced first! - .replace(//g, '>') - .replace(/ /g, '  ') // replace double space with an nbsp and space - .replace(/^ /, ' ') // space at start - .replace(/ $/, ' '); // space at end + // TODO: cleanup redundant character replacements + // .replace(/&/g, '&') // must be replaced first! + // .replace(//g, '>') + .replace(/ /g, ' \u00a0') // replace double space with an nbsp and space + .replace(/^ /, '\u00a0') // space at start + .replace(/ $/, '\u00a0') // space at end var json = JSON.stringify(htmlEscaped) var html = json.substring(1, json.length - 1) diff --git a/src/utils/valueType.js b/src/utils/valueType.js new file mode 100644 index 0000000..9566c18 --- /dev/null +++ b/src/utils/valueType.js @@ -0,0 +1,42 @@ + +/** + * Get the type of a value + * @param {*} value + * @return {String} type + */ +export default function valueType(value) { + if (value === null) { + return 'null' + } + if (value === undefined) { + return 'undefined' + } + if (typeof value === 'number') { + return 'number' + } + if (typeof value === 'string') { + return 'string' + } + if (typeof value === 'boolean') { + return 'boolean' + } + if (value instanceof RegExp) { + return 'regexp' + } + if (exports.isArray(value)) { + return 'array' + } + + return 'object' +} + + +/** + * Test whether a text contains a url (matches when a string starts + * with 'http://*' or 'https://*' and has no whitespace characters) + * @param {String} text + */ +var isUrlRegex = /^https?:\/\/\S+$/ +export function isUrl (text) { + return (typeof text === 'string') && isUrlRegex.test(text) +}