Apply changes in fields
This commit is contained in:
parent
0d250cbdcd
commit
4e0aa5659c
|
@ -24,17 +24,17 @@ ul.jsoneditor-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor-key,
|
.jsoneditor-field,
|
||||||
.jsoneditor-value {
|
.jsoneditor-value {
|
||||||
min-width: 32px;
|
min-width: 24px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor-key:focus,
|
.jsoneditor-field:focus,
|
||||||
.jsoneditor-value:focus,
|
.jsoneditor-value:focus,
|
||||||
.jsoneditor-key:hover,
|
.jsoneditor-field:hover,
|
||||||
.jsoneditor-value:hover {
|
.jsoneditor-value:hover {
|
||||||
background-color: #FFFFAB;
|
background-color: #FFFFAB;
|
||||||
border-color: #ff0;
|
border-color: #ff0;
|
||||||
|
@ -44,6 +44,6 @@ ul.jsoneditor-list {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor-info {
|
.jsoneditor-readonly {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
import { h, Component } from 'preact'
|
import { h, Component } from 'preact'
|
||||||
import isObject from './utils/isObject'
|
import isObject from './utils/isObject'
|
||||||
|
import escapeHTML from './utils/escapeHTML'
|
||||||
|
import unescapeHTML from './utils/unescapeHTML'
|
||||||
|
import getInnerText from './utils/getInnerText'
|
||||||
|
|
||||||
export default class JSONNode extends Component {
|
export default class JSONNode extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.onValueInput = this.onValueInput.bind(this)
|
this.onBlurField = this.onBlurField.bind(this)
|
||||||
|
this.onBlurValue = this.onBlurValue.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
render (props) {
|
render (props) {
|
||||||
|
@ -20,47 +24,51 @@ export default class JSONNode extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderObject ({field, value, onChangeValue}) {
|
renderObject ({parent, field, value, onChangeValue, onChangeField}) {
|
||||||
//console.log('JSONObject', field,value)
|
//console.log('JSONObject', field,value)
|
||||||
|
const hasParent = parent !== null
|
||||||
|
|
||||||
return h('li', {class: 'jsoneditor-object'}, [
|
return h('li', {class: 'jsoneditor-object'}, [
|
||||||
h('div', {class: 'jsoneditor-node'}, [
|
h('div', {class: 'jsoneditor-node'}, [
|
||||||
h('div', {class: 'jsoneditor-field', contentEditable: true}, field),
|
h('div', {class: 'jsoneditor-field', contentEditable: hasParent, onBlur: this.onBlurField}, hasParent ? field : 'object'),
|
||||||
h('div', {class: 'jsoneditor-separator'}, ':'),
|
h('div', {class: 'jsoneditor-separator'}, ':'),
|
||||||
h('div', {class: 'jsoneditor-info'}, '{' + Object.keys(value).length + '}')
|
h('div', {class: 'jsoneditor-readonly', contentEditable: false}, '{' + Object.keys(value).length + '}')
|
||||||
]),
|
]),
|
||||||
h('ul',
|
h('ul',
|
||||||
{class: 'jsoneditor-list'},
|
{class: 'jsoneditor-list'},
|
||||||
Object.keys(value).map(f => h(JSONNode, {parent: this, field: f, value: value[f], onChangeValue})))
|
Object
|
||||||
|
.keys(value)
|
||||||
|
.map(f => h(JSONNode, {parent: this, field: f, value: value[f], onChangeValue, onChangeField})))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
renderArray ({field, value, onChangeValue}) {
|
renderArray ({parent, field, value, onChangeValue, onChangeField}) {
|
||||||
|
const hasParent = parent !== null
|
||||||
|
|
||||||
return h('li', {}, [
|
return h('li', {}, [
|
||||||
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
|
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
|
||||||
h('div', {class: 'jsoneditor-field', contentEditable: true}, field),
|
h('div', {class: 'jsoneditor-field', contentEditable: hasParent, onBlur: this.onBlurField}, hasParent ? field : 'array'),
|
||||||
h('div', {class: 'jsoneditor-separator'}, ':'),
|
h('div', {class: 'jsoneditor-separator'}, ':'),
|
||||||
h('div', {class: 'jsoneditor-info'}, '{' + value.length + '}')
|
h('div', {class: 'jsoneditor-readonly', contentEditable: false}, '{' + value.length + '}')
|
||||||
]),
|
]),
|
||||||
h('ul',
|
h('ul',
|
||||||
{class: 'jsoneditor-list'},
|
{class: 'jsoneditor-list'},
|
||||||
value.map((v, f) => h(JSONNode, {parent: this, field: f, value: v, onChangeValue})))
|
value
|
||||||
|
.map((v, i) => h(JSONNode, {parent: this, index: i, value: v, onChangeValue, onChangeField})))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
renderValue ({field, value}) {
|
renderValue ({parent, index, field, value}) {
|
||||||
|
const hasParent = parent !== null
|
||||||
//console.log('JSONValue', field, value)
|
//console.log('JSONValue', field, value)
|
||||||
|
|
||||||
return h('li', {}, [
|
return h('li', {}, [
|
||||||
h('div', {class: 'jsoneditor-node'}, [
|
h('div', {class: 'jsoneditor-node'}, [
|
||||||
h('div', {class: 'jsoneditor-field', contentEditable: true}, field),
|
index !== undefined
|
||||||
|
? h('div', {class: 'jsoneditor-readonly', contentEditable: false}, index)
|
||||||
|
: h('div', {class: 'jsoneditor-field', contentEditable: hasParent, onBlur: this.onBlurField}, hasParent ? field : 'value'),
|
||||||
h('div', {class: 'jsoneditor-separator'}, ':'),
|
h('div', {class: 'jsoneditor-separator'}, ':'),
|
||||||
h('div', {
|
h('div', {class: 'jsoneditor-value', contentEditable: true, onBlur: this.onBlurValue}, escapeHTML(value))
|
||||||
class: 'jsoneditor-value',
|
|
||||||
contentEditable: true,
|
|
||||||
// 'data-path': JSON.stringify(this.getPath())
|
|
||||||
onInput: this.onValueInput
|
|
||||||
}, value)
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -69,10 +77,21 @@ export default class JSONNode extends Component {
|
||||||
return nextProps.field !== this.props.field || nextProps.value !== this.props.value
|
return nextProps.field !== this.props.field || nextProps.value !== this.props.value
|
||||||
}
|
}
|
||||||
|
|
||||||
onValueInput (event) {
|
onBlurField (event) {
|
||||||
|
const path = this.props.parent.getPath()
|
||||||
|
const newField = getInnerText(event.target)
|
||||||
|
const oldField = this.props.field
|
||||||
|
if (newField !== oldField) {
|
||||||
|
this.props.onChangeField(path, newField, oldField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurValue (event) {
|
||||||
const path = this.getPath()
|
const path = this.getPath()
|
||||||
const value = event.target.innerHTML
|
const value = unescapeHTML(getInnerText(event.target))
|
||||||
this.props.onChangeValue(path, value)
|
if (value !== this.props.value) {
|
||||||
|
this.props.onChangeValue(path, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPath () {
|
getPath () {
|
||||||
|
@ -80,7 +99,7 @@ export default class JSONNode extends Component {
|
||||||
|
|
||||||
let node = this
|
let node = this
|
||||||
while (node) {
|
while (node) {
|
||||||
path.unshift(node.props.field)
|
path.unshift(node.props.field || node.props.index)
|
||||||
|
|
||||||
node = node.props.parent
|
node = node.props.parent
|
||||||
}
|
}
|
||||||
|
@ -89,4 +108,12 @@ export default class JSONNode extends Component {
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRoot () {
|
||||||
|
let node = this
|
||||||
|
while (node && node.props.parent) {
|
||||||
|
node = node.props.parent
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
26
src/Main.js
26
src/Main.js
|
@ -1,5 +1,7 @@
|
||||||
import { h, Component } from 'preact'
|
import { h, Component } from 'preact'
|
||||||
import setIn from './utils/setIn'
|
import setIn from './utils/setIn'
|
||||||
|
import getIn from './utils/getIn'
|
||||||
|
import clone from './utils/clone'
|
||||||
import JSONNode from './JSONNode'
|
import JSONNode from './JSONNode'
|
||||||
|
|
||||||
export default class Main extends Component {
|
export default class Main extends Component {
|
||||||
|
@ -11,12 +13,19 @@ export default class Main extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onChangeValue = this.onChangeValue.bind(this)
|
this.onChangeValue = this.onChangeValue.bind(this)
|
||||||
|
this.onChangeField = this.onChangeField.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
render(props, state) {
|
render(props, state) {
|
||||||
return h('div', {class: 'jsoneditor', onInput: this.onInput}, [
|
return h('div', {class: 'jsoneditor', onInput: this.onInput}, [
|
||||||
h('ul', {class: 'jsoneditor-list'}, [
|
h('ul', {class: 'jsoneditor-list'}, [
|
||||||
h(JSONNode, {parent: null, field: null, value: state.json, onChangeValue: this.onChangeValue})
|
h(JSONNode, {
|
||||||
|
parent: null,
|
||||||
|
field: null,
|
||||||
|
value: state.json,
|
||||||
|
onChangeField: this.onChangeField,
|
||||||
|
onChangeValue: this.onChangeValue
|
||||||
|
})
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -28,6 +37,21 @@ export default class Main extends Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeField (path, newField, oldField) {
|
||||||
|
console.log('onChangeField', path, newField, oldField)
|
||||||
|
|
||||||
|
const value = clone(getIn(this.state.json, path))
|
||||||
|
|
||||||
|
console.log('value', value)
|
||||||
|
|
||||||
|
value[newField] = value[oldField]
|
||||||
|
delete value[oldField]
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
json: setIn(this.state.json, path, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
get () {
|
get () {
|
||||||
return this.state.json
|
return this.state.json
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* escape a text, such that it can be displayed safely in an HTML element
|
||||||
|
* @param {String} text
|
||||||
|
* @param {boolean} [escapeUnicode=false]
|
||||||
|
* @return {String} escapedText
|
||||||
|
*/
|
||||||
|
export default function escapeHTML (text, escapeUnicode = false) {
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
return String(text)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var htmlEscaped = String(text)
|
||||||
|
.replace(/&/g, '&') // must be replaced first!
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/ /g, ' ') // replace double space with an nbsp and space
|
||||||
|
.replace(/^ /, ' ') // space at start
|
||||||
|
.replace(/ $/, ' '); // space at end
|
||||||
|
|
||||||
|
var json = JSON.stringify(htmlEscaped)
|
||||||
|
var html = json.substring(1, json.length - 1)
|
||||||
|
if (escapeUnicode === true) {
|
||||||
|
html = escapeUnicodeChars(html)
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape unicode characters.
|
||||||
|
* For example input '\u2661' (length 1) will output '\\u2661' (length 5).
|
||||||
|
* @param {string} text
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
function escapeUnicodeChars (text) {
|
||||||
|
// see https://www.wikiwand.com/en/UTF-16
|
||||||
|
// note: we leave surrogate pairs as two individual chars,
|
||||||
|
// as JSON doesn't interpret them as a single unicode char.
|
||||||
|
return text.replace(/[\u007F-\uFFFF]/g, function(c) {
|
||||||
|
return '\\u'+('0000' + c.charCodeAt(0).toString(16)).slice(-4)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import isObject from './isObject'
|
||||||
|
|
||||||
|
// TODO: unit test getIn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helper function to get a nested property in an object or array
|
||||||
|
*
|
||||||
|
* @param {Object | Array} object
|
||||||
|
* @param {Array.<string | number>} path
|
||||||
|
* @return {* | undefined} Returns the field when found, or undefined when the
|
||||||
|
* path doesn't exist
|
||||||
|
*/
|
||||||
|
export default function getIn (object, path) {
|
||||||
|
let value = object
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while(i < path.length) {
|
||||||
|
if (Array.isArray(value) || isObject(value)) {
|
||||||
|
value = value[path[i]]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.getIn = getIn // TODO: cleanup
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* Get the inner text of an HTML element (for example a div element)
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {Object} [buffer]
|
||||||
|
* @return {String} innerText
|
||||||
|
*/
|
||||||
|
export default function getInnerText (element, buffer) {
|
||||||
|
var first = (buffer == undefined)
|
||||||
|
if (first) {
|
||||||
|
buffer = {
|
||||||
|
'text': '',
|
||||||
|
'flush': function () {
|
||||||
|
var text = this.text
|
||||||
|
this.text = ''
|
||||||
|
return text
|
||||||
|
},
|
||||||
|
'set': function (text) {
|
||||||
|
this.text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// text node
|
||||||
|
if (element.nodeValue) {
|
||||||
|
return buffer.flush() + element.nodeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// divs or other HTML elements
|
||||||
|
if (element.hasChildNodes()) {
|
||||||
|
var childNodes = element.childNodes
|
||||||
|
var innerText = ''
|
||||||
|
|
||||||
|
for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
|
||||||
|
var child = childNodes[i]
|
||||||
|
|
||||||
|
if (child.nodeName == 'DIV' || child.nodeName == 'P') {
|
||||||
|
var prevChild = childNodes[i - 1]
|
||||||
|
var prevName = prevChild ? prevChild.nodeName : undefined
|
||||||
|
if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') {
|
||||||
|
innerText += '\n'
|
||||||
|
buffer.flush()
|
||||||
|
}
|
||||||
|
innerText += getInnerText(child, buffer)
|
||||||
|
buffer.set('\n')
|
||||||
|
}
|
||||||
|
else if (child.nodeName == 'BR') {
|
||||||
|
innerText += buffer.flush()
|
||||||
|
buffer.set('\n')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
innerText += getInnerText(child, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerText
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (element.nodeName == 'P' && getInternetExplorerVersion() != -1) {
|
||||||
|
// On Internet Explorer, a <p> with hasChildNodes()==false is
|
||||||
|
// rendered with a new line. Note that a <p> with
|
||||||
|
// hasChildNodes()==true is rendered without a new line
|
||||||
|
// Other browsers always ensure there is a <br> inside the <p>,
|
||||||
|
// and if not, the <p> does not render a new line
|
||||||
|
return buffer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// br or unknown
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the version of Internet Explorer or a -1
|
||||||
|
* (indicating the use of another browser).
|
||||||
|
* Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
|
||||||
|
* @return {Number} Internet Explorer version, or -1 in case of an other browser
|
||||||
|
*/
|
||||||
|
export function getInternetExplorerVersion() {
|
||||||
|
if (_ieVersion == -1) {
|
||||||
|
var rv = -1 // Return value assumes failure.
|
||||||
|
if (navigator.appName == 'Microsoft Internet Explorer')
|
||||||
|
{
|
||||||
|
var ua = navigator.userAgent
|
||||||
|
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})")
|
||||||
|
if (re.exec(ua) != null) {
|
||||||
|
rv = parseFloat( RegExp.$1 )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ieVersion = rv
|
||||||
|
}
|
||||||
|
|
||||||
|
return _ieVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cached internet explorer version
|
||||||
|
* @type {Number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
var _ieVersion = -1
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Parse JSON using the parser built-in in the browser.
|
||||||
|
* On exception, the jsonString is validated and a detailed error is thrown.
|
||||||
|
* @param {String} jsonString
|
||||||
|
* @return {JSON} json
|
||||||
|
*/
|
||||||
|
export default function parseJSON(jsonString) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonString)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// try to throw a more detailed error message using validate
|
||||||
|
exports.validate(jsonString)
|
||||||
|
|
||||||
|
// rethrow the original error
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a string containing a JSON object
|
||||||
|
* This method uses JSONLint to validate the String. If JSONLint is not
|
||||||
|
* available, the built-in JSON parser of the browser is used.
|
||||||
|
* @param {String} jsonString String with an (invalid) JSON object
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
export function validate(jsonString) {
|
||||||
|
if (typeof(window.jsonlint) !== 'undefined') {
|
||||||
|
window.jsonlint.parse(jsonString)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
JSON.parse(jsonString)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import parseJSON from './parseJSON'
|
||||||
|
/**
|
||||||
|
* unescape a string.
|
||||||
|
* @param {String} escapedText
|
||||||
|
* @return {String} text
|
||||||
|
*/
|
||||||
|
export default function unescapeHTML (escapedText) {
|
||||||
|
var json = '"' + escapeJSON(escapedText) + '"'
|
||||||
|
var htmlEscaped = parseJSON(json)
|
||||||
|
|
||||||
|
return htmlEscaped
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/ |\u00A0/g, ' ')
|
||||||
|
.replace(/&/g, '&') // must be replaced last
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* escape a text to make it a valid JSON string. The method will:
|
||||||
|
* - replace unescaped double quotes with '\"'
|
||||||
|
* - replace unescaped backslash with '\\'
|
||||||
|
* - replace returns with '\n'
|
||||||
|
* @param {String} text
|
||||||
|
* @return {String} escapedText
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function escapeJSON (text) {
|
||||||
|
// TODO: replace with some smart regex (only when a new solution is faster!)
|
||||||
|
var escaped = ''
|
||||||
|
var i = 0
|
||||||
|
while (i < text.length) {
|
||||||
|
var c = text.charAt(i)
|
||||||
|
if (c == '\n') {
|
||||||
|
escaped += '\\n'
|
||||||
|
}
|
||||||
|
else if (c == '\\') {
|
||||||
|
escaped += c
|
||||||
|
i++
|
||||||
|
|
||||||
|
c = text.charAt(i)
|
||||||
|
if (c === '' || '"\\/bfnrtu'.indexOf(c) == -1) {
|
||||||
|
escaped += '\\' // no valid escape character
|
||||||
|
}
|
||||||
|
escaped += c
|
||||||
|
}
|
||||||
|
else if (c == '"') {
|
||||||
|
escaped += '\\"'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
escaped += c
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return escaped
|
||||||
|
}
|
Loading…
Reference in New Issue