diff --git a/src/demo/Demo.js b/src/demo/Demo.js index 7d71958..88b74e7 100644 --- a/src/demo/Demo.js +++ b/src/demo/Demo.js @@ -39,7 +39,13 @@ const json = { 'string': 'Hello World', 'unicode': 'A unicode character: \u260E', 'url': 'http://jsoneditoronline.org', - 'largeArray': [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] + 'largeArray': [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], + 'structureArray': [ + {name: 'Joe', age: 24}, + {name: 'Sarah', age: 28}, + {name: 'Brett', age: 21}, + {name: 'Emma', age: 31}, + ] } function expandAll (path) { diff --git a/src/jsoneditor/actions.js b/src/jsoneditor/actions.js index 7912946..226b2c4 100644 --- a/src/jsoneditor/actions.js +++ b/src/jsoneditor/actions.js @@ -2,12 +2,12 @@ import last from 'lodash/last' import initial from 'lodash/initial' import isEmpty from 'lodash/isEmpty' import first from 'lodash/first' -import { findRootPath, pathsFromSelection } from './eson' +import { findRootPath } from './eson' import { getIn } from './utils/immutabilityHelpers' import { findUniqueName } from './utils/stringUtils' import { isObject, stringConvert } from './utils/typeUtils' import { compareAsc, compareDesc } from './utils/arrayUtils' -import { compileJSONPointer } from './jsonPointer' +import { compileJSONPointer, parseJSONPointer } from './jsonPointer' /** * Create a JSONPatch to change the value of a property or item @@ -81,13 +81,13 @@ export function changeType (json, path, type) { */ export function duplicate (json, selection) { // console.log('duplicate', path) - if (!selection.start || !selection.end) { + if (isEmpty(selection.multi)) { return [] } const rootPath = findRootPath(selection) const root = getIn(json, rootPath) - const paths = pathsFromSelection(json, selection) + const paths = selection.multi.map(parseJSONPointer) if (Array.isArray(root)) { const lastPath = last(paths) @@ -224,9 +224,11 @@ export function insertInside (json, parentPath, values) { export function replace (json, selection, values) { // TODO: find a better name and define datastructure for values const rootPath = findRootPath(selection) const root = getIn(json, rootPath) + const paths = selection.multi + ? selection.multi.map(parseJSONPointer) + : [] if (Array.isArray(root)) { - const paths = pathsFromSelection(json, selection) const firstPath = first(paths) const offset = firstPath ? parseInt(last(firstPath), 10) : 0 @@ -240,7 +242,7 @@ export function replace (json, selection, values) { // TODO: find a better name return removeActions.concat(insertActions) } else { // root is Object - const removeActions = removeAll(pathsFromSelection(json, selection)) + const removeActions = removeAll(paths) const insertActions = values.map(entry => { const newProp = findUniqueName(entry.name, root) return { @@ -293,7 +295,7 @@ export function append (json, parentPath, type) { /** * Create a JSONPatch for a remove action * @param {Path} path - * @return {ESONPatchDocument} + * @return {JSONPatchDocument} */ export function remove (path) { return [{ @@ -305,7 +307,7 @@ export function remove (path) { /** * Create a JSONPatch for a multiple remove action * @param {Path[]} paths - * @return {ESONPatchDocument} + * @return {JSONPatchDocument} */ export function removeAll (paths) { return paths diff --git a/src/jsoneditor/components/JSONNode.js b/src/jsoneditor/components/JSONNode.js index 5247c72..8338e6e 100644 --- a/src/jsoneditor/components/JSONNode.js +++ b/src/jsoneditor/components/JSONNode.js @@ -6,13 +6,20 @@ import naturalSort from 'javascript-natural-sort' import { escapeHTML, unescapeHTML } from '../utils/stringUtils' import { getInnerText, insideRect } from '../utils/domUtils' -import { stringConvert, valueType, isUrl } from '../utils/typeUtils' +import { isUrl, stringConvert, valueType } from '../utils/typeUtils' import { - SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE, - SELECTED_FIRST, SELECTED_LAST + ERROR, + EXPANDED, + ID, + SEARCH_PROPERTY, + SEARCH_VALUE, + SELECTED_AFTER, SELECTED_BEFORE_CHILDS, + SELECTED_INSIDE, + SELECTION, + TYPE, + VALUE } from '../eson' import { compileJSONPointer } from '../jsonPointer' -import { ERROR, EXPANDED, ID, SEARCH_PROPERTY, SEARCH_VALUE, SELECTION, TYPE, VALUE } from '../eson' import fontawesome from '@fortawesome/fontawesome' import faExclamationTriangle from '@fortawesome/fontawesome-free-solid/faExclamationTriangle' @@ -45,9 +52,6 @@ export default class JSONNode extends PureComponent { super(props) this.state = { - menu: null, // can contain object {anchor, root} - appendMenu: null, // can contain object {anchor, root} - hover: null, path: null // initialized via getDerivedStateFromProps } } @@ -106,7 +110,8 @@ export default class JSONNode extends PureComponent { this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed') ] : null, - this.renderError(this.props.eson[ERROR]) + this.renderError(this.props.eson[ERROR]), + this.renderBeforeChilds() ]) let childs @@ -155,7 +160,7 @@ export default class JSONNode extends PureComponent { 'data-path': compileJSONPointer(this.state.path), 'data-area': 'empty', // TODO: remove 'data-selection-area': this.props.eson[EXPANDED] ? 'before-childs' : 'after', - className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover), + className: this.getContainerClassName(this.props.eson[SELECTION]), // onMouseOver: this.handleMouseOver, // onMouseLeave: this.handleMouseLeave }, [nodeStart, childs, nodeEnd]) @@ -165,11 +170,11 @@ export default class JSONNode extends PureComponent { // TODO: refactor renderJSONArray (too large/complex) const count = this.props.eson.length const nodeStart = h('div', { - key: 'node', - onKeyDown: this.handleKeyDown, - 'data-selection-area': 'inside', - className: 'jsoneditor-node jsoneditor-array' - }, [ + key: 'node', + onKeyDown: this.handleKeyDown, + 'data-selection-area': 'inside', + className: 'jsoneditor-node jsoneditor-array' + }, [ this.renderExpandButton(), this.renderProperty(), this.renderSeparator(), @@ -181,7 +186,8 @@ export default class JSONNode extends PureComponent { this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'), ] : null, - this.renderError(this.props.eson[ERROR]) + this.renderError(this.props.eson[ERROR]), + this.renderBeforeChilds() ]) let childs @@ -230,7 +236,7 @@ export default class JSONNode extends PureComponent { 'data-path': compileJSONPointer(this.state.path), 'data-area': 'empty', // TODO: remove data-area 'data-selection-area': this.props.eson[EXPANDED] ? 'before-childs' : 'after', - className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover), + className: this.getContainerClassName(this.props.eson[SELECTION]), // onMouseOver: this.handleMouseOver, // onMouseLeave: this.handleMouseLeave }, [nodeStart, childs, nodeEnd]) @@ -256,7 +262,7 @@ export default class JSONNode extends PureComponent { 'data-path': compileJSONPointer(this.state.path), 'data-area': 'empty', // TODO: remove 'data-selection-area': 'after', - className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover), + className: this.getContainerClassName(this.props.eson[SELECTION]), // onMouseOver: this.handleMouseOver, // onMouseLeave: this.handleMouseLeave }, [node]) @@ -350,6 +356,14 @@ export default class JSONNode extends PureComponent { } } + renderBeforeChilds () { + return h('div', { + key: 'before-childs', + className: 'jsoneditor-before-childs', + 'data-selection-area': 'before-childs' + }) + } + renderSeparator() { const isProp = typeof this.props.prop === 'string' if (!isProp) { @@ -423,33 +437,23 @@ export default class JSONNode extends PureComponent { } } - getContainerClassName (selected, hover) { - let classNames = ['jsoneditor-node-container'] + getContainerClassName (selected) { + let classNames = [ + 'jsoneditor-node-container', + // `jsoneditor-node-${this.props.eson[TYPE]}` + this.props.eson[EXPANDED] ? 'jsoneditor-node-expanded' : 'jsoneditor-node-collapsed' + ] - if ((selected & SELECTED_INSIDE) !== 0) { - classNames.push('jsoneditor-selected-insert-before') - } - else if ((selected & SELECTED_AFTER) !== 0) { - classNames.push('jsoneditor-selected-insert-after') - } - else { - if ((selected & SELECTED) !== 0) { classNames.push('jsoneditor-selected') } - if ((selected & SELECTED_START) !== 0) { classNames.push('jsoneditor-selected-start') } - if ((selected & SELECTED_END) !== 0) { classNames.push('jsoneditor-selected-end') } - if ((selected & SELECTED_FIRST) !== 0) { classNames.push('jsoneditor-selected-first') } - if ((selected & SELECTED_LAST) !== 0) { classNames.push('jsoneditor-selected-last') } + if (selected === SELECTED_INSIDE) { + classNames.push('jsoneditor-selected') } - if ((hover & SELECTED_INSIDE) !== 0) { - classNames.push('jsoneditor-hover-insert-before') + if (selected === SELECTED_AFTER) { + classNames.push('jsoneditor-selected-after') } - else if ((hover & SELECTED_AFTER) !== 0) { - classNames.push('jsoneditor-hover-insert-after') - } - else { - if ((hover & SELECTED) !== 0) { classNames.push('jsoneditor-hover') } - if ((hover & SELECTED_START) !== 0) { classNames.push('jsoneditor-hover-start') } - if ((hover & SELECTED_END) !== 0) { classNames.push('jsoneditor-hover-end') } + + if (selected === SELECTED_BEFORE_CHILDS) { + classNames.push('jsoneditor-selected-before-childs') } return classNames.join(' ') @@ -574,34 +578,6 @@ export default class JSONNode extends PureComponent { ) } - handleMouseOver = (event) => { - if (event.buttons === 0) { // no mouse button down, no dragging - event.stopPropagation() - - const hover = (event.target.className.indexOf('jsoneditor-insert-area') !== -1) - ? (SELECTED + SELECTED_AFTER) - : SELECTED - - if (hoveredNode && hoveredNode !== this) { - // FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted? - hoveredNode.setState({hover: null}) - } - - if (hover !== this.state.hover) { - this.setState({hover}) - hoveredNode = this - } - } - } - - handleMouseLeave = (event) => { - event.stopPropagation() - // FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted? - hoveredNode.setState({hover: null}) - - this.setState({hover: null}) - } - /** @private */ handleChangeProperty = (event) => { const parentPath = initial(this.state.path) diff --git a/src/jsoneditor/components/TreeMode.js b/src/jsoneditor/components/TreeMode.js index 8d66d46..fc78096 100644 --- a/src/jsoneditor/components/TreeMode.js +++ b/src/jsoneditor/components/TreeMode.js @@ -1,6 +1,9 @@ import { createElement as h, PureComponent } from 'react' import mitt from 'mitt' +import cloneDeepWith from 'lodash/cloneDeepWith' +import isEmpty from 'lodash/isEmpty' import isEqual from 'lodash/isEqual' +import first from 'lodash/first' import reverse from 'lodash/reverse' import initial from 'lodash/initial' import pick from 'lodash/pick' @@ -8,7 +11,7 @@ import Hammer from 'react-hammerjs' import jump from '../assets/jump.js/src/jump' import Ajv from 'ajv' -import { existsIn, setIn, updateIn } from '../utils/immutabilityHelpers' +import { existsIn, getIn, setIn, updateIn } from '../utils/immutabilityHelpers' import { parseJSON } from '../utils/jsonUtils' import { enrichSchemaError } from '../utils/schemaUtils' import { compileJSONPointer, parseJSONPointer } from '../jsonPointer' @@ -52,10 +55,10 @@ import { expand, EXPANDED, expandPath, + findRootPath, findSharedPath, immutableESONPatch, nextSearchResult, - pathsFromSelection, previousSearchResult, search, SELECTION, @@ -273,7 +276,7 @@ export default class TreeMode extends PureComponent { renderMenu () { const hasCursor = true // FIXME: implement hasCursor - const hasSelection = !!this.state.selection + const hasSelection = this.state.selection ? this.state.selection.type !== 'none' : false const hasClipboard = this.state.clipboard ? this.state.clipboard.length > 0 : false @@ -295,7 +298,7 @@ export default class TreeMode extends PureComponent { canInsert: hasCursor, canDuplicate: hasSelection, canRemove: hasSelection, - onInsert: this.handleInsertBefore, + onInsert: this.handleInsert, onDuplicate: this.handleDuplicate, onRemove: this.handleRemove, @@ -428,10 +431,11 @@ export default class TreeMode extends PureComponent { } handleRemove = () => { - if (this.state.selection) { + if (this.state.selection && this.state.selection.multi) { // remove selection // TODO: select next property? (same as when removing a path?) - const paths = pathsFromSelection(this.state.eson, this.state.selection) + // TODO: inefficient: first parsing the paths, and removeAll stringifies them again + const paths = this.state.selection.multi.map(parseJSONPointer) this.setState({ selection: null }) this.handlePatch(removeAll(paths)) } @@ -495,7 +499,7 @@ export default class TreeMode extends PureComponent { handleKeyDownDuplicate = (event) => { const path = this.findDataPathFromElement(event.target) if (path) { - const selection = { start: path, end: path } + const selection = { type: 'multi', multi: [path] } this.handlePatch(duplicate(this.state.eson, selection)) // apply focus to the duplicated node @@ -520,9 +524,9 @@ export default class TreeMode extends PureComponent { handleCut = () => { const selection = this.state.selection - if (selection && selection.start && selection.end) { + if (selection && selection.multi) { const eson = this.state.eson - const paths = pathsFromSelection(eson, selection) + const paths = selection.multi.map(parseJSONPointer) const clipboard = contentsFromPaths(eson, paths) this.setState({ clipboard, selection: null }) @@ -540,9 +544,9 @@ export default class TreeMode extends PureComponent { handleCopy = () => { const selection = this.state.selection - if (selection && selection.start && selection.end) { + if (selection && selection.multi) { const eson = this.state.eson - const paths = pathsFromSelection(eson, selection) + const paths = selection.multi.map(parseJSONPointer) const clipboard = contentsFromPaths(eson, paths) this.setState({ clipboard }) @@ -559,14 +563,14 @@ export default class TreeMode extends PureComponent { if (selection && clipboard && clipboard.length > 0) { this.setState({ selection: null }) - if (selection.start && selection.end) { + if (selection.multi) { this.handlePatch(replace(eson, selection, clipboard)) } else if (selection.after) { - this.handlePatch(insertAfter(eson, selection.after, clipboard)) + this.handlePatch(insertAfter(eson, parseJSONPointer(selection.after), clipboard)) } - else if (selection.inside) { - this.handlePatch(insertInside(eson, selection.inside, clipboard)) + else if (selection.type === 'before-childs') { + this.handlePatch(insertInside(eson, parseJSONPointer(selection.beforeChildsOf), clipboard)) } else { throw new Error(`Cannot paste at current selection ${JSON.stringify(selection)}`) @@ -579,9 +583,71 @@ export default class TreeMode extends PureComponent { } } - handleInsertBefore = (insertType) => { - // FIXME: implement handleInsertBefore - console.error('Insert not yet implemented...', insertType) + handleInsert = (insertType) => { + const { eson, selection } = this.state + + if (selection) { + this.setState({ selection: null }) + + const clipboard = [{ + name: '', + value: this.createValue(insertType, selection) + }] + + if (selection.multi) { + this.handlePatch(replace(eson, selection, clipboard)) + } + else if (selection.after) { + this.handlePatch(insertAfter(eson, parseJSONPointer(selection.after), clipboard)) + + } + else if (selection.beforeChildsOf) { + this.handlePatch(insertInside(eson, parseJSONPointer(selection.beforeChildsOf), clipboard)) + } + else { + throw new Error(`Cannot insert at current selection ${JSON.stringify(selection)}`) + } + + // TODO: expand the inserted contents when array/object/structure + // TODO: select the inserted contents + } + } + + /** + * Create an eson value + * @param insertType + * @param selection + * @returns {*} + */ + createValue = (insertType, selection) => { + if (insertType === 'array') { + return [] + } + + if (insertType === 'object') { + return {} + } + + if (insertType === 'structure') { + const rootPath = findRootPath(selection) + const parent = getIn(this.state.json, rootPath) + + if (Array.isArray(parent) && !isEmpty(parent)) { + const jsonExample = first(parent) + const structure = cloneDeepWith(jsonExample, (value) => { + return (Array.isArray(value) || typeof value === 'object') + ? undefined // leave as is + : '' + }) + + console.log('structure', jsonExample, structure) + + return structure + } + } + + // value or unknown type + return '' } /** @@ -662,13 +728,15 @@ export default class TreeMode extends PureComponent { } toggleSearch = () => { - this.setState({ - showSearch: !this.state.showSearch - }) + if (this.state.showSearch) { + this.handleCloseSearch() + } + else { + this.setState({ showSearch: true }) + } } handleSearch = (text) => { - // FIXME // FIXME: also apply search when eson is changed const { eson, searchResult } = search(this.state.eson, text) if (searchResult.matches.length > 0) { @@ -786,47 +854,16 @@ export default class TreeMode extends PureComponent { this.selectionStartPointer = this.findSelectionPointerFromEvent(event.target, event.clientY) - console.log('selectionPointer', this.selectionStartPointer) - - const pointer = this.findJSONPointerFromElement(event.target) - const clickedOnEmptySpace = (event.target.nodeName === 'DIV') && - (event.target.contentEditable !== 'true') && - (event.target.className !== 'jsoneditor-tag') // FIXME: this is an ugly hack to prevent an object/array from being selected when expanding it by clicking the tag - - // TODO: cleanup - // console.log('handleTouchStart', clickedOnEmptySpace && pointer, pointer && this.selectionFromJSONPointer(pointer)) - - if (clickedOnEmptySpace && pointer) { - this.setState({ selection: this.selectionFromJSONPointer(pointer)}) - } - else { - this.setState({ selection: null }) - } + this.setState({ selection: null }) } handlePan = (event) => { this.selectionEndPointer = this.findSelectionPointerFromEvent(event.target, event.center.y) - const selection2 = this.findSelectionFromPointers(this.selectionStartPointer, this.selectionEndPointer) - console.log('selection', JSON.stringify(selection2)) - - const selection = this.state.selection - const path = this.findDataPathFromElement(event.target.firstChild) - if (path && selection && !isEqual(path, selection.end)) { - - // TODO: cleanup - // console.log('handlePan', { - // start: selection.start || selection.inside || selection.after || selection.empty || selection.emptyBefore, - // end: path - // }) - - // FIXME: when selection.empty, start should be set to the next node - this.setState({ - selection: { - start: selection.start || selection.inside || selection.after || selection.empty || selection.emptyBefore, - end: path - } - }) + const selection = this.findSelectionFromPointers(this.selectionStartPointer, this.selectionEndPointer) + if (!isEqual(selection, this.state.selection)) { + this.setState({ selection }) + console.log('selection', JSON.stringify(selection)) // TODO: cleanup logging } } @@ -891,6 +928,7 @@ export default class TreeMode extends PureComponent { /** * @param {SelectionPointer} start * @param {SelectionPointer} end + * @return {Selection} */ findSelectionFromPointers (start, end) { if (start && end) { @@ -909,6 +947,10 @@ export default class TreeMode extends PureComponent { return { type: 'after', after: compileJSONPointer(sharedPath) } } + if (start.area === 'before-childs' && end.area === 'before-childs' && start.path === end.path) { + return { type: 'before-childs', beforeChildsOf: compileJSONPointer(sharedPath) } + } + if (start.path !== end.path || start.area !== end.area || start.area === 'inside' || end.area === 'inside') { return { type: 'multi', multi: [ compileJSONPointer(sharedPath) ] } } @@ -934,6 +976,7 @@ export default class TreeMode extends PureComponent { if (firstIndex < lastIndex) { return { type: 'multi', + after: includeFirst ? undefined : first.path, multi: childs .slice(firstIndex, lastIndex) .map(element => element.getAttribute('data-path')) @@ -943,7 +986,7 @@ export default class TreeMode extends PureComponent { // selection starts after first node and ends before last node return { type: 'after', - path: first.path + after: first.path } } } diff --git a/src/jsoneditor/components/jsoneditor.scss b/src/jsoneditor/components/jsoneditor.scss index 335d907..7c2e13d 100644 --- a/src/jsoneditor/components/jsoneditor.scss +++ b/src/jsoneditor/components/jsoneditor.scss @@ -294,52 +294,7 @@ div.jsoneditor-menu-panel-right { div.jsoneditor-node-container { position: relative; - transition: background-color 100ms ease-in; - - // TODO: cleanup insert-area css? - div.jsoneditor-insert-area { - //background: gray; - position: absolute; - width: 100%; - height: $insert-area-height; - left: 0; - top: -$insert-area-height/2; - box-sizing: border-box; - z-index: 1; // must be on top of next node, it overlaps a bit - - $top: -$line-height / 2 + 3px; - - // FIXME: nice border color - //&:before { - // content: ''; - // position: absolute; - // right: 60px; - // top: $top; - // width: 0; - // height: 0; - // border-top: $line-height / 2 solid transparent; - // border-right: $line-height / 2 solid red; - // border-bottom: $line-height / 2 solid transparent; - //} - - &:after { - content: ''; - position: absolute; - top: $top; - right: 0; - width: 60px; - height: $line-height; - background: inherit; - } - } - - //&.jsoneditor-hover { - // > .jsoneditor-node > .jsoneditor-delimiter-start, - // > .jsoneditor-delimiter-end { - // color: $theme-color; - // font-weight: bold; - // } - //} + //transition: background-color 100ms ease-in; &.jsoneditor-selected { .jsoneditor-node { @@ -356,6 +311,35 @@ div.jsoneditor-node-container { } } + &.jsoneditor-selected-after { + + &.jsoneditor-node-expanded { + .jsoneditor-node-end { + background-color: $selectedColor; + + > .jsoneditor-delimiter-end { + background-color: white; + } + } + } + + &.jsoneditor-node-collapsed { + background-color: $selectedColor; + + > .jsoneditor-node { + background-color: white; + } + } + } + + &.jsoneditor-selected-before-childs { + + > .jsoneditor-node > .jsoneditor-before-childs { + background-color: $selectedColor; + width: 40px; // FIXME: should use full remaining width + } + } + &.jsoneditor-hover { > .jsoneditor-node > .jsoneditor-button-container, > .jsoneditor-node > .jsoneditor-button-placeholder { diff --git a/src/jsoneditor/components/style.scss b/src/jsoneditor/components/style.scss index fc5ad7c..502629f 100644 --- a/src/jsoneditor/components/style.scss +++ b/src/jsoneditor/components/style.scss @@ -14,6 +14,6 @@ $hoverColor: #d3d3d3; $hoverAndSelectedColor: #ffdb80; $warning-color: #FBB917; $gray: #9d9d9d; -$gray-icon: #5e5e5e; +$gray-icon: $gray; $light-gray: #c0c0c0; $input-padding: 5px; diff --git a/src/jsoneditor/eson.js b/src/jsoneditor/eson.js index c541ef5..b43e47b 100644 --- a/src/jsoneditor/eson.js +++ b/src/jsoneditor/eson.js @@ -1,12 +1,10 @@ -import { deleteIn, getIn, setIn, shallowCloneWithSymbols, transform, updateIn } from './utils/immutabilityHelpers' -import range from 'lodash/range' +import { deleteIn, getIn, setIn, transform } from './utils/immutabilityHelpers' import { compileJSONPointer, parseJSONPointer } from './jsonPointer' +import first from 'lodash/first' import last from 'lodash/last' import initial from 'lodash/initial' import isEmpty from 'lodash/isEmpty' import isEqual from 'lodash/isEqual' -import naturalSort from 'javascript-natural-sort' -import times from 'lodash/times' import { immutableJSONPatch } from './immutableJSONPatch' import { compareArrays } from './utils/arrayUtils' import { compareStrings } from './utils/stringUtils' @@ -27,8 +25,7 @@ export const SELECTED_FIRST = 8 export const SELECTED_LAST = 16 export const SELECTED_INSIDE = 32 export const SELECTED_AFTER = 64 -export const SELECTED_EMPTY = 128 -export const SELECTED_EMPTY_BEFORE = 256 +export const SELECTED_BEFORE_CHILDS = 128 // TODO: comment export function syncEson(json, eson) { @@ -355,87 +352,34 @@ function setSearchStatus (eson, esonPointer, searchStatus) { * @return {ESON} Returns updated eson object */ export function applySelection (eson, selection) { - if (!selection) { - return cleanupMetaData(eson, 'selected') + if (selection && selection.type === 'after') { + const updatedEson = setIn(eson, parseJSONPointer(selection.after).concat([SELECTION]), SELECTED_AFTER) + return cleanupMetaData(updatedEson, SELECTION, [selection.after]) } - else if (selection.inside) { - const updatedEson = setIn(eson, selection.inside.concat([SELECTION]), SELECTED_INSIDE) - return cleanupMetaData(updatedEson, 'selected', [selection.inside]) + + if (selection && selection.type === 'before-childs') { + const updatedEson = setIn(eson, parseJSONPointer(selection.beforeChildsOf).concat([SELECTION]), SELECTED_BEFORE_CHILDS) + return cleanupMetaData(updatedEson, SELECTION, [selection.beforeChildsOf]) } - else if (selection.after) { - const updatedEson = setIn(eson, selection.after.concat([SELECTION]), SELECTED_AFTER) - return cleanupMetaData(updatedEson, 'selected', [selection.after]) + + if (selection && selection.type === 'multi') { + let updatedEson = eson + + if (selection.after) { + updatedEson = setIn(updatedEson, parseJSONPointer(selection.after).concat([SELECTION]), SELECTED_AFTER) + } + + for (const path of selection.multi) { + updatedEson = setIn(updatedEson, parseJSONPointer(path).concat([SELECTION]), SELECTED_INSIDE) + } + + const ignorePaths = selection.after + ? selection.multi.concat([selection.after]) + : selection.multi + return cleanupMetaData(updatedEson, SELECTION, ignorePaths) } - else if (selection.empty) { - const updatedEson = setIn(eson, selection.empty.concat([SELECTION]), SELECTED_EMPTY) - return cleanupMetaData(updatedEson, 'selected', [selection.empty]) - } - else if (selection.emptyBefore) { - const updatedEson = setIn(eson, selection.emptyBefore.concat([SELECTION]), SELECTED_EMPTY_BEFORE) - return cleanupMetaData(updatedEson, 'selected', [selection.emptyBefore]) - } - else { // selection.start and selection.end - // find the parent node shared by both start and end of the selection - const rootPath = findRootPath(selection) - let selectedPaths = null - const updatedEson = updateIn(eson, rootPath, (root) => { - const start = selection.start[rootPath.length] - const end = selection.end[rootPath.length] - - // TODO: simplify the update function. Use pathsFromSelection ? - - if (root[TYPE] === 'object') { - const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps - const startIndex = props.indexOf(start) - const endIndex = props.indexOf(end) - - const firstIndex = Math.min(startIndex, endIndex) - const lastIndex = Math.max(startIndex, endIndex) - const firstProp = props[firstIndex] - const lastProp = props[lastIndex] - - const selectedProps = props.slice(firstIndex, lastIndex + 1)// include max index itself - selectedPaths = selectedProps.map(prop => rootPath.concat(prop)) - let updatedObj = shallowCloneWithSymbols(root) - selectedProps.forEach(prop => { - const selected = SELECTED + - (prop === start ? SELECTED_START : 0) + - (prop === end ? SELECTED_END : 0) + - (prop === firstProp ? SELECTED_FIRST : 0) + - (prop === lastProp ? SELECTED_LAST : 0) - updatedObj[prop] = setIn(updatedObj[prop], [SELECTION], selected) - }) - - return updatedObj - } - else { // root[TYPE] === 'array' - const startIndex = parseInt(start, 10) - const endIndex = parseInt(end, 10) - - const firstIndex = Math.min(startIndex, endIndex) - const lastIndex = Math.max(startIndex, endIndex) - - const selectedIndices = range(firstIndex, lastIndex + 1)// include max index itself - selectedPaths = selectedIndices.map(index => rootPath.concat(String(index))) - - let updatedArr = root.slice() - updatedArr = shallowCloneWithSymbols(root) - selectedIndices.forEach(index => { - const selected = SELECTED + - (index === startIndex ? SELECTED_START : 0) + - (index === endIndex ? SELECTED_END : 0) + - (index === firstIndex ? SELECTED_FIRST : 0) + - (index === lastIndex ? SELECTED_LAST : 0) - updatedArr[index] = setIn(updatedArr[index], [SELECTION], selected) - }) - - return updatedArr - } - }) - - return cleanupMetaData(updatedEson, 'selected', selectedPaths) - } + return cleanupMetaData(eson, SELECTION) } /** @@ -462,30 +406,44 @@ export function contentsFromPaths (eson, paths) { * @return {Path} */ export function findRootPath(selection) { - if (selection.inside) { - return initial(selection.inside) - } - else if (selection.after) { - return initial(selection.after) - } - else if (selection.empty) { - return initial(selection.empty) - } - else if (selection.emptyBefore) { - return initial(selection.emptyBefore) - } - else { // selection.start and selection.end - const sharedPath = findSharedPath(selection.start, selection.end) + if (selection.multi) { + const firstPath = parseJSONPointer(first(selection.multi)) - if (sharedPath.length === selection.start.length || - sharedPath.length === selection.end.length) { - // there is just one node selected, return it's parent - return initial(sharedPath) - } - else { - return sharedPath - } + return initial(firstPath) } + + if (selection.after) { + return initial(parseJSONPointer(selection.after)) + } + + // TODO: handle area === 'before-childs' and area === 'after-childs' + + + // TODO: cleanup + // if (selection.inside) { + // return initial(selection.inside) + // } + // else if (selection.after) { + // return initial(selection.after) + // } + // else if (selection.empty) { + // return initial(selection.empty) + // } + // else if (selection.emptyBefore) { + // return initial(selection.emptyBefore) + // } + // else { // selection.start and selection.end + // const sharedPath = findSharedPath(selection.start, selection.end) + // + // if (sharedPath.length === selection.start.length || + // sharedPath.length === selection.end.length) { + // // there is just one node selected, return it's parent + // return initial(sharedPath) + // } + // else { + // return sharedPath + // } + // } } /** @@ -504,43 +462,6 @@ export function findSharedPath (path1, path2) { return path1.slice(0, i) } -/** - * Get the JSON paths from a selection, sorted from first to last - * @param {ESON} eson - * @param {Selection} selection - * @return {Path[]} - */ -// TODO: move pathsFromSelection to a separate file clipboard.js or selection.js? -export function pathsFromSelection (eson, selection) { - // find the parent node shared by both start and end of the selection - const rootPath = findRootPath(selection) - const root = getIn(eson, rootPath) - - const start = (selection.after || selection.inside || selection.start)[rootPath.length] - const end = (selection.after || selection.inside || selection.end)[rootPath.length] - - if (getType(root) === 'object') { - // TODO: create a util function getSortedProps, cache results? - const props = Object.keys(root).sort(naturalSort) - const startIndex = props.indexOf(start) - const endIndex = props.indexOf(end) - - const minIndex = Math.min(startIndex, endIndex) - const maxIndex = Math.max(startIndex, endIndex) + ((selection.after || selection.inside) ? 0 : 1) // include max index itself - - return times(maxIndex - minIndex, i => rootPath.concat(props[i + minIndex])) - } - else { // root[TYPE] === 'array' - const startIndex = parseInt(start, 10) - const endIndex = parseInt(end, 10) - - const minIndex = Math.min(startIndex, endIndex) - const maxIndex = Math.max(startIndex, endIndex) + ((selection.after || selection.inside) ? 0 : 1) // include max index itself - - return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex))) - } -} - /** * Apply a JSON patch document to an ESON object. * - Applies meta information to added values @@ -557,7 +478,11 @@ export function immutableESONPatch (eson, operations) { }) } -// TODO: comment +/** + * Get the JSON type of any input + * @param {*} any + * @returns {string} Returns 'array', 'object', 'value', or 'undefined' + */ export function getType (any) { if (any === undefined) { return 'undefined' diff --git a/src/jsoneditor/eson.test.js b/src/jsoneditor/eson.test.js index 3985bfe..c538e3b 100644 --- a/src/jsoneditor/eson.test.js +++ b/src/jsoneditor/eson.test.js @@ -346,6 +346,7 @@ test('previousSearchResult', () => { expect(getIn(first.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal') }) +// FIXME: test selection test('selection (object)', () => { const eson = syncEson({ "obj": { @@ -380,6 +381,7 @@ test('selection (object)', () => { assertEqualEson(actual2, expected2) }) +// FIXME: test selection test('selection (array)', () => { const eson = syncEson({ "obj": { @@ -405,6 +407,7 @@ test('selection (array)', () => { assertEqualEson(actual, expected) }) +// FIXME: test selection test('selection (value)', () => { const eson = syncEson({ "obj": { @@ -425,6 +428,7 @@ test('selection (value)', () => { assertEqualEson(actual, expected) }) +// FIXME: test selection test('selection (node)', () => { const eson = syncEson({ "obj": { @@ -444,78 +448,3 @@ test('selection (node)', () => { SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST) assertEqualEson(actual, expected) }) - -test('pathsFromSelection (object)', () => { - const json = { - "bool": false, - "nill": null, - "obj": { - "arr": [1,2, {"first":3,"last":4}] - }, - "str": "hello world", - } - const selection = { - start: ['obj', 'arr', '2', 'last'], - end: ['nill'] - } - - expect(pathsFromSelection(json, selection)).toEqual([ - ['nill'], - ['obj'] - ]) -}) - -test('pathsFromSelection (array)', () => { - const json = { - "obj": { - "arr": [1,2, {"first":3,"last":4}] - }, - "str": "hello world", - "nill": null, - "bool": false - } - const selection = { - start: ['obj', 'arr', '1'], - end: ['obj', 'arr', '0'] // note the "backward" order of start and end - } - - expect(pathsFromSelection(json, selection)).toEqual([ - ['obj', 'arr', '0'], - ['obj', 'arr', '1'] - ]) -}) - -test('pathsFromSelection (value)', () => { - const json = { - "obj": { - "arr": [1,2, {"first":3,"last":4}] - }, - "str": "hello world", - "nill": null, - "bool": false - } - const selection = { - start: ['obj', 'arr', '2', 'first'], - end: ['obj', 'arr', '2', 'first'] - } - - expect(pathsFromSelection(json, selection)).toEqual([ - ['obj', 'arr', '2', 'first'], - ]) -}) - -test('pathsFromSelection (after)', () => { - const json = { - "obj": { - "arr": [1,2, {"first":3,"last":4}] - }, - "str": "hello world", - "nill": null, - "bool": false - } - const selection = { - after: ['obj', 'arr', '2', 'first'] - } - - expect(pathsFromSelection(json, selection)).toEqual([]) -}) diff --git a/src/jsoneditor/types.js b/src/jsoneditor/types.js index 6dd23f3..cf2d5b6 100644 --- a/src/jsoneditor/types.js +++ b/src/jsoneditor/types.js @@ -30,10 +30,10 @@ /** * @typedef {{ - * start?: Path, - * end?: Path, - * before?: Path, - * after?: Path, + * type: 'multi' | 'after' | 'before-childs', 'none' + * after? string + * multi?: string[] + * beforeChildsOf?: string * }} Selection */ diff --git a/src/jsoneditor/utils/assertEqualEson.js b/src/jsoneditor/utils/assertEqualEson.js index 45a1cd4..8ae310e 100644 --- a/src/jsoneditor/utils/assertEqualEson.js +++ b/src/jsoneditor/utils/assertEqualEson.js @@ -1,4 +1,4 @@ -import { ID } from '../eson' +import { EXPANDED, ID, SELECTION, TYPE } from '../eson' import { deleteIn, transform } from './immutabilityHelpers' export function createAssertEqualEson(expect) { @@ -17,6 +17,10 @@ export function createAssertEqualEson(expect) { else { expect(actual).toEqual(expected) } + + expect(actual[TYPE]).toEqual(expected[TYPE]) + expect(actual[SELECTION]).toEqual(expected[SELECTION]) + expect(actual[EXPANDED]).toEqual(expected[EXPANDED]) } function stripIdSymbols (eson) {