Fixed TreeMode rendering everything again when any options changed

This commit is contained in:
jos 2017-12-27 15:34:30 +01:00
parent 6385e4b193
commit 9e0249f550
6 changed files with 97 additions and 59 deletions

View File

@ -52,17 +52,18 @@ class App extends Component {
this.state = {
logging: false,
options: {
editorProps: {
json,
schema: null,
name: 'myObject',
onPatch: this.handlePatch,
onPatchText: this.handlePatchText,
onChange: this.handleChange,
onChangeText: this.handleChangeText,
onChangeMode: this.handleChangeMode,
onError: this.handleError,
name: 'myObject',
mode: 'tree',
modes: ['text', 'code', 'tree', 'form', 'view'],
keyBindings: {
@ -74,7 +75,6 @@ class App extends Component {
escapeUnicode: true,
history: true,
search: true,
expand: expandAll
}
}
@ -87,7 +87,7 @@ class App extends Component {
<button onClick={this.handleGetJson}>Get JSON</button>
<label>mode:
<select value={this.state.options.mode} onChange={this.handleSetMode}>
<select value={this.state.editorProps.mode} onChange={this.handleSetMode}>
<option value="text">text</option>
<option value="code">code</option>
<option value="tree">tree</option>
@ -98,7 +98,7 @@ class App extends Component {
<label>
<input type="checkbox"
value={this.state.options.schema !== null}
value={this.state.editorProps.schema !== null}
onChange={this.handleToggleJSONSchema} /> JSON Schema
</label>
@ -107,29 +107,30 @@ class App extends Component {
value={this.state.logging}
onChange={this.handleToggleLogging} /> Log events
</label>
<button onClick={this.forceChangeState}>Change state</button>
</div>
<div className="contents">
<JSONEditor {...this.state.options} />
<JSONEditor {...this.state.editorProps} />
</div>
</div>
}
handleSetJson = () => {
this.setState({
options: setIn(this.state.options, ['json'], largeJson)
editorProps: setIn(this.state.editorProps, ['json'], largeJson)
})
}
handleGetJson = () => {
// FIXME: get updating json in the state working
const json = this.state.options.json
const json = this.state.editorProps.json
alert(JSON.stringify(json, null, 2))
}
handleSetMode = (event) => {
const mode = event.target.value
this.setState({
options: setIn(this.state.options, ['mode'], mode)
editorProps: setIn(this.state.editorProps, ['mode'], mode)
})
}
@ -139,18 +140,18 @@ class App extends Component {
}
handleToggleJSONSchema = (event) => {
const s = event.target.checked ? schema : null
const value = event.target.checked ? schema : null
this.setState({
options: setIn(this.state.options, ['schema'], s)
editorProps: setIn(this.state.editorProps, ['schema'], value)
})
}
handleChange = (json) => {
this.log('onChange json=', json)
// FIXME: update the json in the state (after JSONEditor neatly updates it instead of generating new json every time
// this.setState({
// options: setIn(this.state.options, ['json'], json)
// })
this.setState({
editorProps: setIn(this.state.editorProps, ['json'], json)
})
}
handleChangeText = (text) => {
@ -172,7 +173,7 @@ class App extends Component {
this.log('switched mode from', prevMode, 'to', mode)
this.setState({
options: setIn(this.state.options, ['mode'], mode)
editorProps: setIn(this.state.editorProps, ['mode'], mode)
})
}

View File

@ -16,7 +16,7 @@
font-family: sans-serif;
font-size: 11pt;
}
#editor {
#container {
height: 400px;
width: 100%;
max-width : 800px;
@ -141,7 +141,7 @@
}
// set json
document.getElementById('setJSON').onclick = function () {
document.getElementById('setJSON').onclick = async function () {
editor.set(largeJson, {
expand: function (path) {
return true

View File

@ -1,4 +1,4 @@
export const largeJson = {
const largeJson = {
"version": "1.0",
"encoding": "UTF-8",
"feed": {
@ -12603,3 +12603,12 @@ export const largeJson = {
]
}
}
// export for node.js or web
if (typeof module !== 'undefined' && typeof exports !== 'undefined') {
exports.largeJson = largeJson
}
else {
window.largeJson = largeJson
}

View File

@ -1,4 +1,5 @@
import { createElement as h, PureComponent } from 'react'
import PropTypes from 'prop-types'
import initial from 'lodash/initial'
import ActionMenu from './menu/ActionMenu'
@ -26,15 +27,28 @@ const HOVERED_CLASS_NAMES = {
export default class JSONNode extends PureComponent {
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
static propTypes = {
prop: PropTypes.string, // in case of an object property
index: PropTypes.number, // in case of an array item
eson: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
events: PropTypes.object.isRequired,
// options
options: PropTypes.shape({
name: PropTypes.string, // name of the root item
isPropertyEditable: PropTypes.func,
isValueEditable: PropTypes.func,
escapeUnicode: PropTypes.bool
})
}
state = {
menu: null, // can contain object {anchor, root}
appendMenu: null, // can contain object {anchor, root}
hover: false
}
componentWillMount (props) {
}
componentWillUnmount () {
if (hoveredNode === this) {
hoveredNode = null
@ -42,6 +56,7 @@ export default class JSONNode extends PureComponent {
}
render () {
// console.log('JSONNode.render ' + JSON.stringify(this.props.eson[META].path))
if (this.props.eson[META].type === 'Object') {
return this.renderJSONObject(this.props)
}
@ -64,7 +79,7 @@ export default class JSONNode extends PureComponent {
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderProperty(prop, index, eson, options),
this.renderReadonly(`{${props.length}}`, `Array containing ${props.length} items`),
this.renderReadonly(`{${props.length}}`, `Object containing ${props.length} items`),
// this.renderFloatingMenuButton(),
this.renderError(eson[META].error)
])
@ -77,8 +92,8 @@ export default class JSONNode extends PureComponent {
// parent: this,
prop,
eson: eson[prop],
options,
events
events,
options
}))
childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, propsChilds)

View File

@ -1,7 +1,8 @@
import { createElement as h, Component } from 'react'
import { createElement as h, PureComponent } from 'react'
import isEqual from 'lodash/isEqual'
import reverse from 'lodash/reverse'
import initial from 'lodash/initial'
import pick from 'lodash/pick'
import Hammer from 'react-hammerjs'
import jump from '../assets/jump.js/src/jump'
import Ajv from 'ajv'
@ -44,7 +45,7 @@ const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept
const SEARCH_DEBOUNCE = 300 // milliseconds
const SCROLL_DURATION = 400 // milliseconds
export default class TreeMode extends Component {
export default class TreeMode extends PureComponent {
id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
// TODO: define propTypes
@ -110,6 +111,8 @@ export default class TreeMode extends Component {
findKeyBinding: this.handleFindKeyBinding
},
options: {},
searchResult: {
text: '',
matches: null,
@ -155,8 +158,6 @@ export default class TreeMode extends Component {
// Apply json
if (nextProps.json !== this.state.json) {
// FIXME: merge meta data from existing eson
// FIXME: keep state as is
const expandCallback = this.props.expand || TreeMode.expandRoot
const json = nextProps.json
const eson = expand(jsonToEson(json), expandCallback)
@ -165,7 +166,7 @@ export default class TreeMode extends Component {
json,
eson
})
// TODO: cleanup
// FIXME: use patch again -> patch should keep existing meta data when for the unchanged parts of the json
// this.patch([{
// op: 'replace',
// path: '',
@ -188,18 +189,22 @@ export default class TreeMode extends Component {
// TODO: apply patchText
// TODO: apply patch
// Apply JSONNode options
const options = pick(nextProps, ['name', 'isPropertyEditable', 'isValueEditable', 'escapeUnicode'])
if (!isEqual(options, this.state.options)) {
this.setState({ options })
}
}
render() {
const { props, state } = this
const Node = (props.mode === 'view')
const Node = (this.props.mode === 'view')
? JSONNodeView
: (props.mode === 'form')
: (this.props.mode === 'form')
? JSONNodeForm
: JSONNode
let eson = state.eson
let eson = this.state.eson
// enrich the eson with selection and JSON Schema errors
// TODO: for optimization, we can apply errors only when the eson is changed? (a wrapper around setState or something?)
@ -207,7 +212,7 @@ export default class TreeMode extends Component {
eson = applySelection(eson, this.state.selection)
return h('div', {
className: `jsoneditor jsoneditor-mode-${props.mode}`,
className: `jsoneditor jsoneditor-mode-${this.props.mode}`,
onKeyDown: this.handleKeyDown,
'data-jsoneditor': 'true'
}, [
@ -231,9 +236,8 @@ export default class TreeMode extends Component {
(eson[META].selected ? ' jsoneditor-selected' : '')},
h(Node, {
eson,
events: state.events,
options: props,
prop: null
events: this.state.events,
options: this.state.options
})
)
)
@ -679,7 +683,7 @@ export default class TreeMode extends Component {
// apply changes
const result = this.patch(actions)
this.emitOnChange (actions, result.revert, result.data)
this.emitOnChange (actions, result.revert, result.eson, result.json)
}
handleTouchStart = (event) => {
@ -781,28 +785,31 @@ export default class TreeMode extends Component {
/**
* Emit an onChange event when there is a listener for it.
* events will be fired on the next tick (after any changed state is applied)
* @private
* @param {ESONPatch} patch
* @param {ESONPatch} revert
* @param {ESON} eson
* @param {JSON} json
*/
emitOnChange (patch, revert, eson) {
if (this.props.onPatch) {
this.props.onPatch(patch, revert)
emitOnChange (patch, revert, eson, json) {
const onPatch = this.props.onPatch
if (onPatch) {
setTimeout(() => onPatch(patch, revert))
}
if (this.props.onChange || this.props.onChangeText) {
const json = esonToJson(eson)
if (this.props.onChange) {
this.props.onChange(json)
const onChange = this.props.onChange
const onChangeText = this.props.onChangeText
if (onChange || onChangeText) {
if (onChange) {
setTimeout(() => onChange(json))
}
if (this.props.onChangeText) {
if (onChangeText) {
const indentation = this.props.indentation || 2
const text = JSON.stringify(json, null, indentation)
this.props.onChangeText(text)
setTimeout(() => onChangeText(text))
}
}
}
@ -840,7 +847,7 @@ export default class TreeMode extends Component {
historyIndex: historyIndex + 1
})
this.emitOnChange (historyItem.undo, historyItem.redo, result.data)
this.emitOnChange(historyItem.undo, historyItem.redo, result.eson, result.json)
}
}
@ -859,7 +866,7 @@ export default class TreeMode extends Component {
historyIndex
})
this.emitOnChange (historyItem.redo, historyItem.undo, result.data)
this.emitOnChange(historyItem.redo, historyItem.undo, result.eson, result.json)
}
}
@ -882,6 +889,7 @@ export default class TreeMode extends Component {
const expand = options.expand || (path => this.expandKeepOrExpandAll(path))
const result = patchEson(this.state.eson, actions, expand)
const eson = result.data
const json = esonToJson(eson) // FIXME: apply the patch to the json too, instead of completely replacing it
if (this.props.history !== false) {
// update data and store history
@ -897,6 +905,7 @@ export default class TreeMode extends Component {
// FIXME: apply search
this.setState({
eson,
json,
history,
historyIndex: 0
})
@ -904,14 +913,19 @@ export default class TreeMode extends Component {
else {
// update data and don't store history
// FIXME: apply search
this.setState({ eson })
this.setState({
eson,
json
})
}
return {
patch: actions,
revert: result.revert,
error: result.error,
data: eson // FIXME: shouldn't pass data here
data: eson, // FIXME: shouldn't pass data here?
eson, // FIXME: shouldn't pass eson here
json // FIXME: shouldn't pass json here
}
}
@ -940,8 +954,7 @@ export default class TreeMode extends Component {
* @returns {Object | Array | string | number | boolean | null} json
*/
get () {
// FIXME: keep a copy of the json file up to date with the internal eson
return esonToJson(this.state.eson)
return this.state.json
}
/**

View File

@ -344,7 +344,7 @@ function setSearchStatus (eson, esonPointer, searchStatus) {
/**
* Merge selection status into the eson object, cleanup previous selection
* @param {ESON} eson
* @param {Selection} selection
* @param {Selection} [selection]
* @return {ESON} Returns updated eson object
*/
export function applySelection (eson, selection) {
@ -462,7 +462,7 @@ export function pathsFromSelection (eson, selection) {
* Get the contents of a list with paths
* @param {ESON} data
* @param {Path[]} paths
* @return {Array.<{name: string, value: JSONType}>}
* @return {Array.<{name: string, value: JSON}>}
*/
export function contentsFromPaths (data, paths) {
return paths.map(path => {