Reorganize files in folders /components and /logic

This commit is contained in:
Jos de Jong 2020-07-27 09:42:15 +02:00
parent e629b404a6
commit b99d4b4d5d
19 changed files with 279 additions and 291 deletions

View File

@ -1,4 +1,4 @@
@import './styles.scss';
@import '../styles.scss';
.jsoneditor {
border: 1px solid $theme-color;

View File

@ -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>

View File

@ -1,4 +1,4 @@
@import './styles.scss';
@import '../styles.scss';
.json-node {
position: relative;

View File

@ -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>

View File

@ -1,4 +1,4 @@
@import './styles.scss';
@import '../styles.scss';
.menu {
font-family: $font-family-menu;

View File

@ -115,4 +115,4 @@
{/if}
</div>
<style src="Menu.scss"></style>
<style src="./Menu.scss"></style>

View File

@ -1,4 +1,4 @@
@import './styles.scss';
@import '../styles.scss';
$search-size: 24px;

View File

@ -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>

210
src/logic/documentState.js Normal file
View File

@ -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 []
}

View File

@ -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
})

View File

@ -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.

View File

@ -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'
/**

View File

@ -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

View File

@ -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']

View File

@ -1,4 +1,4 @@
import JSONEditor from './JSONEditor.svelte'
import JSONEditor from './components/JSONEditor.svelte'
export default function jsoneditor (config) {
return new JSONEditor(config)

View File

@ -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
}

View File

@ -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 []
}

View File

@ -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)
})
})