Implemented actions for ContextMenu (not all fully working yet)
This commit is contained in:
parent
c3c836fa89
commit
667d3f32aa
|
@ -45,7 +45,7 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
renderJSONObject ({data, index, options, events}) {
|
||||
renderJSONObject ({data, path, index, options, events}) {
|
||||
const childCount = data.childs.length
|
||||
const contents = [
|
||||
h('div', {class: 'jsoneditor-node jsoneditor-object'}, [
|
||||
|
@ -60,6 +60,7 @@ export default class JSONNode extends Component {
|
|||
if (data.expanded) {
|
||||
const childs = data.childs.map(child => {
|
||||
return h(JSONNode, {
|
||||
path: path + '/' + child.prop,
|
||||
data: child,
|
||||
options,
|
||||
events
|
||||
|
@ -72,7 +73,7 @@ export default class JSONNode extends Component {
|
|||
return h('li', {}, contents)
|
||||
}
|
||||
|
||||
renderJSONArray ({data, index, options, events}) {
|
||||
renderJSONArray ({data, path, index, options, events}) {
|
||||
const childCount = data.childs.length
|
||||
const contents = [
|
||||
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
|
||||
|
@ -87,6 +88,7 @@ export default class JSONNode extends Component {
|
|||
if (data.expanded) {
|
||||
const childs = data.childs.map((child, index) => {
|
||||
return h(JSONNode, {
|
||||
path: path + '/' + index,
|
||||
data: child,
|
||||
index,
|
||||
options,
|
||||
|
@ -172,8 +174,10 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
|
||||
renderContextMenu ({anchor, root}) {
|
||||
const hasParent = this.props.data.path !== ''
|
||||
const path = this.props.path
|
||||
const hasParent = path !== ''
|
||||
const type = this.props.data.type
|
||||
const events = this.props.events
|
||||
const items = [] // array with menu items
|
||||
|
||||
items.push({
|
||||
|
@ -185,35 +189,25 @@ export default class JSONNode extends Component {
|
|||
text: 'Value',
|
||||
className: 'jsoneditor-type-value' + (type == 'value' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.value,
|
||||
click: function () {
|
||||
alert('value') // TODO
|
||||
}
|
||||
click: () => events.onChangeType(path, 'value')
|
||||
},
|
||||
{
|
||||
text: 'Array',
|
||||
className: 'jsoneditor-type-array' + (type == 'array' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.array,
|
||||
click: function () {
|
||||
alert('array') // TODO
|
||||
}
|
||||
click: () => events.onChangeType(path, 'array')
|
||||
},
|
||||
{
|
||||
text: 'Object',
|
||||
className: 'jsoneditor-type-object' + (type == 'object' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.object,
|
||||
click: function () {
|
||||
//node._onChangeType('object');
|
||||
alert('object') // TODO
|
||||
}
|
||||
click: () => events.onChangeType(path, 'object')
|
||||
},
|
||||
{
|
||||
text: 'String',
|
||||
className: 'jsoneditor-type-string' + (type == 'string' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.string,
|
||||
click: function () {
|
||||
// node._onChangeType('string');
|
||||
alert('string') // TODO
|
||||
}
|
||||
click: () => events.onChangeType(path, 'string')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -224,28 +218,19 @@ export default class JSONNode extends Component {
|
|||
text: 'Sort',
|
||||
title: 'Sort the childs of this ' + TYPE_TITLES.type,
|
||||
className: 'jsoneditor-sort-' + direction,
|
||||
click: function () {
|
||||
// node.sort(direction);
|
||||
alert('sort') // TODO
|
||||
},
|
||||
click: () => events.onSort(path),
|
||||
submenu: [
|
||||
{
|
||||
text: 'Ascending',
|
||||
className: 'jsoneditor-sort-asc',
|
||||
title: 'Sort the childs of this ' + TYPE_TITLES.type + ' in ascending order',
|
||||
click: function () {
|
||||
// node.sort('asc');
|
||||
alert('asc') // TODO
|
||||
}
|
||||
click: () => events.onSort(path, 'asc')
|
||||
},
|
||||
{
|
||||
text: 'Descending',
|
||||
className: 'jsoneditor-sort-desc',
|
||||
title: 'Sort the childs of this ' + TYPE_TITLES.type +' in descending order',
|
||||
click: function () {
|
||||
// node.sort('desc');
|
||||
alert('desc') // TODO
|
||||
}
|
||||
click: () => events.onSort(path, 'desc')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -265,46 +250,31 @@ export default class JSONNode extends Component {
|
|||
title: 'Insert a new item with type \'value\' after this item (Ctrl+Ins)',
|
||||
submenuTitle: 'Select the type of the item to be inserted',
|
||||
className: 'jsoneditor-insert',
|
||||
click: function () {
|
||||
// node._onInsertBefore('', '', 'value');
|
||||
alert('insert') // TODO
|
||||
},
|
||||
click: () => events.onInsert(path, '', ''),
|
||||
submenu: [
|
||||
{
|
||||
text: 'Value',
|
||||
className: 'jsoneditor-type-value',
|
||||
title: TYPE_TITLES.value,
|
||||
click: function () {
|
||||
// node._onInsertBefore('', '', 'value');
|
||||
alert('insert value') // TODO
|
||||
}
|
||||
click: () => events.onInsert(path, '', '')
|
||||
},
|
||||
{
|
||||
text: 'Array',
|
||||
className: 'jsoneditor-type-array',
|
||||
title: TYPE_TITLES.array,
|
||||
click: function () {
|
||||
// node._onInsertBefore('', []);
|
||||
alert('insert array') // TODO
|
||||
}
|
||||
click: () => events.onInsert(path, '', [])
|
||||
},
|
||||
{
|
||||
text: 'Object',
|
||||
className: 'jsoneditor-type-object',
|
||||
title: TYPE_TITLES.object,
|
||||
click: function () {
|
||||
// node._onInsertBefore('', {});
|
||||
alert('insert object') // TODO
|
||||
}
|
||||
click: () => events.onInsert(path, '', {})
|
||||
},
|
||||
{
|
||||
text: 'String',
|
||||
className: 'jsoneditor-type-string',
|
||||
title: TYPE_TITLES.string,
|
||||
click: function () {
|
||||
// node._onInsertBefore('', '', 'string');
|
||||
alert('insert string') // TODO
|
||||
}
|
||||
click: () => events.onInsert(path, '', '', 'string')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -315,10 +285,7 @@ export default class JSONNode extends Component {
|
|||
text: 'Duplicate',
|
||||
title: 'Duplicate this item (Ctrl+D)',
|
||||
className: 'jsoneditor-duplicate',
|
||||
click: function () {
|
||||
// Node.onDuplicate(node);
|
||||
alert('duplicate') // TODO
|
||||
}
|
||||
click: () => events.onDuplicate(path)
|
||||
});
|
||||
|
||||
// create remove button
|
||||
|
@ -326,10 +293,7 @@ export default class JSONNode extends Component {
|
|||
text: 'Remove',
|
||||
title: 'Remove this item (Ctrl+Del)',
|
||||
className: 'jsoneditor-remove',
|
||||
click: function () {
|
||||
//Node.onRemove(node);
|
||||
alert('remove') // TODO
|
||||
}
|
||||
click: () => events.onRemove(path)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -357,8 +321,8 @@ export default class JSONNode extends Component {
|
|||
const newProp = unescapeHTML(getInnerText(event.target))
|
||||
|
||||
// remove last entry from the path to get the path of the parent object
|
||||
const index = this.props.data.path.lastIndexOf('/')
|
||||
const path = this.props.data.path.substr(0, index)
|
||||
const index = this.props.path.lastIndexOf('/')
|
||||
const path = this.props.path.substr(0, index)
|
||||
|
||||
this.props.events.onChangeProperty(path, oldProp, newProp)
|
||||
}
|
||||
|
@ -366,7 +330,7 @@ export default class JSONNode extends Component {
|
|||
handleChangeValue (event) {
|
||||
const value = JSONNode._getValueFromEvent(event)
|
||||
|
||||
this.props.events.onChangeValue(this.props.data.path, value)
|
||||
this.props.events.onChangeValue(this.props.path, value)
|
||||
}
|
||||
|
||||
handleClickValue (event) {
|
||||
|
@ -382,7 +346,7 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
|
||||
handleExpand (event) {
|
||||
this.props.events.onExpand(this.props.data.path, !this.props.data.expanded)
|
||||
this.props.events.onExpand(this.props.path, !this.props.data.expanded)
|
||||
}
|
||||
|
||||
handleContextMenu (event) {
|
||||
|
@ -393,7 +357,7 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
else {
|
||||
this.props.events.showContextMenu({
|
||||
path: this.props.data.path,
|
||||
path: this.props.path,
|
||||
anchor: event.target,
|
||||
root: JSONNode._findRootElement(event)
|
||||
})
|
||||
|
|
211
src/Main.js
211
src/Main.js
|
@ -1,8 +1,10 @@
|
|||
import { h, Component } from 'preact'
|
||||
import * as pointer from 'json-pointer'
|
||||
|
||||
import { setIn, getIn } from './utils/objectUtils'
|
||||
import { setIn, updateIn, getIn, deleteIn, cloneDeep } from './utils/objectUtils'
|
||||
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
||||
import { isObject } from './utils/typeUtils'
|
||||
import bindMethods from './utils/bindMethods'
|
||||
import JSONNode from './JSONNode'
|
||||
|
||||
export default class Main extends Component {
|
||||
|
@ -10,11 +12,7 @@ export default class Main extends Component {
|
|||
super(props)
|
||||
|
||||
// TODO: create a function bindMethods(this)
|
||||
this.handleChangeProperty = this.handleChangeProperty.bind(this)
|
||||
this.handleChangeValue = this.handleChangeValue.bind(this)
|
||||
this.handleExpand = this.handleExpand.bind(this)
|
||||
this.handleShowContextMenu = this.handleShowContextMenu.bind(this)
|
||||
this.handleHideContextMenu = this.handleHideContextMenu.bind(this)
|
||||
bindMethods(this)
|
||||
|
||||
this.state = {
|
||||
options: Object.assign({
|
||||
|
@ -32,7 +30,14 @@ export default class Main extends Component {
|
|||
events: {
|
||||
onChangeProperty: this.handleChangeProperty,
|
||||
onChangeValue: this.handleChangeValue,
|
||||
onChangeType: this.handleChangeType,
|
||||
onInsert: this.handleInsert,
|
||||
onDuplicate: this.handleDuplicate,
|
||||
onRemove: this.handleRemove,
|
||||
onSort: this.handleSort,
|
||||
|
||||
onExpand: this.handleExpand,
|
||||
|
||||
showContextMenu: this.handleShowContextMenu,
|
||||
hideContextMenu: this.handleHideContextMenu
|
||||
},
|
||||
|
@ -47,7 +52,12 @@ export default class Main extends Component {
|
|||
render() {
|
||||
return h('div', {class: 'jsoneditor', onClick: this.handleHideContextMenu}, [
|
||||
h('ul', {class: 'jsoneditor-list'}, [
|
||||
h(JSONNode, this.state)
|
||||
h(JSONNode, {
|
||||
data: this.state.data,
|
||||
events: this.state.events,
|
||||
options: this.state.options,
|
||||
path: ''
|
||||
})
|
||||
])
|
||||
])
|
||||
}
|
||||
|
@ -55,36 +65,137 @@ export default class Main extends Component {
|
|||
handleChangeValue (path, value) {
|
||||
console.log('handleChangeValue', path, value)
|
||||
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path)).concat('value')
|
||||
|
||||
this.setState({
|
||||
data: setIn(this.state.data, modelPath, value)
|
||||
})
|
||||
this._setIn(path, ['value'], value)
|
||||
}
|
||||
|
||||
handleChangeProperty (path, oldProp, newProp) {
|
||||
console.log('handleChangeProperty', path, oldProp, newProp)
|
||||
|
||||
const modelPath = Main._pathToModelPath(this.state.data, pointer.parse(path))
|
||||
const parent = getIn(this.state.data, modelPath)
|
||||
const index = parent.childs.findIndex(child => child.prop === oldProp)
|
||||
const index = this._findIndex(path, oldProp)
|
||||
const newPath = path + '/' + pointer.escape(newProp)
|
||||
|
||||
let data = this.state.data
|
||||
data = setIn(data, modelPath.concat(['childs', index, 'path']), newPath)
|
||||
data = setIn(data, modelPath.concat(['childs', index, 'prop']), newProp)
|
||||
this._setIn(path, ['childs', index, 'path'], newPath)
|
||||
this._setIn(path, ['childs', index, 'prop'], newProp)
|
||||
}
|
||||
|
||||
this.setState({ data })
|
||||
handleChangeType (path, type) {
|
||||
console.log('handleChangeType', path, type)
|
||||
|
||||
this._setIn(path, ['type'], type)
|
||||
}
|
||||
|
||||
handleInsert (path, prop, value, type) {
|
||||
console.log('handleInsert', path, prop, value, type)
|
||||
|
||||
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
|
||||
|
||||
// TODO: this method is quite complicated. Can we simplify it?
|
||||
|
||||
const parsedPath = pointer.parse(path)
|
||||
const afterProp = parsedPath[parsedPath.length - 1]
|
||||
const parentPath = parsedPath.slice(0, parsedPath.length - 1)
|
||||
.map(entry => '/' + pointer.escape(entry)).join('')
|
||||
|
||||
const parent = this._getIn(parentPath)
|
||||
|
||||
const index = parent.type === 'array'
|
||||
? parseInt(afterProp)
|
||||
: this._findIndex(parentPath, afterProp)
|
||||
|
||||
this._updateIn(parentPath, ['childs'], function (childs) {
|
||||
const updated = childs.slice(0)
|
||||
const type = isObject(value) ? 'object' : Array.isArray(value) ? 'array' : (type || 'value')
|
||||
const newEntry = {
|
||||
expanded: true,
|
||||
type,
|
||||
prop,
|
||||
value,
|
||||
childs: []
|
||||
}
|
||||
|
||||
updated.splice(index + 1, 0, newEntry)
|
||||
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
handleDuplicate (path) {
|
||||
console.log('handleDuplicate', path)
|
||||
|
||||
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
|
||||
|
||||
// TODO: this method is quite complicated. Can we simplify it?
|
||||
|
||||
const parsedPath = pointer.parse(path)
|
||||
const prop = parsedPath[parsedPath.length - 1]
|
||||
const parentPath = parsedPath.slice(0, parsedPath.length - 1)
|
||||
.map(entry => '/' + pointer.escape(entry)).join('')
|
||||
|
||||
const parent = this._getIn(parentPath)
|
||||
|
||||
const index = parent.type === 'array'
|
||||
? parseInt(prop)
|
||||
: this._findIndex(parentPath, prop)
|
||||
|
||||
this._updateIn(parentPath, ['childs'], function (childs) {
|
||||
const updated = childs.slice(0)
|
||||
const original = childs[index]
|
||||
const duplicate = cloneDeep(original)
|
||||
|
||||
updated.splice(index + 1, 0, duplicate)
|
||||
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
handleRemove (path) {
|
||||
console.log('handleRemove', path)
|
||||
|
||||
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
|
||||
|
||||
this._deleteIn(path)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path
|
||||
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
|
||||
*/
|
||||
handleSort (path, order = null) {
|
||||
console.log('handleSort', path, order)
|
||||
|
||||
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
|
||||
|
||||
const comparators = {
|
||||
asc: compareAsc,
|
||||
desc: compareDesc
|
||||
}
|
||||
|
||||
let _order
|
||||
if (order === 'asc' || order === 'desc') {
|
||||
_order = order
|
||||
}
|
||||
else {
|
||||
// toggle previous order
|
||||
const current = this._getIn(path, ['order'])
|
||||
_order = current !== 'asc' ? 'asc' : 'desc'
|
||||
this._setIn(path, ['order'], _order)
|
||||
}
|
||||
|
||||
this._updateIn(path, ['childs'], function (childs) {
|
||||
const ordered = childs.slice(0)
|
||||
const compare = comparators[_order] || comparators['asc']
|
||||
|
||||
ordered.sort((a, b) => compare(a.value, b.value))
|
||||
|
||||
return ordered
|
||||
})
|
||||
}
|
||||
|
||||
handleExpand(path, expand) {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||
console.log('handleExpand', path, expand)
|
||||
|
||||
console.log('handleExpand', path, modelPath)
|
||||
|
||||
this.setState({
|
||||
data: setIn(this.state.data, modelPath.concat('expanded'), expand)
|
||||
})
|
||||
this._setIn(path, ['expanded'], expand)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,29 +208,62 @@ export default class Main extends Component {
|
|||
handleShowContextMenu({path, anchor, root}) {
|
||||
let data = this.state.data
|
||||
|
||||
// TODO: remove this cached this.state.contextMenuPath and do a brute-force sweep over the data instead?
|
||||
// hide previous context menu (if any)
|
||||
if (this.state.contextMenuPath !== null) {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(this.state.contextMenuPath))
|
||||
data = setIn(data, modelPath.concat('contextMenu'), false)
|
||||
this._setIn(this.state.contextMenuPath, ['contextMenu'], null)
|
||||
}
|
||||
|
||||
// show new menu
|
||||
if (typeof path === 'string') {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||
data = setIn(data, modelPath.concat('contextMenu'), {anchor, root})
|
||||
this._setIn(path, ['contextMenu'], {anchor, root})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
contextMenuPath: typeof path === 'string' ? path : null, // store path of current menu, just to easily find it next time
|
||||
data
|
||||
contextMenuPath: typeof path === 'string' ? path : null // store path of current menu, just to easily find it next time
|
||||
})
|
||||
}
|
||||
|
||||
handleHideContextMenu (event) {
|
||||
handleHideContextMenu () {
|
||||
// FIXME: find a different way to show/hide the context menu. create a single instance in the Main, pass a reference to it into the JSON nodes?
|
||||
this.handleShowContextMenu({})
|
||||
}
|
||||
|
||||
_getIn (path, modelProps = []) {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||
|
||||
return getIn(this.state.data, modelPath.concat(modelProps))
|
||||
}
|
||||
|
||||
_setIn (path, modelProps = [], value) {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||
|
||||
this.setState({
|
||||
data: setIn(this.state.data, modelPath.concat(modelProps), value)
|
||||
})
|
||||
}
|
||||
|
||||
_updateIn (path, modelProps = [], callback) {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||
|
||||
this.setState({
|
||||
data: updateIn(this.state.data, modelPath.concat(modelProps), callback)
|
||||
})
|
||||
}
|
||||
|
||||
_deleteIn (path, modelProps = []) {
|
||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||
|
||||
this.setState({
|
||||
data: deleteIn(this.state.data, modelPath.concat(modelProps))
|
||||
})
|
||||
}
|
||||
|
||||
_findIndex(path, prop) {
|
||||
const object = this._getIn(path)
|
||||
return object.childs.findIndex(child => child.prop === prop)
|
||||
}
|
||||
|
||||
// TODO: comment
|
||||
get () {
|
||||
return Main._modelToJson(this.state.data)
|
||||
|
@ -198,7 +342,6 @@ export default class Main extends Component {
|
|||
return {
|
||||
type: 'array',
|
||||
expanded: expand(path),
|
||||
path,
|
||||
prop,
|
||||
childs: value.map((child, index) => Main._jsonToModel(path + '/' + index, null, child, expand))
|
||||
}
|
||||
|
@ -207,7 +350,6 @@ export default class Main extends Component {
|
|||
return {
|
||||
type: 'object',
|
||||
expanded: expand(path),
|
||||
path,
|
||||
prop,
|
||||
childs: Object.keys(value).map(prop => {
|
||||
return Main._jsonToModel(path + '/' + pointer.escape(prop), prop, value[prop], expand)
|
||||
|
@ -217,7 +359,6 @@ export default class Main extends Component {
|
|||
else {
|
||||
return {
|
||||
type: 'value',
|
||||
path,
|
||||
prop,
|
||||
value
|
||||
}
|
||||
|
|
|
@ -35,9 +35,9 @@
|
|||
|
||||
// set json
|
||||
document.getElementById('setJSON').onclick = function () {
|
||||
// console.time('set')
|
||||
console.time('set')
|
||||
editor.set(largeJSON);
|
||||
// console.timeEnd('set')
|
||||
console.timeEnd('set')
|
||||
};
|
||||
|
||||
// get json
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* type: string,
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* path: string,
|
||||
* prop: string?,
|
||||
* value: *?,
|
||||
* childs: Model[]?
|
||||
|
|
|
@ -5,4 +5,32 @@
|
|||
*/
|
||||
export function last (array) {
|
||||
return array[array.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator to sort an array in ascending order
|
||||
*
|
||||
* Usage:
|
||||
* [4,2,5].sort(compareAsc) // [2,4,5]
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @return {number}
|
||||
*/
|
||||
export function compareAsc (a, b) {
|
||||
return a > b ? 1 : a < b ? -1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator to sort an array in ascending order
|
||||
*
|
||||
* Usage:
|
||||
* [4,2,5].sort(compareDesc) // [5,4,2]
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @return {number}
|
||||
*/
|
||||
export function compareDesc (a, b) {
|
||||
return a > b ? -1 : a < b ? 1 : 0
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Helper function to bind all methods of a class instance to the instance
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* import bindMethods from './bindMethods'
|
||||
*
|
||||
* class MyClass {
|
||||
* constructor () {
|
||||
* bindMethods(this)
|
||||
* }
|
||||
*
|
||||
* myMethod () {
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param {Object} instance Instance of an ES6 class or prototype
|
||||
*/
|
||||
export default function bindMethods (instance) {
|
||||
const prototype = Object.getPrototypeOf(instance)
|
||||
|
||||
Object.getOwnPropertyNames(prototype).forEach(name => {
|
||||
if (typeof instance[name] === 'function') {
|
||||
instance[name] = instance[name].bind(instance);
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,5 +1,13 @@
|
|||
import { isObject } from './typeUtils'
|
||||
// TODO: unit test getIn
|
||||
|
||||
|
||||
// inspiration:
|
||||
//
|
||||
// https://www.npmjs.com/package/seamless-immutable
|
||||
// https://www.npmjs.com/package/ih
|
||||
// https://www.npmjs.com/package/mutatis
|
||||
|
||||
// TODO: unit test clone
|
||||
|
||||
/**
|
||||
* Flat clone the properties of an object or array
|
||||
|
@ -25,6 +33,34 @@ export function clone (value) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: test cloneDeep
|
||||
|
||||
/**
|
||||
* Deep clone the properties of an object or array
|
||||
* @param {Object | Array} value
|
||||
* @return {Object | Array} Returns a deep clone of the object or Array
|
||||
*/
|
||||
export function cloneDeep (value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(cloneDeep)
|
||||
}
|
||||
else if (isObject(value)) {
|
||||
const cloned = {}
|
||||
|
||||
Object.keys(value).forEach(key => {
|
||||
cloned[key] = cloneDeep(value[key])
|
||||
})
|
||||
|
||||
return cloned
|
||||
}
|
||||
else {
|
||||
// a primitive value
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: unit test getIn
|
||||
|
||||
/**
|
||||
* helper function to get a nested property in an object or array
|
||||
*
|
||||
|
@ -67,15 +103,93 @@ export function setIn (object, path, value) {
|
|||
return value
|
||||
}
|
||||
|
||||
// TODO: change array into object and vice versa when key is a number/string
|
||||
|
||||
const key = path[0]
|
||||
const child = (Array.isArray(object[key]) || isObject(object[key]))
|
||||
? object[key]
|
||||
: (typeof path[1] === 'number' ? [] : {})
|
||||
const updated = clone(object)
|
||||
let updated
|
||||
if (typeof key === 'string' && !isObject(object)) {
|
||||
updated = {} // change into an object
|
||||
}
|
||||
else if (typeof key === 'number' && !Array.isArray(object)) {
|
||||
updated = [] // change into an array
|
||||
}
|
||||
else {
|
||||
updated = clone(object)
|
||||
}
|
||||
|
||||
updated[key] = setIn(child, path.slice(1), value)
|
||||
updated[key] = setIn(updated[key], path.slice(1), value)
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
// TODO: unit test updateIn
|
||||
|
||||
/**
|
||||
* 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 {function} callback
|
||||
* @return {Object | Array} Returns a new, updated object or array
|
||||
*/
|
||||
export function updateIn (object, path, callback) {
|
||||
if (path.length === 0) {
|
||||
return callback(object)
|
||||
}
|
||||
|
||||
const key = path[0]
|
||||
let updated
|
||||
if (typeof key === 'string' && !isObject(object)) {
|
||||
updated = {} // change into an object
|
||||
}
|
||||
else if (typeof key === 'number' && !Array.isArray(object)) {
|
||||
updated = [] // change into an array
|
||||
}
|
||||
else {
|
||||
updated = clone(object)
|
||||
}
|
||||
|
||||
updated[key] = updateIn(updated[key], path.slice(1), callback)
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
// TODO: unit test deleteIn
|
||||
|
||||
/**
|
||||
* helper function to delete a nested property in an object
|
||||
* without mutating the object itself.
|
||||
*
|
||||
* @param {Object | Array} object
|
||||
* @param {Array.<string | number>} path
|
||||
* @return {Object | Array} Returns a new, updated object or array
|
||||
*/
|
||||
export function deleteIn (object, path) {
|
||||
if (path.length === 0) {
|
||||
return object
|
||||
}
|
||||
|
||||
if (path.length === 1) {
|
||||
const key = path[0]
|
||||
const updated = clone(object)
|
||||
if (Array.isArray(updated)) {
|
||||
updated.splice(key, 1)
|
||||
}
|
||||
else {
|
||||
delete updated[key]
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
const key = path[0]
|
||||
const child = object[key]
|
||||
if (Array.isArray(child) || isObject(child)) {
|
||||
const updated = clone(object)
|
||||
updated[key] = deleteIn(child, path.slice(1))
|
||||
return updated
|
||||
}
|
||||
else {
|
||||
// child property doesn't exist. just do nothing
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue