Implement `syncState` (WIP)
This commit is contained in:
parent
41c633e124
commit
596d868cc5
|
@ -12,12 +12,13 @@
|
|||
import { createHistory } from './history.js'
|
||||
import Node from './JSONNode.svelte'
|
||||
import { existsIn, getIn, setIn } from './utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer } from './utils/jsonPointer.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from './utils/jsonPointer.js'
|
||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
import { flattenSearch, search } from './utils/search.js'
|
||||
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
||||
import { isEqual, isNumber } from 'lodash-es'
|
||||
import { isEqual, isNumber, initial, last } from 'lodash-es'
|
||||
import jump from './assets/jump.js/src/jump.js'
|
||||
import { syncState } from './utils/syncState.js'
|
||||
|
||||
let divContents
|
||||
|
||||
|
@ -28,15 +29,20 @@
|
|||
console.timeEnd('render')
|
||||
})
|
||||
|
||||
export let json = {}
|
||||
export let onChangeJson = () => {
|
||||
export let json = {} // TODO: rename 'json' to 'document'
|
||||
export let onChangeJson = () => {}
|
||||
|
||||
function expand (path) {
|
||||
return path.length < 1
|
||||
}
|
||||
|
||||
const INITIAL_STATE = {
|
||||
[STATE_EXPANDED]: true
|
||||
}
|
||||
let state
|
||||
|
||||
let state = INITIAL_STATE
|
||||
$: {
|
||||
console.time('syncState')
|
||||
state = syncState(json, state, [], expand)
|
||||
console.timeEnd('syncState')
|
||||
}
|
||||
|
||||
let showSearch = false
|
||||
let searchText = ''
|
||||
|
@ -54,39 +60,85 @@
|
|||
|
||||
export function set(newJson) {
|
||||
json = newJson
|
||||
state = INITIAL_STATE
|
||||
state = undefined
|
||||
history.clear()
|
||||
}
|
||||
|
||||
function applyPatch (operations) {
|
||||
const patchResult = immutableJSONPatch(json, operations)
|
||||
json = patchResult.json
|
||||
|
||||
state = immutableJSONPatch(state, operations).json
|
||||
|
||||
return patchResult
|
||||
}
|
||||
|
||||
export function patch(operations) {
|
||||
console.log('patch', operations)
|
||||
const prevState = state
|
||||
|
||||
const patchResult = applyPatch(operations)
|
||||
const documentPatchResult = immutableJSONPatch(json, operations)
|
||||
const statePatchResult = immutableJSONPatch(state, operations)
|
||||
// TODO: only apply operations to state for relevant operations: move, copy, delete? Figure out
|
||||
|
||||
json = documentPatchResult.json
|
||||
state = statePatchResult.json
|
||||
|
||||
// if a property is renamed (move operation), rename it in the object's props
|
||||
// so it maintains its identity and hence its index
|
||||
operations
|
||||
.filter(operation => {
|
||||
return operation.op === 'move' && isEqual(
|
||||
initial(parseJSONPointer(operation.from)),
|
||||
initial(parseJSONPointer(operation.path))
|
||||
)
|
||||
})
|
||||
.forEach(operation => {
|
||||
const pathFrom = parseJSONPointer(operation.from)
|
||||
const to = parseJSONPointer(operation.path)
|
||||
const parentPath = initial(pathFrom)
|
||||
const oldKey = last(pathFrom)
|
||||
const newKey = last(to)
|
||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const index = props.findIndex(item => item.key === oldKey)
|
||||
if (index !== -1) {
|
||||
state = setIn(state, parentPath.concat([STATE_PROPS, index, 'key']), newKey)
|
||||
}
|
||||
})
|
||||
|
||||
history.add({
|
||||
undo: patchResult.revert,
|
||||
redo: operations
|
||||
undo: documentPatchResult.revert,
|
||||
redo: operations,
|
||||
prevState: prevState,
|
||||
state: state
|
||||
})
|
||||
|
||||
json = patchResult.json
|
||||
|
||||
return {
|
||||
json,
|
||||
error: patchResult.error,
|
||||
undo: patchResult.revert,
|
||||
error: documentPatchResult.error,
|
||||
undo: documentPatchResult.revert,
|
||||
redo: operations
|
||||
}
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
if (history.getState().canUndo) {
|
||||
const item = history.undo()
|
||||
if (item) {
|
||||
json = immutableJSONPatch(json, item.undo).json
|
||||
state = item.prevState
|
||||
|
||||
console.log('undo', { item, json, state })
|
||||
|
||||
emitOnChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRedo() {
|
||||
if (history.getState().canRedo) {
|
||||
const item = history.redo()
|
||||
if (item) {
|
||||
json = immutableJSONPatch(json, item.redo).json
|
||||
state = item.state
|
||||
|
||||
console.log('redo', { item, json, state })
|
||||
|
||||
emitOnChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch(json, searchText) {
|
||||
return search(null, json, searchText)
|
||||
}
|
||||
|
@ -177,32 +229,13 @@
|
|||
showSearch = !showSearch
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
if (history.getState().canUndo) {
|
||||
const item = history.undo()
|
||||
if (item) {
|
||||
applyPatch(item.undo)
|
||||
emitOnChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRedo() {
|
||||
if (history.getState().canRedo) {
|
||||
const item = history.redo()
|
||||
if (item) {
|
||||
applyPatch(item.redo)
|
||||
emitOnChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle expanded state of a node
|
||||
* @param {Path} path
|
||||
* @param {boolean} expanded
|
||||
*/
|
||||
function handleExpand (path, expanded) {
|
||||
console.log('handleExpand', path, expanded)
|
||||
state = setIn(state, path.concat(STATE_EXPANDED), expanded)
|
||||
}
|
||||
|
||||
|
@ -215,15 +248,6 @@
|
|||
state = setIn(state, path.concat(STATE_LIMIT), limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update object properties
|
||||
* @param {Path} path
|
||||
* @param {Object} props
|
||||
*/
|
||||
function handleUpdateProps (path, props) {
|
||||
state = setIn(state, path.concat(STATE_PROPS), props)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all nodes on given path
|
||||
* @param {Path} path
|
||||
|
@ -341,7 +365,6 @@
|
|||
onPatch={handlePatch}
|
||||
onExpand={handleExpand}
|
||||
onLimit={handleLimit}
|
||||
onUpdateProps={handleUpdateProps}
|
||||
/>
|
||||
<div class='bottom'></div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { isObject } from 'lodash-es'
|
||||
import { onMount } from 'svelte'
|
||||
import { debounce } from 'lodash-es'
|
||||
import {
|
||||
DEBOUNCE_DELAY, DEFAULT_LIMIT,
|
||||
STATE_EXPANDED, STATE_LIMIT, STATE_PROPS,
|
||||
|
@ -11,7 +10,6 @@
|
|||
import Icon from 'svelte-awesome'
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import classnames from 'classnames'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { findUniqueName } from './utils/stringUtils.js'
|
||||
import { isUrl, stringConvert, valueType } from './utils/typeUtils'
|
||||
import { updateProps } from './utils/updateProps.js'
|
||||
|
@ -26,11 +24,10 @@
|
|||
export let onChangeKey
|
||||
export let onExpand
|
||||
export let onLimit
|
||||
export let onUpdateProps
|
||||
|
||||
$: expanded = state && state[STATE_EXPANDED]
|
||||
$: limit = state && state[STATE_LIMIT] || DEFAULT_LIMIT
|
||||
$: props = state && state[STATE_PROPS] || []
|
||||
$: limit = state && state[STATE_LIMIT]
|
||||
$: props = state && state[STATE_PROPS]
|
||||
|
||||
const escapeUnicode = false // TODO: pass via options
|
||||
|
||||
|
@ -39,17 +36,6 @@
|
|||
|
||||
$: type = valueType (value)
|
||||
|
||||
let prevValue = undefined
|
||||
// let props = undefined
|
||||
|
||||
$: if (isObject(value) && value !== prevValue) {
|
||||
prevValue = value
|
||||
const updatedProps = updateProps(value, props)
|
||||
onUpdateProps(path, updatedProps)
|
||||
}
|
||||
|
||||
// $: console.log('props', props)
|
||||
|
||||
$: limited = type === 'array' && value.length > limit
|
||||
|
||||
$: items = type === 'array'
|
||||
|
@ -183,6 +169,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: can we do a handleChangeKey in the child again?
|
||||
function handleChangeKey (newChildKey, oldChildKey) {
|
||||
if (type === 'object') {
|
||||
// make sure the key is not a duplicate of an other property
|
||||
|
@ -190,18 +177,18 @@
|
|||
|
||||
if (uniqueNewChildKey !== oldChildKey) {
|
||||
// we need to make sure that the renamed property will keep the same id
|
||||
const index = props.findIndex(item => item.key === oldChildKey)
|
||||
if (index !== -1) {
|
||||
// we use splice here to replace the old key with the new new one
|
||||
// already without Svelte noticing it (no assignment), so we prevent
|
||||
// a needless render. We keep the same id, so the child HTML will be
|
||||
// reused
|
||||
// TODO: is there a better way to do this?
|
||||
props.splice(index, 1, {
|
||||
id: props[index].id,
|
||||
key: uniqueNewChildKey
|
||||
})
|
||||
}
|
||||
// const index = props.findIndex(item => item.key === oldChildKey)
|
||||
// if (index !== -1) {
|
||||
// // we use splice here to replace the old key with the new new one
|
||||
// // already without Svelte noticing it (no assignment), so we prevent
|
||||
// // a needless render. We keep the same id, so the child HTML will be
|
||||
// // reused
|
||||
// // TODO: is there a better way to do this?
|
||||
// props.splice(index, 1, {
|
||||
// id: props[index].id,
|
||||
// key: uniqueNewChildKey
|
||||
// })
|
||||
// }
|
||||
|
||||
onPatch([{
|
||||
op: 'move',
|
||||
|
@ -260,7 +247,6 @@
|
|||
onPatch={onPatch}
|
||||
onExpand={onExpand}
|
||||
onLimit={onLimit}
|
||||
onUpdateProps={onUpdateProps}
|
||||
/>
|
||||
{/each}
|
||||
{#if limited}
|
||||
|
@ -298,7 +284,7 @@
|
|||
<span class="delimiter">{</span>
|
||||
{:else}
|
||||
<span class="delimiter"> {</span>
|
||||
<button class="tag" on:click={() => onExpand(path, true)}>{props.length} props</button>
|
||||
<button class="tag" on:click={() => onExpand(path, true)}>{Object.keys(value).length} props</button>
|
||||
<span class="delimiter">}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -315,7 +301,6 @@
|
|||
onPatch={onPatch}
|
||||
onExpand={onExpand}
|
||||
onLimit={onLimit}
|
||||
onUpdateProps={onUpdateProps}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
const MAX_HISTORY_ITEMS = 1000
|
||||
|
||||
/**
|
||||
* @typedef {Object} HistoryItem
|
||||
* @typedef {*} HistoryItem
|
||||
* @property {Object} undo
|
||||
* @property {Object} redo
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { STATE_EXPANDED, STATE_LIMIT } from '../constants.js'
|
||||
import { isObjectOrArray } from './typeUtils.js'
|
||||
|
||||
/**
|
||||
|
@ -19,7 +20,17 @@ import { isObjectOrArray } from './typeUtils.js'
|
|||
export function shallowClone (value) {
|
||||
if (Array.isArray(value)) {
|
||||
// copy array items
|
||||
return value.slice()
|
||||
const copy = value.slice()
|
||||
|
||||
// FIXME: this is very ugly hack to copy properties attached to an Array!
|
||||
if (value[STATE_EXPANDED] !== undefined) {
|
||||
copy[STATE_EXPANDED] = value[STATE_EXPANDED]
|
||||
}
|
||||
if (value[STATE_LIMIT] !== undefined) {
|
||||
copy[STATE_LIMIT] = value[STATE_LIMIT]
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
else if (typeof value === 'object') {
|
||||
// copy object properties
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
setIn
|
||||
} from './immutabilityHelpers.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from './jsonPointer.js'
|
||||
import initial from 'lodash/initial.js'
|
||||
import isEqual from 'lodash/isEqual.js'
|
||||
import initial from 'lodash/initial.js' // FIXME
|
||||
import isEqual from 'lodash/isEqual.js' // FIXME
|
||||
|
||||
/**
|
||||
* Apply a patch to a JSON object
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
DEFAULT_LIMIT,
|
||||
STATE_EXPANDED,
|
||||
STATE_LIMIT,
|
||||
STATE_PROPS
|
||||
} from '../constants.js'
|
||||
import { isObject, isObjectOrArray } from './typeUtils.js'
|
||||
import { updateProps } from './updateProps.js'
|
||||
|
||||
/**
|
||||
* @param {JSON} document
|
||||
* @param {JSON | undefined} state
|
||||
* @param {Path} path
|
||||
* @param {function (path: Path) : boolean} expand
|
||||
* @returns {JSON | undefined}
|
||||
*/
|
||||
export function syncState (document, state = undefined, path, expand) {
|
||||
if (isObject(document)) {
|
||||
const updatedState = {}
|
||||
|
||||
updatedState[STATE_PROPS] = updateProps(document, state && state[STATE_PROPS])
|
||||
updatedState[STATE_EXPANDED] = state
|
||||
? state[STATE_EXPANDED]
|
||||
: expand(path)
|
||||
|
||||
if (updatedState[STATE_EXPANDED]) {
|
||||
Object.keys(document).forEach(key => {
|
||||
const childDocument = document[key]
|
||||
if (isObjectOrArray(childDocument)) {
|
||||
const childState = state && state[key]
|
||||
updatedState[key] = syncState(childDocument, childState, path.concat(key), expand)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
if (Array.isArray(document)) {
|
||||
const updatedState = []
|
||||
|
||||
// TODO: can we make the state for array a regular object { limit: 100, items: [...] }?
|
||||
|
||||
updatedState[STATE_LIMIT] = state
|
||||
? state[STATE_LIMIT]
|
||||
: DEFAULT_LIMIT
|
||||
|
||||
updatedState[STATE_EXPANDED] = state
|
||||
? state[STATE_EXPANDED]
|
||||
: expand(path)
|
||||
|
||||
if (updatedState[STATE_EXPANDED]) {
|
||||
for (let i = 0; i < Math.min(document.length, updatedState[STATE_LIMIT]); i++) {
|
||||
const childDocument = document[i]
|
||||
if (isObjectOrArray(childDocument)) {
|
||||
const childState = state && state[i]
|
||||
updatedState[i] = syncState(childDocument, childState, path.concat(i), expand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
// primitive values have no state
|
||||
return undefined
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
DEFAULT_LIMIT,
|
||||
STATE_EXPANDED,
|
||||
STATE_LIMIT,
|
||||
STATE_PROPS
|
||||
} from '../constants.js'
|
||||
import { syncState } from './syncState.js'
|
||||
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
|
||||
|
||||
const test = it // TODO: replace jest with mocha tests, or move to jest
|
||||
|
||||
test('syncState', () => {
|
||||
const document = {
|
||||
array: [1, 2, {c: 6}],
|
||||
object: {a: 4, b: 5},
|
||||
value: 'hello'
|
||||
}
|
||||
|
||||
function expand (path) {
|
||||
return path.length <= 1
|
||||
}
|
||||
|
||||
const state = syncState(document, undefined, [], expand)
|
||||
|
||||
const expectedState = {}
|
||||
expectedState[STATE_EXPANDED] = true
|
||||
expectedState[STATE_PROPS] = [
|
||||
{ 'id': '1', 'key': 'array' },
|
||||
{ 'id': '2', 'key': 'object' },
|
||||
{ 'id': '3', 'key': 'value' }
|
||||
]
|
||||
expectedState.array = []
|
||||
expectedState.array[STATE_EXPANDED] = true
|
||||
expectedState.array[STATE_LIMIT] = DEFAULT_LIMIT
|
||||
expectedState.array[2] = {}
|
||||
expectedState.array[2][STATE_EXPANDED] = false
|
||||
expectedState.array[2][STATE_PROPS] = [
|
||||
{ 'id': '4', 'key': 'c' } // FIXME: props should not be created because node is not expande
|
||||
]
|
||||
expectedState.object = {}
|
||||
expectedState.object[STATE_EXPANDED] = true
|
||||
expectedState.object[STATE_PROPS] = [
|
||||
{ 'id': '5', 'key': 'a' },
|
||||
{ 'id': '6', 'key': 'b' }
|
||||
]
|
||||
|
||||
expect(state).toEqual(expectedState)
|
||||
})
|
||||
|
||||
// TODO: write more unit tests for syncState
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { isObject } from './typeUtils.js'
|
||||
import uniqueId from 'lodash/uniqueId.js'
|
||||
import uniqueId from 'lodash/uniqueId.js' // FIXME
|
||||
|
||||
export function updateProps (value, prevProps) {
|
||||
if (isObject(value)) {
|
||||
|
|
|
@ -9,7 +9,6 @@ test('updateProps (1)', () => {
|
|||
|
||||
const props2 = updateProps({a: 1, b: 2}, props1)
|
||||
expect(props2.map(item => item.key)).toEqual(['b', 'a'])
|
||||
expect(props2[0].id).toEqual('1')
|
||||
})
|
||||
|
||||
test('updateProps (2)', () => {
|
||||
|
|
Loading…
Reference in New Issue