All floating menus working now

This commit is contained in:
jos 2018-01-10 22:11:23 +01:00
parent 7cdfb44345
commit 1127c15ea2
7 changed files with 241 additions and 83 deletions

View File

@ -173,6 +173,74 @@ export function insertBefore (eson, path, values) { // TODO: find a better name
}
}
/**
* 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} eson
* @param {Path} path
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
* @return {Array}
*/
export function insertAfter (eson, path, values) { // TODO: find a better name and define datastructure for values
const parentPath = initial(path)
const parent = getIn(eson, parentPath)
if (parent[META].type === 'Array') {
const startIndex = parseInt(last(path), 10)
return values.map((entry, offset) => ({
op: 'add',
path: compileJSONPointer(parentPath.concat(startIndex + 1 + offset)), // +1 to insert after
value: entry.value,
meta: {
type: entry.type
}
}))
}
else { // parent[META].type === 'Object'
const prop = last(path)
const propIndex = parent[META].props.indexOf(prop)
const before = parent[META].props[propIndex + 1]
return values.map(entry => {
const newProp = findUniqueName(entry.name, parent[META].props)
return {
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value: entry.value,
meta: {
type: entry.type,
before
}
}
})
}
}
/**
* Insert values at the start of an Object or Array
* @param {ESON} eson
* @param {Path} parentPath
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
* @return {Array}
*/
export function insertInside (eson, parentPath, values) {
const parent = getIn(eson, parentPath)
if (parent[META].type === 'Array') {
return insertBefore(eson, parentPath.concat('0'), values)
}
else if (parent[META].type === 'Object') {
const firstProp = parent[META].props[0] || null
return insertBefore(eson, parentPath.concat(firstProp), values)
}
else {
throw new Error('Cannot insert in a value, only in an Object or Array')
}
}
/**
* Create a JSONPatch for an insert action.
*

View File

@ -9,45 +9,10 @@ import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import {
compileJSONPointer,
META,
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE,
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE,
SELECTED_FIRST, SELECTED_LAST
} from '../eson'
const MENU_ITEMS_OBJECT = [
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
const MENU_ITEMS_ARRAY = [
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
const MENU_ITEMS_VALUE = [
// {text: 'String', onClick: this.props.emit('changeType', {type: 'checkbox', checked: false}}),
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
const MENU_ITEMS_INSERT_BEFORE = [
{type: 'insertStructure'},
{type: 'insertValue'},
{type: 'insertObject'},
{type: 'insertArray'},
{type: 'paste'},
]
export default class JSONNode extends PureComponent {
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
@ -139,7 +104,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_OBJECT, meta.selected)
const floatingMenu = this.renderFloatingMenu('Object', meta.selected)
const nodeEnd = meta.expanded
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
this.renderDelimiter('}', 'jsoneditor-delimiter-end'),
@ -201,7 +166,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_ARRAY, meta.selected)
const floatingMenu = this.renderFloatingMenu('Array', meta.selected)
const nodeEnd = meta.expanded
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
this.renderDelimiter(']', 'jsoneditor-delimiter-end'),
@ -233,7 +198,7 @@ export default class JSONNode extends PureComponent {
this.renderError(meta.error)
])
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_VALUE, meta.selected)
const floatingMenu = this.renderFloatingMenu('value', meta.selected)
// const insertArea = this.renderInsertBeforeArea()
@ -251,7 +216,7 @@ export default class JSONNode extends PureComponent {
key: 'insert',
className: 'jsoneditor-insert jsoneditor-insert-before',
title: 'Insert a new item or paste clipboard',
'data-area': 'before'
'data-area': 'inside'
})
}
@ -276,7 +241,7 @@ export default class JSONNode extends PureComponent {
className: 'jsoneditor-node',
onKeyDown: this.handleKeyDownAppend
}, [
this.renderPlaceholder('before'),
this.renderPlaceholder('inside'),
this.renderReadonly(text)
])
}
@ -289,7 +254,7 @@ export default class JSONNode extends PureComponent {
})
}
renderReadonly (text, title = null, dataArea = 'before') {
renderReadonly (text, title = null, dataArea = 'inside') {
return h('div', {
key: 'readonly',
'data-area': dataArea,
@ -419,7 +384,7 @@ export default class JSONNode extends PureComponent {
getContainerClassName (selected, hover) {
let classNames = ['jsoneditor-node-container']
if ((selected & SELECTED_BEFORE) !== 0) {
if ((selected & SELECTED_INSIDE) !== 0) {
classNames.push('jsoneditor-selected-insert-before')
}
else if ((selected & SELECTED_AFTER) !== 0) {
@ -433,7 +398,7 @@ export default class JSONNode extends PureComponent {
if ((selected & SELECTED_LAST) !== 0) { classNames.push('jsoneditor-selected-last') }
}
if ((hover & SELECTED_BEFORE) !== 0) {
if ((hover & SELECTED_INSIDE) !== 0) {
classNames.push('jsoneditor-hover-insert-before')
}
else if ((hover & SELECTED_AFTER) !== 0) {
@ -560,8 +525,10 @@ export default class JSONNode extends PureComponent {
)
}
renderFloatingMenu (items, selected) {
if ((selected & SELECTED_END) === 0) {
renderFloatingMenu (type, selected) {
if (((selected & SELECTED_END) === 0) &&
((selected & SELECTED_INSIDE) === 0) &&
((selected & SELECTED_AFTER) === 0)) {
return null
}
@ -572,11 +539,68 @@ export default class JSONNode extends PureComponent {
key: 'floating-menu',
path: this.props.value[META].path,
emit: this.props.emit,
items,
items: this.getFloatingMenuItems(type, selected),
position: isLastOfMultiple ? 'bottom' : 'top'
})
}
getFloatingMenuItems (type, selected) {
if ((selected & SELECTED_AFTER) !== 0) {
return [
{type: 'insertStructureAfter'},
{type: 'insertValueAfter'},
{type: 'insertObjectAfter'},
{type: 'insertArrayAfter'},
{type: 'paste'},
]
}
if ((selected & SELECTED_INSIDE) !== 0) {
return [
{type: 'insertStructureInside'},
{type: 'insertValueInside'},
{type: 'insertObjectInside'},
{type: 'insertArrayInside'},
{type: 'paste'},
]
}
if (type === 'Object') {
return [
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
}
if (type === 'Array') {
return [
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
}
if (type === 'value') {
return [
// {text: 'String', onClick: this.props.emit('changeType', {type: 'checkbox', checked: false}}),
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]
}
throw new Error(`Cannot create FloatingMenu items for type: ${type}, selected: ${selected})`)
}
handleMouseOver = (event) => {
if (event.buttons === 0) { // no mouse button down, no dragging
event.stopPropagation()

View File

@ -21,7 +21,7 @@ import {
} from '../eson'
import { patchEson } from '../patchEson'
import {
duplicate, insertBefore, append, remove, removeAll, replace,
duplicate, insertBefore, insertAfter, insertInside, append, remove, removeAll, replace,
createEntry, changeType, changeValue, changeProperty, sort
} from '../actions'
import JSONNode from './JSONNode'
@ -86,7 +86,8 @@ export default class TreeMode extends PureComponent {
this.emitter.on('changeProperty', this.handleChangeProperty)
this.emitter.on('changeValue', this.handleChangeValue)
this.emitter.on('changeType', this.handleChangeType)
this.emitter.on('insert', this.handleInsert)
this.emitter.on('insertAfter', this.handleInsertAfter)
this.emitter.on('insertInside', this.handleInsertInside)
this.emitter.on('insertStructure', this.handleInsertStructure)
this.emitter.on('append', this.handleAppend)
this.emitter.on('duplicate', this.handleDuplicate)
@ -98,7 +99,6 @@ export default class TreeMode extends PureComponent {
this.emitter.on('expand', this.handleExpand)
this.emitter.on('select', this.handleSelect)
this.state = {
json,
eson,
@ -355,17 +355,30 @@ export default class TreeMode extends PureComponent {
this.handlePatch(changeType(this.state.eson, path, type))
}
handleInsert = ({path, type}) => {
this.handlePatch(insertBefore(this.state.eson, path, [{
handleInsertInside = ({path, type}) => {
this.handlePatch(insertInside(this.state.eson, path, [{
type,
name: '',
value: createEntry(type)
value: createEntry(type) // TODO: give objects and arrays an expanded state
}]))
this.setState({ selection : null }) // TODO: select the inserted entry
// apply focus to new node
this.focusToPrevious(path)
this.focusToNext(path)
}
handleInsertAfter = ({path, type}) => {
this.handlePatch(insertAfter(this.state.eson, path, [{
type,
name: '',
value: createEntry(type) // TODO: give objects and arrays an expanded state
}]))
this.setState({ selection : null }) // TODO: select the inserted entry
// apply focus to new node
this.focusToNext(path)
}
handleInsertStructure = ({path}) => {
@ -519,7 +532,20 @@ export default class TreeMode extends PureComponent {
if (selection && clipboard && clipboard.length > 0) {
this.setState({ selection: null })
if (selection.start && selection.end) {
this.handlePatch(replace(eson, selection, clipboard))
}
else if (selection.after) {
this.handlePatch(insertAfter(eson, selection.after, clipboard))
}
else if (selection.inside) {
this.handlePatch(insertInside(eson, selection.inside, clipboard))
}
else {
throw new Error(`Cannot paste at current selection ${JSON.stringify(selection)}`)
}
// TODO: select the pasted contents
}
}
@ -713,14 +739,14 @@ export default class TreeMode extends PureComponent {
// TODO: cleanup
// console.log('handlePan', {
// start: selection.start || selection.before || selection.after || selection.empty || selection.emptyBefore,
// 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.before || selection.after || selection.empty || selection.emptyBefore,
start: selection.start || selection.inside || selection.after || selection.empty || selection.emptyBefore,
end: path
}
})
@ -775,8 +801,8 @@ export default class TreeMode extends PureComponent {
if (pointer.area === 'after') {
return {after: pointer.path}
}
else if (pointer.area === 'before') {
return {before: pointer.path}
else if (pointer.area === 'inside') {
return {inside: pointer.path}
}
else if (pointer.area === 'empty') {
return {empty: pointer.path}

View File

@ -174,7 +174,8 @@
.jsoneditor-node-end .jsoneditor-button-container:hover {
visibility: visible; }
.jsoneditor-root > .jsoneditor-node-container > .jsoneditor-node-end > .jsoneditor-button-container {
.jsoneditor-root > .jsoneditor-node-container > .jsoneditor-node-end > .jsoneditor-button-container,
.jsoneditor-root > .jsoneditor-node-container > .jsoneditor-node-end > div.jsoneditor-insert {
display: none; }
.jsoneditor-node > div {
@ -416,6 +417,9 @@ div.jsoneditor-node-container {
div.jsoneditor-node-container.jsoneditor-selected-insert-before > .jsoneditor-node-end > .jsoneditor-insert-before {
background-image: url("img/jsoneditor-icons.svg");
background-position: -2px -2px !important; }
div.jsoneditor-node-container.jsoneditor-selected-insert-before > .jsoneditor-list {
border-top: 1px dashed #c0c0c0;
margin-top: -1px; }
div.jsoneditor-node-container.jsoneditor-hover > .jsoneditor-node > .jsoneditor-button-container,
div.jsoneditor-node-container.jsoneditor-hover > .jsoneditor-node > .jsoneditor-button-placeholder {
background-color: #d3d3d3; }

View File

@ -136,8 +136,11 @@ $insert-area-height: 6px;
}
}
.jsoneditor-root > .jsoneditor-node-container > .jsoneditor-node-end > .jsoneditor-button-container {
.jsoneditor-root > .jsoneditor-node-container > .jsoneditor-node-end {
> .jsoneditor-button-container,
> div.jsoneditor-insert {
display: none;
}
}
.jsoneditor-node > div {
@ -702,6 +705,11 @@ div.jsoneditor-node-container {
background-image: url('img/jsoneditor-icons.svg');
background-position: -2px -2px !important;
}
> .jsoneditor-list {
border-top: 1px dashed $light-gray;
margin-top: -1px;
}
}
&.jsoneditor-hover {

View File

@ -84,31 +84,59 @@ const CREATE_TYPE = {
title: 'Remove'
}, 'Remove'),
insertStructure: (path, emit) => h('button', {
insertStructureAfter: (path, emit) => h('button', {
key: 'insertStructure',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insertStructure', {path}),
onClick: () => emit('insertStructureAfter', {path}),
title: 'Insert a new object with the same data structure as the item above'
}, 'Insert structure'),
insertValue: (path, emit) => h('button', {
key: 'insertValue',
insertValueAfter: (path, emit) => h('button', {
key: 'insertValueAfter',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insert', {path, type: 'value'}),
onClick: () => emit('insertAfter', {path, type: 'value'}),
title: 'Insert value'
}, 'Insert value'),
insertObject: (path, emit) => h('button', {
key: 'insertObject',
insertObjectAfter: (path, emit) => h('button', {
key: 'insertObjectAfter',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insert', {path, type: 'Object'}),
onClick: () => emit('insertAfter', {path, type: 'Object'}),
title: 'Insert Object'
}, 'Insert Object'),
insertArray: (path, emit) => h('button', {
key: 'insertArray',
insertArrayAfter: (path, emit) => h('button', {
key: 'insertArrayAfter',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insert', {path, type: 'Array'}),
onClick: () => emit('insertAfter', {path, type: 'Array'}),
title: 'Insert Array'
}, 'Insert Array'),
insertStructureInside: (path, emit) => h('button', {
key: 'insertStructureInside',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insertStructureInside', {path}),
title: 'Insert a new object with the same data structure as the item above'
}, 'Insert structure'),
insertValueInside: (path, emit) => h('button', {
key: 'insertValueInside',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insertInside', {path, type: 'value'}),
title: 'Insert value'
}, 'Insert value'),
insertObjectInside: (path, emit) => h('button', {
key: 'insertObjectInside',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insertInside', {path, type: 'Object'}),
title: 'Insert Object'
}, 'Insert Object'),
insertArrayInside: (path, emit) => h('button', {
key: 'insertArrayInside',
className: MENU_ITEM_CLASS_NAME,
onClick: () => emit('insertInside', {path, type: 'Array'}),
title: 'Insert Array'
}, 'Insert Array'),

View File

@ -17,7 +17,7 @@ export const SELECTED_START = 2
export const SELECTED_END = 4
export const SELECTED_FIRST = 8
export const SELECTED_LAST = 16
export const SELECTED_BEFORE = 32
export const SELECTED_INSIDE = 32
export const SELECTED_AFTER = 64
export const SELECTED_EMPTY = 128
export const SELECTED_EMPTY_BEFORE = 256
@ -361,10 +361,10 @@ export function applySelection (eson, selection) {
if (!selection) {
return cleanupMetaData(eson, 'selected')
}
else if (selection.before) {
const updatedEson = setIn(eson, selection.before.concat([META, 'selected']),
SELECTED_BEFORE)
return cleanupMetaData(updatedEson, 'selected', [selection.before])
else if (selection.inside) {
const updatedEson = setIn(eson, selection.inside.concat([META, 'selected']),
SELECTED_INSIDE)
return cleanupMetaData(updatedEson, 'selected', [selection.inside])
}
else if (selection.after) {
const updatedEson = setIn(eson, selection.after.concat([META, 'selected']),
@ -455,8 +455,8 @@ export function applySelection (eson, selection) {
* @return {{minIndex: number, maxIndex: number}}
*/
export function findSelectionIndices (root, rootPath, selection) {
const start = (selection.after || selection.before || selection.start)[rootPath.length]
const end = (selection.after || selection.before || selection.end)[rootPath.length]
const start = (selection.after || selection.inside || selection.start)[rootPath.length]
const end = (selection.after || selection.inside || selection.end)[rootPath.length]
// if no object we assume it's an Array
const startIndex = root[META].type === 'Object' ? root[META].props.indexOf(start) : parseInt(start, 10)
@ -464,7 +464,7 @@ export function findSelectionIndices (root, rootPath, selection) {
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) +
((selection.after || selection.before) ? 0 : 1) // include max index itself
((selection.after || selection.inside) ? 0 : 1) // include max index itself
return { minIndex, maxIndex }
}
@ -562,8 +562,8 @@ export function applyEsonState(data, state) {
* @return {Path}
*/
export function findRootPath(selection) {
if (selection.before) {
return initial(selection.before)
if (selection.inside) {
return initial(selection.inside)
}
else if (selection.after) {
return initial(selection.after)