diff --git a/src/components/JSONNode.js b/src/components/JSONNode.js index 7f123c6..3493a2b 100644 --- a/src/components/JSONNode.js +++ b/src/components/JSONNode.js @@ -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) -} diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index 7476922..b5ec31a 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -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 diff --git a/src/components/utils/domSelector.js b/src/components/utils/domSelector.js index b971772..ad38f2e 100644 --- a/src/components/utils/domSelector.js +++ b/src/components/utils/domSelector.js @@ -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) } diff --git a/src/constants.js b/src/constants.js index b83304e..2c0b307 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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'], diff --git a/src/eson.js b/src/eson.js index 998f765..ee463ef 100644 --- a/src/eson.js +++ b/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] diff --git a/src/patchEson.js b/src/patchEson.js index d9ee192..d15f1c3 100644 --- a/src/patchEson.js +++ b/src/patchEson.js @@ -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) diff --git a/src/utils/arrayUtils.js b/src/utils/arrayUtils.js index 212d4ac..6c16450 100644 --- a/src/utils/arrayUtils.js +++ b/src/utils/arrayUtils.js @@ -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 * diff --git a/test/eson.test.js b/test/eson.test.js index 1ae063e..6e4e31b 100644 --- a/test/eson.test.js +++ b/test/eson.test.js @@ -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')) } diff --git a/test/patchEson.test.js b/test/patchEson.test.js index 322dd3f..e19b6a3 100644 --- a/test/patchEson.test.js +++ b/test/patchEson.test.js @@ -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')) +}