Reorganize files in folders /components and /logic
This commit is contained in:
parent
e629b404a6
commit
b99d4b4d5d
|
@ -1,4 +1,4 @@
|
|||
@import './styles.scss';
|
||||
@import '../styles.scss';
|
||||
|
||||
.jsoneditor {
|
||||
border: 1px solid $theme-color;
|
|
@ -6,34 +6,33 @@
|
|||
insertBefore,
|
||||
removeAll,
|
||||
replace
|
||||
} from './operations.js'
|
||||
} from '../logic/operations.js'
|
||||
import {
|
||||
STATE_EXPANDED,
|
||||
STATE_LIMIT,
|
||||
SCROLL_DURATION,
|
||||
STATE_PROPS
|
||||
} from './constants.js'
|
||||
import { createHistory } from './history.js'
|
||||
} from '../constants.js'
|
||||
import { createHistory } from '../logic/history.js'
|
||||
import JSONNode from './JSONNode.svelte'
|
||||
import {
|
||||
createPathsMap,
|
||||
createSelectionFromOperations,
|
||||
expandSelection
|
||||
} from './selection.js'
|
||||
import { isContentEditableDiv } from './utils/domUtils.js'
|
||||
} from '../logic/selection.js'
|
||||
import { isContentEditableDiv } from '../utils/domUtils.js'
|
||||
import {
|
||||
getIn,
|
||||
setIn,
|
||||
updateIn
|
||||
} from './utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from './utils/jsonPointer.js'
|
||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
import { search, searchNext, searchPrevious } from './utils/search.js'
|
||||
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
||||
} from '../utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
|
||||
import { keyComboFromEvent } from '../utils/keyBindings.js'
|
||||
import { search, searchNext, searchPrevious } from '../logic/search.js'
|
||||
import { immutableJSONPatch } from '../utils/immutableJSONPatch'
|
||||
import { initial, last, cloneDeep } from 'lodash-es'
|
||||
import jump from './assets/jump.js/src/jump.js'
|
||||
import { expandPath, stateUtils } from './utils/stateUtils.js'
|
||||
import { getNextKeys, patchProps } from './utils/updateProps.js'
|
||||
import jump from '../assets/jump.js/src/jump.js'
|
||||
import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js'
|
||||
import Menu from './Menu.svelte'
|
||||
|
||||
let divContents
|
||||
|
@ -49,7 +48,7 @@
|
|||
$: hasSelectionContents = selection != null && selection.paths != null
|
||||
$: hasClipboardContents = clipboard != null && selection != null
|
||||
|
||||
$: state = stateUtils(doc, state, [], (path) => path.length < 1)
|
||||
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
||||
|
||||
let showSearch = false
|
||||
let searchText = ''
|
||||
|
@ -63,11 +62,11 @@
|
|||
let historyState = history.getState()
|
||||
|
||||
export function expand (callback = () => true) {
|
||||
state = stateUtils(doc, state, [], callback, true)
|
||||
state = syncState(doc, state, [], callback, true)
|
||||
}
|
||||
|
||||
export function collapse (callback = () => false) {
|
||||
state = stateUtils(doc, state, [], callback, true)
|
||||
state = syncState(doc, state, [], callback, true)
|
||||
}
|
||||
|
||||
export function get() {
|
||||
|
@ -168,7 +167,7 @@
|
|||
const parentPath = initial(selection.beforePath)
|
||||
const beforeKey = last(selection.beforePath)
|
||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const nextKeys = getNextKeys(props, parentPath, beforeKey, true)
|
||||
const nextKeys = getNextKeys(props, beforeKey, true)
|
||||
const operations = insertBefore(doc, selection.beforePath, clipboard, nextKeys)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
||||
|
@ -183,7 +182,7 @@
|
|||
const parentPath = initial(lastPath)
|
||||
const beforeKey = last(lastPath)
|
||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const nextKeys = getNextKeys(props, parentPath, beforeKey, true)
|
||||
const nextKeys = getNextKeys(props, beforeKey, true)
|
||||
const operations = replace(doc, selection.paths, clipboard, nextKeys)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
||||
|
@ -200,7 +199,7 @@
|
|||
const parentPath = initial(lastPath)
|
||||
const beforeKey = last(lastPath)
|
||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const nextKeys = getNextKeys(props, parentPath, beforeKey, false)
|
||||
const nextKeys = getNextKeys(props, beforeKey, false)
|
||||
|
||||
const operations = duplicate(doc, selection.paths, nextKeys)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
@ -318,7 +317,7 @@
|
|||
function handleExpand (path, expanded, recursive = false) {
|
||||
if (recursive) {
|
||||
state = updateIn(state, path, (childState) => {
|
||||
return stateUtils(getIn(doc, path), childState, [], () => expanded, true)
|
||||
return syncState(getIn(doc, path), childState, [], () => expanded, true)
|
||||
})
|
||||
} else {
|
||||
state = setIn(state, path.concat(STATE_EXPANDED), expanded, true)
|
||||
|
@ -471,4 +470,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<style src="JSONEditor.scss"></style>
|
||||
<style src="./JSONEditor.scss"></style>
|
|
@ -1,4 +1,4 @@
|
|||
@import './styles.scss';
|
||||
@import '../styles.scss';
|
||||
|
||||
.json-node {
|
||||
position: relative;
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { debounce, isEqual } from 'lodash-es'
|
||||
import { rename } from './operations.js'
|
||||
import { rename } from '../logic/operations.js'
|
||||
import {
|
||||
DEBOUNCE_DELAY,
|
||||
DEFAULT_LIMIT,
|
||||
|
@ -10,8 +10,8 @@
|
|||
STATE_SEARCH_PROPERTY,
|
||||
STATE_SEARCH_VALUE,
|
||||
INDENTATION_WIDTH
|
||||
} from './constants.js'
|
||||
import { singleton } from './singleton.js'
|
||||
} from '../constants.js'
|
||||
import { singleton } from '../singleton.js'
|
||||
import {
|
||||
getPlainText,
|
||||
isAppendNodeSelector,
|
||||
|
@ -19,14 +19,14 @@
|
|||
isChildOfButton,
|
||||
isContentEditableDiv,
|
||||
setPlainText
|
||||
} from './utils/domUtils.js'
|
||||
} from '../utils/domUtils.js'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import classnames from 'classnames'
|
||||
import { findUniqueName } from './utils/stringUtils.js'
|
||||
import { isUrl, stringConvert, valueType } from './utils/typeUtils'
|
||||
import { compileJSONPointer } from './utils/jsonPointer'
|
||||
import { getNextKeys } from './utils/updateProps.js'
|
||||
import { findUniqueName } from '../utils/stringUtils.js'
|
||||
import { isUrl, stringConvert, valueType } from '../utils/typeUtils'
|
||||
import { compileJSONPointer } from '../utils/jsonPointer'
|
||||
import { getNextKeys } from '../logic/documentState.js'
|
||||
|
||||
export let key = undefined // only applicable for object properties
|
||||
export let value
|
||||
|
@ -125,7 +125,7 @@
|
|||
|
||||
function handleUpdateKey (oldKey, newKey) {
|
||||
const newKeyUnique = findUniqueName(newKey, value)
|
||||
const nextKeys = getNextKeys(props, path, key, false)
|
||||
const nextKeys = getNextKeys(props, key, false)
|
||||
|
||||
onPatch(rename(path, oldKey, newKeyUnique, nextKeys))
|
||||
}
|
||||
|
@ -505,4 +505,4 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<style src="JSONNode.scss"></style>
|
||||
<style src="./JSONNode.scss"></style>
|
|
@ -1,4 +1,4 @@
|
|||
@import './styles.scss';
|
||||
@import '../styles.scss';
|
||||
|
||||
.menu {
|
||||
font-family: $font-family-menu;
|
|
@ -115,4 +115,4 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<style src="Menu.scss"></style>
|
||||
<style src="./Menu.scss"></style>
|
|
@ -1,4 +1,4 @@
|
|||
@import './styles.scss';
|
||||
@import '../styles.scss';
|
||||
|
||||
$search-size: 24px;
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
import { debounce } from 'lodash-es'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { DEBOUNCE_DELAY } from './constants.js'
|
||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
import { DEBOUNCE_DELAY } from '../constants.js'
|
||||
import { keyComboFromEvent } from '../utils/keyBindings.js'
|
||||
|
||||
export let text = ''
|
||||
let inputText = ''
|
||||
|
@ -84,4 +84,4 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<style src="SearchBox.scss"></style>
|
||||
<style src="./SearchBox.scss"></style>
|
|
@ -0,0 +1,210 @@
|
|||
import { initial, isEqual, isNumber, last, uniqueId } from 'lodash-es'
|
||||
import {
|
||||
DEFAULT_LIMIT,
|
||||
STATE_EXPANDED,
|
||||
STATE_LIMIT,
|
||||
STATE_PROPS
|
||||
} from '../constants.js'
|
||||
import { deleteIn, getIn, insertAt, setIn } from '../utils/immutabilityHelpers.js'
|
||||
import { parseJSONPointer } from '../utils/jsonPointer.js'
|
||||
import { isObject, isObjectOrArray } from '../utils/typeUtils.js'
|
||||
|
||||
/**
|
||||
* Sync a state object with the doc it belongs to: update props, limit, and expanded state
|
||||
*
|
||||
* @param {JSON} doc
|
||||
* @param {JSON | undefined} state
|
||||
* @param {Path} path
|
||||
* @param {function (path: Path) : boolean} expand
|
||||
* @param {boolean} [forceRefresh=false] if true, force refreshing the expanded state
|
||||
* @returns {JSON | undefined}
|
||||
*/
|
||||
export function syncState (doc, state = undefined, path, expand, forceRefresh = false) {
|
||||
// TODO: this function can be made way more efficient if we pass prevState:
|
||||
// when immutable, we can simply be done already when the state === prevState
|
||||
|
||||
if (isObject(doc)) {
|
||||
const updatedState = {}
|
||||
|
||||
updatedState[STATE_PROPS] = updateProps(doc, state && state[STATE_PROPS])
|
||||
|
||||
updatedState[STATE_EXPANDED] = (state && !forceRefresh)
|
||||
? state[STATE_EXPANDED]
|
||||
: expand(path)
|
||||
|
||||
if (updatedState[STATE_EXPANDED]) {
|
||||
Object.keys(doc).forEach(key => {
|
||||
const childDocument = doc[key]
|
||||
if (isObjectOrArray(childDocument)) {
|
||||
const childState = state && state[key]
|
||||
updatedState[key] = syncState(childDocument, childState, path.concat(key), expand, forceRefresh)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
if (Array.isArray(doc)) {
|
||||
const updatedState = []
|
||||
|
||||
updatedState[STATE_EXPANDED] = (state && !forceRefresh)
|
||||
? state[STATE_EXPANDED]
|
||||
: expand(path)
|
||||
|
||||
// note that we reset the limit when the state is not expanded
|
||||
updatedState[STATE_LIMIT] = (state && updatedState[STATE_EXPANDED])
|
||||
? state[STATE_LIMIT]
|
||||
: DEFAULT_LIMIT
|
||||
|
||||
if (updatedState[STATE_EXPANDED]) {
|
||||
for (let i = 0; i < Math.min(doc.length, updatedState[STATE_LIMIT]); i++) {
|
||||
const childDocument = doc[i]
|
||||
if (isObjectOrArray(childDocument)) {
|
||||
const childState = state && state[i]
|
||||
updatedState[i] = syncState(childDocument, childState, path.concat(i), expand, forceRefresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
// primitive values have no state
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all nodes on given path
|
||||
* @param {JSON} state
|
||||
* @param {Path} path
|
||||
* @return {JSON} returns the updated state
|
||||
*/
|
||||
// TODO: write unit tests for expandPath
|
||||
export function expandPath (state, path) {
|
||||
let updatedState = state
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const partialPath = path.slice(0, i)
|
||||
// FIXME: setIn has to create object first
|
||||
updatedState = setIn(updatedState, partialPath.concat(STATE_EXPANDED), true, true)
|
||||
|
||||
// if needed, enlarge the limit such that the search result becomes visible
|
||||
const key = path[i]
|
||||
if (isNumber(key)) {
|
||||
const limit = getIn(updatedState, partialPath.concat(STATE_LIMIT)) || DEFAULT_LIMIT
|
||||
if (key > limit) {
|
||||
const newLimit = Math.ceil(key / DEFAULT_LIMIT) * DEFAULT_LIMIT
|
||||
updatedState = setIn(updatedState, partialPath.concat(STATE_LIMIT), newLimit, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
export function updateProps (value, prevProps) {
|
||||
if (!isObject(value)) {
|
||||
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 oldIndex = props.findIndex(item => item.key === oldKey)
|
||||
|
||||
if (oldIndex !== -1) {
|
||||
if (oldKey !== newKey) {
|
||||
// A property is renamed.
|
||||
|
||||
// in case the new key shadows an existing key, remove the existing key
|
||||
const newIndex = props.findIndex(item => item.key === newKey)
|
||||
if (newIndex !== -1) {
|
||||
const updatedProps = deleteIn(props, [newIndex])
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
|
||||
}
|
||||
|
||||
// Rename the key in the object's props so it maintains its identity and hence its index
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, oldIndex, 'key']), newKey, true)
|
||||
} 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[oldIndex]
|
||||
const updatedProps = insertAt(deleteIn(props, [oldIndex]), [props.length - 1], oldProp)
|
||||
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation.op === 'add' || operation.op === 'copy') {
|
||||
const pathTo = parseJSONPointer(operation.path)
|
||||
const parentPath = initial(pathTo)
|
||||
const key = last(pathTo)
|
||||
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
|
||||
if (props) {
|
||||
const index = props.findIndex(item => item.key === key)
|
||||
if (index === -1) {
|
||||
const newProp = {
|
||||
id: uniqueId(),
|
||||
key
|
||||
}
|
||||
const updatedProps = insertAt(props, [props.length], newProp)
|
||||
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
export function getNextKeys(props, key, includeKey = false) {
|
||||
if (props) {
|
||||
const index = props.findIndex(prop => prop.key === key)
|
||||
if (index !== -1) {
|
||||
return props.slice(index + (includeKey ? 0 : 1)).map(prop => prop.key)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
|
@ -5,9 +5,9 @@ import {
|
|||
STATE_LIMIT,
|
||||
STATE_PROPS
|
||||
} from '../constants.js'
|
||||
import { stateUtils } from './stateUtils.js'
|
||||
import { syncState, updateProps } from './documentState.js'
|
||||
|
||||
describe('syncState', () => {
|
||||
describe('documentState', () => {
|
||||
it('syncState', () => {
|
||||
const document = {
|
||||
array: [1, 2, {c: 6}],
|
||||
|
@ -19,7 +19,7 @@ describe('syncState', () => {
|
|||
return path.length <= 1
|
||||
}
|
||||
|
||||
const state = stateUtils(document, undefined, [], expand)
|
||||
const state = syncState(document, undefined, [], expand)
|
||||
|
||||
const expectedState = {}
|
||||
expectedState[STATE_EXPANDED] = true
|
||||
|
@ -46,5 +46,20 @@ describe('syncState', () => {
|
|||
assert.deepStrictEqual(state, expectedState)
|
||||
})
|
||||
|
||||
// TODO: write more unit tests for stateUtils
|
||||
it('updateProps (1)', () => {
|
||||
const props1 = updateProps({b: 2})
|
||||
assert.deepStrictEqual(props1.map(item => item.key), ['b'])
|
||||
|
||||
const props2 = updateProps({a: 1, b: 2}, props1)
|
||||
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)', () => {
|
||||
const props1 = updateProps({a: 1, b: 2})
|
||||
const props2 = updateProps({a: 1, b: 2}, props1)
|
||||
assert.deepStrictEqual(props2, props1)
|
||||
})
|
||||
|
||||
// TODO: write more unit tests
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
import { first, initial, last, pickBy } from 'lodash-es'
|
||||
import { getIn } from './utils/immutabilityHelpers'
|
||||
import { compileJSONPointer } from './utils/jsonPointer'
|
||||
import { findUniqueName } from './utils/stringUtils'
|
||||
import { getIn } from '../utils/immutabilityHelpers'
|
||||
import { compileJSONPointer } from '../utils/jsonPointer'
|
||||
import { findUniqueName } from '../utils/stringUtils'
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for an insert operation.
|
|
@ -1,7 +1,7 @@
|
|||
import { isEqual, isNumber } from 'lodash-es'
|
||||
import { STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE } from '../constants.js'
|
||||
import { existsIn, setIn } from './immutabilityHelpers.js'
|
||||
import { valueType } from './typeUtils.js'
|
||||
import { existsIn, setIn } from '../utils/immutabilityHelpers.js'
|
||||
import { valueType } from '../utils/typeUtils.js'
|
||||
|
||||
|
||||
/**
|
|
@ -1,8 +1,8 @@
|
|||
import { isEqual } from 'lodash-es'
|
||||
import { STATE_PROPS } from './constants.js'
|
||||
import { getIn } from './utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from './utils/jsonPointer.js'
|
||||
import { isObject } from './utils/typeUtils.js'
|
||||
import { STATE_PROPS } from '../constants.js'
|
||||
import { getIn } from '../utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
|
||||
import { isObject } from '../utils/typeUtils.js'
|
||||
|
||||
/**
|
||||
* Expand a selection start and end into an array containing all paths
|
|
@ -1,6 +1,6 @@
|
|||
import assert from 'assert'
|
||||
import { expandSelection } from './selection.js'
|
||||
import { stateUtils } from './utils/stateUtils.js'
|
||||
import { syncState } from './documentState.js'
|
||||
|
||||
describe ('selection', () => {
|
||||
const doc = {
|
||||
|
@ -11,7 +11,7 @@ describe ('selection', () => {
|
|||
"nill": null,
|
||||
"bool": false
|
||||
}
|
||||
const state = stateUtils(doc, undefined, [], () => true)
|
||||
const state = syncState(doc, undefined, [], () => true)
|
||||
|
||||
it('should expand a selection (object)', () => {
|
||||
const start = ['obj', 'arr', '2', 'last']
|
|
@ -1,4 +1,4 @@
|
|||
import JSONEditor from './JSONEditor.svelte'
|
||||
import JSONEditor from './components/JSONEditor.svelte'
|
||||
|
||||
export default function jsoneditor (config) {
|
||||
return new JSONEditor(config)
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
import { isNumber } from 'lodash-es'
|
||||
import {
|
||||
DEFAULT_LIMIT,
|
||||
STATE_EXPANDED,
|
||||
STATE_LIMIT,
|
||||
STATE_PROPS
|
||||
} from '../constants.js'
|
||||
import { getIn, setIn } from './immutabilityHelpers.js'
|
||||
import { isObject, isObjectOrArray } from './typeUtils.js'
|
||||
import { updateProps } from './updateProps.js'
|
||||
|
||||
/**
|
||||
* Sync a state object with the doc it belongs to: update props, limit, and expanded state
|
||||
*
|
||||
* @param {JSON} doc
|
||||
* @param {JSON | undefined} state
|
||||
* @param {Path} path
|
||||
* @param {function (path: Path) : boolean} expand
|
||||
* @param {boolean} [forceRefresh=false] if true, force refreshing the expanded state
|
||||
* @returns {JSON | undefined}
|
||||
*/
|
||||
export function stateUtils (doc, state = undefined, path, expand, forceRefresh = false) {
|
||||
// TODO: this function can be made way more efficient if we pass prevState:
|
||||
// when immutable, we can simply be done already when the state === prevState
|
||||
|
||||
if (isObject(doc)) {
|
||||
const updatedState = {}
|
||||
|
||||
updatedState[STATE_PROPS] = updateProps(doc, state && state[STATE_PROPS])
|
||||
|
||||
updatedState[STATE_EXPANDED] = (state && !forceRefresh)
|
||||
? state[STATE_EXPANDED]
|
||||
: expand(path)
|
||||
|
||||
if (updatedState[STATE_EXPANDED]) {
|
||||
Object.keys(doc).forEach(key => {
|
||||
const childDocument = doc[key]
|
||||
if (isObjectOrArray(childDocument)) {
|
||||
const childState = state && state[key]
|
||||
updatedState[key] = stateUtils(childDocument, childState, path.concat(key), expand, forceRefresh)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
if (Array.isArray(doc)) {
|
||||
const updatedState = []
|
||||
|
||||
updatedState[STATE_EXPANDED] = (state && !forceRefresh)
|
||||
? state[STATE_EXPANDED]
|
||||
: expand(path)
|
||||
|
||||
// note that we reset the limit when the state is not expanded
|
||||
updatedState[STATE_LIMIT] = (state && updatedState[STATE_EXPANDED])
|
||||
? state[STATE_LIMIT]
|
||||
: DEFAULT_LIMIT
|
||||
|
||||
if (updatedState[STATE_EXPANDED]) {
|
||||
for (let i = 0; i < Math.min(doc.length, updatedState[STATE_LIMIT]); i++) {
|
||||
const childDocument = doc[i]
|
||||
if (isObjectOrArray(childDocument)) {
|
||||
const childState = state && state[i]
|
||||
updatedState[i] = stateUtils(childDocument, childState, path.concat(i), expand, forceRefresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
// primitive values have no state
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all nodes on given path
|
||||
* @param {JSON} state
|
||||
* @param {Path} path
|
||||
* @return {JSON} returns the updated state
|
||||
*/
|
||||
// TODO: write unit tests for expandPath
|
||||
export function expandPath (state, path) {
|
||||
let updatedState = state
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const partialPath = path.slice(0, i)
|
||||
// FIXME: setIn has to create object first
|
||||
updatedState = setIn(updatedState, partialPath.concat(STATE_EXPANDED), true, true)
|
||||
|
||||
// if needed, enlarge the limit such that the search result becomes visible
|
||||
const key = path[i]
|
||||
if (isNumber(key)) {
|
||||
const limit = getIn(updatedState, partialPath.concat(STATE_LIMIT)) || DEFAULT_LIMIT
|
||||
if (key > limit) {
|
||||
const newLimit = Math.ceil(key / DEFAULT_LIMIT) * DEFAULT_LIMIT
|
||||
updatedState = setIn(updatedState, partialPath.concat(STATE_LIMIT), newLimit, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedState
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
import { initial, isEqual, last, uniqueId } from 'lodash-es'
|
||||
import { STATE_PROPS } from '../constants.js'
|
||||
import { deleteIn, getIn, insertAt, setIn } from './immutabilityHelpers.js'
|
||||
import { parseJSONPointer } from './jsonPointer.js'
|
||||
import { isObject } from './typeUtils.js'
|
||||
|
||||
export function updateProps (value, prevProps) {
|
||||
if (!isObject(value)) {
|
||||
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 oldIndex = props.findIndex(item => item.key === oldKey)
|
||||
|
||||
if (oldIndex !== -1) {
|
||||
if (oldKey !== newKey) {
|
||||
// A property is renamed.
|
||||
|
||||
// in case the new key shadows an existing key, remove the existing key
|
||||
const newIndex = props.findIndex(item => item.key === newKey)
|
||||
if (newIndex !== -1) {
|
||||
const updatedProps = deleteIn(props, [newIndex])
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
|
||||
}
|
||||
|
||||
// Rename the key in the object's props so it maintains its identity and hence its index
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, oldIndex, 'key']), newKey, true)
|
||||
} 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[oldIndex]
|
||||
const updatedProps = insertAt(deleteIn(props, [oldIndex]), [props.length - 1], oldProp)
|
||||
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation.op === 'add' || operation.op === 'copy') {
|
||||
const pathTo = parseJSONPointer(operation.path)
|
||||
const parentPath = initial(pathTo)
|
||||
const key = last(pathTo)
|
||||
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
|
||||
if (props) {
|
||||
const index = props.findIndex(item => item.key === key)
|
||||
if (index === -1) {
|
||||
const newProp = {
|
||||
id: uniqueId(),
|
||||
key
|
||||
}
|
||||
const updatedProps = insertAt(props, [props.length], newProp)
|
||||
|
||||
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
export function getNextKeys(props, parentPath, key, includeKey = false) {
|
||||
if (props) {
|
||||
const index = props.findIndex(prop => prop.key === key)
|
||||
if (index !== -1) {
|
||||
return props.slice(index + (includeKey ? 0 : 1)).map(prop => prop.key)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import assert from 'assert'
|
||||
import { updateProps } from './updateProps.js'
|
||||
|
||||
describe('updateProps', () => {
|
||||
|
||||
it('updateProps (1)', () => {
|
||||
const props1 = updateProps({b: 2})
|
||||
assert.deepStrictEqual(props1.map(item => item.key), ['b'])
|
||||
|
||||
const props2 = updateProps({a: 1, b: 2}, props1)
|
||||
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)', () => {
|
||||
const props1 = updateProps({a: 1, b: 2})
|
||||
const props2 = updateProps({a: 1, b: 2}, props1)
|
||||
assert.deepStrictEqual(props2, props1)
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in New Issue