Apply changes in fields

This commit is contained in:
jos 2016-07-12 15:43:42 +02:00
parent 0d250cbdcd
commit 4e0aa5659c
8 changed files with 344 additions and 27 deletions

View File

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

View File

@ -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,18 +77,29 @@ 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))
if (value !== this.props.value) {
this.props.onChangeValue(path, value) this.props.onChangeValue(path, value)
} }
}
getPath () { getPath () {
const path = [] const path = []
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
}
} }

View File

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

42
src/utils/escapeHTML.js Normal file
View File

@ -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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/ /g, ' &nbsp;') // replace double space with an nbsp and space
.replace(/^ /, '&nbsp;') // space at start
.replace(/ $/, '&nbsp;'); // 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)
})
}

32
src/utils/getIn.js Normal file
View File

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

101
src/utils/getInnerText.js Normal file
View File

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

35
src/utils/parseJSON.js Normal file
View File

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

56
src/utils/unescapeHTML.js Normal file
View File

@ -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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&nbsp;|\u00A0/g, ' ')
.replace(/&amp;/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
}