New ESON model (WIP)

This commit is contained in:
jos 2017-11-29 21:52:18 +01:00
parent a9174edf16
commit c19334894c
11 changed files with 414 additions and 227 deletions

69
package-lock.json generated
View File

@ -2130,6 +2130,15 @@
"array-find-index": "1.0.2"
}
},
"d": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"dev": true,
"requires": {
"es5-ext": "0.10.37"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -2200,6 +2209,17 @@
"integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=",
"dev": true
},
"deep-map": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/deep-map/-/deep-map-1.5.0.tgz",
"integrity": "sha1-6qWVy4F4PKKADyakLgnxbn1PuJA=",
"dev": true,
"requires": {
"es6-weak-map": "2.0.2",
"lodash": "4.17.4",
"tslib": "1.8.0"
}
},
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@ -2547,6 +2567,49 @@
"is-arrayish": "0.2.1"
}
},
"es5-ext": {
"version": "0.10.37",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz",
"integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=",
"dev": true,
"requires": {
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
"d": "1.0.0",
"es5-ext": "0.10.37",
"es6-symbol": "3.1.1"
}
},
"es6-symbol": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
"dev": true,
"requires": {
"d": "1.0.0",
"es5-ext": "0.10.37"
}
},
"es6-weak-map": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz",
"integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
"dev": true,
"requires": {
"d": "1.0.0",
"es5-ext": "0.10.37",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -7372,6 +7435,12 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true
},
"tslib": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.0.tgz",
"integrity": "sha512-ymKWWZJST0/CkgduC2qkzjMOWr4bouhuURNXCn/inEX0L57BnRG6FhX76o7FOnsjHazCjfU2LKeSrlS2sIKQJg==",
"dev": true
},
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",

View File

@ -42,6 +42,7 @@
"babel-preset-stage-3": "6.17.0",
"browser-sync": "2.18.6",
"css-loader": "0.26.1",
"deep-map": "1.5.0",
"flow-bin": "0.37.4",
"graceful-fs": "4.1.11",
"gulp": "3.9.1",

View File

