Cut/copy/paste/insert/duplicate/remove mostly working for selections (WIP)
This commit is contained in:
parent
f1ddc03c6d
commit
ce37b88296
|
@ -39,7 +39,13 @@ const json = {
|
|||
'string': 'Hello World',
|
||||
'unicode': 'A unicode character: \u260E',
|
||||
'url': 'http://jsoneditoronline.org',
|
||||
'largeArray': [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
|
||||
'largeArray': [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
|
||||
'structureArray': [
|
||||
{name: 'Joe', age: 24},
|
||||
{name: 'Sarah', age: 28},
|
||||
{name: 'Brett', age: 21},
|
||||
{name: 'Emma', age: 31},
|
||||
]
|
||||
}
|
||||
|
||||
function expandAll (path) {
|
||||
|
|
|
@ -2,12 +2,12 @@ import last from 'lodash/last'
|
|||
import initial from 'lodash/initial'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import first from 'lodash/first'
|
||||
import { findRootPath, pathsFromSelection } from './eson'
|
||||
import { findRootPath } from './eson'
|
||||
import { getIn } from './utils/immutabilityHelpers'
|
||||
import { findUniqueName } from './utils/stringUtils'
|
||||
import { isObject, stringConvert } from './utils/typeUtils'
|
||||
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
||||
import { compileJSONPointer } from './jsonPointer'
|
||||
import { compileJSONPointer, parseJSONPointer } from './jsonPointer'
|
||||
|
||||
/**
|
||||
* Create a JSONPatch to change the value of a property or item
|
||||
|
@ -81,13 +81,13 @@ export function changeType (json, path, type) {
|
|||
*/
|
||||
export function duplicate (json, selection) {
|
||||
// console.log('duplicate', path)
|
||||
if (!selection.start || !selection.end) {
|
||||
if (isEmpty(selection.multi)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(json, rootPath)
|
||||
const paths = pathsFromSelection(json, selection)
|
||||
const paths = selection.multi.map(parseJSONPointer)
|
||||
|
||||
if (Array.isArray(root)) {
|
||||
const lastPath = last(paths)
|
||||
|
@ -224,9 +224,11 @@ export function insertInside (json, parentPath, values) {
|
|||
export function replace (json, selection, values) { // TODO: find a better name and define datastructure for values
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(json, rootPath)
|
||||
const paths = selection.multi
|
||||
? selection.multi.map(parseJSONPointer)
|
||||
: []
|
||||
|
||||
if (Array.isArray(root)) {
|
||||
const paths = pathsFromSelection(json, selection)
|
||||
const firstPath = first(paths)
|
||||
const offset = firstPath ? parseInt(last(firstPath), 10) : 0
|
||||
|
||||
|
@ -240,7 +242,7 @@ export function replace (json, selection, values) { // TODO: find a better name
|
|||
return removeActions.concat(insertActions)
|
||||
}
|
||||
else { // root is Object
|
||||
const removeActions = removeAll(pathsFromSelection(json, selection))
|
||||
const removeActions = removeAll(paths)
|
||||
const insertActions = values.map(entry => {
|
||||
const newProp = findUniqueName(entry.name, root)
|
||||
return {
|
||||
|
@ -293,7 +295,7 @@ export function append (json, parentPath, type) {
|
|||
/**
|
||||
* Create a JSONPatch for a remove action
|
||||
* @param {Path} path
|
||||
* @return {ESONPatchDocument}
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function remove (path) {
|
||||
return [{
|
||||
|
@ -305,7 +307,7 @@ export function remove (path) {
|
|||
/**
|
||||
* Create a JSONPatch for a multiple remove action
|
||||
* @param {Path[]} paths
|
||||
* @return {ESONPatchDocument}
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function removeAll (paths) {
|
||||
return paths
|
||||
|
|
|
@ -6,13 +6,20 @@ import naturalSort from 'javascript-natural-sort'
|
|||
|
||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||
import { getInnerText, insideRect } from '../utils/domUtils'
|
||||
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
||||
import { isUrl, stringConvert, valueType } from '../utils/typeUtils'
|
||||
import {
|
||||
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE,
|
||||
SELECTED_FIRST, SELECTED_LAST
|
||||
ERROR,
|
||||
EXPANDED,
|
||||
ID,
|
||||
SEARCH_PROPERTY,
|
||||
SEARCH_VALUE,
|
||||
SELECTED_AFTER, SELECTED_BEFORE_CHILDS,
|
||||
SELECTED_INSIDE,
|
||||
SELECTION,
|
||||
TYPE,
|
||||
VALUE
|
||||
} from '../eson'
|
||||
import { compileJSONPointer } from '../jsonPointer'
|
||||
import { ERROR, EXPANDED, ID, SEARCH_PROPERTY, SEARCH_VALUE, SELECTION, TYPE, VALUE } from '../eson'
|
||||
|
||||
import fontawesome from '@fortawesome/fontawesome'
|
||||
import faExclamationTriangle from '@fortawesome/fontawesome-free-solid/faExclamationTriangle'
|
||||
|
@ -45,9 +52,6 @@ export default class JSONNode extends PureComponent {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
menu: null, // can contain object {anchor, root}
|
||||
appendMenu: null, // can contain object {anchor, root}
|
||||
hover: null,
|
||||
path: null // initialized via getDerivedStateFromProps
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +110,8 @@ export default class JSONNode extends PureComponent {
|
|||
this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed')
|
||||
]
|
||||
: null,
|
||||
this.renderError(this.props.eson[ERROR])
|
||||
this.renderError(this.props.eson[ERROR]),
|
||||
this.renderBeforeChilds()
|
||||
])
|
||||
|
||||
let childs
|
||||
|
@ -155,7 +160,7 @@ export default class JSONNode extends PureComponent {
|
|||
'data-path': compileJSONPointer(this.state.path),
|
||||
'data-area': 'empty', // TODO: remove
|
||||
'data-selection-area': this.props.eson[EXPANDED] ? 'before-childs' : 'after',
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION]),
|
||||
// onMouseOver: this.handleMouseOver,
|
||||
// onMouseLeave: this.handleMouseLeave
|
||||
}, [nodeStart, childs, nodeEnd])
|
||||
|
@ -181,7 +186,8 @@ export default class JSONNode extends PureComponent {
|
|||
this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
||||
]
|
||||
: null,
|
||||
this.renderError(this.props.eson[ERROR])
|
||||
this.renderError(this.props.eson[ERROR]),
|
||||
this.renderBeforeChilds()
|
||||
])
|
||||
|
||||
let childs
|
||||
|
@ -230,7 +236,7 @@ export default class JSONNode extends PureComponent {
|
|||
'data-path': compileJSONPointer(this.state.path),
|
||||
'data-area': 'empty', // TODO: remove data-area
|
||||
'data-selection-area': this.props.eson[EXPANDED] ? 'before-childs' : 'after',
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION]),
|
||||
// onMouseOver: this.handleMouseOver,
|
||||
// onMouseLeave: this.handleMouseLeave
|
||||
}, [nodeStart, childs, nodeEnd])
|
||||
|
@ -256,7 +262,7 @@ export default class JSONNode extends PureComponent {
|
|||
'data-path': compileJSONPointer(this.state.path),
|
||||
'data-area': 'empty', // TODO: remove
|
||||
'data-selection-area': 'after',
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION]),
|
||||
// onMouseOver: this.handleMouseOver,
|
||||
// onMouseLeave: this.handleMouseLeave
|
||||
}, [node])
|
||||
|
@ -350,6 +356,14 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
renderBeforeChilds () {
|
||||
return h('div', {
|
||||
key: 'before-childs',
|
||||
className: 'jsoneditor-before-childs',
|
||||
'data-selection-area': 'before-childs'
|
||||
})
|
||||
}
|
||||
|
||||
renderSeparator() {
|
||||
const isProp = typeof this.props.prop === 'string'
|
||||
if (!isProp) {
|
||||
|
@ -423,33 +437,23 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
getContainerClassName (selected, hover) {
|
||||
let classNames = ['jsoneditor-node-container']
|
||||
getContainerClassName (selected) {
|
||||
let classNames = [
|
||||
'jsoneditor-node-container',
|
||||
// `jsoneditor-node-${this.props.eson[TYPE]}`
|
||||
this.props.eson[EXPANDED] ? 'jsoneditor-node-expanded' : 'jsoneditor-node-collapsed'
|
||||
]
|
||||
|
||||
if ((selected & SELECTED_INSIDE) !== 0) {
|
||||
classNames.push('jsoneditor-selected-insert-before')
|
||||
}
|
||||
else if ((selected & SELECTED_AFTER) !== 0) {
|
||||
classNames.push('jsoneditor-selected-insert-after')
|
||||
}
|
||||
else {
|
||||
if ((selected & SELECTED) !== 0) { classNames.push('jsoneditor-selected') }
|
||||
if ((selected & SELECTED_START) !== 0) { classNames.push('jsoneditor-selected-start') }
|
||||
if ((selected & SELECTED_END) !== 0) { classNames.push('jsoneditor-selected-end') }
|
||||
if ((selected & SELECTED_FIRST) !== 0) { classNames.push('jsoneditor-selected-first') }
|
||||
if ((selected & SELECTED_LAST) !== 0) { classNames.push('jsoneditor-selected-last') }
|
||||
if (selected === SELECTED_INSIDE) {
|
||||
classNames.push('jsoneditor-selected')
|
||||
}
|
||||
|
||||
if ((hover & SELECTED_INSIDE) !== 0) {
|
||||
classNames.push('jsoneditor-hover-insert-before')
|
||||
if (selected === SELECTED_AFTER) {
|
||||
classNames.push('jsoneditor-selected-after')
|
||||
}
|
||||
else if ((hover & SELECTED_AFTER) !== 0) {
|
||||
classNames.push('jsoneditor-hover-insert-after')
|
||||
}
|
||||
else {
|
||||
if ((hover & SELECTED) !== 0) { classNames.push('jsoneditor-hover') }
|
||||
if ((hover & SELECTED_START) !== 0) { classNames.push('jsoneditor-hover-start') }
|
||||
if ((hover & SELECTED_END) !== 0) { classNames.push('jsoneditor-hover-end') }
|
||||
|
||||
if (selected === SELECTED_BEFORE_CHILDS) {
|
||||
classNames.push('jsoneditor-selected-before-childs')
|
||||
}
|
||||
|
||||
return classNames.join(' ')
|
||||
|
@ -574,34 +578,6 @@ export default class JSONNode extends PureComponent {
|
|||
)
|
||||
}
|
||||
|
||||
handleMouseOver = (event) => {
|
||||
if (event.buttons === 0) { // no mouse button down, no dragging
|
||||
event.stopPropagation()
|
||||
|
||||
const hover = (event.target.className.indexOf('jsoneditor-insert-area') !== -1)
|
||||
? (SELECTED + SELECTED_AFTER)
|
||||
: SELECTED
|
||||
|
||||
if (hoveredNode && hoveredNode !== this) {
|
||||
// FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted?
|
||||
hoveredNode.setState({hover: null})
|
||||
}
|
||||
|
||||
if (hover !== this.state.hover) {
|
||||
this.setState({hover})
|
||||
hoveredNode = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave = (event) => {
|
||||
event.stopPropagation()
|
||||
// FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted?
|
||||
hoveredNode.setState({hover: null})
|
||||
|
||||
this.setState({hover: null})
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleChangeProperty = (event) => {
|
||||
const parentPath = initial(this.state.path)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { createElement as h, PureComponent } from 'react'
|
||||
import mitt from 'mitt'
|
||||
import cloneDeepWith from 'lodash/cloneDeepWith'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import first from 'lodash/first'
|
||||
import reverse from 'lodash/reverse'
|
||||
import initial from 'lodash/initial'
|
||||
import pick from 'lodash/pick'
|
||||
|
@ -8,7 +11,7 @@ import Hammer from 'react-hammerjs'
|
|||
import jump from '../assets/jump.js/src/jump'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { existsIn, setIn, updateIn } from '../utils/immutabilityHelpers'
|
||||
import { existsIn, getIn, setIn, updateIn } from '../utils/immutabilityHelpers'
|
||||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { enrichSchemaError } from '../utils/schemaUtils'
|
||||
import { compileJSONPointer, parseJSONPointer } from '../jsonPointer'
|
||||
|
@ -52,10 +55,10 @@ import {
|
|||
expand,
|
||||
EXPANDED,
|
||||
expandPath,
|
||||
findRootPath,
|
||||
findSharedPath,
|
||||
immutableESONPatch,
|
||||
nextSearchResult,
|
||||
pathsFromSelection,
|
||||
previousSearchResult,
|
||||
search,
|
||||
SELECTION,
|
||||
|
@ -273,7 +276,7 @@ export default class TreeMode extends PureComponent {
|
|||
|
||||
renderMenu () {
|
||||
const hasCursor = true // FIXME: implement hasCursor
|
||||
const hasSelection = !!this.state.selection
|
||||
const hasSelection = this.state.selection ? this.state.selection.type !== 'none' : false
|
||||
const hasClipboard = this.state.clipboard
|
||||
? this.state.clipboard.length > 0
|
||||
: false
|
||||
|
@ -295,7 +298,7 @@ export default class TreeMode extends PureComponent {
|
|||
canInsert: hasCursor,
|
||||
canDuplicate: hasSelection,
|
||||
canRemove: hasSelection,
|
||||
onInsert: this.handleInsertBefore,
|
||||
onInsert: this.handleInsert,
|
||||
onDuplicate: this.handleDuplicate,
|
||||
onRemove: this.handleRemove,
|
||||
|
||||
|
@ -428,10 +431,11 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
|
||||
handleRemove = () => {
|
||||
if (this.state.selection) {
|
||||
if (this.state.selection && this.state.selection.multi) {
|
||||
// remove selection
|
||||
// TODO: select next property? (same as when removing a path?)
|
||||
const paths = pathsFromSelection(this.state.eson, this.state.selection)
|
||||
// TODO: inefficient: first parsing the paths, and removeAll stringifies them again
|
||||
const paths = this.state.selection.multi.map(parseJSONPointer)
|
||||
this.setState({ selection: null })
|
||||
this.handlePatch(removeAll(paths))
|
||||
}
|
||||
|
@ -495,7 +499,7 @@ export default class TreeMode extends PureComponent {
|
|||
handleKeyDownDuplicate = (event) => {
|
||||
const path = this.findDataPathFromElement(event.target)
|
||||
if (path) {
|
||||
const selection = { start: path, end: path }
|
||||
const selection = { type: 'multi', multi: [path] }
|
||||
this.handlePatch(duplicate(this.state.eson, selection))
|
||||
|
||||
// apply focus to the duplicated node
|
||||
|
@ -520,9 +524,9 @@ export default class TreeMode extends PureComponent {
|
|||
|
||||
handleCut = () => {
|
||||
const selection = this.state.selection
|
||||
if (selection && selection.start && selection.end) {
|
||||
if (selection && selection.multi) {
|
||||
const eson = this.state.eson
|
||||
const paths = pathsFromSelection(eson, selection)
|
||||
const paths = selection.multi.map(parseJSONPointer)
|
||||
const clipboard = contentsFromPaths(eson, paths)
|
||||
|
||||
this.setState({ clipboard, selection: null })
|
||||
|
@ -540,9 +544,9 @@ export default class TreeMode extends PureComponent {
|
|||
|
||||
handleCopy = () => {
|
||||
const selection = this.state.selection
|
||||
if (selection && selection.start && selection.end) {
|
||||
if (selection && selection.multi) {
|
||||
const eson = this.state.eson
|
||||
const paths = pathsFromSelection(eson, selection)
|
||||
const paths = selection.multi.map(parseJSONPointer)
|
||||
const clipboard = contentsFromPaths(eson, paths)
|
||||
|
||||
this.setState({ clipboard })
|
||||
|
@ -559,14 +563,14 @@ export default class TreeMode extends PureComponent {
|
|||
if (selection && clipboard && clipboard.length > 0) {
|
||||
this.setState({ selection: null })
|
||||
|
||||
if (selection.start && selection.end) {
|
||||
if (selection.multi) {
|
||||
this.handlePatch(replace(eson, selection, clipboard))
|
||||
}
|
||||
else if (selection.after) {
|
||||
this.handlePatch(insertAfter(eson, selection.after, clipboard))
|
||||
this.handlePatch(insertAfter(eson, parseJSONPointer(selection.after), clipboard))
|
||||
}
|
||||
else if (selection.inside) {
|
||||
this.handlePatch(insertInside(eson, selection.inside, clipboard))
|
||||
else if (selection.type === 'before-childs') {
|
||||
this.handlePatch(insertInside(eson, parseJSONPointer(selection.beforeChildsOf), clipboard))
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot paste at current selection ${JSON.stringify(selection)}`)
|
||||
|
@ -579,9 +583,71 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleInsertBefore = (insertType) => {
|
||||
// FIXME: implement handleInsertBefore
|
||||
console.error('Insert not yet implemented...', insertType)
|
||||
handleInsert = (insertType) => {
|
||||
const { eson, selection } = this.state
|
||||
|
||||
if (selection) {
|
||||
this.setState({ selection: null })
|
||||
|
||||
const clipboard = [{
|
||||
name: '',
|
||||
value: this.createValue(insertType, selection)
|
||||
}]
|
||||
|
||||
if (selection.multi) {
|
||||
this.handlePatch(replace(eson, selection, clipboard))
|
||||
}
|
||||
else if (selection.after) {
|
||||
this.handlePatch(insertAfter(eson, parseJSONPointer(selection.after), clipboard))
|
||||
|
||||
}
|
||||
else if (selection.beforeChildsOf) {
|
||||
this.handlePatch(insertInside(eson, parseJSONPointer(selection.beforeChildsOf), clipboard))
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot insert at current selection ${JSON.stringify(selection)}`)
|
||||
}
|
||||
|
||||
// TODO: expand the inserted contents when array/object/structure
|
||||
// TODO: select the inserted contents
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an eson value
|
||||
* @param insertType
|
||||
* @param selection
|
||||
* @returns {*}
|
||||
*/
|
||||
createValue = (insertType, selection) => {
|
||||
if (insertType === 'array') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (insertType === 'object') {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (insertType === 'structure') {
|
||||
const rootPath = findRootPath(selection)
|
||||
const parent = getIn(this.state.json, rootPath)
|
||||
|
||||
if (Array.isArray(parent) && !isEmpty(parent)) {
|
||||
const jsonExample = first(parent)
|
||||
const structure = cloneDeepWith(jsonExample, (value) => {
|
||||
return (Array.isArray(value) || typeof value === 'object')
|
||||
? undefined // leave as is
|
||||
: ''
|
||||
})
|
||||
|
||||
console.log('structure', jsonExample, structure)
|
||||
|
||||
return structure
|
||||
}
|
||||
}
|
||||
|
||||
// value or unknown type
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -662,13 +728,15 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
|
||||
toggleSearch = () => {
|
||||
this.setState({
|
||||
showSearch: !this.state.showSearch
|
||||
})
|
||||
if (this.state.showSearch) {
|
||||
this.handleCloseSearch()
|
||||
}
|
||||
else {
|
||||
this.setState({ showSearch: true })
|
||||
}
|
||||
}
|
||||
|
||||
handleSearch = (text) => {
|
||||
// FIXME
|
||||
// FIXME: also apply search when eson is changed
|
||||
const { eson, searchResult } = search(this.state.eson, text)
|
||||
if (searchResult.matches.length > 0) {
|
||||
|
@ -786,47 +854,16 @@ export default class TreeMode extends PureComponent {
|
|||
|
||||
this.selectionStartPointer = this.findSelectionPointerFromEvent(event.target, event.clientY)
|
||||
|
||||
console.log('selectionPointer', this.selectionStartPointer)
|
||||
|
||||
const pointer = this.findJSONPointerFromElement(event.target)
|
||||
const clickedOnEmptySpace = (event.target.nodeName === 'DIV') &&
|
||||
(event.target.contentEditable !== 'true') &&
|
||||
(event.target.className !== 'jsoneditor-tag') // FIXME: this is an ugly hack to prevent an object/array from being selected when expanding it by clicking the tag
|
||||
|
||||
// TODO: cleanup
|
||||
// console.log('handleTouchStart', clickedOnEmptySpace && pointer, pointer && this.selectionFromJSONPointer(pointer))
|
||||
|
||||
if (clickedOnEmptySpace && pointer) {
|
||||
this.setState({ selection: this.selectionFromJSONPointer(pointer)})
|
||||
}
|
||||
else {
|
||||
this.setState({ selection: null })
|
||||
}
|
||||
}
|
||||
|
||||
handlePan = (event) => {
|
||||
this.selectionEndPointer = this.findSelectionPointerFromEvent(event.target, event.center.y)
|
||||
|
||||
const selection2 = this.findSelectionFromPointers(this.selectionStartPointer, this.selectionEndPointer)
|
||||
console.log('selection', JSON.stringify(selection2))
|
||||
|
||||
const selection = this.state.selection
|
||||
const path = this.findDataPathFromElement(event.target.firstChild)
|
||||
if (path && selection && !isEqual(path, selection.end)) {
|
||||
|
||||
// TODO: cleanup
|
||||
// console.log('handlePan', {
|
||||
// start: selection.start || selection.inside || selection.after || selection.empty || selection.emptyBefore,
|
||||
// end: path
|
||||
// })
|
||||
|
||||
// FIXME: when selection.empty, start should be set to the next node
|
||||
this.setState({
|
||||
selection: {
|
||||
start: selection.start || selection.inside || selection.after || selection.empty || selection.emptyBefore,
|
||||
end: path
|
||||
}
|
||||
})
|
||||
const selection = this.findSelectionFromPointers(this.selectionStartPointer, this.selectionEndPointer)
|
||||
if (!isEqual(selection, this.state.selection)) {
|
||||
this.setState({ selection })
|
||||
console.log('selection', JSON.stringify(selection)) // TODO: cleanup logging
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -891,6 +928,7 @@ export default class TreeMode extends PureComponent {
|
|||
/**
|
||||
* @param {SelectionPointer} start
|
||||
* @param {SelectionPointer} end
|
||||
* @return {Selection}
|
||||
*/
|
||||
findSelectionFromPointers (start, end) {
|
||||
if (start && end) {
|
||||
|
@ -909,6 +947,10 @@ export default class TreeMode extends PureComponent {
|
|||
return { type: 'after', after: compileJSONPointer(sharedPath) }
|
||||
}
|
||||
|
||||
if (start.area === 'before-childs' && end.area === 'before-childs' && start.path === end.path) {
|
||||
return { type: 'before-childs', beforeChildsOf: compileJSONPointer(sharedPath) }
|
||||
}
|
||||
|
||||
if (start.path !== end.path || start.area !== end.area || start.area === 'inside' || end.area === 'inside') {
|
||||
return { type: 'multi', multi: [ compileJSONPointer(sharedPath) ] }
|
||||
}
|
||||
|
@ -934,6 +976,7 @@ export default class TreeMode extends PureComponent {
|
|||
if (firstIndex < lastIndex) {
|
||||
return {
|
||||
type: 'multi',
|
||||
after: includeFirst ? undefined : first.path,
|
||||
multi: childs
|
||||
.slice(firstIndex, lastIndex)
|
||||
.map(element => element.getAttribute('data-path'))
|
||||
|
@ -943,7 +986,7 @@ export default class TreeMode extends PureComponent {
|
|||
// selection starts after first node and ends before last node
|
||||
return {
|
||||
type: 'after',
|
||||
path: first.path
|
||||
after: first.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -294,52 +294,7 @@ div.jsoneditor-menu-panel-right {
|
|||
|
||||
div.jsoneditor-node-container {
|
||||
position: relative;
|
||||
transition: background-color 100ms ease-in;
|
||||
|
||||
// TODO: cleanup insert-area css?
|
||||
div.jsoneditor-insert-area {
|
||||
//background: gray;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: $insert-area-height;
|
||||
left: 0;
|
||||
top: -$insert-area-height/2;
|
||||
box-sizing: border-box;
|
||||
z-index: 1; // must be on top of next node, it overlaps a bit
|
||||
|
||||
$top: -$line-height / 2 + 3px;
|
||||
|
||||
// FIXME: nice border color
|
||||
//&:before {
|
||||
// content: '';
|
||||
// position: absolute;
|
||||
// right: 60px;
|
||||
// top: $top;
|
||||
// width: 0;
|
||||
// height: 0;
|
||||
// border-top: $line-height / 2 solid transparent;
|
||||
// border-right: $line-height / 2 solid red;
|
||||
// border-bottom: $line-height / 2 solid transparent;
|
||||
//}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: $top;
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: $line-height;
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
//&.jsoneditor-hover {
|
||||
// > .jsoneditor-node > .jsoneditor-delimiter-start,
|
||||
// > .jsoneditor-delimiter-end {
|
||||
// color: $theme-color;
|
||||
// font-weight: bold;
|
||||
// }
|
||||
//}
|
||||
//transition: background-color 100ms ease-in;
|
||||
|
||||
&.jsoneditor-selected {
|
||||
.jsoneditor-node {
|
||||
|
@ -356,6 +311,35 @@ div.jsoneditor-node-container {
|
|||
}
|
||||
}
|
||||
|
||||
&.jsoneditor-selected-after {
|
||||
|
||||
&.jsoneditor-node-expanded {
|
||||
.jsoneditor-node-end {
|
||||
background-color: $selectedColor;
|
||||
|
||||
> .jsoneditor-delimiter-end {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.jsoneditor-node-collapsed {
|
||||
background-color: $selectedColor;
|
||||
|
||||
> .jsoneditor-node {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.jsoneditor-selected-before-childs {
|
||||
|
||||
> .jsoneditor-node > .jsoneditor-before-childs {
|
||||
background-color: $selectedColor;
|
||||
width: 40px; // FIXME: should use full remaining width
|
||||
}
|
||||
}
|
||||
|
||||
&.jsoneditor-hover {
|
||||
> .jsoneditor-node > .jsoneditor-button-container,
|
||||
> .jsoneditor-node > .jsoneditor-button-placeholder {
|
||||
|
|
|
@ -14,6 +14,6 @@ $hoverColor: #d3d3d3;
|
|||
$hoverAndSelectedColor: #ffdb80;
|
||||
$warning-color: #FBB917;
|
||||
$gray: #9d9d9d;
|
||||
$gray-icon: #5e5e5e;
|
||||
$gray-icon: $gray;
|
||||
$light-gray: #c0c0c0;
|
||||
$input-padding: 5px;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { deleteIn, getIn, setIn, shallowCloneWithSymbols, transform, updateIn } from './utils/immutabilityHelpers'
|
||||
import range from 'lodash/range'
|
||||
import { deleteIn, getIn, setIn, transform } from './utils/immutabilityHelpers'
|
||||
import { compileJSONPointer, parseJSONPointer } from './jsonPointer'
|
||||
import first from 'lodash/first'
|
||||
import last from 'lodash/last'
|
||||
import initial from 'lodash/initial'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import naturalSort from 'javascript-natural-sort'
|
||||
import times from 'lodash/times'
|
||||
import { immutableJSONPatch } from './immutableJSONPatch'
|
||||
import { compareArrays } from './utils/arrayUtils'
|
||||
import { compareStrings } from './utils/stringUtils'
|
||||
|
@ -27,8 +25,7 @@ export const SELECTED_FIRST = 8
|
|||
export const SELECTED_LAST = 16
|
||||
export const SELECTED_INSIDE = 32
|
||||
export const SELECTED_AFTER = 64
|
||||
export const SELECTED_EMPTY = 128
|
||||
export const SELECTED_EMPTY_BEFORE = 256
|
||||
export const SELECTED_BEFORE_CHILDS = 128
|
||||
|
||||
// TODO: comment
|
||||
export function syncEson(json, eson) {
|
||||
|
@ -355,87 +352,34 @@ function setSearchStatus (eson, esonPointer, searchStatus) {
|
|||
* @return {ESON} Returns updated eson object
|
||||
*/
|
||||
export function applySelection (eson, selection) {
|
||||
if (!selection) {
|
||||
return cleanupMetaData(eson, 'selected')
|
||||
if (selection && selection.type === 'after') {
|
||||
const updatedEson = setIn(eson, parseJSONPointer(selection.after).concat([SELECTION]), SELECTED_AFTER)
|
||||
return cleanupMetaData(updatedEson, SELECTION, [selection.after])
|
||||
}
|
||||
else if (selection.inside) {
|
||||
const updatedEson = setIn(eson, selection.inside.concat([SELECTION]), SELECTED_INSIDE)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.inside])
|
||||
|
||||
if (selection && selection.type === 'before-childs') {
|
||||
const updatedEson = setIn(eson, parseJSONPointer(selection.beforeChildsOf).concat([SELECTION]), SELECTED_BEFORE_CHILDS)
|
||||
return cleanupMetaData(updatedEson, SELECTION, [selection.beforeChildsOf])
|
||||
}
|
||||
else if (selection.after) {
|
||||
const updatedEson = setIn(eson, selection.after.concat([SELECTION]), SELECTED_AFTER)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.after])
|
||||
|
||||
if (selection && selection.type === 'multi') {
|
||||
let updatedEson = eson
|
||||
|
||||
if (selection.after) {
|
||||
updatedEson = setIn(updatedEson, parseJSONPointer(selection.after).concat([SELECTION]), SELECTED_AFTER)
|
||||
}
|
||||
else if (selection.empty) {
|
||||
const updatedEson = setIn(eson, selection.empty.concat([SELECTION]), SELECTED_EMPTY)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.empty])
|
||||
|
||||
for (const path of selection.multi) {
|
||||
updatedEson = setIn(updatedEson, parseJSONPointer(path).concat([SELECTION]), SELECTED_INSIDE)
|
||||
}
|
||||
else if (selection.emptyBefore) {
|
||||
const updatedEson = setIn(eson, selection.emptyBefore.concat([SELECTION]), SELECTED_EMPTY_BEFORE)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.emptyBefore])
|
||||
|
||||
const ignorePaths = selection.after
|
||||
? selection.multi.concat([selection.after])
|
||||
: selection.multi
|
||||
return cleanupMetaData(updatedEson, SELECTION, ignorePaths)
|
||||
}
|
||||
else { // selection.start and selection.end
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findRootPath(selection)
|
||||
let selectedPaths = null
|
||||
|
||||
const updatedEson = updateIn(eson, rootPath, (root) => {
|
||||
const start = selection.start[rootPath.length]
|
||||
const end = selection.end[rootPath.length]
|
||||
|
||||
// TODO: simplify the update function. Use pathsFromSelection ?
|
||||
|
||||
if (root[TYPE] === 'object') {
|
||||
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||
const startIndex = props.indexOf(start)
|
||||
const endIndex = props.indexOf(end)
|
||||
|
||||
const firstIndex = Math.min(startIndex, endIndex)
|
||||
const lastIndex = Math.max(startIndex, endIndex)
|
||||
const firstProp = props[firstIndex]
|
||||
const lastProp = props[lastIndex]
|
||||
|
||||
const selectedProps = props.slice(firstIndex, lastIndex + 1)// include max index itself
|
||||
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
|
||||
let updatedObj = shallowCloneWithSymbols(root)
|
||||
selectedProps.forEach(prop => {
|
||||
const selected = SELECTED +
|
||||
(prop === start ? SELECTED_START : 0) +
|
||||
(prop === end ? SELECTED_END : 0) +
|
||||
(prop === firstProp ? SELECTED_FIRST : 0) +
|
||||
(prop === lastProp ? SELECTED_LAST : 0)
|
||||
updatedObj[prop] = setIn(updatedObj[prop], [SELECTION], selected)
|
||||
})
|
||||
|
||||
return updatedObj
|
||||
}
|
||||
else { // root[TYPE] === 'array'
|
||||
const startIndex = parseInt(start, 10)
|
||||
const endIndex = parseInt(end, 10)
|
||||
|
||||
const firstIndex = Math.min(startIndex, endIndex)
|
||||
const lastIndex = Math.max(startIndex, endIndex)
|
||||
|
||||
const selectedIndices = range(firstIndex, lastIndex + 1)// include max index itself
|
||||
selectedPaths = selectedIndices.map(index => rootPath.concat(String(index)))
|
||||
|
||||
let updatedArr = root.slice()
|
||||
updatedArr = shallowCloneWithSymbols(root)
|
||||
selectedIndices.forEach(index => {
|
||||
const selected = SELECTED +
|
||||
(index === startIndex ? SELECTED_START : 0) +
|
||||
(index === endIndex ? SELECTED_END : 0) +
|
||||
(index === firstIndex ? SELECTED_FIRST : 0) +
|
||||
(index === lastIndex ? SELECTED_LAST : 0)
|
||||
updatedArr[index] = setIn(updatedArr[index], [SELECTION], selected)
|
||||
})
|
||||
|
||||
return updatedArr
|
||||
}
|
||||
})
|
||||
|
||||
return cleanupMetaData(updatedEson, 'selected', selectedPaths)
|
||||
}
|
||||
return cleanupMetaData(eson, SELECTION)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -462,30 +406,44 @@ export function contentsFromPaths (eson, paths) {
|
|||
* @return {Path}
|
||||
*/
|
||||
export function findRootPath(selection) {
|
||||
if (selection.inside) {
|
||||
return initial(selection.inside)
|
||||
}
|
||||
else if (selection.after) {
|
||||
return initial(selection.after)
|
||||
}
|
||||
else if (selection.empty) {
|
||||
return initial(selection.empty)
|
||||
}
|
||||
else if (selection.emptyBefore) {
|
||||
return initial(selection.emptyBefore)
|
||||
}
|
||||
else { // selection.start and selection.end
|
||||
const sharedPath = findSharedPath(selection.start, selection.end)
|
||||
if (selection.multi) {
|
||||
const firstPath = parseJSONPointer(first(selection.multi))
|
||||
|
||||
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
|
||||
return initial(firstPath)
|
||||
}
|
||||
|
||||
if (selection.after) {
|
||||
return initial(parseJSONPointer(selection.after))
|
||||
}
|
||||
|
||||
// TODO: handle area === 'before-childs' and area === 'after-childs'
|
||||
|
||||
|
||||
// TODO: cleanup
|
||||
// if (selection.inside) {
|
||||
// return initial(selection.inside)
|
||||
// }
|
||||
// else if (selection.after) {
|
||||
// return initial(selection.after)
|
||||
// }
|
||||
// else if (selection.empty) {
|
||||
// return initial(selection.empty)
|
||||
// }
|
||||
// else if (selection.emptyBefore) {
|
||||
// return initial(selection.emptyBefore)
|
||||
// }
|
||||
// else { // selection.start and selection.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
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -504,43 +462,6 @@ export function findSharedPath (path1, path2) {
|
|||
return path1.slice(0, i)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON paths from a selection, sorted from first to last
|
||||
* @param {ESON} eson
|
||||
* @param {Selection} selection
|
||||
* @return {Path[]}
|
||||
*/
|
||||
// TODO: move pathsFromSelection to a separate file clipboard.js or selection.js?
|
||||
export function pathsFromSelection (eson, selection) {
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(eson, rootPath)
|
||||
|
||||
const start = (selection.after || selection.inside || selection.start)[rootPath.length]
|
||||
const end = (selection.after || selection.inside || selection.end)[rootPath.length]
|
||||
|
||||
if (getType(root) === 'object') {
|
||||
// TODO: create a util function getSortedProps, cache results?
|
||||
const props = Object.keys(root).sort(naturalSort)
|
||||
const startIndex = props.indexOf(start)
|
||||
const endIndex = props.indexOf(end)
|
||||
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex) + ((selection.after || selection.inside) ? 0 : 1) // include max index itself
|
||||
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(props[i + minIndex]))
|
||||
}
|
||||
else { // root[TYPE] === 'array'
|
||||
const startIndex = parseInt(start, 10)
|
||||
const endIndex = parseInt(end, 10)
|
||||
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex) + ((selection.after || selection.inside) ? 0 : 1) // include max index itself
|
||||
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a JSON patch document to an ESON object.
|
||||
* - Applies meta information to added values
|
||||
|
@ -557,7 +478,11 @@ export function immutableESONPatch (eson, operations) {
|
|||
})
|
||||
}
|
||||
|
||||
// TODO: comment
|
||||
/**
|
||||
* Get the JSON type of any input
|
||||
* @param {*} any
|
||||
* @returns {string} Returns 'array', 'object', 'value', or 'undefined'
|
||||
*/
|
||||
export function getType (any) {
|
||||
if (any === undefined) {
|
||||
return 'undefined'
|
||||
|
|
|
@ -346,6 +346,7 @@ test('previousSearchResult', () => {
|
|||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
})
|
||||
|
||||
// FIXME: test selection
|
||||
test('selection (object)', () => {
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
|
@ -380,6 +381,7 @@ test('selection (object)', () => {
|
|||
assertEqualEson(actual2, expected2)
|
||||
})
|
||||
|
||||
// FIXME: test selection
|
||||
test('selection (array)', () => {
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
|
@ -405,6 +407,7 @@ test('selection (array)', () => {
|
|||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
// FIXME: test selection
|
||||
test('selection (value)', () => {
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
|
@ -425,6 +428,7 @@ test('selection (value)', () => {
|
|||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
// FIXME: test selection
|
||||
test('selection (node)', () => {
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
|
@ -444,78 +448,3 @@ test('selection (node)', () => {
|
|||
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
||||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
test('pathsFromSelection (object)', () => {
|
||||
const json = {
|
||||
"bool": false,
|
||||
"nill": null,
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
}
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '2', 'last'],
|
||||
end: ['nill']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(json, selection)).toEqual([
|
||||
['nill'],
|
||||
['obj']
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (array)', () => {
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
}
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '1'],
|
||||
end: ['obj', 'arr', '0'] // note the "backward" order of start and end
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(json, selection)).toEqual([
|
||||
['obj', 'arr', '0'],
|
||||
['obj', 'arr', '1']
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (value)', () => {
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
}
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '2', 'first'],
|
||||
end: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(json, selection)).toEqual([
|
||||
['obj', 'arr', '2', 'first'],
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (after)', () => {
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
}
|
||||
const selection = {
|
||||
after: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(json, selection)).toEqual([])
|
||||
})
|
||||
|
|
|
@ -30,10 +30,10 @@
|
|||
|
||||
/**
|
||||
* @typedef {{
|
||||
* start?: Path,
|
||||
* end?: Path,
|
||||
* before?: Path,
|
||||
* after?: Path,
|
||||
* type: 'multi' | 'after' | 'before-childs', 'none'
|
||||
* after? string
|
||||
* multi?: string[]
|
||||
* beforeChildsOf?: string
|
||||
* }} Selection
|
||||
*/
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ID } from '../eson'
|
||||
import { EXPANDED, ID, SELECTION, TYPE } from '../eson'
|
||||
import { deleteIn, transform } from './immutabilityHelpers'
|
||||
|
||||
export function createAssertEqualEson(expect) {
|
||||
|
@ -17,6 +17,10 @@ export function createAssertEqualEson(expect) {
|
|||
else {
|
||||
expect(actual).toEqual(expected)
|
||||
}
|
||||
|
||||
expect(actual[TYPE]).toEqual(expected[TYPE])
|
||||
expect(actual[SELECTION]).toEqual(expected[SELECTION])
|
||||
expect(actual[EXPANDED]).toEqual(expected[EXPANDED])
|
||||
}
|
||||
|
||||
function stripIdSymbols (eson) {
|
||||
|
|
Loading…
Reference in New Issue