Replace/insert working. Insert before instead of after

This commit is contained in:
jos 2017-11-22 10:09:53 +01:00
parent 2e75e5a9cf
commit bac12dcc5a
7 changed files with 137 additions and 128 deletions

View File

@ -92,17 +92,18 @@ export function changeType (data, path, type) {
* and object property * and object property
* *
* @param {ESON} data * @param {ESON} data
* @param {ESONSelection} selection * @param {Selection} selection
* @return {Array} * @return {Array}
*/ */
export function duplicate (data, selection) { export function duplicate (data, selection) {
// console.log('duplicate', path) // console.log('duplicate', path)
if (!selection.start || !selection.end) {
return []
}
const rootPath = findRootPath(selection) const rootPath = findRootPath(selection)
const start = selection.start.path[rootPath.length]
const end = selection.end.path[rootPath.length]
const root = getIn(data, toEsonPath(data, rootPath)) const root = getIn(data, toEsonPath(data, rootPath))
const { maxIndex } = findSelectionIndices(root, start, end) const { maxIndex } = findSelectionIndices(root, rootPath, selection)
const paths = pathsFromSelection(data, selection) const paths = pathsFromSelection(data, selection)
if (root.type === 'Array') { if (root.type === 'Array') {
@ -113,7 +114,6 @@ export function duplicate (data, selection) {
})) }))
} }
else { // object.type === 'Object' else { // object.type === 'Object'
const { maxIndex } = findSelectionIndices(root, start, end)
const nextProp = root.props && root.props[maxIndex] const nextProp = root.props && root.props[maxIndex]
const before = nextProp ? nextProp.name : null const before = nextProp ? nextProp.name : null
@ -133,53 +133,6 @@ export function duplicate (data, selection) {
} }
} }
/**
* Create a JSONPatch for an insert action.
*
* This function needs the current data in order to be able to determine
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {ESON} data
* @param {Path} path
* @param {ESONType} type
* @return {Array}
*/
export function insert (data, path, type) {
// console.log('insert', path, type)
const parentPath = path.slice(0, path.length - 1)
const esonPath = toEsonPath(data, parentPath)
const parent = getIn(data, esonPath)
const value = createEntry(type)
if (parent.type === 'Array') {
const index = parseInt(path[path.length - 1]) + 1
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat(index)),
value,
jsoneditor: {
type
}
}]
}
else { // object.type === 'Object'
const prop = path[path.length - 1]
const newProp = findUniqueName('', parent.props.map(p => p.name))
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value,
jsoneditor: {
type,
before: findNextProp(parent, prop)
}
}]
}
}
/** /**
* Create a JSONPatch for an insert action. * Create a JSONPatch for an insert action.
* *
@ -233,17 +186,14 @@ export function insertBefore (data, path, values) { // TODO: find a better name
* and object property * and object property
* *
* @param {ESON} data * @param {ESON} data
* @param {ESONSelection} selection * @param {Selection} selection
* @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values * @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
* @return {Array} * @return {Array}
*/ */
export function replace (data, selection, values) { // TODO: find a better name and define datastructure for values export function replace (data, selection, values) { // TODO: find a better name and define datastructure for values
const rootPath = findRootPath(selection) const rootPath = findRootPath(selection)
const start = selection.start.path[rootPath.length]
const end = selection.end.path[rootPath.length]
const root = getIn(data, toEsonPath(data, rootPath)) const root = getIn(data, toEsonPath(data, rootPath))
const { minIndex, maxIndex } = findSelectionIndices(root, start, end) const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Array') { if (root.type === 'Array') {
const removeActions = removeAll(pathsFromSelection(data, selection)) const removeActions = removeAll(pathsFromSelection(data, selection))

View File

@ -16,15 +16,15 @@ import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../type
const SELECTED_CLASS_NAMES = { const SELECTED_CLASS_NAMES = {
[SELECTED]: ' jsoneditor-selected', [SELECTED]: ' jsoneditor-selected',
[SELECTED_END]: ' jsoneditor-selected jsoneditor-selected-end', [SELECTED_END]: ' jsoneditor-selected jsoneditor-selected-end',
[SELECTED_AFTER]: ' jsoneditor-selected jsoneditor-selected-after', [SELECTED_AFTER]: ' jsoneditor-selected jsoneditor-selected-insert-area',
[SELECTED_BEFORE]: ' jsoneditor-selected jsoneditor-selected-before', [SELECTED_BEFORE]: ' jsoneditor-selected jsoneditor-selected-insert-area',
} }
const HOVERED_CLASS_NAMES = { const HOVERED_CLASS_NAMES = {
[SELECTED]: ' jsoneditor-hover', [SELECTED]: ' jsoneditor-hover',
[SELECTED_END]: ' jsoneditor-hover jsoneditor-hover-end', [SELECTED_END]: ' jsoneditor-hover jsoneditor-hover-end',
[SELECTED_AFTER]: ' jsoneditor-hover jsoneditor-hover-after', [SELECTED_AFTER]: ' jsoneditor-hover jsoneditor-hover-insert-area',
[SELECTED_BEFORE]: ' jsoneditor-hover jsoneditor-hover-before', [SELECTED_BEFORE]: ' jsoneditor-hover jsoneditor-hover-insert-area',
} }
export default class JSONNode extends PureComponent { export default class JSONNode extends PureComponent {
@ -196,7 +196,7 @@ export default class JSONNode extends PureComponent {
]) ])
: null : null
const insertArea = this.renderInsertArea() const insertArea = this.renderInsertBeforeArea()
return h('div', { return h('div', {
'data-path': compileJSONPointer(this.props.path), 'data-path': compileJSONPointer(this.props.path),
@ -206,8 +206,8 @@ export default class JSONNode extends PureComponent {
}, [node, floatingMenu, insertArea]) }, [node, floatingMenu, insertArea])
} }
renderInsertArea () { renderInsertBeforeArea () {
const floatingMenu = (this.props.data.selected === SELECTED_AFTER) const floatingMenu = (this.props.data.selected === SELECTED_BEFORE)
? this.renderFloatingMenu([ ? this.renderFloatingMenu([
{type: 'insertStructure'}, {type: 'insertStructure'},
{type: 'insertValue'}, {type: 'insertValue'},
@ -220,7 +220,7 @@ export default class JSONNode extends PureComponent {
return h('div', { return h('div', {
key: 'menu', key: 'menu',
className: 'jsoneditor-insert-area', className: 'jsoneditor-insert-area',
'data-area': 'after' 'data-area': 'before'
}, [floatingMenu]) }, [floatingMenu])
} }

View File

@ -37,7 +37,7 @@ import {
import { createFindKeyBinding } from '../utils/keyBindings' import { createFindKeyBinding } from '../utils/keyBindings'
import { KEY_BINDINGS } from '../constants' import { KEY_BINDINGS } from '../constants'
import type { ESON, ESONPatch, JSONPath, ESONSelection, ESONPointer } from '../types' import type { ESON, ESONPatch, JSONPath, Selection, ESONPointer } from '../types'
const AJV_OPTIONS = { const AJV_OPTIONS = {
allErrors: true, allErrors: true,
@ -348,17 +348,21 @@ export default class TreeMode extends Component {
} }
handleInsert = (path, type) => { handleInsert = (path, type) => {
this.handlePatch(insert(this.state.data, path, createEntry(type), type)) this.handlePatch(insertBefore(this.state.data, path, [{
type,
name: '',
value: createEntry(type)
}]))
this.setState({ selection : null }) // TODO: select the inserted entry this.setState({ selection : null }) // TODO: select the inserted entry
// apply focus to new node // apply focus to new node
this.focusToNext(path) this.focusToPrevious(path)
} }
handleInsertStructure = (path) => { handleInsertStructure = (path) => {
// TODO: implement handleInsertStructure // TODO: implement handleInsertStructure
console.log('handleInsertStructure', path) console.log('TODO: handleInsertStructure', path)
alert('not yet implemented...') alert('not yet implemented...')
} }
@ -456,7 +460,7 @@ export default class TreeMode extends Component {
handleKeyDownDuplicate = (event) => { handleKeyDownDuplicate = (event) => {
const path = this.findDataPathFromElement(event.target) const path = this.findDataPathFromElement(event.target)
if (path) { if (path) {
const selection = { start: {path}, end: {path} } const selection = { start: path, end: path }
this.handlePatch(duplicate(this.state.data, selection)) this.handlePatch(duplicate(this.state.data, selection))
// apply focus to the duplicated node // apply focus to the duplicated node
@ -510,11 +514,10 @@ export default class TreeMode extends Component {
} }
/** /**
* Move focus to the next search result * Move focus to the next node
* @param {Path} path * @param {Path} path
*/ */
focusToNext (path) { focusToNext (path) {
// apply focus to new element
setTimeout(() => { setTimeout(() => {
const element = findNode(this.refs.contents, path) const element = findNode(this.refs.contents, path)
if (element) { if (element) {
@ -523,12 +526,24 @@ export default class TreeMode extends Component {
}) })
} }
/**
* Move focus to the previous node
* @param {Path} path
*/
focusToPrevious (path) {
setTimeout(() => {
const element = findNode(this.refs.contents, path)
if (element) {
moveUp(element, 'property')
}
})
}
handleSort = (path, order = null) => { handleSort = (path, order = null) => {
this.handlePatch(sort(this.state.data, path, order)) this.handlePatch(sort(this.state.data, path, order))
} }
handleSelect = (selection: ESONSelection) => { handleSelect = (selection: Selection) => {
console.log('handleSelect', selection)
this.setState({ selection }) this.setState({ selection })
} }
@ -664,7 +679,7 @@ export default class TreeMode extends Component {
(event.target.contentEditable !== 'true') (event.target.contentEditable !== 'true')
if (clickedOnEmptySpace && pointer) { if (clickedOnEmptySpace && pointer) {
this.setState({ selection: {start: pointer, end: pointer}}) this.setState({ selection: this.selectionFromESONPointer(pointer)})
} }
else { else {
this.setState({ selection: null }) this.setState({ selection: null })
@ -672,12 +687,13 @@ export default class TreeMode extends Component {
} }
handlePan = (event) => { handlePan = (event) => {
const selection = this.state.selection
const path = this.findDataPathFromElement(event.target.firstChild) const path = this.findDataPathFromElement(event.target.firstChild)
if (path && this.state.selection && !isEqual(path, this.state.selection.end.path)) { if (path && selection && !isEqual(path, selection.end)) {
this.setState({ this.setState({
selection: { selection: {
start: this.state.selection.start, start: selection.start || selection.before || selection.after,
end: {path} end: path
} }
}) })
} }
@ -711,6 +727,18 @@ export default class TreeMode extends Component {
return path ? { path, area } : null return path ? { path, area } : null
} }
selectionFromESONPointer (pointer: ESONPointer) : Selection {
if (pointer.area === 'after') {
return {after: pointer.path}
}
else if (pointer.area === 'before') {
return {before: pointer.path}
}
else {
return {start: pointer.path, end: pointer.path}
}
}
/** /**
* Scroll the window vertically to the node with given path * Scroll the window vertically to the node with given path
* @param {Path} path * @param {Path} path
@ -770,7 +798,6 @@ export default class TreeMode extends Component {
} }
undo = () => { undo = () => {
console.log('undo')
if (this.canUndo()) { if (this.canUndo()) {
const history = this.state.history const history = this.state.history
const historyIndex = this.state.historyIndex const historyIndex = this.state.historyIndex

View File

@ -12,6 +12,10 @@
<script src="./resources/largeJson.js"></script> <script src="./resources/largeJson.js"></script>
<style> <style>
body, input, select {
font-family: sans-serif;
font-size: 11pt;
}
#container { #container {
height: 400px; height: 400px;
width: 100%; width: 100%;
@ -35,6 +39,10 @@
<option value="view">view</option> <option value="view">view</option>
</select> </select>
</label> </label>
<label>
<input type="checkbox" id="logEvents" > Log events
</label>
</p> </p>
<div id="container"></div> <div id="container"></div>
@ -57,22 +65,22 @@
const options = { const options = {
name: 'myObject', name: 'myObject',
onPatch: function (patch, revert) { onPatch: function (patch, revert) {
console.log('onPatch patch=', patch, ', revert=', revert) log('onPatch patch=', patch, ', revert=', revert)
window.patch = patch window.patch = patch
window.revert = revert window.revert = revert
}, },
onPatchText: function (patch, revert) { onPatchText: function (patch, revert) {
// FIXME: implement onPatchText // FIXME: implement onPatchText
console.log('onPatchText patch=', patch, ', revert=', revert) log('onPatchText patch=', patch, ', revert=', revert)
}, },
onChange: function (json) { onChange: function (json) {
console.log('onChange json=', json) log('onChange json=', json)
}, },
onChangeText: function (text) { onChangeText: function (text) {
console.log('onChangeText', text) log('onChangeText', text)
}, },
onChangeMode: function (mode, prevMode) { onChangeMode: function (mode, prevMode) {
console.log('switched mode from', prevMode, 'to', mode) log('switched mode from', prevMode, 'to', mode)
document.getElementById('mode').value = mode document.getElementById('mode').value = mode
}, },
onError: function (err) { onError: function (err) {
@ -159,6 +167,12 @@
editor.setMode(mode) editor.setMode(mode)
} }
function log (...args) {
if (document.getElementById('logEvents').checked) {
console.log(...args)
}
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -13,7 +13,7 @@ import initial from 'lodash/initial'
import last from 'lodash/last' import last from 'lodash/last'
import type { import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONSelection, ESONType, ESONPath, ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONType, ESONPath,
Path, Path,
JSONPath, JSONType JSONPath, JSONType
} from './types' } from './types'
@ -357,29 +357,36 @@ export function applySearchResults (eson: ESON, searchResults: ESONPointer[], ac
/** /**
* Merge searchResults into the eson object * Merge searchResults into the eson object
*/ */
export function applySelection (eson: ESON, selection: ESONSelection) { export function applySelection (eson: ESON, selection: Selection) {
if (!selection) { if (!selection) {
return eson return eson
} }
// find the parent node shared by both start and end of the selection if (selection.before) {
const rootPath = findRootPath(selection) const esonPath = toEsonPath(eson, selection.before)
const rootEsonPath = toEsonPath(eson, rootPath) return setIn(eson, esonPath.concat('selected'), SELECTED_BEFORE)
}
else if (selection.after) {
const esonPath = toEsonPath(eson, selection.after)
return setIn(eson, esonPath.concat('selected'), SELECTED_AFTER)
}
else { // selection.start and selection.end
// find the parent node shared by both start and end of the selection
const rootPath = findRootPath(selection)
return updateIn(eson, rootEsonPath, (root) => { return updateIn(eson, toEsonPath(eson, rootPath), (root) => {
const start = selection.start.path[rootPath.length] const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
const end = selection.end.path[rootPath.length]
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
const childsBefore = root[childsKey].slice(0, minIndex) const childsBefore = root[childsKey].slice(0, minIndex)
const childsUpdated = root[childsKey].slice(minIndex, maxIndex) const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
.map((child, index) => setIn(child, ['value', 'selected'], index === 0 ? SELECTED_END : SELECTED)) .map((child, index) => setIn(child, ['value', 'selected'], index === 0 ? SELECTED_END : SELECTED))
const childsAfter = root[childsKey].slice(maxIndex) const childsAfter = root[childsKey].slice(maxIndex)
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index // FIXME: actually mark the end index as SELECTED_END, currently we select the first index
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter)) return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
}) })
}
} }
/** /**
@ -387,13 +394,17 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
* Start and end can be a property name in case of an Object, * Start and end can be a property name in case of an Object,
* or a matrix index (string with a number) in case of an Array. * or a matrix index (string with a number) in case of an Array.
*/ */
export function findSelectionIndices (root: ESON, start: string, end: string) : { minIndex: number, maxIndex: number } { export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection: Selection) : { minIndex: number, maxIndex: number } {
const start = (selection.after || selection.before || selection.start)[rootPath.length]
const end = (selection.after || selection.before || selection.end)[rootPath.length]
// if no object we assume it's an Array // if no object we assume it's an Array
const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start) const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start)
const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end) const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end)
const minIndex = Math.min(startIndex, endIndex) const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself const maxIndex = Math.max(startIndex, endIndex) +
((selection.after || selection.before) ? 0 : 1) // include max index itself
return { minIndex, maxIndex } return { minIndex, maxIndex }
} }
@ -401,15 +412,12 @@ export function findSelectionIndices (root: ESON, start: string, end: string) :
/** /**
* Get the JSON paths from a selection, sorted from first to last * Get the JSON paths from a selection, sorted from first to last
*/ */
export function pathsFromSelection (eson: ESON, selection: ESONSelection): JSONPath[] { export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[] {
// find the parent node shared by both start and end of the selection // find the parent node shared by both start and end of the selection
const rootPath = findRootPath(selection) const rootPath = findRootPath(selection)
const rootEsonPath = toEsonPath(eson, rootPath) const root = getIn(eson, toEsonPath(eson, rootPath))
const root = getIn(eson, rootEsonPath) const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
const start = selection.start.path[rootPath.length]
const end = selection.end.path[rootPath.length]
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
if (root.type === 'Object') { if (root.type === 'Object') {
return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name)) return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name))
@ -443,15 +451,23 @@ export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
* @return {JSONPath} * @return {JSONPath}
*/ */
export function findRootPath(selection) { export function findRootPath(selection) {
const sharedPath = findSharedPath(selection.start.path, selection.end.path) if (selection.before) {
return initial(selection.before)
if (sharedPath.length === selection.start.path.length &&
sharedPath.length === selection.end.path.length) {
// there is just one node selected, return it's parent
return initial(sharedPath)
} }
else { else if (selection.after) {
return sharedPath return initial(selection.after)
}
else { // .start and .end
const sharedPath = findSharedPath(selection.start, selection.end)
if (sharedPath.length === selection.start.length &&
sharedPath.length === selection.end.length) {
// there is just one node selected, return it's parent
return initial(sharedPath)
}
else {
return sharedPath
}
} }
} }

View File

@ -573,7 +573,7 @@ div.jsoneditor-node-container {
&.jsoneditor-hover { &.jsoneditor-hover {
background-color: @hoverAndSelectedColor; background-color: @hoverAndSelectedColor;
&.jsoneditor-hover-after { &.jsoneditor-hover-insert-area {
background-color: @selectedColor; background-color: @selectedColor;
div.jsoneditor-insert-area { div.jsoneditor-insert-area {
@ -581,7 +581,7 @@ div.jsoneditor-node-container {
background-color: @hoverColor; background-color: @hoverColor;
} }
&.jsoneditor-selected-after { &.jsoneditor-selected-insert-area {
div.jsoneditor-insert-area { div.jsoneditor-insert-area {
border: 1px dashed #f4af41; border: 1px dashed #f4af41;
background-color: @hoverAndSelectedColor; background-color: @hoverAndSelectedColor;
@ -590,13 +590,13 @@ div.jsoneditor-node-container {
} }
} }
&.jsoneditor-selected-after { &.jsoneditor-selected-insert-area {
background-color: inherit; background-color: inherit;
&.jsoneditor-hover { &.jsoneditor-hover {
background-color: @hoverColor; background-color: @hoverColor;
&.jsoneditor-hover-after { &.jsoneditor-hover-insert-area {
background-color: inherit; background-color: inherit;
} }
} }
@ -611,7 +611,7 @@ div.jsoneditor-node-container {
.jsoneditor-hover { .jsoneditor-hover {
background-color: @hoverAndSelectedColor; background-color: @hoverAndSelectedColor;
&.jsoneditor-hover-after { &.jsoneditor-hover-insert-area {
background-color: inherit; background-color: inherit;
div.jsoneditor-insert-area { div.jsoneditor-insert-area {
@ -625,7 +625,7 @@ div.jsoneditor-node-container {
&.jsoneditor-hover { &.jsoneditor-hover {
background-color: @hoverColor; background-color: @hoverColor;
&.jsoneditor-hover-after { &.jsoneditor-hover-insert-area {
background-color: inherit; background-color: inherit;
div.jsoneditor-insert-area { div.jsoneditor-insert-area {
@ -642,7 +642,7 @@ div.jsoneditor-node-container {
width: 100%; width: 100%;
height: @height; height: @height;
left: 0; left: 0;
bottom: -@height/2; top: -@height/2;
border: 1px transparent; border: 1px transparent;
box-sizing: border-box; box-sizing: border-box;
z-index: 1; // must be on top of next node, it overlaps a bit z-index: 1; // must be on top of next node, it overlaps a bit

View File

@ -33,7 +33,7 @@ export type JSONArrayType = JSONType[]
/********************** TYPES FOR THE ESON OBJECT MODEL *************************/ /********************** TYPES FOR THE ESON OBJECT MODEL *************************/
export type SearchResultStatus = 'normal' | 'active' export type SearchResultStatus = 'normal' | 'active'
export type ESONPointerArea = 'value' | 'property' | 'before' | 'after' export type ESONPointerArea = 'value' | 'property'
export type ESONObjectProperty = { export type ESONObjectProperty = {
id: number, id: number,
@ -81,9 +81,11 @@ export type ESONPointer = {
area?: ESONPointerArea area?: ESONPointerArea
} }
export type ESONSelection = { export type Selection = {
start: ESONPointer, start?: JSONPath,
end: ESONPointer end?: JSONPath,
before?: JSONPath,
after?: JSONPath
} }
// TODO: ESONPointer.path is an array, JSONSchemaError.path is a string -> make this consistent // TODO: ESONPointer.path is an array, JSONSchemaError.path is a string -> make this consistent