@ -8,7 +8,7 @@ import FloatingMenu from './menu/FloatingMenu'
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils'
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import { compileJSONPointer, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
import { compileJSONPointer, mapEsonArray, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
@ -36,12 +36,6 @@ export default class JSONNode extends PureComponent {
hover: false
}
constructor (props) {
super(props)
this.path = this.getPath(props)
}
componentWillMount (props) {
}
@ -51,26 +45,20 @@ export default class JSONNode extends PureComponent {
}
}
componentWillReceiveProps (nextProps) {
this.path = this.getPath(nextProps)
}
render () {
const { props } = this
if (props.data.type === 'Array') {
return this.renderJSONArray(props)
if (this.props.eson._meta.type === 'Object') {
return this.renderJSONObject(this.props)
}
else if (props.data.type === 'Object') {
return this.renderJSONObject(props)
else if (this.props.eson._meta.type === 'Array') {
return this.renderJSONArray(this.props)
}
else {
return this.renderJSONValue(props)
else { // no Object or Array
return this.renderJSONValue(this.props)
}
}
renderJSONObject ({prop, index, data, options, events}) {
const childCount = data.props.length
renderJSONObject ({prop, index, eson, options, events}) {
const keys = eson._meta.keys
const node = h('div', {
key: 'node',
onKeyDown: this.handleKeyDown,
@ -79,20 +67,20 @@ export default class JSONNode extends PureComponent {
this.renderExpandButton(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options),
this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`),
this.renderProperty(prop, index, eson, options),
this.renderReadonly(`{${keys.length}}`, `Array containing ${keys.length} items`),
// this.renderFloatingMenuButton(),
this.renderError(data.error)
this.renderError(eson._meta.error) // FIXME: render error
])
let childs
if (data.expanded) {
if (data.props.length > 0) {
const props = data.props.map(prop => h(this.constructor, {
key: prop.id,
parent: this,
prop,
data: prop.value,
if (eson._meta.expanded) {
if (keys.length > 0) {
const props = keys.map(key => h(this.constructor, {
key: eson[key]._meta.id,
// parent: this,
prop: key,
eson: eson[key],
options,
events
}))
@ -106,7 +94,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = (data.selected === SELECTED_END)
const floatingMenu = (eson._meta.selected === SELECTED_END)
? this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
@ -120,16 +108,14 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea()
return h('div', {
'data-path': compileJSONPointer(this.path),
className: this.getContainerClassName(data.selected, this.state.hover),
'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(eson._meta.selected, this.state.hover),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea, childs])
}
// TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?)
renderJSONArray ({prop, index, data, options, events}) {
const childCount = data.items.length
renderJSONArray ({prop, index, eson, options, events}) {
const node = h('div', {
key: 'node',
onKeyDown: this.handleKeyDown,
@ -138,20 +124,20 @@ export default class JSONNode extends PureComponent {
this.renderExpandButton(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options),
this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`),
this.renderProperty(prop, index, eson, options),
this.renderReadonly(`[${eson._meta.length}]`, `Array containing ${eson._meta.length} items`),
// this.renderFloatingMenuButton(),
this.renderError(data.error)
this.renderError(eson._meta.error)
])
let childs
if (data.expanded) {
if (data.items.length > 0) {
const items = data.items.map((item, index) => h(this.constructor, {
key : item.id,
parent: this,
if (eson._meta.expanded) {
if (eson._meta.length > 0) {
const items = mapEsonArray(eson, (item, index) => h(this.constructor, {
key : item._meta.id,
// parent: this,
index,
data: item.value,
eson: item,
options,
events
}))
@ -165,7 +151,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = (data.selected === SELECTED_END)
const floatingMenu = (eson._meta.selected === SELECTED_END)
? this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
@ -179,14 +165,14 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea()
return h('div', {
'data-path': compileJSONPointer(this.path),
className: this.getContainerClassName(data.selected, this.state.hover),
'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(eson._meta.selected, this.state.hover),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea, childs])
}
renderJSONValue ({prop, index, data, options}) {
renderJSONValue ({prop, index, eson, options}) {
const node = h('div', {
key: 'node',
onKeyDown: this.handleKeyDown,
@ -195,14 +181,14 @@ export default class JSONNode extends PureComponent {
this.renderPlaceholder(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options),
this.renderProperty(prop, index, eson, options),
this.renderSeparator(),
this.renderValue(data.value, data.searchResult, options),
this.renderValue(eson._meta.value, eson._meta.searchResult, options),
// this.renderFloatingMenuButton(),
this.renderError(data.error)
this.renderError(eson._meta.error)
])
const floatingMenu = (data.selected === SELECTED_END)
const floatingMenu = (eson._meta.selected === SELECTED_END)
? this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
{type: 'duplicate'},
@ -216,15 +202,15 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea()
return h('div', {
'data-path': compileJSONPointer(this.path),
className: this.getContainerClassName(data.selected, this.state.hover),
'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(eson._meta.selected, this.state.hover),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea])
}
renderInsertBeforeArea () {
const floatingMenu = (this.props.data.selected === SELECTED_BEFORE)
const floatingMenu = (this.props.eson._meta.selected === SELECTED_BEFORE)
? this.renderFloatingMenu([
{type: 'insertStructure'},
{type: 'insertValue'},
@ -248,7 +234,7 @@ export default class JSONNode extends PureComponent {
*/
renderAppend (text) {
return h('div', {
'data-path': compileJSONPointer(this.path) + '/-',
'data-path': compileJSONPointer(this.props.eson._meta.path) + '/-',
className: 'jsoneditor-node',
onKeyDown: this.handleKeyDownAppend
}, [
@ -268,12 +254,12 @@ export default class JSONNode extends PureComponent {
}
// TODO: simplify the method renderProperty
renderProperty (prop?: ESONObjectProperty, index?: number, data: ESON, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) {
renderProperty (prop?: ESONObjectProperty, index?: number, eson: ESON, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) {
const isIndex = typeof index === 'number'
if (!prop && !isIndex) {
// root node
const rootName = JSONNode.getRootName(data, options)
const rootName = JSONNode.getRootName(eson, options)
return h('div', {
key: 'property',
@ -283,11 +269,11 @@ export default class JSONNode extends PureComponent {
}, rootName)
}
const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.path))
const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.props.eson._meta.path))
const emptyClassName = (prop && prop.name.length === 0) ? ' jsoneditor-empty' : ''
const searchClassName = prop ? JSONNode.getSearchResultClass(prop.searchResult) : ''
const escapedPropName = prop ? escapeHTML(prop.name, options.escapeUnicode) : null
const emptyClassName = (prop != null && prop.length === 0) ? ' jsoneditor-empty' : ''
const searchClassName = prop != null ? JSONNode.getSearchResultClass(prop.searchResult) : ''
const escapedPropName = prop != null ? escapeHTML(prop, options.escapeUnicode) : null
if (editable) {
return h('div', {
@ -317,7 +303,7 @@ export default class JSONNode extends PureComponent {
const itsAnUrl = isUrl(value)
const isEmpty = escapedValue.length === 0
const editable = !options.isValueEditable || options.isValueEditable(this.path)
const editable = !options.isValueEditable || options.isValueEditable(this.props.eson._meta.path)
if (editable) {
return h('div', {
key: 'value',
@ -409,7 +395,7 @@ export default class JSONNode extends PureComponent {
}
target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) +
JSONNode.getSearchResultClass(this.props.data.searchResult)
JSONNode.getSearchResultClass(this.props.eson._meta.searchResult)
target.title = itsAnUrl ? JSONNode.URL_TITLE : ''
// remove all classNames from childs (needed for IE and Edge)
@ -462,7 +448,7 @@ export default class JSONNode extends PureComponent {
}
renderExpandButton () {
const className = `jsoneditor-button jsoneditor-${this.props.data.expanded ? 'expanded' : 'collapsed'}`
const className = `jsoneditor-button jsoneditor-${this.props.eson._meta.expanded ? 'expanded' : 'collapsed'}`
return h('div', {key: 'expand', className: 'jsoneditor-button-container'},
h('button', {
@ -483,9 +469,9 @@ export default class JSONNode extends PureComponent {
return h(ActionMenu, {
key: 'menu',
path: this.path,
path: this.props.eson._meta.path,
events: this.props.events,
type: this.props.data.type,
type: this.props.eson._meta.type, // TODO: fix type
menuType,
open: true,
@ -527,7 +513,7 @@ export default class JSONNode extends PureComponent {
renderFloatingMenu (items) {
return h(FloatingMenu, {
key: 'floating-menu',
path: this.path,
path: this.props.eson._meta.path,
events: this.props.events,
items
})
@ -609,17 +595,15 @@ export default class JSONNode extends PureComponent {
this.setState({ appendMenu: null })
}
static getRootName (data, options) {
static getRootName (eson, options) {
return typeof options.name === 'string'
? options.name
: (data.type === 'Object' || data.type === 'Array')
? data.type
: valueType(data.value)
: valueType(eson)
}
/** @private */
handleChangeProperty = (event) => {
const parentPath = initial(this.path)
const parentPath = initial(this.props.eson._meta.path)
const oldProp = this.props.prop.name
const newProp = unescapeHTML(getInnerText(event.target))
@ -632,8 +616,8 @@ export default class JSONNode extends PureComponent {
handleChangeValue = (event) => {
const value = this.getValueFromEvent(event)
if (value !== this.props.data.value) {
this.props.events.onChangeValue(this.path, value)
if (value !== this.props.eson._meta.value) {
this.props.events.onChangeValue(this.props.eson._meta.path, value)
}
}
@ -650,24 +634,24 @@ export default class JSONNode extends PureComponent {
if (keyBinding === 'duplicate') {
event.preventDefault()
this.props.events.onDuplicate(this.path)
this.props.events.onDuplicate(this.props.eson._meta.path)
}
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onInsert(this.path, 'value')
this.props.events.onInsert(this.props.eson._meta.path, 'value')
}
if (keyBinding === 'remove') {
event.preventDefault()
this.props.events.onRemove(this.path)
this.props.events.onRemove(this.props.eson._meta.path)
}
if (keyBinding === 'expand') {
event.preventDefault()
const recurse = false
const expanded = !this.props.data.expanded
this.props.events.onExpand(this.path, expanded, recurse)
const expanded = !this.props.eson._meta.expanded
this.props.events.onExpand(this.props.eson._meta.path, expanded, recurse)
}
if (keyBinding === 'actionMenu') {
@ -682,7 +666,7 @@ export default class JSONNode extends PureComponent {
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onAppend(this.path, 'value')
this.props.events.onAppend(this.props.eson._meta.path, 'value')
}
if (keyBinding === 'actionMenu') {
@ -703,9 +687,10 @@ export default class JSONNode extends PureComponent {
/** @private */
handleExpand = (event) => {
const recurse = event.ctrlKey
const expanded = !this.props.data.expanded
const path = this.props.eson._meta.path
const expanded = !this.props.eson._meta.expanded
this.props.events.onExpand(this.path, expanded, recurse)
this.props.events.onExpand(path, expanded, recurse)
}
/**
@ -724,17 +709,6 @@ export default class JSONNode extends PureComponent {
}
}
// FIXME: this construction with passing parents to determine the path is not very nice. Move determining of the path to the ESON model. We cannot generate the path whilst rendering, that defeats the efficiency of PureComponent
getPath (props = this.props) {
const parentPath = props.parent ? props.parent.path : []
return props.prop
? parentPath.concat(props.prop.name)
: typeof props.index !== 'undefined'
? parentPath.concat(props.index)
: parentPath
}
/**
* Get the value of the target of an event, and convert it to it's type
* @param event
@ -743,7 +717,7 @@ export default class JSONNode extends PureComponent {
*/
getValueFromEvent (event) {
const stringValue = unescapeHTML(getInnerText(event.target))
return this.props.data.type === 'string'
return this.props.eson._meta.type === 'string'
? stringValue
: stringConvert(stringValue)
}

View File

@ -8,11 +8,11 @@ import Hammer from 'react-hammerjs'
import jump from '../assets/jump.js/src/jump'
import Ajv from 'ajv'
import { setIn } from '../utils/immutabilityHelpers'
import { setIn, updateIn } from '../utils/immutabilityHelpers'
import { parseJSON } from '../utils/jsonUtils'
import { enrichSchemaError } from '../utils/schemaUtils'
import {
jsonToEson, esonToJson, getInEson, updateInEson, pathExists,
toEson2, jsonToEson, esonToJson, getInEson, updateInEson, pathExists,
expand, expandPath, addErrors,
search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection, pathsFromSelection, contentsFromPaths,
@ -20,7 +20,7 @@ import {
} from '../eson'
import { patchEson } from '../patchEson'
import {
duplicate, insert, insertBefore, append, remove, removeAll, replace,
duplicate, insertBefore, append, remove, removeAll, replace,
createEntry, changeType, changeValue, changeProperty, sort
} from '../actions'
import JSONNode from './JSONNode'
@ -56,7 +56,9 @@ export default class TreeMode extends Component {
constructor (props) {
super(props)
const data = jsonToEson(this.props.data || {}, TreeMode.expandAll, [])
const json = this.props.json || {}
const expandCallback = this.props.expand || TreeMode.expandRoot
const eson = expand(toEson2(json), expandCallback)
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
@ -79,9 +81,10 @@ export default class TreeMode extends Component {
}
this.state = {
data,
json,
eson,
history: [data],
history: [eson],
historyIndex: 0,
events: {
@ -148,11 +151,17 @@ export default class TreeMode extends Component {
// Apply json
if (nextProps.json !== currentProps.json) {
this.patch([{
op: 'replace',
path: '',
value: nextProps.json
}])
// FIXME: merge _meta from existing eson
this.setState({
json: nextProps.json,
eson: toEson2(nextProps.json) // FIXME: how to handle expand?
})
// TODO: cleanup
// this.patch([{
// op: 'replace',
// path: '',
// value: nextProps.json
// }])
}
// Apply JSON Schema
@ -182,21 +191,23 @@ export default class TreeMode extends Component {
: JSONNode
// enrich the data with JSON Schema errors
let data = state.data
const errors = this.getErrors()
if (errors.length) {
data = addErrors(data, this.getErrors())
}
let eson = state.eson
// TODO: reimplement errors
// const errors = this.getErrors()
// if (errors.length) {
// data = addErrors(data, this.getErrors())
// }
// enrich the data with search results
// TODO: performance improvements in search would be nice though it's acceptable right now
const searchResults = this.state.search.text ? search(data, this.state.search.text) : null
if (searchResults) {
data = applySearchResults(data, searchResults, this.state.search.active)
}
if (this.state.selection) {
data = applySelection(data, this.state.selection)
}
// TODO: reimplement search and selection
const searchResults = []
// const searchResults = this.state.search.text ? search(data, this.state.search.text) : null
// if (searchResults) {
// data = applySearchResults(data, searchResults, this.state.search.active)
// }
// if (this.state.selection) {
// data = applySelection(data, this.state.selection)
// }
return h('div', {
className: `jsoneditor jsoneditor-mode-${props.mode}`,
@ -220,12 +231,11 @@ export default class TreeMode extends Component {
onMouseDown: this.handleTouchStart,
onTouchStart: this.handleTouchStart,
className: 'jsoneditor-list jsoneditor-root' +
(data.selected ? ' jsoneditor-selected' : '')},
(eson._meta.selected ? ' jsoneditor-selected' : '')},
h(Node, {
data,
eson,
events: state.events,
options: props,
path: [],
prop: null
})
)
@ -313,7 +323,7 @@ export default class TreeMode extends Component {
*/
getErrors () {
if (this.state.compiledSchema) {
const valid = this.state.compiledSchema(esonToJson(this.state.data))
const valid = this.state.compiledSchema(this.state.json)
if (!valid) {
return this.state.compiledSchema.errors.map(enrichSchemaError)
}
@ -547,14 +557,14 @@ export default class TreeMode extends Component {
handleExpand = (path, expanded, recurse) => {
if (recurse) {
this.setState({
data: updateInEson(this.state.data, path, function (child) {
eson: updateIn(this.state.eson, path, function (child) {
return expand(child, (path) => true, expanded)
})
})
}
else {
this.setState({
data: expand(this.state.data, path, expanded)
eson: expand(this.state.eson, path, expanded)
})
}
}
@ -885,10 +895,11 @@ export default class TreeMode extends Component {
set (json) {
// FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called
// TODO: document option expand
const expand = this.props.expand || TreeMode.expandRoot
const expandCallback = this.props.expand || TreeMode.expandRoot
this.setState({
data: jsonToEson(json, expand, []),
json: json,
eson: expand(toEson2(json), expandCallback), // FIXME: expand eson
// TODO: do we want to keep history when .set(json) is called? (currently we remove history)
history: [],
@ -901,7 +912,7 @@ export default class TreeMode extends Component {
* @returns {Object | Array | string | number | boolean | null} json
*/
get () {
return esonToJson(this.state.data)
return this.state.json
}
/**

View File

@ -5,7 +5,7 @@
* All functions are pure and don't mutate the ESON.
*/
import { setIn, getIn, updateIn, deleteIn } from './utils/immutabilityHelpers'
import { setIn, getIn, updateIn, deleteIn, transform } from './utils/immutabilityHelpers'
import { isObject } from './utils/typeUtils'
import isEqual from 'lodash/isEqual'
import times from 'lodash/times'
@ -25,6 +25,51 @@ export const SELECTED_END = 2
export const SELECTED_BEFORE = 3
export const SELECTED_AFTER = 4
/**
*
* @param {JSONType} json
* @param {JSONPath} path
* @return {ESON}
*/
// TODO: rename to jsonToEson after refactoring of ESON is finished
export function toEson2 (json, path = []) {
const id = createId()
if (isObject(json)) {
let eson = {}
const keys = Object.keys(json)
keys.forEach((key) => eson[key] = toEson2(json[key], path.concat(key)))
eson._meta = { id, path, type: 'Object', keys }
return eson
}
else if (Array.isArray(json)) {
let eson = {}
json.forEach((value, index) => eson[index] = toEson2(value, path.concat(index)))
eson._meta = { id, path, type: 'Array', length: json.length }
return eson
}
else { // json is a number, string, boolean, or null
return {
_meta: { id, path, type: 'value', value: json }
}
}
}
/**
* Map over an eson array
* @param {ESONArray} esonArray
* @param {function (value, index, array)} callback
* @return {Array}
*/
export function mapEsonArray (esonArray, callback) {
const length = esonArray._meta.length
let result = []
for (let i = 0; i < length; i++) {
result[i] = callback(esonArray[i], i, esonArray)
}
return result
}
/**
* Expand function which will expand all nodes
* @param {Path} path
@ -49,7 +94,7 @@ export function jsonToEson (json, expand = expandAll, path: JSONPath = [], type:
expanded: expand(path),
items: json.map((child, index) => {
return {
id: getId(), // TODO: use id based on index (only has to be unique within this array)
id: createId(), // TODO: use id based on index (only has to be unique within this array)
value: jsonToEson(child, expand, path.concat(index))
}
})
@ -61,7 +106,7 @@ export function jsonToEson (json, expand = expandAll, path: JSONPath = [], type:
expanded: expand(path),
props: Object.keys(json).map((name, index) => {
return {
id: getId(), // TODO: use id based on index (only has to be unique within this array)
id: createId(), // TODO: use id based on index (only has to be unique within this array)
name,
value: jsonToEson(json[name], expand, path.concat(name))
}
@ -207,31 +252,25 @@ export function deleteInEson (eson: ESON, jsonPath: JSONPath) : JSONType {
/**
* Expand or collapse one or multiple items or properties
* @param {ESON} eson
* @param {function(path: Path) : boolean | Path} callback
* @param {function(Path) : boolean | Path} filterCallback
* 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=true] New expanded state: true to expand, false to collapse
* @return {ESON}
*/
export function expand (eson: ESON, callback: Path | (Path) => boolean, expanded: boolean = true) {
export function expand (eson, filterCallback, expanded = true) {
// console.log('expand', callback, expand)
if (typeof callback === 'function') {
return transform(eson, function (value: ESON, path: Path, root: ESON) : ESON {
if (value.type === 'Array' || value.type === 'Object') {
if (callback(path)) {
return setIn(value, ['expanded'], expanded)
}
}
return value
if (typeof filterCallback === 'function') {
return transform(eson, function (value, path) {
return (value && value._meta && (value._meta.type === 'Array' || value._meta.type === 'Object') && filterCallback(path))
? setIn(value, ['_meta', 'expanded'], expanded)
: value
})
}
else if (Array.isArray(callback)) {
const esonPath: Path = toEsonPath(eson, callback)
return setIn(eson, esonPath.concat(['expanded']), expanded)
else if (Array.isArray(filterCallback)) {
return setIn(eson, filterCallback.concat(['_meta', 'expanded']), expanded)
}
else {
throw new Error('Callback function or path expected')
@ -511,54 +550,54 @@ function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath {
return path1.slice(0, i)
}
/**
* Recursively transform ESON: a recursive "map" function
* @param {ESON} eson
* @param {function(value: ESON, path: Path, root: ESON)} callback
* @return {ESON} Returns the transformed eson object
*/
export function transform (eson: ESON, callback: RecurseCallback) : ESON {
return recurseTransform (eson, [], eson, callback)
}
/**
* Recursively transform ESON
* @param {ESON} value
* @param {JSONPath} path
* @param {ESON} root The root object, object at path=[]
* @param {function(value: ESON, path: Path, root: ESON)} callback
* @return {ESON} Returns the transformed eson object
*/
function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON {
let updatedValue: ESON = callback(value, path, root)
if (value.type === 'Array') {
let updatedItems = updatedValue.items
updatedValue.items.forEach((item, index) => {
const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback)
updatedItems = setIn(updatedItems, [index, 'value'], updatedItem)
})
updatedValue = setIn(updatedValue, ['items'], updatedItems)
}
if (value.type === 'Object') {
let updatedProps = updatedValue.props
updatedValue.props.forEach((prop, index) => {
const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback)
updatedProps = setIn(updatedProps, [index, 'value'], updatedItem)
})
updatedValue = setIn(updatedValue, ['props'], updatedProps)
}
// (for type 'string' or 'value' there are no childs to traverse)
return updatedValue
}
//
// /**
// * Recursively transform ESON: a recursive "map" function
// * @param {ESON} eson
// * @param {function(value: ESON, path: Path, root: ESON)} callback
// * @return {ESON} Returns the transformed eson object
// */
// export function transform (eson: ESON, callback: RecurseCallback) : ESON {
// return recurseTransform (eson, [], eson, callback)
// }
//
// /**
// * Recursively transform ESON
// * @param {ESON} value
// * @param {JSONPath} path
// * @param {ESON} root The root object, object at path=[]
// * @param {function(value: ESON, path: Path, root: ESON)} callback
// * @return {ESON} Returns the transformed eson object
// */
// function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON {
// let updatedValue: ESON = callback(value, path, root)
//
// if (value.type === 'Array') {
// let updatedItems = updatedValue.items
//
// updatedValue.items.forEach((item, index) => {
// const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback)
// updatedItems = setIn(updatedItems, [index, 'value'], updatedItem)
// })
//
// updatedValue = setIn(updatedValue, ['items'], updatedItems)
// }
//
// if (value.type === 'Object') {
// let updatedProps = updatedValue.props
//
// updatedValue.props.forEach((prop, index) => {
// const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback)
// updatedProps = setIn(updatedProps, [index, 'value'], updatedItem)
// })
//
// updatedValue = setIn(updatedValue, ['props'], updatedProps)
// }
//
// // (for type 'string' or 'value' there are no childs to traverse)
//
// return updatedValue
// }
/**
* Recursively loop over a ESON object: a recursive "forEach" function.
@ -709,7 +748,7 @@ export function compileJSONPointer (path: Path) {
.join('')
}
// TODO: move getId and createUniqueId to a separate file
// TODO: move createId to a separate file
/**
* Do a case insensitive search for a search text in a text
@ -725,19 +764,8 @@ export function containsCaseInsensitive (text: string, search: string): boolean
* Get a new "unique" id. Id's are created from an incremental counter.
* @return {number}
*/
// TODO: use createUniqueId instead of getId()
export function getId () : number {
export function createId () : number {
_id++
return _id
}
let _id = 0
/**
* Find a unique id from an array with properties each having an id field.
* The
* @param {{id: string}} array
*/
// TODO: use createUniqueId instead of getId()
function createUniqueId (array) {
return Math.max(...array.map(item => item.id)) + 1
}

View File

@ -8,7 +8,7 @@ import {
jsonToEson, esonToJson, toEsonPath,
getInEson, setInEson, deleteInEson,
parseJSONPointer, compileJSONPointer,
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, createId
} from './eson'
/**
@ -197,7 +197,7 @@ export function remove (data: ESON, path: string) {
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function add (data: ESON, path: string, value: ESON, options, id = getId()) {
export function add (data: ESON, path: string, value: ESON, options, id = createId()) {
const pathArray = parseJSONPointer(path)
const parentPath = pathArray.slice(0, pathArray.length - 1)
const esonPath = toEsonPath(data, parentPath)

View File

@ -20,8 +20,12 @@
* ace: Object?
* }} Options
*
* @typedef {string[]} Path
*
*/
// FIXME: redefine all ESON related types
/**************************** GENERIC JSON TYPES ******************************/
@ -48,25 +52,33 @@ export type ESONArrayItem = {
}
export type ESONObject = {
_meta: {
type: 'Object',
path: JSONPath,
expanded?: boolean,
selected?: boolean,
props: ESONObjectProperty[]
}
}
export type ESONArray = {
_meta: {
type: 'Array',
path: JSONPath,
expanded?: boolean,
selected?: boolean,
items: ESONArrayItem[]
length: number
}
}
export type ESONValue = {
_meta: {
type: 'value' | 'string',
value?: any,
path: JSONPath,
value: null | boolean | string | number,
selected?: boolean,
searchResult?: SearchResultStatus
}
}
export type ESON = ESONObject | ESONArray | ESONValue

View File

@ -1,7 +1,7 @@
'use strict';
import clone from 'lodash/clone'
import { isObjectOrArray } from './typeUtils'
import { isObjectOrArray, isObject } from './typeUtils'
/**
* Immutability helpers
@ -11,6 +11,7 @@ import { isObjectOrArray } from './typeUtils'
* https://www.npmjs.com/package/seamless-immutable
* https://www.npmjs.com/package/ih
* https://www.npmjs.com/package/mutatis
* https://github.com/mariocasciaro/object-path-immutable
*/
@ -70,6 +71,35 @@ export function setIn (object, path, value) {
return updatedObject
}
}
export function transform (object, callback, path = []) {
const updated = callback(object, path)
if (Array.isArray(updated)) {
let changed = false
let updatedItems = []
for (let i = 0; i < updated.length; i++) {
updatedItems[i] = transform(updated[i], callback, path.concat(i))
changed = changed || updatedItems[i] !== updated[i]
}
return changed ? updatedItems : updated
}
else if (isObject(updated)) {
let changed = false
let updatedProps = {}
for (let key in updated) {
if (updated.hasOwnProperty(key)) {
updatedProps[key] = transform(updated[key], callback, path.concat(key))
changed = changed || updatedProps[key] !== updated[key]
}
}
return changed ? updatedProps : updated
}
else { // updated is a value
return updated
}
}
/**
* helper function to replace a nested property in an object with a new value
* without mutating the object itself.

View File

@ -10,7 +10,21 @@
export function isObject (value) {
return typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
!Array.isArray(value) &&
(!value._meta || typeof value._meta.value === 'undefined')
}
/**
* Test whether a value is not an object or array, but null, number, string, or
* boolean.
* @param {*} value
* @return {boolean}
*/
export function isValue (value) {
return (value === null ||
typeof value === 'number' ||
typeof value === 'string' ||
typeof value === 'boolean')
}
/**

View File

@ -4,10 +4,12 @@ import { setIn, getIn } from '../src/utils/immutabilityHelpers'
import {
jsonToEson, esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer,
toEson2,
expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection, pathsFromSelection,
SELECTED, SELECTED_END
} from '../src/eson'
import deepMap from "deep-map/lib/index"
const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json')
@ -35,6 +37,25 @@ test('toJsonPath', t => {
t.deepEqual(toJsonPath(ESON1, esonPath), jsonPath)
})
test('toEson2', t => {
t.deepEqual(replaceIds2(toEson2(1)), {_meta: {id: '[ID]', path: [], type: 'value', value: 1}})
t.deepEqual(replaceIds2(toEson2("foo")), {_meta: {id: '[ID]', path: [], type: 'value', value: "foo"}})
t.deepEqual(replaceIds2(toEson2(null)), {_meta: {id: '[ID]', path: [], type: 'value', value: null}})
t.deepEqual(replaceIds2(toEson2(false)), {_meta: {id: '[ID]', path: [], type: 'value', value: false}})
t.deepEqual(replaceIds2(toEson2({a:1, b: 2})), {
_meta: {id: '[ID]', path: [], type: 'Object', keys: ['a', 'b']},
a: {_meta: {id: '[ID]', path: ['a'], type: 'value', value: 1}},
b: {_meta: {id: '[ID]', path: ['b'], type: 'value', value: 2}}
})
printJSON(replaceIds2(toEson2([1,2])))
t.deepEqual(replaceIds2(toEson2([1,2])), {
_meta: {id: '[ID]', path: [], type: 'Array', length: 2},
0: {_meta: {id: '[ID]', path: [0], type: 'value', value: 1}},
1: {_meta: {id: '[ID]', path: [1], type: 'value', value: 2}}
})
})
test('jsonToEson', t => {
function expand (path) {
return true
@ -396,6 +417,11 @@ function replaceIds (data, value = '[ID]') {
}
}
// helper function to replace all id properties with a constant value
function replaceIds2 (data, key = 'id', value = '[ID]') {
return deepMap(data, (v, k) => k === key ? value : v)
}
// helper function to print JSON in the console
function printJSON (json, message = null) {
if (message) {

View File

@ -1,5 +1,5 @@
import test from 'ava';
import { getIn, setIn, updateIn, deleteIn, insertAt } from '../src/utils/immutabilityHelpers'
import { getIn, setIn, updateIn, deleteIn, insertAt, transform } from '../src/utils/immutabilityHelpers'
test('getIn', t => {
@ -276,3 +276,25 @@ test('insertAt', t => {
const updated = insertAt(obj, ['a', '2'], 8)
t.deepEqual(updated, {a: [1,2,8,3]})
})
test('transform (no change)', t => {
const obj = { a: [1,2,3]}
const updated = transform(obj, (value, path) => value)
t.deepEqual(updated, obj)
t.is(updated, obj)
})
test('transform (change based on value)', t => {
const obj = { a: [1,2,3]}
const updated = transform(obj, (value, path) => value === 2 ? 20 : value)
t.deepEqual(updated, { a: [1,20,3]})
})
test('transform (change based on path)', t => {
const obj = { a: [1,2,3]}
const updated = transform(obj, (value, path) => path.join('.') === 'a.1' ? 20 : value)
t.deepEqual(updated, { a: [1,20,3]})
})