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 = { this.state = {
logging: false, logging: false,
options: { editorProps: {
json, json,
schema: null, schema: null,
name: 'myObject',
onPatch: this.handlePatch, onPatch: this.handlePatch,
onPatchText: this.handlePatchText, onPatchText: this.handlePatchText,
onChange: this.handleChange, onChange: this.handleChange,
onChangeText: this.handleChangeText, onChangeText: this.handleChangeText,
onChangeMode: this.handleChangeMode, onChangeMode: this.handleChangeMode,
onError: this.handleError, onError: this.handleError,
name: 'myObject',
mode: 'tree', mode: 'tree',
modes: ['text', 'code', 'tree', 'form', 'view'], modes: ['text', 'code', 'tree', 'form', 'view'],
keyBindings: { keyBindings: {
@ -74,7 +75,6 @@ class App extends Component {
escapeUnicode: true, escapeUnicode: true,
history: true, history: true,
search: true, search: true,
expand: expandAll expand: expandAll
} }
} }
@ -87,7 +87,7 @@ class App extends Component {
<button onClick={this.handleGetJson}>Get JSON</button> <button onClick={this.handleGetJson}>Get JSON</button>
<label>mode: <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="text">text</option>
<option value="code">code</option> <option value="code">code</option>
<option value="tree">tree</option> <option value="tree">tree</option>
@ -98,7 +98,7 @@ class App extends Component {
<label> <label>
<input type="checkbox" <input type="checkbox"
value={this.state.options.schema !== null} value={this.state.editorProps.schema !== null}
onChange={this.handleToggleJSONSchema} /> JSON Schema onChange={this.handleToggleJSONSchema} /> JSON Schema
</label> </label>
@ -107,29 +107,30 @@ class App extends Component {
value={this.state.logging} value={this.state.logging}
onChange={this.handleToggleLogging} /> Log events onChange={this.handleToggleLogging} /> Log events
</label> </label>
<button onClick={this.forceChangeState}>Change state</button>
</div> </div>
<div className="contents"> <div className="contents">
<JSONEditor {...this.state.options} /> <JSONEditor {...this.state.editorProps} />
</div> </div>
</div> </div>
} }
handleSetJson = () => { handleSetJson = () => {
this.setState({ this.setState({
options: setIn(this.state.options, ['json'], largeJson) editorProps: setIn(this.state.editorProps, ['json'], largeJson)
}) })
} }
handleGetJson = () => { handleGetJson = () => {
// FIXME: get updating json in the state working const json = this.state.editorProps.json
const json = this.state.options.json
alert(JSON.stringify(json, null, 2)) alert(JSON.stringify(json, null, 2))
} }
handleSetMode = (event) => { handleSetMode = (event) => {
const mode = event.target.value const mode = event.target.value
this.setState({ 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) => { handleToggleJSONSchema = (event) => {
const s = event.target.checked ? schema : null const value = event.target.checked ? schema : null
this.setState({ this.setState({
options: setIn(this.state.options, ['schema'], s) editorProps: setIn(this.state.editorProps, ['schema'], value)
}) })
} }
handleChange = (json) => { handleChange = (json) => {
this.log('onChange json=', 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({ this.setState({
// options: setIn(this.state.options, ['json'], json) editorProps: setIn(this.state.editorProps, ['json'], json)
// }) })
} }
handleChangeText = (text) => { handleChangeText = (text) => {
@ -172,7 +173,7 @@ class App extends Component {
this.log('switched mode from', prevMode, 'to', mode) this.log('switched mode from', prevMode, 'to', mode)
this.setState({ 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-family: sans-serif;
font-size: 11pt; font-size: 11pt;
} }
#editor { #container {
height: 400px; height: 400px;
width: 100%; width: 100%;
max-width : 800px; max-width : 800px;
@ -141,7 +141,7 @@
} }
// set json // set json
document.getElementById('setJSON').onclick = function () { document.getElementById('setJSON').onclick = async function () {
editor.set(largeJson, { editor.set(largeJson, {
expand: function (path) { expand: function (path) {
return true return true

View File

@ -1,4 +1,4 @@
export const largeJson = { const largeJson = {
"version": "1.0", "version": "1.0",
"encoding": "UTF-8", "encoding": "UTF-8",
"feed": { "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 { createElement as h, PureComponent } from 'react'
import PropTypes from 'prop-types'
import initial from 'lodash/initial' import initial from 'lodash/initial'
import ActionMenu from './menu/ActionMenu' import ActionMenu from './menu/ActionMenu'
@ -26,15 +27,28 @@ const HOVERED_CLASS_NAMES = {
export default class JSONNode extends PureComponent { export default class JSONNode extends PureComponent {
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url' 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 = { state = {
menu: null, // can contain object {anchor, root} menu: null, // can contain object {anchor, root}
appendMenu: null, // can contain object {anchor, root} appendMenu: null, // can contain object {anchor, root}
hover: false hover: false
} }
componentWillMount (props) {
}
componentWillUnmount () { componentWillUnmount () {
if (hoveredNode === this) { if (hoveredNode === this) {
hoveredNode = null hoveredNode = null
@ -42,6 +56,7 @@ export default class JSONNode extends PureComponent {
} }
render () { render () {
// console.log('JSONNode.render ' + JSON.stringify(this.props.eson[META].path))
if (this.props.eson[META].type === 'Object') { if (this.props.eson[META].type === 'Object') {
return this.renderJSONObject(this.props) return this.renderJSONObject(this.props)
} }
@ -64,7 +79,7 @@ export default class JSONNode extends PureComponent {
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu), // this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(), // this.renderActionMenuButton(),
this.renderProperty(prop, index, eson, options), 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.renderFloatingMenuButton(),
this.renderError(eson[META].error) this.renderError(eson[META].error)
]) ])
@ -77,8 +92,8 @@ export default class JSONNode extends PureComponent {
// parent: this, // parent: this,
prop, prop,
eson: eson[prop], eson: eson[prop],
options, events,
events options
})) }))
childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, propsChilds) 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 isEqual from 'lodash/isEqual'
import reverse from 'lodash/reverse' import reverse from 'lodash/reverse'
import initial from 'lodash/initial' import initial from 'lodash/initial'
import pick from 'lodash/pick'
import Hammer from 'react-hammerjs' import Hammer from 'react-hammerjs'
import jump from '../assets/jump.js/src/jump' import jump from '../assets/jump.js/src/jump'
import Ajv from 'ajv' 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 SEARCH_DEBOUNCE = 300 // milliseconds
const SCROLL_DURATION = 400 // 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? id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
// TODO: define propTypes // TODO: define propTypes
@ -110,6 +111,8 @@ export default class TreeMode extends Component {
findKeyBinding: this.handleFindKeyBinding findKeyBinding: this.handleFindKeyBinding
}, },
options: {},
searchResult: { searchResult: {
text: '', text: '',
matches: null, matches: null,
@ -155,8 +158,6 @@ export default class TreeMode extends Component {
// Apply json // Apply json
if (nextProps.json !== this.state.json) { if (nextProps.json !== this.state.json) {
// FIXME: merge meta data from existing eson // FIXME: merge meta data from existing eson
// FIXME: keep state as is
const expandCallback = this.props.expand || TreeMode.expandRoot const expandCallback = this.props.expand || TreeMode.expandRoot
const json = nextProps.json const json = nextProps.json
const eson = expand(jsonToEson(json), expandCallback) const eson = expand(jsonToEson(json), expandCallback)
@ -165,7 +166,7 @@ export default class TreeMode extends Component {
json, json,
eson eson
}) })
// TODO: cleanup // FIXME: use patch again -> patch should keep existing meta data when for the unchanged parts of the json
// this.patch([{ // this.patch([{
// op: 'replace', // op: 'replace',
// path: '', // path: '',
@ -188,18 +189,22 @@ export default class TreeMode extends Component {
// TODO: apply patchText // TODO: apply patchText
// TODO: apply patch // TODO: apply patch
// Apply JSONNode options
const options = pick(nextProps, ['name', 'isPropertyEditable', 'isValueEditable', 'escapeUnicode'])
if (!isEqual(options, this.state.options)) {
this.setState({ options })
}
} }
render() { render() {
const { props, state } = this const Node = (this.props.mode === 'view')
const Node = (props.mode === 'view')
? JSONNodeView ? JSONNodeView
: (props.mode === 'form') : (this.props.mode === 'form')
? JSONNodeForm ? JSONNodeForm
: JSONNode : JSONNode
let eson = state.eson let eson = this.state.eson
// enrich the eson with selection and JSON Schema errors // 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?) // 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) eson = applySelection(eson, this.state.selection)
return h('div', { return h('div', {
className: `jsoneditor jsoneditor-mode-${props.mode}`, className: `jsoneditor jsoneditor-mode-${this.props.mode}`,
onKeyDown: this.handleKeyDown, onKeyDown: this.handleKeyDown,
'data-jsoneditor': 'true' 'data-jsoneditor': 'true'
}, [ }, [
@ -231,9 +236,8 @@ export default class TreeMode extends Component {
(eson[META].selected ? ' jsoneditor-selected' : '')}, (eson[META].selected ? ' jsoneditor-selected' : '')},
h(Node, { h(Node, {
eson, eson,
events: state.events, events: this.state.events,
options: props, options: this.state.options
prop: null
}) })
) )
) )
@ -679,7 +683,7 @@ export default class TreeMode extends Component {
// apply changes // apply changes
const result = this.patch(actions) const result = this.patch(actions)
this.emitOnChange (actions, result.revert, result.data) this.emitOnChange (actions, result.revert, result.eson, result.json)
} }
handleTouchStart = (event) => { handleTouchStart = (event) => {
@ -781,28 +785,31 @@ export default class TreeMode extends Component {
/** /**
* Emit an onChange event when there is a listener for it. * 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 * @private
* @param {ESONPatch} patch * @param {ESONPatch} patch
* @param {ESONPatch} revert * @param {ESONPatch} revert
* @param {ESON} eson * @param {ESON} eson
* @param {JSON} json
*/ */
emitOnChange (patch, revert, eson) { emitOnChange (patch, revert, eson, json) {
if (this.props.onPatch) { const onPatch = this.props.onPatch
this.props.onPatch(patch, revert) if (onPatch) {
setTimeout(() => onPatch(patch, revert))
} }
if (this.props.onChange || this.props.onChangeText) { const onChange = this.props.onChange
const json = esonToJson(eson) const onChangeText = this.props.onChangeText
if (onChange || onChangeText) {
if (this.props.onChange) { if (onChange) {
this.props.onChange(json) setTimeout(() => onChange(json))
} }
if (this.props.onChangeText) { if (onChangeText) {
const indentation = this.props.indentation || 2 const indentation = this.props.indentation || 2
const text = JSON.stringify(json, null, indentation) 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 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 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 expand = options.expand || (path => this.expandKeepOrExpandAll(path))
const result = patchEson(this.state.eson, actions, expand) const result = patchEson(this.state.eson, actions, expand)
const eson = result.data 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) { if (this.props.history !== false) {
// update data and store history // update data and store history
@ -897,6 +905,7 @@ export default class TreeMode extends Component {
// FIXME: apply search // FIXME: apply search
this.setState({ this.setState({
eson, eson,
json,
history, history,
historyIndex: 0 historyIndex: 0
}) })
@ -904,14 +913,19 @@ export default class TreeMode extends Component {
else { else {
// update data and don't store history // update data and don't store history
// FIXME: apply search // FIXME: apply search
this.setState({ eson }) this.setState({
eson,
json
})
} }
return { return {
patch: actions, patch: actions,
revert: result.revert, revert: result.revert,
error: result.error, 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 * @returns {Object | Array | string | number | boolean | null} json
*/ */
get () { get () {
// FIXME: keep a copy of the json file up to date with the internal eson return this.state.json
return esonToJson(this.state.eson)
} }
/** /**

View File

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