Implemented cut/copy/paste (via quickkeys)
This commit is contained in:
parent
375ea56316
commit
26fa339c35
|
@ -1,6 +1,7 @@
|
|||
// @flow weak
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
import initial from 'lodash/initial'
|
||||
|
||||
import ActionMenu from './menu/ActionMenu'
|
||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||
|
@ -477,7 +478,7 @@ export default class JSONNode extends Component {
|
|||
|
||||
/** @private */
|
||||
handleChangeProperty = (event) => {
|
||||
const parentPath = allButLast(this.props.path)
|
||||
const parentPath = initial(this.props.path)
|
||||
const oldProp = this.props.prop.name
|
||||
const newProp = unescapeHTML(getInnerText(event.target))
|
||||
|
||||
|
@ -595,10 +596,3 @@ export default class JSONNode extends Component {
|
|||
: stringConvert(stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the array having the last item removed
|
||||
*/
|
||||
function allButLast (array: []): any {
|
||||
return array.slice(0, array.length - 1)
|
||||
}
|
||||
|
|
|
@ -2,19 +2,22 @@
|
|||
|
||||
import { createElement as h, Component } from 'react'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import reverse from 'lodash/reverse'
|
||||
import initial from 'lodash/initial'
|
||||
import last from 'lodash/last'
|
||||
import Hammer from 'react-hammerjs'
|
||||
import jump from '../assets/jump.js/src/jump'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { updateIn, getIn, setIn } from '../utils/immutabilityHelpers'
|
||||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { allButLast } from '../utils/arrayUtils'
|
||||
import { findUniqueName } from '../utils/stringUtils'
|
||||
import { enrichSchemaError } from '../utils/schemaUtils'
|
||||
import {
|
||||
jsonToEson, esonToJson, toEsonPath, pathExists,
|
||||
expand, expandPath, addErrors,
|
||||
search, applySearchResults, nextSearchResult, previousSearchResult,
|
||||
applySelection,
|
||||
applySelection, pathsFromSelection, contentsFromPaths,
|
||||
compileJSONPointer, parseJSONPointer
|
||||
} from '../eson'
|
||||
import { patchEson } from '../patchEson'
|
||||
|
@ -29,12 +32,12 @@ import ModeButton from './menu/ModeButton'
|
|||
import Search from './menu/Search'
|
||||
import {
|
||||
moveUp, moveDown, moveLeft, moveRight, moveDownSibling, moveHome, moveEnd,
|
||||
findNode, selectFind, searchHasFocus, setSelection
|
||||
findNode, findBaseNode, selectFind, searchHasFocus, setSelection
|
||||
} from './utils/domSelector'
|
||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||
import { KEY_BINDINGS } from '../constants'
|
||||
|
||||
import type { ESON, ESONPatch, JSONPath } from '../types'
|
||||
import type { ESON, ESONPatch, JSONPath, ESONSelection } from '../types'
|
||||
|
||||
const AJV_OPTIONS = {
|
||||
allErrors: true,
|
||||
|
@ -50,19 +53,7 @@ export default class TreeMode extends Component {
|
|||
id: number
|
||||
state: Object
|
||||
|
||||
keyDownActions = {
|
||||
'up': (event) => moveUp(event.target),
|
||||
'down': (event) => moveDown(event.target),
|
||||
'left': (event) => moveLeft(event.target),
|
||||
'right': (event) => moveRight(event.target),
|
||||
'home': (event) => moveHome(event.target),
|
||||
'end': (event) => moveEnd(event.target),
|
||||
'undo': (event) => this.undo(),
|
||||
'redo': (event) => this.redo(),
|
||||
'find': (event) => selectFind(event.target),
|
||||
'findNext': (event) => this.handleNext(),
|
||||
'findPrevious': (event) => this.handlePrevious()
|
||||
}
|
||||
keyDownActions = null
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
@ -71,6 +62,23 @@ export default class TreeMode extends Component {
|
|||
|
||||
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
|
||||
|
||||
this.keyDownActions = {
|
||||
'up': this.moveUp,
|
||||
'down': this.moveDown,
|
||||
'left': this.moveLeft,
|
||||
'right': this.moveRight,
|
||||
'home': this.moveHome,
|
||||
'end': this.moveEnd,
|
||||
'cut': this.handleCut,
|
||||
'copy': this.handleCopy,
|
||||
'paste': this.handlePaste,
|
||||
'undo': this.handleUndo,
|
||||
'redo': this.handleRedo,
|
||||
'find': this.handleFocusFind,
|
||||
'findNext': this.handleNext,
|
||||
'findPrevious': this.handlePrevious
|
||||
}
|
||||
|
||||
this.state = {
|
||||
data,
|
||||
|
||||
|
@ -101,7 +109,9 @@ export default class TreeMode extends Component {
|
|||
selection: {
|
||||
start: null, // ESONPointer
|
||||
end: null, // ESONPointer
|
||||
}
|
||||
},
|
||||
|
||||
clipboard: null // array entries {prop: string, value: JSON}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,7 +203,8 @@ export default class TreeMode extends Component {
|
|||
direction: 'DIRECTION_VERTICAL',
|
||||
onTap: this.handleTap,
|
||||
onPanStart: this.handlePanStart,
|
||||
onPan: this.handlePan
|
||||
onPan: this.handlePan,
|
||||
onPanEnd: this.handlePanEnd
|
||||
},
|
||||
h('ul', {className: 'jsoneditor-list jsoneditor-root' + (data.selected ? ' jsoneditor-selected' : '')},
|
||||
h(Node, {
|
||||
|
@ -304,27 +315,22 @@ export default class TreeMode extends Component {
|
|||
const action = this.keyDownActions[keyBinding]
|
||||
|
||||
if (action) {
|
||||
event.preventDefault()
|
||||
action(event)
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleChangeValue = (path, value) => {
|
||||
this.handlePatch(changeValue(this.state.data, path, value))
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleChangeProperty = (parentPath, oldProp, newProp) => {
|
||||
this.handlePatch(changeProperty(this.state.data, parentPath, oldProp, newProp))
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleChangeType = (path, type) => {
|
||||
this.handlePatch(changeType(this.state.data, path, type))
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleInsert = (path, type) => {
|
||||
this.handlePatch(insert(this.state.data, path, type))
|
||||
|
||||
|
@ -332,7 +338,6 @@ export default class TreeMode extends Component {
|
|||
this.focusToNext(path)
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleAppend = (parentPath, type) => {
|
||||
this.handlePatch(append(this.state.data, parentPath, type))
|
||||
|
||||
|
@ -340,7 +345,6 @@ export default class TreeMode extends Component {
|
|||
this.focusToNext(parentPath)
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleDuplicate = (path) => {
|
||||
this.handlePatch(duplicate(this.state.data, path))
|
||||
|
||||
|
@ -348,7 +352,6 @@ export default class TreeMode extends Component {
|
|||
this.focusToNext(path)
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleRemove = (path) => {
|
||||
// apply focus to next sibling element if existing, else to the previous element
|
||||
const fromElement = findNode(this.refs.contents, path)
|
||||
|
@ -360,6 +363,114 @@ export default class TreeMode extends Component {
|
|||
this.handlePatch(remove(path))
|
||||
}
|
||||
|
||||
moveUp = (event) => {
|
||||
event.preventDefault()
|
||||
moveUp(event.target)
|
||||
}
|
||||
|
||||
moveDown = (event) => {
|
||||
event.preventDefault()
|
||||
moveDown(event.target)
|
||||
}
|
||||
|
||||
moveLeft = (event) => {
|
||||
event.preventDefault()
|
||||
moveLeft(event.target)
|
||||
}
|
||||
|
||||
moveRight = (event) => {
|
||||
event.preventDefault()
|
||||
moveRight(event.target)
|
||||
}
|
||||
|
||||
moveHome = (event) => {
|
||||
event.preventDefault()
|
||||
moveHome(event.target)
|
||||
}
|
||||
|
||||
moveEnd = (event) => {
|
||||
event.preventDefault()
|
||||
moveEnd(event.target)
|
||||
}
|
||||
|
||||
handleCut = (event) => {
|
||||
const { data, selection } = this.state
|
||||
|
||||
if (selection) {
|
||||
event.preventDefault()
|
||||
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const clipboard = contentsFromPaths(data, paths)
|
||||
|
||||
this.setState({ clipboard, selection: null })
|
||||
|
||||
// note that we reverse the order, else we will mess up indices to be deleted in case of an array
|
||||
const patch = reverse(paths).map(path => ({op: 'remove', path: compileJSONPointer(path)}))
|
||||
|
||||
this.handlePatch(patch)
|
||||
}
|
||||
else {
|
||||
// clear clipboard
|
||||
this.setState({ clipboard: null, selection: null })
|
||||
}
|
||||
}
|
||||
|
||||
handleCopy = (event) => {
|
||||
const { data, selection } = this.state
|
||||
|
||||
if (selection) {
|
||||
event.preventDefault()
|
||||
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const clipboard = contentsFromPaths(data, paths)
|
||||
|
||||
this.setState({ clipboard })
|
||||
}
|
||||
else {
|
||||
// clear clipboard
|
||||
this.setState({ clipboard: null, selection: null })
|
||||
}
|
||||
}
|
||||
|
||||
handlePaste = (event) => {
|
||||
const { data, clipboard } = this.state
|
||||
|
||||
if (clipboard && clipboard.length > 0) {
|
||||
event.preventDefault()
|
||||
|
||||
// FIXME: handle pasting in an empty object or array
|
||||
|
||||
const path = this.findDataPathFromElement(event.target)
|
||||
if (path && path.length > 0) {
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(data, toEsonPath(data, parentPath))
|
||||
const isObject = parent.type === 'Object'
|
||||
|
||||
if (parent.type === 'Object') {
|
||||
const existingProps = parent.props.map(p => p.name)
|
||||
const prop = last(path)
|
||||
const patch = clipboard.map(entry => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(findUniqueName(entry.name, existingProps))),
|
||||
value: entry.value,
|
||||
jsoneditor: { before: prop }
|
||||
}))
|
||||
|
||||
this.handlePatch(patch)
|
||||
}
|
||||
else { // parent.type === 'Array'
|
||||
const patch = clipboard.map(entry => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(path),
|
||||
value: entry.value
|
||||
}))
|
||||
|
||||
this.handlePatch(patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move focus to the next search result
|
||||
* @param {Path} path
|
||||
|
@ -374,12 +485,10 @@ export default class TreeMode extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleSort = (path, order = null) => {
|
||||
this.handlePatch(sort(this.state.data, path, order))
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleExpand = (path, expanded, recurse) => {
|
||||
if (recurse) {
|
||||
const esonPath = toEsonPath(this.state.data, path)
|
||||
|
@ -397,13 +506,11 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleFindKeyBinding = (event) => {
|
||||
// findKeyBinding can change on the fly, so we can't bind it statically
|
||||
return this.findKeyBinding (event)
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleExpandAll = () => {
|
||||
const expanded = true
|
||||
|
||||
|
@ -412,7 +519,6 @@ export default class TreeMode extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleCollapseAll = () => {
|
||||
const expanded = false
|
||||
|
||||
|
@ -421,7 +527,6 @@ export default class TreeMode extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleSearch = (text) => {
|
||||
const searchResults = search(this.state.data, text)
|
||||
|
||||
|
@ -430,7 +535,7 @@ export default class TreeMode extends Component {
|
|||
|
||||
this.setState({
|
||||
search: { text, active },
|
||||
data: expandPath(this.state.data, allButLast(active.path))
|
||||
data: expandPath(this.state.data, initial(active.path))
|
||||
})
|
||||
|
||||
// scroll to active search result (on next tick, after this path has been expanded)
|
||||
|
@ -443,15 +548,21 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleNext = () => {
|
||||
handleFocusFind = (event) => {
|
||||
event.preventDefault()
|
||||
selectFind(event.target)
|
||||
}
|
||||
|
||||
handleNext = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
const searchResults = search(this.state.data, this.state.search.text)
|
||||
if (searchResults) {
|
||||
const next = nextSearchResult(searchResults, this.state.search.active)
|
||||
|
||||
this.setState({
|
||||
search: setIn(this.state.search, ['active'], next),
|
||||
data: next ? expandPath(this.state.data, allButLast(next.path)) : this.state.data
|
||||
data: next ? expandPath(this.state.data, initial(next.path)) : this.state.data
|
||||
})
|
||||
|
||||
// scroll to the active result (on next tick, after this path has been expanded)
|
||||
|
@ -467,15 +578,16 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handlePrevious = () => {
|
||||
handlePrevious = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
const searchResults = search(this.state.data, this.state.search.text)
|
||||
if (searchResults) {
|
||||
const previous = previousSearchResult(searchResults, this.state.search.active)
|
||||
|
||||
this.setState({
|
||||
search: setIn(this.state.search, ['active'], previous),
|
||||
data: previous ? expandPath(this.state.data, allButLast(previous.path)) : this.state.data
|
||||
data: previous ? expandPath(this.state.data, initial(previous.path)) : this.state.data
|
||||
})
|
||||
|
||||
// scroll to the active result (on next tick, after this path has been expanded)
|
||||
|
@ -533,10 +645,24 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handlePanEnd = (event) => {
|
||||
const path = this.findDataPathFromElement(event.target.firstChild)
|
||||
if (path) {
|
||||
// TODO: implement a better solution to keep focus in the editor than selecting the action menu. Most also be solved for undo/redo for example
|
||||
const element = findNode(this.refs.contents, path)
|
||||
const actionMenuButton = element && element.querySelector('button.jsoneditor-actionmenu')
|
||||
if (actionMenuButton) {
|
||||
actionMenuButton.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findDataPathFromElement (element: Element) : JSONPath | null {
|
||||
const base = findBaseNode(element)
|
||||
const attr = base && base.getAttribute && base.getAttribute('data-path')
|
||||
|
||||
// The .replace is to change paths like `/myarray/-` into `/myarray`
|
||||
const attr = element && element.getAttribute && element.getAttribute('data-path').replace(/\/-$/, '')
|
||||
return attr ? parseJSONPointer(attr) : null
|
||||
return attr ? parseJSONPointer(attr.replace(/\/-$/, '')) : null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -579,6 +705,16 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleUndo = (event) => {
|
||||
event.preventDefault()
|
||||
this.undo()
|
||||
}
|
||||
|
||||
handleRedo = (event) => {
|
||||
event.preventDefault()
|
||||
this.redo()
|
||||
}
|
||||
|
||||
canUndo = () => {
|
||||
return this.state.historyIndex < this.state.history.length
|
||||
}
|
||||
|
@ -588,6 +724,7 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
undo = () => {
|
||||
console.log('undo')
|
||||
if (this.canUndo()) {
|
||||
const history = this.state.history
|
||||
const historyIndex = this.state.historyIndex
|
||||
|
|
|
@ -193,7 +193,8 @@ export function findEditorContainer (element) {
|
|||
return findParentWithAttribute (element, EDITOR_CONTAINER_ATTRIBUTE, 'true')
|
||||
}
|
||||
|
||||
function findBaseNode (element) {
|
||||
// TODO: find a better name for this function
|
||||
export function findBaseNode (element) {
|
||||
return findParentWithClassName (element, NODE_CONTAINER_CLASS_NAME)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ export const KEY_BINDINGS = {
|
|||
'remove': ['Ctrl+Delete', 'Command+Delete'],
|
||||
'expand': ['Ctrl+E', 'Command+E'],
|
||||
'actionMenu': ['Ctrl+M', 'Command+M'],
|
||||
'cut': ['Ctrl+X', 'Command+X'],
|
||||
'copy': ['Ctrl+C', 'Command+C'],
|
||||
'paste': ['Ctrl+V', 'Command+V'],
|
||||
'undo': ['Ctrl+Z', 'Command+Z'],
|
||||
'redo': ['Ctrl+Shift+Z', 'Command+Shift+Z'],
|
||||
'find': ['Ctrl+F', 'Command+F'],
|
||||
|
|
101
src/eson.js
101
src/eson.js
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { setIn, getIn, updateIn } from './utils/immutabilityHelpers'
|
||||
import { isObject } from './utils/typeUtils'
|
||||
import { last, allButLast } from './utils/arrayUtils'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import times from 'lodash/times'
|
||||
import initial from 'lodash/initial'
|
||||
import last from 'lodash/last'
|
||||
|
||||
import type {
|
||||
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONSelection, ESONType, ESONPath,
|
||||
|
@ -113,7 +115,7 @@ export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
|
|||
throw new Error('Array item "' + index + '" not found')
|
||||
}
|
||||
|
||||
return ['items', index, 'value'].concat(toEsonPath(item.value, path.slice(1)))
|
||||
return ['items', String(index), 'value'].concat(toEsonPath(item.value, path.slice(1)))
|
||||
}
|
||||
else if (eson.type === 'Object') {
|
||||
// object property. find the index of this property
|
||||
|
@ -123,7 +125,7 @@ export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
|
|||
throw new Error('Object property "' + path[0] + '" not found')
|
||||
}
|
||||
|
||||
return ['props', index, 'value']
|
||||
return ['props', String(index), 'value']
|
||||
.concat(toEsonPath(prop.value, path.slice(1)))
|
||||
}
|
||||
else {
|
||||
|
@ -131,6 +133,42 @@ export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ESON object to a JSON object
|
||||
* @param {ESON} eson
|
||||
* @param {ESONPath} esonPath
|
||||
* @return {JSONPath} path
|
||||
*/
|
||||
export function toJsonPath (eson: ESON, esonPath: ESONPath) : JSONPath {
|
||||
if (esonPath.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (eson.type === 'Array') {
|
||||
// index of an array
|
||||
const index = esonPath[1]
|
||||
const item = eson.items[parseInt(index)]
|
||||
if (!item) {
|
||||
throw new Error('Array item "' + index + '" not found')
|
||||
}
|
||||
|
||||
return [index].concat(toJsonPath(item.value, esonPath.slice(3)))
|
||||
}
|
||||
else if (eson.type === 'Object') {
|
||||
// object property. find the index of this property
|
||||
const index = esonPath[1]
|
||||
const prop = eson.props[parseInt(index)]
|
||||
if (!prop) {
|
||||
throw new Error('Object property "' + esonPath[1] + '" not found')
|
||||
}
|
||||
|
||||
return [prop.name].concat(toJsonPath(prop.value, esonPath.slice(3)))
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
type ExpandCallback = (Path) => boolean
|
||||
|
||||
/**
|
||||
|
@ -219,7 +257,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
|
|||
if (containsCaseInsensitive(prop, text)) {
|
||||
// only add search result when this is an object property name,
|
||||
// don't add search result for array indices
|
||||
const parentPath = allButLast(path)
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(eson, toEsonPath(eson, parentPath))
|
||||
if (parent.type === 'Object') {
|
||||
results.push({path, field: 'property'})
|
||||
|
@ -302,7 +340,7 @@ export function applySearchResults (eson: ESON, searchResults: ESONPointer[], ac
|
|||
|
||||
if (searchResult.field === 'property') {
|
||||
const esonPath = toEsonPath(updatedEson, searchResult.path)
|
||||
const propertyPath = allButLast(esonPath).concat('searchResult')
|
||||
const propertyPath = initial(esonPath).concat('searchResult')
|
||||
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
||||
updatedEson = setIn(updatedEson, propertyPath, value)
|
||||
}
|
||||
|
@ -337,7 +375,7 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
|
|||
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
|
||||
const childsBefore = root[childsKey].slice(0, minIndex)
|
||||
const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
|
||||
.map((child, index) => setIn(child, ['value', 'selected'], true))
|
||||
.map(child => setIn(child, ['value', 'selected'], true))
|
||||
const childsAfter = root[childsKey].slice(maxIndex)
|
||||
|
||||
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
|
||||
|
@ -361,6 +399,51 @@ export function findSelectionIndices (root: ESON, start: string, end: string) :
|
|||
return { minIndex, maxIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON paths from a selection, sorted from first to last
|
||||
*/
|
||||
export function pathsFromSelection (eson: ESON, selection: ESONSelection): JSONPath[] {
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findSharedPath(selection.start.path, selection.end.path)
|
||||
const rootEsonPath = toEsonPath(eson, rootPath)
|
||||
|
||||
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
|
||||
// select a single node
|
||||
return [ rootPath ]
|
||||
}
|
||||
else {
|
||||
// select multiple childs of an object or array
|
||||
const root = getIn(eson, rootEsonPath)
|
||||
const start = selection.start.path[rootPath.length]
|
||||
const end = selection.end.path[rootPath.length]
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
|
||||
|
||||
if (root.type === 'Object') {
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name))
|
||||
}
|
||||
else { // root.type === 'Array'
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a list with paths
|
||||
* @param {ESON} data
|
||||
* @param {JSONPath[]} paths
|
||||
* @return {Array.<{name: string, value: JSONType}>}
|
||||
*/
|
||||
export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
|
||||
return paths.map(path => {
|
||||
const esonPath = toEsonPath(data, path)
|
||||
return {
|
||||
name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]),
|
||||
value: esonToJson(getIn(data, esonPath))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the common path of two paths.
|
||||
* For example findCommonRoot(['arr', '1', 'name'], ['arr', '1', 'address', 'contact']) returns ['arr', '1']
|
||||
|
@ -468,7 +551,7 @@ function recurseTraverse (value: ESON, path: JSONPath, root: ESON, callback: Rec
|
|||
* @return {boolean} Returns true if the path exists, else returns false
|
||||
* @private
|
||||
*/
|
||||
export function pathExists (eson, path) {
|
||||
export function pathExists (eson: ESON, path: JSONPath) {
|
||||
if (eson === undefined) {
|
||||
return false
|
||||
}
|
||||
|
@ -480,11 +563,11 @@ export function pathExists (eson, path) {
|
|||
if (eson.type === 'Array') {
|
||||
// index of an array
|
||||
const index = path[0]
|
||||
const item = eson.items[index]
|
||||
const item = eson.items[parseInt(index)]
|
||||
|
||||
return pathExists(item && item.value, path.slice(1))
|
||||
}
|
||||
else {
|
||||
else { // eson.type === 'Object'
|
||||
// object property. find the index of this property
|
||||
const index = findPropertyIndex(eson, path[0])
|
||||
const prop = eson.props[index]
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import isEqual from 'lodash/isEqual'
|
||||
import initial from 'lodash/initial'
|
||||
|
||||
import type { ESON, Path, JSONPatch, ESONPatchAction, ESONPatchOptions, ESONPatchResult } from './types'
|
||||
import type { ESON, Path, ESONPatch, ESONPatchOptions, ESONPatchResult, ESONSelection } from './types'
|
||||
import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutabilityHelpers'
|
||||
import { allButLast } from './utils/arrayUtils'
|
||||
import {
|
||||
jsonToEson, esonToJson, toEsonPath,
|
||||
parseJSONPointer, compileJSONPointer,
|
||||
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId
|
||||
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId,
|
||||
pathsFromSelection
|
||||
} from './eson'
|
||||
|
||||
/**
|
||||
|
@ -17,7 +18,7 @@ import {
|
|||
* what nodes must be expanded
|
||||
* @return {{data: ESON, revert: Object[], error: Error | null}}
|
||||
*/
|
||||
export function patchEson (eson: ESON, patch: ESONPatchAction[], expand = expandAll) {
|
||||
export function patchEson (eson: ESON, patch: ESONPatch, expand = expandAll) {
|
||||
let updatedEson = eson
|
||||
let revert = []
|
||||
|
||||
|
@ -99,11 +100,11 @@ export function patchEson (eson: ESON, patch: ESONPatchAction[], expand = expand
|
|||
}
|
||||
|
||||
default: {
|
||||
// unknown jsonpatch operation. Cancel the whole patch and return an error
|
||||
// unknown ESONPatch operation. Cancel the whole patch and return an error
|
||||
return {
|
||||
data: eson,
|
||||
revert: [],
|
||||
error: new Error('Unknown jsonpatch op ' + JSON.stringify(action.op))
|
||||
error: new Error('Unknown ESONPatch op ' + JSON.stringify(action.op))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +124,7 @@ export function patchEson (eson: ESON, patch: ESONPatchAction[], expand = expand
|
|||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @param {ESON} value
|
||||
* @return {{data: ESON, revert: JSONPatch}}
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
*/
|
||||
export function replace (data: ESON, path: Path, value: ESON) {
|
||||
const esonPath = toEsonPath(data, path)
|
||||
|
@ -146,7 +147,7 @@ export function replace (data: ESON, path: Path, value: ESON) {
|
|||
* Remove an item or property
|
||||
* @param {ESON} data
|
||||
* @param {string} path
|
||||
* @return {{data: ESON, revert: JSONPatch}}
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
*/
|
||||
export function remove (data: ESON, path: string) {
|
||||
// console.log('remove', path)
|
||||
|
@ -198,13 +199,13 @@ export function remove (data: ESON, path: string) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Remove redundant actions from a JSONPatch array.
|
||||
* Remove redundant actions from a ESONPatch array.
|
||||
* Actions are redundant when they are followed by an action
|
||||
* acting on the same path.
|
||||
* @param {JSONPatch} patch
|
||||
* @param {ESONPatch} patch
|
||||
* @return {Array}
|
||||
*/
|
||||
export function simplifyPatch(patch: JSONPatch) {
|
||||
export function simplifyPatch(patch: ESONPatch) {
|
||||
const simplifiedPatch = []
|
||||
const paths = {}
|
||||
|
||||
|
@ -235,7 +236,7 @@ export function simplifyPatch(patch: JSONPatch) {
|
|||
* @param {ESON} value
|
||||
* @param {{before?: string}} [options]
|
||||
* @param {number} [id] Optional id for the new item
|
||||
* @return {{data: ESON, revert: JSONPatch}}
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function add (data: ESON, path: string, value: ESON, options, id = getId()) {
|
||||
|
@ -303,7 +304,7 @@ export function add (data: ESON, path: string, value: ESON, options, id = getId(
|
|||
* @param {string} path
|
||||
* @param {string} from
|
||||
* @param {{before?: string}} [options]
|
||||
* @return {{data: ESON, revert: JSONPatch}}
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function copy (data: ESON, path: string, from: string, options) {
|
||||
|
@ -318,17 +319,17 @@ export function copy (data: ESON, path: string, from: string, options) {
|
|||
* @param {string} path
|
||||
* @param {string} from
|
||||
* @param {{before?: string}} [options]
|
||||
* @return {{data: ESON, revert: JSONPatch}}
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function move (data: ESON, path: string, from: string, options) {
|
||||
const fromArray = parseJSONPointer(from)
|
||||
const prop = getIn(data, allButLast(toEsonPath(data, fromArray)))
|
||||
const prop = getIn(data, initial(toEsonPath(data, fromArray)))
|
||||
const dataValue = prop.value
|
||||
const id = prop.id // we want to use the existing id in case the move is a renaming a property
|
||||
// FIXME: only reuse the existing id when move is renaming a property in the same object
|
||||
|
||||
const parentPathFrom = allButLast(fromArray)
|
||||
const parentPathFrom = initial(fromArray)
|
||||
const parent = getIn(data, toEsonPath(data, parentPathFrom))
|
||||
|
||||
const result1 = remove(data, from)
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
/**
|
||||
* Returns the last item of an array
|
||||
* @param {Array} array
|
||||
* @return {*}
|
||||
*/
|
||||
export function last (array) {
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the array having the last item removed
|
||||
*/
|
||||
export function allButLast (array: []): [] {
|
||||
return array.slice(0, -1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator to sort an array in ascending order
|
||||
*
|
||||
|
|
|
@ -1,17 +1,39 @@
|
|||
import { readFileSync } from 'fs'
|
||||
import test from 'ava';
|
||||
import test from 'ava'
|
||||
import { setIn, getIn } from '../src/utils/immutabilityHelpers'
|
||||
import {
|
||||
jsonToEson, esonToJson, toEsonPath, pathExists, transform, traverse,
|
||||
jsonToEson, esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
|
||||
parseJSONPointer, compileJSONPointer,
|
||||
expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
|
||||
applySelection, getSelection
|
||||
applySelection, pathsFromSelection
|
||||
} from '../src/eson'
|
||||
|
||||
const JSON1 = loadJSON('./resources/json1.json')
|
||||
const ESON1 = loadJSON('./resources/eson1.json')
|
||||
const ESON2 = loadJSON('./resources/eson2.json')
|
||||
|
||||
test('toEsonPath', t => {
|
||||
const jsonPath = ['obj', 'arr', '2', 'last']
|
||||
const esonPath = [
|
||||
'props', '0', 'value',
|
||||
'props', '0', 'value',
|
||||
'items', '2', 'value',
|
||||
'props', '1', 'value'
|
||||
]
|
||||
t.deepEqual(toEsonPath(ESON1, jsonPath), esonPath)
|
||||
})
|
||||
|
||||
test('toJsonPath', t => {
|
||||
const jsonPath = ['obj', 'arr', '2', 'last']
|
||||
const esonPath = [
|
||||
'props', '0', 'value',
|
||||
'props', '0', 'value',
|
||||
'items', '2', 'value',
|
||||
'props', '1', 'value'
|
||||
]
|
||||
t.deepEqual(toJsonPath(ESON1, esonPath), jsonPath)
|
||||
})
|
||||
|
||||
test('jsonToEson', t => {
|
||||
function expand (path) {
|
||||
return true
|
||||
|
@ -303,6 +325,42 @@ test('selection (node)', t => {
|
|||
t.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
test('pathsFromSelection (object)', t => {
|
||||
const selection = {
|
||||
start: {path: ['obj', 'arr', '2', 'last']},
|
||||
end: {path: ['nill']}
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [
|
||||
['obj'],
|
||||
['str'],
|
||||
['nill']
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (array)', t => {
|
||||
const selection = {
|
||||
start: {path: ['obj', 'arr', '1']},
|
||||
end: {path: ['obj', 'arr', '0']} // note the "wrong" order of start and end
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [
|
||||
['obj', 'arr', '0'],
|
||||
['obj', 'arr', '1']
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (value)', t => {
|
||||
const selection = {
|
||||
start: {path: ['obj', 'arr', '2', 'first']},
|
||||
end: {path: ['obj', 'arr', '2', 'first']}
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [
|
||||
['obj', 'arr', '2', 'first'],
|
||||
])
|
||||
})
|
||||
|
||||
// helper function to replace all id properties with a constant value
|
||||
function replaceIds (data, value = '[ID]') {
|
||||
if (data.type === 'Object') {
|
||||
|
@ -328,6 +386,7 @@ function printJSON (json, message = null) {
|
|||
console.log(JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
// helper function to load a JSON file
|
||||
function loadJSON (filename) {
|
||||
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import test from 'ava';
|
||||
import { jsonToEson, esonToJson } from '../src/eson'
|
||||
import { patchEson } from '../src/patchEson'
|
||||
import { readFileSync } from 'fs'
|
||||
import test from 'ava'
|
||||
import { jsonToEson, esonToJson, toEsonPath } from '../src/eson'
|
||||
import { patchEson, cut } from '../src/patchEson'
|
||||
|
||||
const ESON1 = loadJSON('./resources/eson1.json')
|
||||
|
||||
test('jsonpatch add', t => {
|
||||
const json = {
|
||||
|
@ -501,3 +504,16 @@ function replaceIds (data, value = '[ID]') {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to print JSON in the console
|
||||
function printJSON (json, message = null) {
|
||||
if (message) {
|
||||
console.log(message)
|
||||
}
|
||||
console.log(JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
// helper function to load a JSON file
|
||||
function loadJSON (filename) {
|
||||
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue