Fix pasting properties inline in an object instead of at the bottom
This commit is contained in:
parent
e44284df90
commit
2f393e5948
|
@ -19,9 +19,7 @@
|
||||||
import { createHistory } from './history.js'
|
import { createHistory } from './history.js'
|
||||||
import Node from './JSONNode.svelte'
|
import Node from './JSONNode.svelte'
|
||||||
import { expandSelection } from './selection.js'
|
import { expandSelection } from './selection.js'
|
||||||
import { singleton } from './singleton.js'
|
|
||||||
import {
|
import {
|
||||||
deleteIn,
|
|
||||||
existsIn,
|
existsIn,
|
||||||
getIn,
|
getIn,
|
||||||
setIn,
|
setIn,
|
||||||
|
@ -35,6 +33,7 @@
|
||||||
import jump from './assets/jump.js/src/jump.js'
|
import jump from './assets/jump.js/src/jump.js'
|
||||||
import { syncState } from './utils/syncState.js'
|
import { syncState } from './utils/syncState.js'
|
||||||
import { isObject } from './utils/typeUtils.js'
|
import { isObject } from './utils/typeUtils.js'
|
||||||
|
import { patchProps } from './utils/updateProps.js'
|
||||||
|
|
||||||
let divContents
|
let divContents
|
||||||
|
|
||||||
|
@ -99,25 +98,7 @@
|
||||||
|
|
||||||
// if a property is renamed (move operation), rename it in the object's props
|
// if a property is renamed (move operation), rename it in the object's props
|
||||||
// so it maintains its identity and hence its index
|
// so it maintains its identity and hence its index
|
||||||
operations
|
state = patchProps(state, 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({
|
history.add({
|
||||||
undo: documentPatchResult.revert,
|
undo: documentPatchResult.revert,
|
||||||
|
@ -303,6 +284,10 @@
|
||||||
emitOnChange()
|
emitOnChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUpdateKey (oldKey, newKey) {
|
||||||
|
// should never be called on the root
|
||||||
|
}
|
||||||
|
|
||||||
function handleToggleSearch() {
|
function handleToggleSearch() {
|
||||||
showSearch = !showSearch
|
showSearch = !showSearch
|
||||||
}
|
}
|
||||||
|
@ -526,6 +511,7 @@
|
||||||
state={state}
|
state={state}
|
||||||
searchResult={searchResultWithActive}
|
searchResult={searchResultWithActive}
|
||||||
onPatch={handlePatch}
|
onPatch={handlePatch}
|
||||||
|
onUpdateKey={handleUpdateKey}
|
||||||
onExpand={handleExpand}
|
onExpand={handleExpand}
|
||||||
onLimit={handleLimit}
|
onLimit={handleLimit}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { debounce, initial, isEqual, last } from 'lodash-es'
|
import { debounce, isEqual } from 'lodash-es'
|
||||||
|
import { rename } from './actions.js'
|
||||||
import {
|
import {
|
||||||
DEBOUNCE_DELAY,
|
DEBOUNCE_DELAY,
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
export let state
|
export let state
|
||||||
export let searchResult
|
export let searchResult
|
||||||
export let onPatch
|
export let onPatch
|
||||||
|
export let onUpdateKey
|
||||||
export let onExpand
|
export let onExpand
|
||||||
export let onLimit
|
export let onLimit
|
||||||
export let onSelect
|
export let onSelect
|
||||||
|
@ -98,16 +100,21 @@
|
||||||
|
|
||||||
function updateKey () {
|
function updateKey () {
|
||||||
const newKey = getPlainText(domKey)
|
const newKey = getPlainText(domKey)
|
||||||
const parentPath = initial(path)
|
|
||||||
|
|
||||||
onPatch([{
|
// must be handled by the parent which has knowledge about the other keys
|
||||||
op: 'move',
|
onUpdateKey(key, newKey)
|
||||||
from: compileJSONPointer(parentPath.concat(key)),
|
|
||||||
path: compileJSONPointer(parentPath.concat(newKey))
|
|
||||||
}])
|
|
||||||
}
|
}
|
||||||
const updateKeyDebounced = debounce(updateKey, DEBOUNCE_DELAY)
|
const updateKeyDebounced = debounce(updateKey, DEBOUNCE_DELAY)
|
||||||
|
|
||||||
|
function handleUpdateKey (oldKey, newKey) {
|
||||||
|
const index = props.findIndex(prop => prop.key === oldKey)
|
||||||
|
const nextKeys = (index !== -1)
|
||||||
|
? props.slice(index + 1).map(prop => prop.key)
|
||||||
|
: []
|
||||||
|
|
||||||
|
onPatch(rename(path, oldKey, newKey, nextKeys))
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyInput (event) {
|
function handleKeyInput (event) {
|
||||||
const newKey = getPlainText(event.target)
|
const newKey = getPlainText(event.target)
|
||||||
keyClass = getKeyClass(newKey, searchResult)
|
keyClass = getKeyClass(newKey, searchResult)
|
||||||
|
@ -342,6 +349,7 @@
|
||||||
state={state && state[index]}
|
state={state && state[index]}
|
||||||
searchResult={searchResult ? searchResult[index] : undefined}
|
searchResult={searchResult ? searchResult[index] : undefined}
|
||||||
onPatch={onPatch}
|
onPatch={onPatch}
|
||||||
|
onUpdateKey={handleUpdateKey}
|
||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
onLimit={onLimit}
|
onLimit={onLimit}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
@ -409,6 +417,7 @@
|
||||||
state={state && state[prop.key]}
|
state={state && state[prop.key]}
|
||||||
searchResult={searchResult ? searchResult[prop.key] : undefined}
|
searchResult={searchResult ? searchResult[prop.key] : undefined}
|
||||||
onPatch={onPatch}
|
onPatch={onPatch}
|
||||||
|
onUpdateKey={handleUpdateKey}
|
||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
onLimit={onLimit}
|
onLimit={onLimit}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
|
|
@ -97,6 +97,38 @@ export function append (json, path, values) { // TODO: find a better name and d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename an object key
|
||||||
|
*
|
||||||
|
* @param {Path} parentPath
|
||||||
|
* @param {string} oldKey
|
||||||
|
* @param {string} newKey
|
||||||
|
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
|
||||||
|
* used to maintain the position of the renamed key.
|
||||||
|
* If not provided, the renamed key will be moved
|
||||||
|
* to the bottom of the list with keys.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export function rename(parentPath, oldKey, newKey, nextKeys) {
|
||||||
|
return [
|
||||||
|
// rename a key
|
||||||
|
{
|
||||||
|
op: 'move',
|
||||||
|
from: compileJSONPointer(parentPath.concat(oldKey)),
|
||||||
|
path: compileJSONPointer(parentPath.concat(newKey))
|
||||||
|
},
|
||||||
|
|
||||||
|
// move all lower down keys so the renamed key will maintain it's position
|
||||||
|
...nextKeys.map(key => {
|
||||||
|
return {
|
||||||
|
op: 'move',
|
||||||
|
from: compileJSONPointer(parentPath.concat(key)),
|
||||||
|
path: compileJSONPointer(parentPath.concat(key))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch for an insert action.
|
* Create a JSONPatch for an insert action.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,26 +1,90 @@
|
||||||
|
import initial from 'lodash-es/initial.js'
|
||||||
|
import { STATE_PROPS } from '../constants.js'
|
||||||
|
import { deleteIn, getIn, insertAt, setIn } from './immutabilityHelpers.js'
|
||||||
|
import { parseJSONPointer } from './jsonPointer.js'
|
||||||
import { isObject } from './typeUtils.js'
|
import { isObject } from './typeUtils.js'
|
||||||
import { uniqueId } from 'lodash-es'
|
import { isEqual, last, uniqueId } from 'lodash-es'
|
||||||
|
|
||||||
export function updateProps (value, prevProps) {
|
export function updateProps (value, prevProps) {
|
||||||
if (isObject(value)) {
|
if (!isObject(value)) {
|
||||||
// copy the props that still exist
|
|
||||||
const props = prevProps
|
|
||||||
? prevProps.filter(item => value[item.key] !== undefined)
|
|
||||||
: []
|
|
||||||
|
|
||||||
// add new props
|
|
||||||
const prevKeys = new Set(props.map(item => item.key))
|
|
||||||
Object.keys(value).forEach(key => {
|
|
||||||
if (!prevKeys.has(key)) {
|
|
||||||
props.push({
|
|
||||||
id: uniqueId(),
|
|
||||||
key
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return props
|
|
||||||
} else {
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy the props that still exist
|
||||||
|
const props = prevProps
|
||||||
|
? prevProps.filter(item => value[item.key] !== undefined)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// add new props
|
||||||
|
const prevKeys = new Set(props.map(item => item.key))
|
||||||
|
Object.keys(value).forEach(key => {
|
||||||
|
if (!prevKeys.has(key)) {
|
||||||
|
props.push({
|
||||||
|
id: uniqueId(),
|
||||||
|
key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: write unit tests
|
||||||
|
// TODO: split this function in smaller functions
|
||||||
|
export function patchProps (state, operations) {
|
||||||
|
let updatedState = state
|
||||||
|
|
||||||
|
operations.map(operation => {
|
||||||
|
if (operation.op === 'move') {
|
||||||
|
if (isEqual(
|
||||||
|
initial(parseJSONPointer(operation.from)),
|
||||||
|
initial(parseJSONPointer(operation.path))
|
||||||
|
)) {
|
||||||
|
// move inside the same object
|
||||||
|
const pathFrom = parseJSONPointer(operation.from)
|
||||||
|
const pathTo = parseJSONPointer(operation.path)
|
||||||
|
const parentPath = initial(pathFrom)
|
||||||
|
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
|
||||||
|
|
||||||
|
if (props) {
|
||||||
|
const oldKey = last(pathFrom)
|
||||||
|
const newKey = last(pathTo)
|
||||||
|
const index = props.findIndex(item => item.key === oldKey)
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
if (oldKey !== newKey) {
|
||||||
|
// A property is renamed. Rename it in the object's props
|
||||||
|
// so it maintains its identity and hence its index
|
||||||
|
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, index, 'key']), newKey)
|
||||||
|
} else {
|
||||||
|
// operation.from and operation.path are the same:
|
||||||
|
// property is moved but stays the same -> move it to the end of the props
|
||||||
|
const oldProp = props[index]
|
||||||
|
const updatedProps = insertAt(deleteIn(props, [index]), [props.length - 1], oldProp)
|
||||||
|
|
||||||
|
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.op === 'add') {
|
||||||
|
const path = parseJSONPointer(operation.path)
|
||||||
|
const parentPath = initial(path)
|
||||||
|
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
|
||||||
|
if (props) {
|
||||||
|
const key = last(path)
|
||||||
|
const newProp = {
|
||||||
|
key,
|
||||||
|
id: uniqueId()
|
||||||
|
}
|
||||||
|
const updatedProps = insertAt(props, [props.length], newProp)
|
||||||
|
|
||||||
|
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedState
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ describe('updateProps', () => {
|
||||||
|
|
||||||
const props2 = updateProps({a: 1, b: 2}, props1)
|
const props2 = updateProps({a: 1, b: 2}, props1)
|
||||||
assert.deepStrictEqual(props2.map(item => item.key), ['b', 'a'])
|
assert.deepStrictEqual(props2.map(item => item.key), ['b', 'a'])
|
||||||
|
assert.deepStrictEqual(props2[0], props1[0]) // b must still have the same id
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updateProps (2)', () => {
|
it('updateProps (2)', () => {
|
||||||
|
|
Loading…
Reference in New Issue