Implemented expand/collapse methods and ctrl+expand/ctrl+collapse
This commit is contained in:
parent
d8a0079032
commit
c5a68b1da3
|
@ -217,7 +217,13 @@ export default class JSONNode extends Component {
|
|||
renderExpandButton () {
|
||||
const className = `jsoneditor-button jsoneditor-${this.props.data.expanded ? 'expanded' : 'collapsed'}`
|
||||
return h('div', {class: 'jsoneditor-button-container'},
|
||||
h('button', {class: className, onClick: this.handleExpand})
|
||||
h('button', {
|
||||
class: className,
|
||||
onClick: this.handleExpand,
|
||||
title:
|
||||
'Click to expand/collapse this field. \n' +
|
||||
'Ctrl+Click to expand/collapse including all childs.'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -480,7 +486,10 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
|
||||
handleExpand (event) {
|
||||
this.props.events.onExpand(this.getPath(), !this.props.data.expanded)
|
||||
const recurse = event.ctrlKey
|
||||
const expanded = !this.props.data.expanded
|
||||
|
||||
this.props.events.onExpand(this.getPath(), expanded, recurse)
|
||||
}
|
||||
|
||||
handleContextMenu (event) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { h, Component } from 'preact'
|
||||
|
||||
import { setIn } from './utils/immutabilityHelpers'
|
||||
import { setIn, updateIn } from './utils/immutabilityHelpers'
|
||||
import {
|
||||
changeValue, changeProperty, changeType,
|
||||
insert, append, duplicate, remove,
|
||||
sort,
|
||||
expand,
|
||||
jsonToData, dataToJson
|
||||
jsonToData, dataToJson, toDataPath
|
||||
} from './jsonData'
|
||||
import JSONNode from './JSONNode'
|
||||
|
||||
|
@ -106,10 +106,21 @@ export default class TreeMode extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleExpand = (path, doExpand) => {
|
||||
handleExpand = (path, expanded, recurse) => {
|
||||
if (recurse) {
|
||||
const dataPath = toDataPath(this.state.data, path)
|
||||
|
||||
this.setState({
|
||||
data: expand(this.state.data, path, doExpand)
|
||||
data: updateIn (this.state.data, dataPath, function (child) {
|
||||
return expand(child, (path) => true, expanded)
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
data: expand(this.state.data, path, expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,6 +144,26 @@ export default class TreeMode extends Component {
|
|||
return dataToJson(this.state.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand one or multiple objects or arrays
|
||||
* @param {Path | function (path: Path) : boolean} callback
|
||||
*/
|
||||
expand (callback) {
|
||||
this.setState({
|
||||
data: expand(this.state.data, callback, true)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse one or multiple objects or arrays
|
||||
* @param {Path | function (path: Path) : boolean} callback
|
||||
*/
|
||||
collapse (callback) {
|
||||
this.setState({
|
||||
data: expand(this.state.data, callback, false)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: implement expand
|
||||
// TODO: implement getText and setText
|
||||
|
||||
|
|
43
src/index.js
43
src/index.js
|
@ -36,11 +36,50 @@ function jsoneditor (container, options) {
|
|||
*/
|
||||
get: function () {
|
||||
return component.get()
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: implement getText
|
||||
// TODO: implement setText
|
||||
// TODO: implement expand
|
||||
|
||||
|
||||
/**
|
||||
* Expand one or multiple objects or arrays.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* // expand one item at a specific path
|
||||
* editor.expand(['foo', 1, 'bar'])
|
||||
*
|
||||
* // expand all items nested at a maximum depth of 2
|
||||
* editor.expand(function (path) {
|
||||
* return path.length <= 2
|
||||
* })
|
||||
*
|
||||
* @param {Path | function (path: Path) : boolean} callback
|
||||
*/
|
||||
expand (callback) {
|
||||
component.expand(callback)
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapse one or multiple objects or arrays
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* // collapse one item at a specific path
|
||||
* editor.collapse(['foo', 1, 'bar'])
|
||||
*
|
||||
* // collapse all items nested deeper than 2
|
||||
* editor.collapse(function (path) {
|
||||
* return path.length > 2
|
||||
* })
|
||||
*
|
||||
* @param {Path | function (path: Path) : boolean} callback
|
||||
*/
|
||||
collapse (callback) {
|
||||
component.collapse(callback)
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -246,19 +246,78 @@ export function sort (data, path, order = null) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Expand or collapse an item or property
|
||||
* Expand or collapse one or multiple items or properties
|
||||
* @param {JSONData} data
|
||||
* @param {Path} path
|
||||
* @param {boolean} expand
|
||||
* @param {function(path: Path) : boolean | Path} callback
|
||||
* When a path, the object/array at this path will be expanded/collapsed
|
||||
* When a function, all objects and arrays for which callback
|
||||
* returns true will be expanded/collapsed
|
||||
* @param {boolean} expanded New expanded state: true to expand, false to collapse
|
||||
* @return {JSONData}
|
||||
*/
|
||||
export function expand (data, path, expand) {
|
||||
console.log('expand', path, expand)
|
||||
export function expand (data, callback, expanded) {
|
||||
// console.log('expand', callback, expand)
|
||||
|
||||
const dataPath = toDataPath(data, path)
|
||||
|
||||
return setIn(data, dataPath.concat(['expanded']), expand)
|
||||
if (typeof callback === 'function') {
|
||||
return expandRecursive(data, [], callback, expanded)
|
||||
}
|
||||
else if (Array.isArray(callback)) {
|
||||
const dataPath = toDataPath(data, callback)
|
||||
|
||||
return setIn(data, dataPath.concat(['expanded']), expanded)
|
||||
}
|
||||
else {
|
||||
throw new Error('Callback function or path expected')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the json data, change the expanded state of all items/properties for
|
||||
* which `callback` returns true
|
||||
* @param {JSONData} data
|
||||
* @param {Path} path
|
||||
* @param {function(path: Path)} callback
|
||||
* All objects and arrays for which callback returns true will be
|
||||
* expanded/collapsed
|
||||
* @param {boolean} expanded New expanded state: true to expand, false to collapse
|
||||
* @return {*}
|
||||
*/
|
||||
export function expandRecursive (data, path, callback, expanded) {
|
||||
switch (data.type) {
|
||||
case 'array': {
|
||||
let updatedData = callback(path)
|
||||
? setIn(data, ['expanded'], expanded)
|
||||
: data
|
||||
let updatedItems = updatedData.items
|
||||
|
||||
updatedData.items.forEach((item, index) => {
|
||||
updatedItems = setIn(updatedItems, [index],
|
||||
expandRecursive(item, path.concat(index), callback, expanded))
|
||||
})
|
||||
|
||||
return setIn(updatedData, ['items'], updatedItems)
|
||||
}
|
||||
|
||||
case 'object': {
|
||||
let updatedData = callback(path)
|
||||
? setIn(data, ['expanded'], expanded)
|
||||
: data
|
||||
let updatedProps = updatedData.props
|
||||
|
||||
updatedData.props.forEach((prop, index) => {
|
||||
updatedProps = setIn(updatedProps, [index, 'value'],
|
||||
expandRecursive(prop.value, path.concat(prop.name), callback, expanded))
|
||||
})
|
||||
|
||||
return setIn(updatedData, ['props'], updatedProps)
|
||||
}
|
||||
|
||||
default: // type 'string' or 'value'
|
||||
// don't do anything: a value can't be expanded, only arrays and objects can
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a path of a JSON object into a path in the corresponding data model
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
|
||||
// TODO: rename type 'array' to 'Array' and 'object' to 'Object'
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'array',
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* props: Array.<{name: string, value: JSONData}>?
|
||||
* }} ObjectData
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'object',
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* items: JSONData[]?
|
||||
* }} ArrayData
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'value' | 'string',
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* value: *?
|
||||
* }} ValueData
|
||||
*
|
||||
|
@ -36,3 +32,77 @@
|
|||
* expand: function (path: Path)?
|
||||
* }} SetOptions
|
||||
*/
|
||||
|
||||
var ans = {
|
||||
"type": "object",
|
||||
"expanded": true,
|
||||
"props": [
|
||||
{
|
||||
"name": "obj",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"expanded": true,
|
||||
"props": [
|
||||
{
|
||||
"name": "arr",
|
||||
"value": {
|
||||
"type": "array",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "value",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"type": "value",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"expanded": true,
|
||||
"props": [
|
||||
{
|
||||
"name": "a",
|
||||
"value": {
|
||||
"type": "value",
|
||||
"value": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "b",
|
||||
"value": {
|
||||
"type": "value",
|
||||
"value": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "str",
|
||||
"value": {
|
||||
"type": "value",
|
||||
"value": "hello world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nill",
|
||||
"value": {
|
||||
"type": "value",
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bool",
|
||||
"value": {
|
||||
"type": "value",
|
||||
"value": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { isObject, clone } from './objectUtils'
|
|||
* helper function to get a nested property in an object or array
|
||||
*
|
||||
* @param {Object | Array} object
|
||||
* @param {Array.<string | number>} path
|
||||
* @param {Path} path
|
||||
* @return {* | undefined} Returns the field when found, or undefined when the
|
||||
* path doesn't exist
|
||||
*/
|
||||
|
@ -43,12 +43,10 @@ export function getIn (object, path) {
|
|||
* helper function to replace a nested property in an object with a new value
|
||||
* without mutating the object itself.
|
||||
*
|
||||
* Note: does not work with Arrays!
|
||||
*
|
||||
* @param {Object} object
|
||||
* @param {Array.<string>} path
|
||||
* @param {Object | Array} object
|
||||
* @param {Path} path
|
||||
* @param {*} value
|
||||
* @return {Object} Returns a new, updated object
|
||||
* @return {Object | Array} Returns a new, updated object or array
|
||||
*/
|
||||
export function setIn (object, path, value) {
|
||||
if (path.length === 0) {
|
||||
|
@ -67,16 +65,22 @@ export function setIn (object, path, value) {
|
|||
updated = clone(object)
|
||||
}
|
||||
|
||||
updated[key] = setIn(updated[key], path.slice(1), value)
|
||||
|
||||
const updatedValue = setIn(updated[key], path.slice(1), value)
|
||||
if (updated[key] === updatedValue) {
|
||||
// return original object unchanged when the new value is identical to the old one
|
||||
return object
|
||||
}
|
||||
else {
|
||||
updated[key] = updatedValue
|
||||
return updated
|
||||
}
|
||||
}
|
||||
/**
|
||||
* helper function to replace a nested property in an object with a new value
|
||||
* without mutating the object itself.
|
||||
*
|
||||
* @param {Object | Array} object
|
||||
* @param {Array.<string | number>} path
|
||||
* @param {Path} path
|
||||
* @param {function} callback
|
||||
* @return {Object | Array} Returns a new, updated object or array
|
||||
*/
|
||||
|
@ -97,17 +101,23 @@ export function updateIn (object, path, callback) {
|
|||
updated = clone(object)
|
||||
}
|
||||
|
||||
updated[key] = updateIn(updated[key], path.slice(1), callback)
|
||||
|
||||
const updatedValue = updateIn(object[key], path.slice(1), callback)
|
||||
if (updated[key] === updatedValue) {
|
||||
// return original object unchanged when the new value is identical to the old one
|
||||
return object
|
||||
}
|
||||
else {
|
||||
updated[key] = updatedValue
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* helper function to delete a nested property in an object
|
||||
* without mutating the object itself.
|
||||
*
|
||||
* @param {Object | Array} object
|
||||
* @param {Array.<string | number>} path
|
||||
* @param {Path} path
|
||||
* @return {Object | Array} Returns a new, updated object or array
|
||||
*/
|
||||
export function deleteIn (object, path) {
|
||||
|
|
|
@ -136,6 +136,22 @@ test('setIn change object into array', t => {
|
|||
t.deepEqual (updated, [, , 'foo'])
|
||||
})
|
||||
|
||||
test('setIn identical value should return the original object', t => {
|
||||
const obj = {a:1, b:2}
|
||||
|
||||
const updated = setIn(obj, ['b'], 2)
|
||||
|
||||
t.is(updated, obj) // strict equal
|
||||
})
|
||||
|
||||
test('setIn identical value should return the original object (2)', t => {
|
||||
const obj = {a:1, b: { c: 2}}
|
||||
|
||||
const updated = setIn(obj, ['b', 'c'], 2)
|
||||
|
||||
t.is(updated, obj) // strict equal
|
||||
})
|
||||
|
||||
test('updateIn', t => {
|
||||
const obj = {
|
||||
a: {
|
||||
|
@ -210,6 +226,16 @@ test('updateIn (3)', t => {
|
|||
})
|
||||
})
|
||||
|
||||
test('updateIn return identical value should return the original object', t => {
|
||||
const obj = {
|
||||
a: 2,
|
||||
b: 3
|
||||
}
|
||||
|
||||
const updated = updateIn(obj, ['b' ], (value) => 3)
|
||||
t.is(updated, obj)
|
||||
})
|
||||
|
||||
test('deleteIn', t => {
|
||||
const obj = {
|
||||
a: {
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
import test from 'ava';
|
||||
import { jsonToData, dataToJson, expand } from '../src/jsonData'
|
||||
|
||||
|
||||
// TODO: test all functions like append, insert, duplicate etc.
|
||||
|
||||
const JSON_EXAMPLE = {
|
||||
obj: {
|
||||
arr: [1,2, {a:3,b:4}]
|
||||
},
|
||||
str: 'hello world',
|
||||
nill: null,
|
||||
bool: false
|
||||
}
|
||||
|
||||
const JSON_DATA_EXAMPLE = {
|
||||
type: 'object',
|
||||
expanded: true,
|
||||
props: [
|
||||
{
|
||||
name: 'obj',
|
||||
value: {
|
||||
type: 'object',
|
||||
expanded: true,
|
||||
props: [
|
||||
{
|
||||
name: 'arr',
|
||||
value: {
|
||||
type: 'array',
|
||||
expanded: true,
|
||||
items: [
|
||||
{
|
||||
type: 'value',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
expanded: true,
|
||||
props: [
|
||||
{
|
||||
name: 'a',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'str',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 'hello world'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'nill',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const JSON_DATA_EXAMPLE_COLLAPSED_1 = {
|
||||
type: 'object',
|
||||
expanded: true,
|
||||
props: [
|
||||
{
|
||||
name: 'obj',
|
||||
value: {
|
||||
type: 'object',
|
||||
expanded: true,
|
||||
props: [
|
||||
{
|
||||
name: 'arr',
|
||||
value: {
|
||||
type: 'array',
|
||||
expanded: true,
|
||||
items: [
|
||||
{
|
||||
type: 'value',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
expanded: false,
|
||||
props: [
|
||||
{
|
||||
name: 'a',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'str',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 'hello world'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'nill',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const JSON_DATA_EXAMPLE_COLLAPSED_2 = {
|
||||
type: 'object',
|
||||
expanded: true,
|
||||
props: [
|
||||
{
|
||||
name: 'obj',
|
||||
value: {
|
||||
type: 'object',
|
||||
expanded: false,
|
||||
props: [
|
||||
{
|
||||
name: 'arr',
|
||||
value: {
|
||||
type: 'array',
|
||||
expanded: false,
|
||||
items: [
|
||||
{
|
||||
type: 'value',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
expanded: false,
|
||||
props: [
|
||||
{
|
||||
name: 'a',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'str',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: 'hello world'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'nill',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
value: {
|
||||
type: 'value',
|
||||
value: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('jsonToData', t => {
|
||||
function expand (path) {
|
||||
return true
|
||||
}
|
||||
|
||||
t.deepEqual(jsonToData([], JSON_EXAMPLE, expand), JSON_DATA_EXAMPLE)
|
||||
})
|
||||
|
||||
test('dataToJson', t => {
|
||||
t.deepEqual(dataToJson(JSON_DATA_EXAMPLE), JSON_EXAMPLE)
|
||||
})
|
||||
|
||||
test('expand a single path', t => {
|
||||
const collapsed = expand(JSON_DATA_EXAMPLE, ['obj', 'arr', 2], false)
|
||||
|
||||
t.deepEqual(collapsed, JSON_DATA_EXAMPLE_COLLAPSED_1)
|
||||
})
|
||||
|
||||
test('expand a callback', t => {
|
||||
function callback (path) {
|
||||
return path.length >= 1
|
||||
}
|
||||
const expanded = false
|
||||
const collapsed = expand(JSON_DATA_EXAMPLE, callback, expanded)
|
||||
|
||||
t.deepEqual(collapsed, JSON_DATA_EXAMPLE_COLLAPSED_2)
|
||||
})
|
||||
|
||||
test('expand a callback should not change the object when nothing happens', t => {
|
||||
function callback (path) {
|
||||
return false
|
||||
}
|
||||
const expanded = false
|
||||
const collapsed = expand(JSON_DATA_EXAMPLE, callback, expanded)
|
||||
|
||||
t.is(collapsed, JSON_DATA_EXAMPLE)
|
||||
})
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue