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 {
|
.jsoneditor {
|
||||||
border: 1px solid $theme-color;
|
border: 1px solid $theme-color;
|
|
@ -6,34 +6,33 @@
|
||||||
insertBefore,
|
insertBefore,
|
||||||
removeAll,
|
removeAll,
|
||||||
replace
|
replace
|
||||||
} from './operations.js'
|
} from '../logic/operations.js'
|
||||||
import {
|
import {
|
||||||
STATE_EXPANDED,
|
STATE_EXPANDED,
|
||||||
STATE_LIMIT,
|
STATE_LIMIT,
|
||||||
SCROLL_DURATION,
|
SCROLL_DURATION,
|
||||||
STATE_PROPS
|
STATE_PROPS
|
||||||
} from './constants.js'
|
} from '../constants.js'
|
||||||
import { createHistory } from './history.js'
|
import { createHistory } from '../logic/history.js'
|
||||||
import JSONNode from './JSONNode.svelte'
|
import JSONNode from './JSONNode.svelte'
|
||||||
import {
|
import {
|
||||||
createPathsMap,
|
createPathsMap,
|
||||||
createSelectionFromOperations,
|
createSelectionFromOperations,
|
||||||
expandSelection
|
expandSelection
|
||||||
} from './selection.js'
|
} from '../logic/selection.js'
|
||||||
import { isContentEditableDiv } from './utils/domUtils.js'
|
import { isContentEditableDiv } from '../utils/domUtils.js'
|
||||||
import {
|
import {
|
||||||
getIn,
|
getIn,
|
||||||
setIn,
|
setIn,
|
||||||
updateIn
|
updateIn
|
||||||
} from './utils/immutabilityHelpers.js'
|
} from '../utils/immutabilityHelpers.js'
|
||||||
import { compileJSONPointer, parseJSONPointer } from './utils/jsonPointer.js'
|
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
|
||||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
import { keyComboFromEvent } from '../utils/keyBindings.js'
|
||||||
import { search, searchNext, searchPrevious } from './utils/search.js'
|
import { search, searchNext, searchPrevious } from '../logic/search.js'
|
||||||
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
import { immutableJSONPatch } from '../utils/immutableJSONPatch'
|
||||||
import { initial, last, cloneDeep } from 'lodash-es'
|
import { initial, last, cloneDeep } from 'lodash-es'
|
||||||
import jump from './assets/jump.js/src/jump.js'
|
import jump from '../assets/jump.js/src/jump.js'
|
||||||
import { expandPath, stateUtils } from './utils/stateUtils.js'
|
import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js'
|
||||||
import { getNextKeys, patchProps } from './utils/updateProps.js'
|
|
||||||
import Menu from './Menu.svelte'
|
import Menu from './Menu.svelte'
|
||||||
|
|
||||||
let divContents
|
let divContents
|
||||||
|
@ -49,7 +48,7 @@
|
||||||
$: hasSelectionContents = selection != null && selection.paths != null
|
$: hasSelectionContents = selection != null && selection.paths != null
|
||||||
$: hasClipboardContents = clipboard != null && selection != 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 showSearch = false
|
||||||
let searchText = ''
|
let searchText = ''
|
||||||
|
@ -63,11 +62,11 @@
|
||||||
let historyState = history.getState()
|
let historyState = history.getState()
|
||||||
|
|
||||||
export function expand (callback = () => true) {
|
export function expand (callback = () => true) {
|
||||||
state = stateUtils(doc, state, [], callback, true)
|
state = syncState(doc, state, [], callback, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collapse (callback = () => false) {
|
export function collapse (callback = () => false) {
|
||||||
state = stateUtils(doc, state, [], callback, true)
|
state = syncState(doc, state, [], callback, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get() {
|
export function get() {
|
||||||
|
@ -168,7 +167,7 @@
|
||||||
const parentPath = initial(selection.beforePath)
|
const parentPath = initial(selection.beforePath)
|
||||||
const beforeKey = last(selection.beforePath)
|
const beforeKey = last(selection.beforePath)
|
||||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
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 operations = insertBefore(doc, selection.beforePath, clipboard, nextKeys)
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
const newSelection = createSelectionFromOperations(operations)
|
||||||
|
|
||||||
|
@ -183,7 +182,7 @@
|
||||||
const parentPath = initial(lastPath)
|
const parentPath = initial(lastPath)
|
||||||
const beforeKey = last(lastPath)
|
const beforeKey = last(lastPath)
|
||||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
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 operations = replace(doc, selection.paths, clipboard, nextKeys)
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
const newSelection = createSelectionFromOperations(operations)
|
||||||
|
|
||||||
|
@ -200,7 +199,7 @@
|
||||||
const parentPath = initial(lastPath)
|
const parentPath = initial(lastPath)
|
||||||
const beforeKey = last(lastPath)
|
const beforeKey = last(lastPath)
|
||||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
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 operations = duplicate(doc, selection.paths, nextKeys)
|
||||||
const newSelection = createSelectionFromOperations(operations)
|
const newSelection = createSelectionFromOperations(operations)
|
||||||
|
@ -318,7 +317,7 @@
|
||||||
function handleExpand (path, expanded, recursive = false) {
|
function handleExpand (path, expanded, recursive = false) {
|
||||||
if (recursive) {
|
if (recursive) {
|
||||||
state = updateIn(state, path, (childState) => {
|
state = updateIn(state, path, (childState) => {
|
||||||
return stateUtils(getIn(doc, path), childState, [], () => expanded, true)
|
return syncState(getIn(doc, path), childState, [], () => expanded, true)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
state = setIn(state, path.concat(STATE_EXPANDED), expanded, true)
|
state = setIn(state, path.concat(STATE_EXPANDED), expanded, true)
|
||||||
|
@ -471,4 +470,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style src="JSONEditor.scss"></style>
|
<style src="./JSONEditor.scss"></style>
|
|
@ -1,4 +1,4 @@
|
||||||
@import './styles.scss';
|
@import '../styles.scss';
|
||||||
|
|
||||||
.json-node {
|
.json-node {
|
||||||
position: relative;
|
position: relative;
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { debounce, isEqual } from 'lodash-es'
|
import { debounce, isEqual } from 'lodash-es'
|
||||||
import { rename } from './operations.js'
|
import { rename } from '../logic/operations.js'
|
||||||
import {
|
import {
|
||||||
DEBOUNCE_DELAY,
|
DEBOUNCE_DELAY,
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
|
@ -10,8 +10,8 @@
|
||||||
STATE_SEARCH_PROPERTY,
|
STATE_SEARCH_PROPERTY,
|
||||||
STATE_SEARCH_VALUE,
|
STATE_SEARCH_VALUE,
|
||||||
INDENTATION_WIDTH
|
INDENTATION_WIDTH
|
||||||
} from './constants.js'
|
} from '../constants.js'
|
||||||
import { singleton } from './singleton.js'
|
import { singleton } from '../singleton.js'
|
||||||
import {
|
import {
|
||||||
getPlainText,
|
getPlainText,
|
||||||
isAppendNodeSelector,
|
isAppendNodeSelector,
|
||||||
|
@ -19,14 +19,14 @@
|
||||||
isChildOfButton,
|
isChildOfButton,
|
||||||
isContentEditableDiv,
|
isContentEditableDiv,
|
||||||
setPlainText
|
setPlainText
|
||||||
} from './utils/domUtils.js'
|
} from '../utils/domUtils.js'
|
||||||
import Icon from 'svelte-awesome'
|
import Icon from 'svelte-awesome'
|
||||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { findUniqueName } from './utils/stringUtils.js'
|
import { findUniqueName } from '../utils/stringUtils.js'
|
||||||
import { isUrl, stringConvert, valueType } from './utils/typeUtils'
|
import { isUrl, stringConvert, valueType } from '../utils/typeUtils'
|
||||||
import { compileJSONPointer } from './utils/jsonPointer'
|
import { compileJSONPointer } from '../utils/jsonPointer'
|
||||||
import { getNextKeys } from './utils/updateProps.js'
|
import { getNextKeys } from '../logic/documentState.js'
|
||||||
|
|
||||||
export let key = undefined // only applicable for object properties
|
export let key = undefined // only applicable for object properties
|
||||||
export let value
|
export let value
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
|
|
||||||
function handleUpdateKey (oldKey, newKey) {
|
function handleUpdateKey (oldKey, newKey) {
|
||||||
const newKeyUnique = findUniqueName(newKey, value)
|
const newKeyUnique = findUniqueName(newKey, value)
|
||||||
const nextKeys = getNextKeys(props, path, key, false)
|
const nextKeys = getNextKeys(props, key, false)
|
||||||
|
|
||||||
onPatch(rename(path, oldKey, newKeyUnique, nextKeys))
|
onPatch(rename(path, oldKey, newKeyUnique, nextKeys))
|
||||||
}
|
}
|
||||||
|
@ -505,4 +505,4 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style src="JSONNode.scss"></style>
|
<style src="./JSONNode.scss"></style>
|
|
@ -1,4 +1,4 @@
|
||||||
@import './styles.scss';
|
@import '../styles.scss';
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
font-family: $font-family-menu;
|
font-family: $font-family-menu;
|
|
@ -115,4 +115,4 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style src="Menu.scss"></style>
|
<style src="./Menu.scss"></style>
|
|
@ -1,4 +1,4 @@
|
||||||
@import './styles.scss';
|
@import '../styles.scss';
|
||||||
|
|
||||||
$search-size: 24px;
|
$search-size: 24px;
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
import Icon from 'svelte-awesome'
|
import Icon from 'svelte-awesome'
|
||||||
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { DEBOUNCE_DELAY } from './constants.js'
|
import { DEBOUNCE_DELAY } from '../constants.js'
|
||||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
import { keyComboFromEvent } from '../utils/keyBindings.js'
|
||||||
|
|
||||||
export let text = ''
|
export let text = ''
|
||||||
let inputText = ''
|
let inputText = ''
|
||||||
|
@ -84,4 +84,4 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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_LIMIT,
|
||||||
STATE_PROPS
|
STATE_PROPS
|
||||||
} from '../constants.js'
|
} from '../constants.js'
|
||||||
import { stateUtils } from './stateUtils.js'
|
import { syncState, updateProps } from './documentState.js'
|
||||||
|
|
||||||
describe('syncState', () => {
|
describe('documentState', () => {
|
||||||
it('syncState', () => {
|
it('syncState', () => {
|
||||||
const document = {
|
const document = {
|
||||||
array: [1, 2, {c: 6}],
|
array: [1, 2, {c: 6}],
|
||||||
|
@ -19,7 +19,7 @@ describe('syncState', () => {
|
||||||
return path.length <= 1
|
return path.length <= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = stateUtils(document, undefined, [], expand)
|
const state = syncState(document, undefined, [], expand)
|
||||||
|
|
||||||
const expectedState = {}
|
const expectedState = {}
|
||||||
expectedState[STATE_EXPANDED] = true
|
expectedState[STATE_EXPANDED] = true
|
||||||
|
@ -46,5 +46,20 @@ describe('syncState', () => {
|
||||||
assert.deepStrictEqual(state, expectedState)
|
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 { first, initial, last, pickBy } from 'lodash-es'
|
||||||
import { getIn } from './utils/immutabilityHelpers'
|
import { getIn } from '../utils/immutabilityHelpers'
|
||||||
import { compileJSONPointer } from './utils/jsonPointer'
|
import { compileJSONPointer } from '../utils/jsonPointer'
|
||||||
import { findUniqueName } from './utils/stringUtils'
|
import { findUniqueName } from '../utils/stringUtils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch for an insert operation.
|
* Create a JSONPatch for an insert operation.
|
|
@ -1,7 +1,7 @@
|
||||||
import { isEqual, isNumber } from 'lodash-es'
|
import { isEqual, isNumber } from 'lodash-es'
|
||||||
import { STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE } from '../constants.js'
|
import { STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE } from '../constants.js'
|
||||||
import { existsIn, setIn } from './immutabilityHelpers.js'
|
import { existsIn, setIn } from '../utils/immutabilityHelpers.js'
|
||||||
import { valueType } from './typeUtils.js'
|
import { valueType } from '../utils/typeUtils.js'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,8 +1,8 @@
|
||||||
import { isEqual } from 'lodash-es'
|
import { isEqual } from 'lodash-es'
|
||||||
import { STATE_PROPS } from './constants.js'
|
import { STATE_PROPS } from '../constants.js'
|
||||||
import { getIn } from './utils/immutabilityHelpers.js'
|
import { getIn } from '../utils/immutabilityHelpers.js'
|
||||||
import { compileJSONPointer, parseJSONPointer } from './utils/jsonPointer.js'
|
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
|
||||||
import { isObject } from './utils/typeUtils.js'
|
import { isObject } from '../utils/typeUtils.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand a selection start and end into an array containing all paths
|
* Expand a selection start and end into an array containing all paths
|
|
@ -1,6 +1,6 @@
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import { expandSelection } from './selection.js'
|
import { expandSelection } from './selection.js'
|
||||||
import { stateUtils } from './utils/stateUtils.js'
|
import { syncState } from './documentState.js'
|
||||||
|
|
||||||
describe ('selection', () => {
|
describe ('selection', () => {
|
||||||
const doc = {
|
const doc = {
|
||||||
|
@ -11,7 +11,7 @@ describe ('selection', () => {
|
||||||
"nill": null,
|
"nill": null,
|
||||||
"bool": false
|
"bool": false
|
||||||
}
|
}
|
||||||
const state = stateUtils(doc, undefined, [], () => true)
|
const state = syncState(doc, undefined, [], () => true)
|
||||||
|
|
||||||
it('should expand a selection (object)', () => {
|
it('should expand a selection (object)', () => {
|
||||||
const start = ['obj', 'arr', '2', 'last']
|
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) {
|
export default function jsoneditor (config) {
|
||||||
return new 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