Fix many small issues in the UI

This commit is contained in:
jos 2017-12-16 14:13:52 +01:00
parent 7064578b31
commit e979e0f016
7 changed files with 126 additions and 110 deletions

View File

@ -27,7 +27,7 @@ export function changeValue (eson, path, value) {
path: compileJSONPointer(path),
value: value,
jsoneditor: {
type: oldDataValue.type
type: oldDataValue[META].type
}
}]
}
@ -102,14 +102,14 @@ export function duplicate (eson, selection) {
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
const paths = pathsFromSelection(eson, selection)
if (root.type === 'Array') {
if (root[META].type === 'Array') {
return paths.map((path, offset) => ({
op: 'copy',
from: compileJSONPointer(path),
path: compileJSONPointer(rootPath.concat(maxIndex + offset))
}))
}
else { // object.type === 'Object'
else { // root[META].type === 'Object'
const before = root[META].props[maxIndex] || null
return paths.map(path => {
@ -144,7 +144,7 @@ export function insertBefore (eson, path, values) { // TODO: find a better name
const parentPath = initial(path)
const parent = getIn(eson, parentPath)
if (parent.type === 'Array') {
if (parent[META].type === 'Array') {
const startIndex = parseInt(last(path))
return values.map((entry, offset) => ({
op: 'add',
@ -155,7 +155,7 @@ export function insertBefore (eson, path, values) { // TODO: find a better name
}
}))
}
else { // object.type === 'Object'
else { // parent[META].type === 'Object'
const before = last(path)
return values.map(entry => {
const newProp = findUniqueName(entry.name, parent[META].props)
@ -189,7 +189,7 @@ export function replace (eson, selection, values) { // TODO: find a better name
const root = getIn(eson, rootPath)
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Array') {
if (root[META].type === 'Array') {
const removeActions = removeAll(pathsFromSelection(eson, selection))
const insertActions = values.map((entry, offset) => ({
op: 'add',
@ -202,7 +202,7 @@ export function replace (eson, selection, values) { // TODO: find a better name
return removeActions.concat(insertActions)
}
else { // object.type === 'Object'
else { // root[META].type === 'Object'
const before = root[META].props[maxIndex] || null
const removeActions = removeAll(pathsFromSelection(eson, selection))
@ -241,7 +241,7 @@ export function append (eson, parentPath, type) {
const parent = getIn(eson, parentPath)
const value = createEntry(type)
if (parent.type === 'Array') {
if (parent[META].type === 'Array') {
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat('-')),
@ -251,7 +251,7 @@ export function append (eson, parentPath, type) {
}
}]
}
else { // object.type === 'Object'
else { // parent[META].type === 'Object'
const newProp = findUniqueName('', parent[META].props)
return [{
@ -306,11 +306,11 @@ export function sort (eson, path, order = null) {
const compare = order === 'desc' ? compareDesc : compareAsc
const object = getIn(eson, path)
if (object.type === 'Array') {
const orderedItems = object.slice()
if (object[META].type === 'Array') {
const orderedItems = cloneWithSymbols(object)
// order the items by value
orderedItems.sort((a, b) => compare(a.value, b.value))
orderedItems.sort((a, b) => compare(a[META].value, b[META].value))
// when no order is provided, test whether ordering ascending
// changed anything. If not, sort descending
@ -318,16 +318,18 @@ export function sort (eson, path, order = null) {
orderedItems.reverse()
}
// TODO: refactor into a set of move actions, so we keep eson state of the items
return [{
op: 'replace',
path: compileJSONPointer(path),
value: orderedItems
value: esonToJson(orderedItems)
}]
}
else { // object.type === 'Object'
else { // object[META].type === 'Object'
// order the properties by key
const orderedProps = object[META].props.slice().sort((a, b) => compare(a.name, b.name))
const orderedProps = object[META].props.slice().sort(compare)
// when no order is provided, test whether ordering ascending
// changed anything. If not, sort descending
@ -338,12 +340,14 @@ export function sort (eson, path, order = null) {
const orderedObject = cloneWithSymbols(object)
orderedObject[META] = setIn(object[META], ['props'], orderedProps)
// TODO: refactor into a set of move actions, so we keep eson state of the items
return [{
op: 'replace',
path: compileJSONPointer(path),
value: esonToJson(orderedObject),
jsoneditor: {
order: orderedProps.map(prop => prop.name)
order: orderedProps // TODO: order isn't used right now in patchEson.
}
}]
}

View File

@ -254,10 +254,11 @@ export default class JSONNode extends PureComponent {
}
// TODO: simplify the method renderProperty
renderProperty (prop?: ESONObjectProperty, index?: number, eson: ESON, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) {
renderProperty (prop?: String, index?: number, eson: ESON, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) {
const isIndex = typeof index === 'number'
const isProp = typeof prop === 'string'
if (!prop && !isIndex) {
if (!isProp && !isIndex) {
// root node
const rootName = JSONNode.getRootName(eson, options)
@ -604,7 +605,7 @@ export default class JSONNode extends PureComponent {
/** @private */
handleChangeProperty = (event) => {
const parentPath = initial(this.props.eson[META].path)
const oldProp = this.props.prop.name
const oldProp = this.props.prop
const newProp = unescapeHTML(getInnerText(event.target))
if (newProp !== oldProp) {

View File

@ -111,7 +111,7 @@ export default class TreeMode extends Component {
findKeyBinding: this.handleFindKeyBinding
},
search: {
searchResult: {
text: '',
matches: null,
active: null // active search result
@ -287,13 +287,13 @@ export default class TreeMode extends Component {
])
}
if (this.props.search !== false) {
if (this.props.searchResult !== false) {
// option search is true or undefined
items = items.concat([
h('div', {key: 'search', className: 'jsoneditor-menu-panel-right'},
h(Search, {
text: this.state.search.text,
resultCount: this.state.search.matches ? this.state.search.matches.length : 0,
text: this.state.searchResult.text,
resultCount: this.state.searchResult.matches ? this.state.searchResult.matches.length : 0,
onChange: this.handleSearch,
onNext: this.handleNext,
onPrevious: this.handlePrevious,
@ -584,20 +584,20 @@ export default class TreeMode extends Component {
handleSearch = (text) => {
// FIXME: also apply search when eson is changed
const { eson, matches, active } = search(this.state.eson, text)
if (matches.length > 0) {
const { eson, searchResult } = search(this.state.eson, text)
if (searchResult.matches.length > 0) {
this.setState({
search: { text, active, matches },
eson: expandPath(eson, initial(active.path))
eson: expandPath(eson, initial(searchResult.active.path)),
searchResult
})
// scroll to active search result (on next tick, after this path has been expanded)
setTimeout(() => this.scrollTo(active.path))
setTimeout(() => this.scrollTo(searchResult.active.path))
}
else {
this.setState({
search: { text, active, matches },
eson
eson,
searchResult
})
}
}
@ -610,22 +610,22 @@ export default class TreeMode extends Component {
handleNext = (event) => {
event.preventDefault()
if (this.state.search) {
const { eson, active } = nextSearchResult(this.state.eson, this.state.search.matches, this.state.search.active)
if (this.state.searchResult) {
const { eson, searchResult } = nextSearchResult(this.state.eson, this.state.searchResult)
this.setState({
search: setIn(this.state.search, ['active'], active),
eson
eson,
searchResult
})
// scroll to the active result (on next tick, after this path has been expanded)
// TODO: this code is duplicate with handlePrevious, move into a separate function
setTimeout(() => {
if (active && active.path) {
this.scrollTo(active.path)
if (searchResult.active && searchResult.active.path) {
this.scrollTo(searchResult.active.path)
if (!searchHasFocus()) {
setSelection(this.refs.contents, active.path, active.area)
setSelection(this.refs.contents, searchResult.active.path, searchResult.active.area)
}
}
})
@ -635,21 +635,21 @@ export default class TreeMode extends Component {
handlePrevious = (event) => {
event.preventDefault()
if (this.state.search) {
const { eson, active } = previousSearchResult(this.state.eson, this.state.search.matches, this.state.search.active)
if (this.state.searchResult) {
const { eson, searchResult } = previousSearchResult(this.state.eson, this.state.searchResult)
this.setState({
search: setIn(this.state.search, ['active'], active),
eson
eson,
searchResult
})
// scroll to the active result (on next tick, after this path has been expanded)
setTimeout(() => {
if (active && active.path) {
this.scrollTo(active.path)
if (searchResult.active && searchResult.active.path) {
this.scrollTo(searchResult.active.path)
if (!searchHasFocus()) {
setSelection(this.refs.contents, active.path, active.area)
setSelection(this.refs.contents, searchResult.active.path, searchResult.active.area)
}
}
})
@ -800,6 +800,7 @@ export default class TreeMode extends Component {
const result = patchEson(this.state.eson, historyItem.undo)
// FIXME: apply search
this.setState({
eson: result.data,
history,
@ -818,6 +819,7 @@ export default class TreeMode extends Component {
const result = patchEson(this.state.eson, historyItem.redo)
// FIXME: apply search
this.setState({
eson: result.data,
history,
@ -859,6 +861,7 @@ export default class TreeMode extends Component {
.concat(this.state.history.slice(this.state.historyIndex))
.slice(0, MAX_HISTORY_ITEMS)
// FIXME: apply search
this.setState({
eson,
history,
@ -867,6 +870,7 @@ export default class TreeMode extends Component {
}
else {
// update data and don't store history
// FIXME: apply search
this.setState({ eson })
}
@ -887,6 +891,7 @@ export default class TreeMode extends Component {
// TODO: document option expand
const expandCallback = this.props.expand || TreeMode.expandRoot
// FIXME: apply search
this.setState({
json: json,
eson: expand(jsonToEson(json), expandCallback),
@ -902,7 +907,8 @@ export default class TreeMode extends Component {
* @returns {Object | Array | string | number | boolean | null} json
*/
get () {
return this.state.json
// FIXME: keep a copy of the json file up to date with the internal eson
return esonToJson(this.state.eson)
}
/**

View File

@ -15,13 +15,11 @@ import initial from 'lodash/initial'
import last from 'lodash/last'
import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONPath,
ESON, ESONPointer, Selection,
Path,
JSONPath, JSONType
} from './types'
type RecurseCallback = (value: ESON, path: Path, root: ESON) => ESON
export const SELECTED = 1
export const SELECTED_END = 2
export const SELECTED_BEFORE = 3
@ -245,7 +243,7 @@ export function cleanupMetaData(eson, field, ignorePaths = []) {
* Search some text in all properties and values
* @param {ESON} eson
* @param {String} text Search text
* @return {{eson: ESON, matches: ESONPointer[], active: ESONPointer}} Returns search result:
* @return {SearchResult} Returns search result:
* An updated eson object containing the search results,
* and an array with the paths of all matches
*/
@ -281,7 +279,14 @@ export function search (eson, text) {
return updatedValue
})
return { eson: updatedEson, matches, active: matches[0] || null }
return {
eson: updatedEson,
searchResult: {
text,
matches,
active: matches[0] || null
}
}
}
/**
@ -290,26 +295,24 @@ export function search (eson, text) {
* and return the last result as next.
*
* @param {ESON} eson
* @param {ESONPointer[]} matches
* @param {ESONPointer} active
* @return {{eson: ESON, matches: ESONPointer[], active: ESONPointer}}
* @param {SearchResult} searchResult
* @return {{eson: ESON, searchResult: SearchResult}}
*/
export function previousSearchResult (eson, matches, active) {
if (matches.length === 0) {
return { eson, matches, active }
export function previousSearchResult (eson, searchResult) {
if (searchResult.matches.length === 0) {
return { eson, searchResult }
}
const index = matches.findIndex(searchResult => isEqual(searchResult, active))
const index = searchResult.matches.findIndex(match => isEqual(match, searchResult.active))
const previous = (index !== -1)
? index > 0
? matches[index - 1]
: last(matches)
: matches[0]
? searchResult.matches[index - 1]
: last(searchResult.matches)
: searchResult.matches[0]
return {
eson: setSearchStatus(setSearchStatus(eson, active, 'normal'), previous, 'active'),
matches,
active: previous
eson: setSearchStatus(setSearchStatus(eson, searchResult.active, 'normal'), previous, 'active'),
searchResult: Object.assign({}, searchResult, { active: previous})
}
}
@ -319,26 +322,24 @@ export function previousSearchResult (eson, matches, active) {
* and return the first result as next.
*
* @param {ESON} eson
* @param {ESONPointer[]} matches
* @param {ESONPointer} active
* @return {{eson: ESON, matches: ESONPointer[], active: ESONPointer}}
* @param {SearchResult} searchResult
* @return {{eson: ESON, searchResult: SearchResult}}
*/
export function nextSearchResult (eson, matches, active) {
if (isEmpty(matches)) {
return { eson, matches, active }
export function nextSearchResult (eson, searchResult) {
if (isEmpty(searchResult.matches)) {
return { eson, searchResult }
}
const index = matches.findIndex(match => isEqual(match, active))
const index = searchResult.matches.findIndex(match => isEqual(match, searchResult.active))
const next = (index !== -1)
? index < matches.length - 1
? matches[index + 1]
: matches[0]
: matches[0]
? index < searchResult.matches.length - 1
? searchResult.matches[index + 1]
: searchResult.matches[0]
: searchResult.matches[0]
return {
eson: setSearchStatus(setSearchStatus(eson, active, 'normal'), next, 'active'),
matches,
active: next
eson: setSearchStatus(setSearchStatus(eson, searchResult.active, 'normal'), next, 'active'),
searchResult: Object.assign({}, searchResult, { active: next})
}
}
@ -463,7 +464,7 @@ export function pathsFromSelection (eson, selection: Selection): JSONPath[] {
if (root[META].type === 'Object') {
return times(maxIndex - minIndex, i => rootPath.concat(root[META].props[i + minIndex]))
}
else { // root.type === 'Array'
else { // root[META].type === 'Array'
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
}
}
@ -477,7 +478,7 @@ export function pathsFromSelection (eson, selection: Selection): JSONPath[] {
export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
return paths.map(path => {
return {
name: getIn(data, last(path)),
name: last(path),
value: esonToJson(getIn(data, path))
// FIXME: also store the type and expanded state
}
@ -544,7 +545,7 @@ export function pathExists (eson, path) {
// index of an array
return pathExists(eson[parseInt(path[0])], path.slice(1))
}
else { // eson.type === 'Object'
else { // Object
// object property. find the index of this property
return pathExists(eson[path[0]], path.slice(1))
}

View File

@ -177,7 +177,7 @@ export function remove (data, path) {
}]
}
}
else { // object.type === 'Object'
else { // parent[META].type === 'Object'
const prop = last(pathArray)
const index = parent[META].props.indexOf(prop)
const nextProp = parent[META].props[index + 1] || null
@ -221,7 +221,7 @@ export function add (data, path, value, options) {
if (parent[META].type === 'Array') {
updatedEson = updatePaths(insertAt(data, resolvedPath, value))
}
else { // parent.type === 'Object'
else { // parent[META].type === 'Object'
updatedEson = updateIn(data, parentPath, (parent) => {
const oldValue = getIn(data, pathArray)
const props = parent[META].props

View File

@ -22,6 +22,8 @@
*
* @typedef {string[]} Path
*
* @typedef {{matches: ESONPointer[], active: ESONPointer, text: String}} SearchResult
*
*/
// FIXME: redefine all ESON related types

View File

@ -1,3 +1,5 @@
'use strict'
import { readFileSync } from 'fs'
import test from 'ava'
import { setIn, getIn, deleteIn } from '../src/utils/immutabilityHelpers'
@ -243,10 +245,10 @@ test('search', t => {
"nill": null,
"bool": false
})
const searchResult = search(eson, 'L')
const esonWithSearch = searchResult.eson
const matches = searchResult.matches
const active = searchResult.active
const result = search(eson, 'L')
const esonWithSearch = result.eson
const matches = result.searchResult.matches
const active = result.searchResult.active
t.deepEqual(matches, [
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
@ -278,33 +280,33 @@ test('nextSearchResult', t => {
"nill": null,
"bool": false
})
const searchResult = search(eson, 'A')
const first = search(eson, 'A')
t.deepEqual(searchResult.matches, [
t.deepEqual(first.searchResult.matches, [
{path: ['obj', 'arr'], area: 'property'},
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
{path: ['bool'], area: 'value'}
])
t.deepEqual(searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(searchResult.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(searchResult.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(searchResult.eson, ['bool', META, 'searchValue']), 'normal')
t.deepEqual(first.searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(first.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(first.eson, ['bool', META, 'searchValue']), 'normal')
const second = nextSearchResult(searchResult.eson, searchResult.matches, searchResult.active)
t.deepEqual(second.active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
const second = nextSearchResult(first.eson, first.searchResult)
t.deepEqual(second.searchResult.active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
t.is(getIn(second.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'active')
t.is(getIn(second.eson, ['bool', META, 'searchValue']), 'normal')
const third = nextSearchResult(second.eson, second.matches, second.active)
t.deepEqual(third.active, {path: ['bool'], area: 'value'})
const third = nextSearchResult(second.eson, second.searchResult)
t.deepEqual(third.searchResult.active, {path: ['bool'], area: 'value'})
t.is(getIn(third.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['bool', META, 'searchValue']), 'active')
const wrappedAround = nextSearchResult(third.eson, third.matches, third.active)
t.deepEqual(wrappedAround.active, {path: ['obj', 'arr'], area: 'property'})
const wrappedAround = nextSearchResult(third.eson, third.searchResult)
t.deepEqual(wrappedAround.searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(wrappedAround.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(wrappedAround.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(wrappedAround.eson, ['bool', META, 'searchValue']), 'normal')
@ -319,33 +321,33 @@ test('previousSearchResult', t => {
"nill": null,
"bool": false
})
const searchResult = search(eson, 'A')
const init = search(eson, 'A')
t.deepEqual(searchResult.matches, [
t.deepEqual(init.searchResult.matches, [
{path: ['obj', 'arr'], area: 'property'},
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
{path: ['bool'], area: 'value'}
])
t.deepEqual(searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(searchResult.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(searchResult.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(searchResult.eson, ['bool', META, 'searchValue']), 'normal')
t.deepEqual(init.searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(init.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(init.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(init.eson, ['bool', META, 'searchValue']), 'normal')
const third = previousSearchResult(searchResult.eson, searchResult.matches, searchResult.active)
t.deepEqual(third.active, {path: ['bool'], area: 'value'})
const third = previousSearchResult(init.eson, init.searchResult)
t.deepEqual(third.searchResult.active, {path: ['bool'], area: 'value'})
t.is(getIn(third.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['bool', META, 'searchValue']), 'active')
const second = previousSearchResult(third.eson, third.matches, third.active)
t.deepEqual(second.active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
const second = previousSearchResult(third.eson, third.searchResult)
t.deepEqual(second.searchResult.active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
t.is(getIn(second.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'active')
t.is(getIn(second.eson, ['bool', META, 'searchValue']), 'normal')
const first = previousSearchResult(second.eson, second.matches, second.active)
t.deepEqual(first.active, {path: ['obj', 'arr'], area: 'property'})
const first = previousSearchResult(second.eson, second.searchResult)
t.deepEqual(first.searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(first.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(first.eson, ['bool', META, 'searchValue']), 'normal')