Cut/Copy/Paste selection starting to work (WIP)
This commit is contained in:
parent
8425579718
commit
dd989f9a64
123
src/actions.js
123
src/actions.js
|
@ -1,4 +1,9 @@
|
||||||
import { compileJSONPointer, toEsonPath, esonToJson, findNextProp } from './eson'
|
import last from 'lodash/last'
|
||||||
|
import initial from 'lodash/initial'
|
||||||
|
import {
|
||||||
|
compileJSONPointer, toEsonPath, esonToJson, findNextProp,
|
||||||
|
pathsFromSelection, findRootPath, findSelectionIndices
|
||||||
|
} from './eson'
|
||||||
import { findUniqueName } from './utils/stringUtils'
|
import { findUniqueName } from './utils/stringUtils'
|
||||||
import { getIn } from './utils/immutabilityHelpers'
|
import { getIn } from './utils/immutabilityHelpers'
|
||||||
import { isObject, stringConvert } from './utils/typeUtils'
|
import { isObject, stringConvert } from './utils/typeUtils'
|
||||||
|
@ -168,6 +173,108 @@ export function insert (data, path, type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
export function insertBefore (data, path, values) { // TODO: find a better name and define datastructure for values
|
||||||
|
const parentPath = initial(path)
|
||||||
|
const esonPath = toEsonPath(data, parentPath)
|
||||||
|
const parent = getIn(data, esonPath)
|
||||||
|
|
||||||
|
if (parent.type === 'Array') {
|
||||||
|
const startIndex = parseInt(last(path))
|
||||||
|
return values.map((entry, offset) => ({
|
||||||
|
op: 'add',
|
||||||
|
path: compileJSONPointer(parentPath.concat(startIndex + offset)),
|
||||||
|
value: entry.value,
|
||||||
|
jsoneditor: {
|
||||||
|
type: entry.type
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
else { // object.type === 'Object'
|
||||||
|
const before = last(path)
|
||||||
|
return values.map(entry => {
|
||||||
|
const newProp = findUniqueName(entry.name, parent.props.map(p => p.name))
|
||||||
|
return {
|
||||||
|
op: 'add',
|
||||||
|
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||||
|
value: entry.value,
|
||||||
|
jsoneditor: {
|
||||||
|
type: entry.type,
|
||||||
|
before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {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]
|
||||||
|
console.log('rootPath', rootPath, start, end)
|
||||||
|
const root = getIn(data, toEsonPath(data, rootPath))
|
||||||
|
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
|
||||||
|
console.log('selection', minIndex, maxIndex)
|
||||||
|
|
||||||
|
if (root.type === 'Array') {
|
||||||
|
const removeActions = removeAll(pathsFromSelection(data, selection))
|
||||||
|
const insertActions = values.map((entry, offset) => ({
|
||||||
|
op: 'add',
|
||||||
|
path: compileJSONPointer(rootPath.concat(minIndex + offset)),
|
||||||
|
value: entry.value,
|
||||||
|
jsoneditor: {
|
||||||
|
type: entry.type
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return removeActions.concat(insertActions)
|
||||||
|
}
|
||||||
|
else { // object.type === 'Object'
|
||||||
|
const nextProp = root.props && root.props[maxIndex]
|
||||||
|
const before = nextProp ? nextProp.name : null
|
||||||
|
|
||||||
|
const removeActions = removeAll(pathsFromSelection(data, selection))
|
||||||
|
const insertActions = values.map(entry => {
|
||||||
|
const newProp = findUniqueName(entry.name, root.props.map(p => p.name))
|
||||||
|
return {
|
||||||
|
op: 'add',
|
||||||
|
path: compileJSONPointer(rootPath.concat(newProp)),
|
||||||
|
value: entry.value,
|
||||||
|
jsoneditor: {
|
||||||
|
type: entry.type,
|
||||||
|
before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return removeActions.concat(insertActions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch for an append action.
|
* Create a JSONPatch for an append action.
|
||||||
*
|
*
|
||||||
|
@ -214,6 +321,7 @@ export function append (data, parentPath, type) {
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch for a remove action
|
* Create a JSONPatch for a remove action
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
|
* @return {ESONPatch}
|
||||||
*/
|
*/
|
||||||
export function remove (path) {
|
export function remove (path) {
|
||||||
return [{
|
return [{
|
||||||
|
@ -222,6 +330,19 @@ export function remove (path) {
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSONPatch for a multiple remove action
|
||||||
|
* @param {Path[]} paths
|
||||||
|
* @return {ESONPatch}
|
||||||
|
*/
|
||||||
|
export function removeAll (paths) {
|
||||||
|
return paths.map(path => ({
|
||||||
|
op: 'remove',
|
||||||
|
path: compileJSONPointer(path)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
// TODO: test removeAll
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
|
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
|
||||||
* or descending order
|
* or descending order
|
||||||
|
|
|
@ -22,8 +22,8 @@ import {
|
||||||
} from '../eson'
|
} from '../eson'
|
||||||
import { patchEson } from '../patchEson'
|
import { patchEson } from '../patchEson'
|
||||||
import {
|
import {
|
||||||
duplicate, insert, append, remove,
|
duplicate, insert, insertBefore, append, remove, removeAll, replace,
|
||||||
changeType, changeValue, changeProperty, sort
|
createEntry, changeType, changeValue, changeProperty, sort
|
||||||
} from '../actions'
|
} from '../actions'
|
||||||
import JSONNode from './JSONNode'
|
import JSONNode from './JSONNode'
|
||||||
import JSONNodeView from './JSONNodeView'
|
import JSONNodeView from './JSONNodeView'
|
||||||
|
@ -90,14 +90,15 @@ export default class TreeMode extends Component {
|
||||||
onChangeValue: this.handleChangeValue,
|
onChangeValue: this.handleChangeValue,
|
||||||
onChangeType: this.handleChangeType,
|
onChangeType: this.handleChangeType,
|
||||||
onInsert: this.handleInsert,
|
onInsert: this.handleInsert,
|
||||||
|
onInsertStructure: this.handleInsertStructure,
|
||||||
onAppend: this.handleAppend,
|
onAppend: this.handleAppend,
|
||||||
onDuplicate: this.handleDuplicate,
|
onDuplicate: this.handleDuplicate,
|
||||||
onRemove: this.handleRemove,
|
onRemove: this.handleRemove,
|
||||||
onSort: this.handleSort,
|
onSort: this.handleSort,
|
||||||
|
|
||||||
onCut: this.handleMenuCut,
|
onCut: this.handleCut,
|
||||||
onCopy: this.handleMenuCopy,
|
onCopy: this.handleCopy,
|
||||||
onPaste: this.handleMenuPaste,
|
onPaste: this.handlePaste,
|
||||||
|
|
||||||
onExpand: this.handleExpand,
|
onExpand: this.handleExpand,
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ export default class TreeMode extends Component {
|
||||||
end: null, // ESONPointer
|
end: null, // ESONPointer
|
||||||
},
|
},
|
||||||
|
|
||||||
clipboard: null // array entries {prop: string, value: JSON}
|
clipboard: null // array entries {name: string, value: JSONType}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +130,15 @@ export default class TreeMode extends Component {
|
||||||
this.applyProps(nextProps, this.props)
|
this.applyProps(nextProps, this.props)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use or cleanup
|
||||||
|
// componentDidMount () {
|
||||||
|
// document.addEventListener('keydown', this.handleKeyDown)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// componentWillUnmount () {
|
||||||
|
// document.removeEventListener('keydown', this.handleKeyDown)
|
||||||
|
// }
|
||||||
|
|
||||||
// TODO: create some sort of watcher structure for these props? Is there a React pattern for that?
|
// TODO: create some sort of watcher structure for these props? Is there a React pattern for that?
|
||||||
applyProps (nextProps, currentProps) {
|
applyProps (nextProps, currentProps) {
|
||||||
// Apply text
|
// Apply text
|
||||||
|
@ -340,12 +350,21 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInsert = (path, type) => {
|
handleInsert = (path, type) => {
|
||||||
this.handlePatch(insert(this.state.data, path, type))
|
this.handlePatch(insert(this.state.data, path, createEntry(type), type))
|
||||||
|
|
||||||
|
this.setState({ selection : null }) // TODO: select the inserted entry
|
||||||
|
|
||||||
// apply focus to new node
|
// apply focus to new node
|
||||||
this.focusToNext(path)
|
this.focusToNext(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInsertStructure = (path) => {
|
||||||
|
// TODO: implement handleInsertStructure
|
||||||
|
console.log('handleInsertStructure', path)
|
||||||
|
alert('not yet implemented...')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
handleAppend = (parentPath, type) => {
|
handleAppend = (parentPath, type) => {
|
||||||
this.handlePatch(append(this.state.data, parentPath, type))
|
this.handlePatch(append(this.state.data, parentPath, type))
|
||||||
|
|
||||||
|
@ -361,6 +380,7 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemove = (path) => {
|
handleRemove = (path) => {
|
||||||
|
if (path) {
|
||||||
// apply focus to next sibling element if existing, else to the previous element
|
// apply focus to next sibling element if existing, else to the previous element
|
||||||
const fromElement = findNode(this.refs.contents, path)
|
const fromElement = findNode(this.refs.contents, path)
|
||||||
const success = moveDownSibling(fromElement, 'property')
|
const success = moveDownSibling(fromElement, 'property')
|
||||||
|
@ -371,6 +391,14 @@ export default class TreeMode extends Component {
|
||||||
this.setState({ selection : null })
|
this.setState({ selection : null })
|
||||||
this.handlePatch(remove(path))
|
this.handlePatch(remove(path))
|
||||||
}
|
}
|
||||||
|
else if (this.state.selection) {
|
||||||
|
// remove selection
|
||||||
|
// TODO: select next property? (same as when removing a path?)
|
||||||
|
const paths = pathsFromSelection(this.state.data, this.state.selection)
|
||||||
|
this.setState({ selection: null })
|
||||||
|
this.handlePatch(removeAll(paths))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
moveUp = (event) => {
|
moveUp = (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -403,54 +431,32 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDownCut = (event) => {
|
handleKeyDownCut = (event) => {
|
||||||
const { selection } = this.state
|
if (this.state.selection) {
|
||||||
if (selection) {
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
this.handleCut(selection)
|
this.handleCut()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDownCopy = (event) => {
|
handleKeyDownCopy = (event) => {
|
||||||
const { selection } = this.state
|
if (this.state.selection) {
|
||||||
if (selection) {
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
this.handleCopy(selection)
|
this.handleCopy()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDownPaste = (event) => {
|
handleKeyDownPaste = (event) => {
|
||||||
const { clipboard, selection } = this.state
|
const { clipboard, data } = this.state
|
||||||
|
|
||||||
if (clipboard && clipboard.length > 0) {
|
if (clipboard && clipboard.length > 0) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (selection) {
|
|
||||||
this.handlePaste(clipboard, selection, null)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// no selection -> paste after current path
|
|
||||||
const path = this.findDataPathFromElement(event.target)
|
const path = this.findDataPathFromElement(event.target)
|
||||||
this.handlePaste(clipboard, null, path)
|
this.handlePatch(insertBefore(data, path, clipboard))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMenuCut = (path) => {
|
|
||||||
const selection = { start: { path }, end: { path }}
|
|
||||||
this.handleCut(selection)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMenuCopy = (path) => {
|
|
||||||
const selection = { start: { path }, end: { path }}
|
|
||||||
this.handleCopy(selection)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMenuPaste = (path) => {
|
|
||||||
const { clipboard } = this.state
|
|
||||||
if (clipboard && clipboard.length > 0) {
|
|
||||||
this.handlePaste(clipboard, null, path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCut = (selection: ESONSelection) => {
|
handleCut = () => {
|
||||||
|
const selection = this.state.selection
|
||||||
if (selection && selection.start && selection.end) {
|
if (selection && selection.start && selection.end) {
|
||||||
const data = this.state.data
|
const data = this.state.data
|
||||||
const paths = pathsFromSelection(data, selection)
|
const paths = pathsFromSelection(data, selection)
|
||||||
|
@ -469,7 +475,8 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCopy = (selection: ESONSelection) => {
|
handleCopy = () => {
|
||||||
|
const selection = this.state.selection
|
||||||
if (selection && selection.start && selection.end) {
|
if (selection && selection.start && selection.end) {
|
||||||
const data = this.state.data
|
const data = this.state.data
|
||||||
const paths = pathsFromSelection(data, selection)
|
const paths = pathsFromSelection(data, selection)
|
||||||
|
@ -483,42 +490,12 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePaste = (clipboard, selection: ESONSelection, path: JSONPath) => {
|
handlePaste = () => {
|
||||||
const { data } = this.state
|
const { data, selection, clipboard } = this.state
|
||||||
|
|
||||||
if (clipboard && clipboard.length > 0) {
|
if (selection && clipboard && clipboard.length > 0) {
|
||||||
// FIXME: handle pasting in an empty object or array
|
// FIXME: handle pasting in an empty object or array
|
||||||
|
this.handlePatch(replace(data, selection, clipboard))
|
||||||
if (path && path.length > 0) {
|
|
||||||
const parentPath = initial(path)
|
|
||||||
const parent = getIn(data, toEsonPath(data, parentPath))
|
|
||||||
const isObject = parent.type === 'Object'
|
|
||||||
|
|
||||||
if (parent.type === 'Object') {
|
|
||||||
const existingProps = parent.props.map(p => p.name)
|
|
||||||
const prop = last(path)
|
|
||||||
const patch = clipboard.map(entry => ({
|
|
||||||
op: 'add',
|
|
||||||
path: compileJSONPointer(parentPath.concat(findUniqueName(entry.name, existingProps))),
|
|
||||||
value: entry.value,
|
|
||||||
jsoneditor: { before: prop }
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.handlePatch(patch)
|
|
||||||
}
|
|
||||||
else { // parent.type === 'Array'
|
|
||||||
const patch = clipboard.map(entry => ({
|
|
||||||
op: 'add',
|
|
||||||
path: compileJSONPointer(path),
|
|
||||||
value: entry.value
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.handlePatch(patch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (selection){
|
|
||||||
console.log('TODO: replace selection')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -673,7 +650,10 @@ export default class TreeMode extends Component {
|
||||||
|
|
||||||
handleTouchStart = (event) => {
|
handleTouchStart = (event) => {
|
||||||
const pointer = this.findESONPointerFromElement(event.target)
|
const pointer = this.findESONPointerFromElement(event.target)
|
||||||
if (pointer) {
|
const clickedOnEmptySpace = (event.target.nodeName === 'DIV') &&
|
||||||
|
(event.target.contentEditable !== 'true')
|
||||||
|
|
||||||
|
if (clickedOnEmptySpace && pointer) {
|
||||||
this.setState({ selection: {start: pointer, end: pointer}})
|
this.setState({ selection: {start: pointer, end: pointer}})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -6,13 +6,13 @@ import { keyComboFromEvent } from '../../utils/keyBindings'
|
||||||
const MENU_CLASS_NAME = 'jsoneditor-floating-menu'
|
const MENU_CLASS_NAME = 'jsoneditor-floating-menu'
|
||||||
const MENU_ITEM_CLASS_NAME = 'jsoneditor-floating-menu-item'
|
const MENU_ITEM_CLASS_NAME = 'jsoneditor-floating-menu-item'
|
||||||
|
|
||||||
// Array: Sort | Map | Filter | Duplicate | Cut | Copy | Remove
|
// Array: Sort | Map | Filter | Duplicate | Cut | Copy | Paste | Remove
|
||||||
// advanced sort (asc, desc, nested fields, custom comparator)
|
// advanced sort (asc, desc, nested fields, custom comparator)
|
||||||
// sort, map, filter, open a popup covering the editor (not the whole page)
|
// sort, map, filter, open a popup covering the editor (not the whole page)
|
||||||
// (or if it's small, can be a dropdown)
|
// (or if it's small, can be a dropdown)
|
||||||
// Object: Sort | Duplicate | Cut | Copy | Remove
|
// Object: Sort | Duplicate | Cut | Copy | Paste | Remove
|
||||||
// simple sort (asc/desc)
|
// simple sort (asc/desc)
|
||||||
// Value: [x] String | Duplicate | Cut | Copy | Remove
|
// Value: [x] String | Duplicate | Cut | Copy | Paste | Remove
|
||||||
// String is a checkmark
|
// String is a checkmark
|
||||||
// Between: Insert Structure | Insert Value | Insert Object | Insert Array | Paste
|
// Between: Insert Structure | Insert Value | Insert Object | Insert Array | Paste
|
||||||
// inserting (value selected): [field] [value]
|
// inserting (value selected): [field] [value]
|
||||||
|
@ -81,52 +81,55 @@ const CREATE_TYPE = {
|
||||||
remove: (path, events) => h('button', {
|
remove: (path, events) => h('button', {
|
||||||
key: 'remove',
|
key: 'remove',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
onClick: () => events.onRemove(path),
|
onClick: () => events.onRemove(null), // do not pass path: we want to remove selection
|
||||||
title: 'Remove'
|
title: 'Remove'
|
||||||
}, 'Remove'),
|
}, 'Remove'),
|
||||||
|
|
||||||
insertStructure: (path, events) => h('button', {
|
insertStructure: (path, events) => h('button', {
|
||||||
key: 'insertStructure',
|
key: 'insertStructure',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
// onClick: () => events.onRemove(path),
|
onClick: () => events.onInsertStructure(path),
|
||||||
title: 'Insert a new object with the same data structure as the item above'
|
title: 'Insert a new object with the same data structure as the item above'
|
||||||
}, 'Insert structure'),
|
}, 'Insert structure'),
|
||||||
|
|
||||||
insertValue: (path, events) => h('button', {
|
insertValue: (path, events) => h('button', {
|
||||||
key: 'insertValue',
|
key: 'insertValue',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
// onClick: () => events.onRemove(path),
|
onClick: () => events.onInsert(path, 'value'),
|
||||||
title: 'Insert value'
|
title: 'Insert value'
|
||||||
}, 'Insert value'),
|
}, 'Insert value'),
|
||||||
|
|
||||||
insertObject: (path, events) => h('button', {
|
insertObject: (path, events) => h('button', {
|
||||||
key: 'insertObject',
|
key: 'insertObject',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
// onClick: () => events.onRemove(path),
|
onClick: () => events.onInsert(path, 'Object'),
|
||||||
title: 'Insert Object'
|
title: 'Insert Object'
|
||||||
}, 'Insert Object'),
|
}, 'Insert Object'),
|
||||||
|
|
||||||
insertArray: (path, events) => h('button', {
|
insertArray: (path, events) => h('button', {
|
||||||
key: 'insertArray',
|
key: 'insertArray',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
// onClick: () => events.onRemove(path),
|
onClick: () => events.onInsert(path, 'Array'),
|
||||||
title: 'Insert Array'
|
title: 'Insert Array'
|
||||||
}, 'Insert Array'),
|
}, 'Insert Array'),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FloatingMenu extends PureComponent {
|
export default class FloatingMenu extends PureComponent {
|
||||||
componentDidMount () {
|
// TODO: use or cleanup
|
||||||
setTimeout(() => {
|
// componentDidMount () {
|
||||||
const firstButton = this.refs.root && this.refs.root.querySelector('button')
|
// setTimeout(() => {
|
||||||
if (firstButton) {
|
// const firstButton = this.refs.root && this.refs.root.querySelector('button')
|
||||||
firstButton.focus()
|
// // TODO: find a better way to ensure the JSONEditor has focus so the quickkeys work
|
||||||
}
|
// // console.log(document.activeElement)
|
||||||
})
|
// if (firstButton && document.activeElement === document.body) {
|
||||||
}
|
// firstButton.focus()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return h('div', {ref: 'root', className: MENU_CLASS_NAME}, this.props.items.map(item => {
|
const items = this.props.items.map(item => {
|
||||||
const type = typeof item === 'string' ? item : item.type
|
const type = typeof item === 'string' ? item : item.type
|
||||||
const createType = CREATE_TYPE[type]
|
const createType = CREATE_TYPE[type]
|
||||||
if (createType) {
|
if (createType) {
|
||||||
|
@ -135,6 +138,17 @@ export default class FloatingMenu extends PureComponent {
|
||||||
else {
|
else {
|
||||||
throw new Error('Unknown type of menu item for floating menu: ' + JSON.stringify(item))
|
throw new Error('Unknown type of menu item for floating menu: ' + JSON.stringify(item))
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
|
|
||||||
|
return h('div', {
|
||||||
|
// ref: 'root',
|
||||||
|
className: MENU_CLASS_NAME,
|
||||||
|
onMouseDown: this.handleTouchStart,
|
||||||
|
onTouchStart: this.handleTouchStart,
|
||||||
|
}, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTouchStart = (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
src/eson.js
40
src/eson.js
|
@ -363,18 +363,9 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = findSharedPath(selection.start.path, selection.end.path)
|
const rootPath = findRootPath(selection)
|
||||||
const rootEsonPath = toEsonPath(eson, rootPath)
|
const rootEsonPath = toEsonPath(eson, rootPath)
|
||||||
|
|
||||||
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
|
|
||||||
// select a single node
|
|
||||||
const selectionType = (selection.start.area === 'after') ? SELECTED_AFTER : SELECTED_END
|
|
||||||
console.log('selectionType', selectionType, selection)
|
|
||||||
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index
|
|
||||||
return setIn(eson, rootEsonPath.concat(['selected']), selectionType)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// select multiple childs of an object or array
|
|
||||||
return updateIn(eson, rootEsonPath, (root) => {
|
return updateIn(eson, rootEsonPath, (root) => {
|
||||||
const start = selection.start.path[rootPath.length]
|
const start = selection.start.path[rootPath.length]
|
||||||
const end = selection.end.path[rootPath.length]
|
const end = selection.end.path[rootPath.length]
|
||||||
|
@ -390,7 +381,6 @@ console.log('selectionType', selectionType, selection)
|
||||||
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
|
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the min and max index of a start and end child.
|
* Find the min and max index of a start and end child.
|
||||||
|
@ -413,15 +403,9 @@ export function findSelectionIndices (root: ESON, start: string, end: string) :
|
||||||
*/
|
*/
|
||||||
export function pathsFromSelection (eson: ESON, selection: ESONSelection): JSONPath[] {
|
export function pathsFromSelection (eson: ESON, selection: ESONSelection): 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 = findSharedPath(selection.start.path, selection.end.path)
|
const rootPath = findRootPath(selection)
|
||||||
const rootEsonPath = toEsonPath(eson, rootPath)
|
const rootEsonPath = toEsonPath(eson, rootPath)
|
||||||
|
|
||||||
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
|
|
||||||
// select a single node
|
|
||||||
return [ rootPath ]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// select multiple childs of an object or array
|
|
||||||
const root = getIn(eson, rootEsonPath)
|
const root = getIn(eson, rootEsonPath)
|
||||||
const start = selection.start.path[rootPath.length]
|
const start = selection.start.path[rootPath.length]
|
||||||
const end = selection.end.path[rootPath.length]
|
const end = selection.end.path[rootPath.length]
|
||||||
|
@ -434,7 +418,6 @@ export function pathsFromSelection (eson: ESON, selection: ESONSelection): JSONP
|
||||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the contents of a list with paths
|
* Get the contents of a list with paths
|
||||||
|
@ -448,10 +431,29 @@ export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
|
||||||
return {
|
return {
|
||||||
name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]),
|
name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]),
|
||||||
value: esonToJson(getIn(data, esonPath))
|
value: esonToJson(getIn(data, esonPath))
|
||||||
|
// FIXME: also store the type and expanded state
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the root path of a selection: the parent node shared by both start
|
||||||
|
* and end of the selection
|
||||||
|
* @param {Selection} selection
|
||||||
|
* @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)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return sharedPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the common path of two paths.
|
* Find the common path of two paths.
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import isEqual from 'lodash/isEqual'
|
import isEqual from 'lodash/isEqual'
|
||||||
import initial from 'lodash/initial'
|
import initial from 'lodash/initial'
|
||||||
|
|
||||||
import type { ESON, Path, ESONPatch, ESONPatchOptions, ESONPatchResult, ESONSelection } from './types'
|
import type { ESON, Path, ESONPatch } from './types'
|
||||||
import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutabilityHelpers'
|
import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutabilityHelpers'
|
||||||
import {
|
import {
|
||||||
jsonToEson, esonToJson, toEsonPath,
|
jsonToEson, esonToJson, toEsonPath,
|
||||||
parseJSONPointer, compileJSONPointer,
|
parseJSONPointer, compileJSONPointer,
|
||||||
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId,
|
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId
|
||||||
pathsFromSelection
|
|
||||||
} from './eson'
|
} from './eson'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue