Apply changes in fields
This commit is contained in:
parent
0d250cbdcd
commit
4e0aa5659c
|
@ -24,17 +24,17 @@ ul.jsoneditor-list {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.jsoneditor-key,
|
||||
.jsoneditor-field,
|
||||
.jsoneditor-value {
|
||||
min-width: 32px;
|
||||
min-width: 24px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.jsoneditor-key:focus,
|
||||
.jsoneditor-field:focus,
|
||||
.jsoneditor-value:focus,
|
||||
.jsoneditor-key:hover,
|
||||
.jsoneditor-field:hover,
|
||||
.jsoneditor-value:hover {
|
||||
background-color: #FFFFAB;
|
||||
border-color: #ff0;
|
||||
|
@ -44,6 +44,6 @@ ul.jsoneditor-list {
|
|||
color: gray;
|
||||
}
|
||||
|
||||
.jsoneditor-info {
|
||||
.jsoneditor-readonly {
|
||||
color: gray;
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
import { h, Component } from 'preact'
|
||||
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 {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.onValueInput = this.onValueInput.bind(this)
|
||||
this.onBlurField = this.onBlurField.bind(this)
|
||||
this.onBlurValue = this.onBlurValue.bind(this)
|
||||
}
|
||||
|
||||
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)
|
||||
const hasParent = parent !== null
|
||||
|
||||
return h('li', {class: 'jsoneditor-object'}, [
|
||||
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-info'}, '{' + Object.keys(value).length + '}')
|
||||
h('div', {class: 'jsoneditor-readonly', contentEditable: false}, '{' + Object.keys(value).length + '}')
|
||||
]),
|
||||
h('ul',
|
||||
{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', {}, [
|
||||
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-info'}, '{' + value.length + '}')
|
||||
h('div', {class: 'jsoneditor-readonly', contentEditable: false}, '{' + value.length + '}')
|
||||
]),
|
||||
h('ul',
|
||||
{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)
|
||||
|
||||
return h('li', {}, [
|
||||
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-value',
|
||||
contentEditable: true,
|
||||
// 'data-path': JSON.stringify(this.getPath())
|
||||
onInput: this.onValueInput
|
||||
}, value)
|
||||
h('div', {class: 'jsoneditor-value', contentEditable: true, onBlur: this.onBlurValue}, escapeHTML(value))
|
||||
])
|
||||
])
|
||||
}
|
||||
|
@ -69,10 +77,21 @@ export default class JSONNode extends Component {
|
|||
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 value = event.target.innerHTML
|
||||
this.props.onChangeValue(path, value)
|
||||
const value = unescapeHTML(getInnerText(event.target))
|
||||
if (value !== this.props.value) {
|
||||
this.props.onChangeValue(path, value)
|
||||
}
|
||||
}
|
||||
|
||||
getPath () {
|
||||
|
@ -80,7 +99,7 @@ export default class JSONNode extends Component {
|
|||
|
||||
let node = this
|
||||
while (node) {
|
||||
path.unshift(node.props.field)
|
||||
path.unshift(node.props.field || node.props.index)
|
||||
|
||||
node = node.props.parent
|
||||
}
|
||||
|
@ -89,4 +108,12 @@ export default class JSONNode extends Component {
|
|||
|
||||
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 setIn from './utils/setIn'
|
||||
import getIn from './utils/getIn'
|
||||
import clone from './utils/clone'
|
||||
import JSONNode from './JSONNode'
|
||||
|
||||
export default class Main extends Component {
|
||||
|
@ -11,12 +13,19 @@ export default class Main extends Component {
|
|||
}
|
||||
|
||||
this.onChangeValue = this.onChangeValue.bind(this)
|
||||
this.onChangeField = this.onChangeField.bind(this)
|
||||
}
|
||||
|
||||
render(props, state) {
|
||||
return h('div', {class: 'jsoneditor', onInput: this.onInput}, [
|
||||
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 () {
|
||||
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