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
*
* @param {ESON} data
* @param {ESONSelection} selection
* @param {Selection} selection
* @return {Array}
*/
export function duplicate (data, selection) {
// console.log('duplicate', path)
if (!selection.start || !selection.end) {
return []
}
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 { maxIndex } = findSelectionIndices(root, start, end)
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
const paths = pathsFromSelection(data, selection)
if (root.type === 'Array') {
@ -113,7 +114,6 @@ export function duplicate (data, selection) {
}))
}
else { // object.type === 'Object'
const { maxIndex } = findSelectionIndices(root, start, end)
const nextProp = root.props && root.props[maxIndex]
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.
*
@ -233,17 +186,14 @@ export function insertBefore (data, path, values) { // TODO: find a better name
* and object property
*
* @param {ESON} data
* @param {ESONSelection} selection
* @param {Selection} selection
* @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
* @return {Array}
*/
export function replace (data, selection, values) { // TODO: find a better name and define datastructure for values
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 { minIndex, maxIndex } = findSelectionIndices(root, start, end)
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Array') {
const removeActions = removeAll(pathsFromSelection(data, selection))

View File

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

View File

@ -37,7 +37,7 @@ import {
import { createFindKeyBinding } from '../utils/keyBindings'
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 = {
allErrors: true,
@ -348,17 +348,21 @@ export default class TreeMode extends Component {
}
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
// apply focus to new node
this.focusToNext(path)
this.focusToPrevious(path)
}
handleInsertStructure = (path) => {
// TODO: implement handleInsertStructure
console.log('handleInsertStructure', path)
console.log('TODO: handleInsertStructure', path)
alert('not yet implemented...')
}
@ -456,7 +460,7 @@ export default class TreeMode extends Component {
handleKeyDownDuplicate = (event) => {
const path = this.findDataPathFromElement(event.target)
if (path) {
const selection = { start: {path}, end: {path} }
const selection = { start: path, end: path }
this.handlePatch(duplicate(this.state.data, selection))
// 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
*/
focusToNext (path) {
// apply focus to new element
setTimeout(() => {
const element = findNode(this.refs.contents, path)
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) => {
this.handlePatch(sort(this.state.data, path, order))
}
handleSelect = (selection: ESONSelection) => {
console.log('handleSelect', selection)
handleSelect = (selection: Selection) => {
this.setState({ selection })
}
@ -664,7 +679,7 @@ export default class TreeMode extends Component {
(event.target.contentEditable !== 'true')
if (clickedOnEmptySpace && pointer) {
this.setState({ selection: {start: pointer, end: pointer}})
this.setState({ selection: this.selectionFromESONPointer(pointer)})
}
else {
this.setState({ selection: null })
@ -672,12 +687,13 @@ export default class TreeMode extends Component {
}
handlePan = (event) => {
const selection = this.state.selection
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({
selection: {
start: this.state.selection.start,
end: {path}
start: selection.start || selection.before || selection.after,
end: path
}
})
}
@ -711,6 +727,18 @@ export default class TreeMode extends Component {
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
* @param {Path} path
@ -770,7 +798,6 @@ export default class TreeMode extends Component {
}
undo = () => {
console.log('undo')
if (this.canUndo()) {
const history = this.state.history
const historyIndex = this.state.historyIndex

View File

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

View File

@ -13,7 +13,7 @@ import initial from 'lodash/initial'
import last from 'lodash/last'
import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONSelection, ESONType, ESONPath,
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONType, ESONPath,
Path,
JSONPath, JSONType
} from './types'
@ -357,29 +357,36 @@ export function applySearchResults (eson: ESON, searchResults: ESONPointer[], ac
/**
* Merge searchResults into the eson object
*/
export function applySelection (eson: ESON, selection: ESONSelection) {
export function applySelection (eson: ESON, selection: Selection) {
if (!selection) {
return eson
}
// find the parent node shared by both start and end of the selection
const rootPath = findRootPath(selection)
const rootEsonPath = toEsonPath(eson, rootPath)
if (selection.before) {
const esonPath = toEsonPath(eson, selection.before)
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) => {
const start = selection.start.path[rootPath.length]
const end = selection.end.path[rootPath.length]
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
return updateIn(eson, toEsonPath(eson, rootPath), (root) => {
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
const childsBefore = root[childsKey].slice(0, minIndex)
const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
.map((child, index) => setIn(child, ['value', 'selected'], index === 0 ? SELECTED_END : SELECTED))
const childsAfter = root[childsKey].slice(maxIndex)
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
const childsBefore = root[childsKey].slice(0, minIndex)
const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
.map((child, index) => setIn(child, ['value', 'selected'], index === 0 ? SELECTED_END : SELECTED))
const childsAfter = root[childsKey].slice(maxIndex)
// 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,
* 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
const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start)
const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end)
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 }
}
@ -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
*/
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
const rootPath = findRootPath(selection)
const rootEsonPath = toEsonPath(eson, rootPath)
const root = getIn(eson, toEsonPath(eson, rootPath))
const root = getIn(eson, rootEsonPath)
const start = selection.start.path[rootPath.length]
const end = selection.end.path[rootPath.length]
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Object') {
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}
*/
export function findRootPath(selection) {
const sharedPath = findSharedPath(selection.start.path, selection.end.path)
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)
if (selection.before) {
return initial(selection.before)
}
else {
return sharedPath
else if (selection.after) {
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 {
background-color: @hoverAndSelectedColor;
&.jsoneditor-hover-after {
&.jsoneditor-hover-insert-area {
background-color: @selectedColor;
div.jsoneditor-insert-area {
@ -581,7 +581,7 @@ div.jsoneditor-node-container {
background-color: @hoverColor;
}
&.jsoneditor-selected-after {
&.jsoneditor-selected-insert-area {
div.jsoneditor-insert-area {
border: 1px dashed #f4af41;
background-color: @hoverAndSelectedColor;
@ -590,13 +590,13 @@ div.jsoneditor-node-container {
}
}
&.jsoneditor-selected-after {
&.jsoneditor-selected-insert-area {
background-color: inherit;
&.jsoneditor-hover {
background-color: @hoverColor;
&.jsoneditor-hover-after {
&.jsoneditor-hover-insert-area {
background-color: inherit;
}
}
@ -611,7 +611,7 @@ div.jsoneditor-node-container {
.jsoneditor-hover {
background-color: @hoverAndSelectedColor;
&.jsoneditor-hover-after {
&.jsoneditor-hover-insert-area {
background-color: inherit;
div.jsoneditor-insert-area {
@ -625,7 +625,7 @@ div.jsoneditor-node-container {
&.jsoneditor-hover {
background-color: @hoverColor;
&.jsoneditor-hover-after {
&.jsoneditor-hover-insert-area {
background-color: inherit;
div.jsoneditor-insert-area {
@ -642,7 +642,7 @@ div.jsoneditor-node-container {
width: 100%;
height: @height;
left: 0;
bottom: -@height/2;
top: -@height/2;
border: 1px transparent;
box-sizing: border-box;
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 *************************/
export type SearchResultStatus = 'normal' | 'active'
export type ESONPointerArea = 'value' | 'property' | 'before' | 'after'
export type ESONPointerArea = 'value' | 'property'
export type ESONObjectProperty = {
id: number,
@ -81,9 +81,11 @@ export type ESONPointer = {
area?: ESONPointerArea
}
export type ESONSelection = {
start: ESONPointer,
end: ESONPointer
export type Selection = {
start?: JSONPath,
end?: JSONPath,
before?: JSONPath,
after?: JSONPath
}
// TODO: ESONPointer.path is an array, JSONSchemaError.path is a string -> make this consistent