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. * Create a JSONPatch for an insert action.
* *

View File

@ -9,45 +9,10 @@ import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import { import {
compileJSONPointer, compileJSONPointer,
META, META,
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE, SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE,
SELECTED_FIRST, SELECTED_LAST SELECTED_FIRST, SELECTED_LAST
} from '../eson' } 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 { export default class JSONNode extends PureComponent {
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url' 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 const nodeEnd = meta.expanded
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [ ? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
this.renderDelimiter('}', 'jsoneditor-delimiter-end'), 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 const nodeEnd = meta.expanded
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [ ? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
this.renderDelimiter(']', 'jsoneditor-delimiter-end'), this.renderDelimiter(']', 'jsoneditor-delimiter-end'),
@ -233,7 +198,7 @@ export default class JSONNode extends PureComponent {
this.renderError(meta.error) this.renderError(meta.error)
]) ])
const floatingMenu = this.renderFloatingMenu(MENU_ITEMS_VALUE, meta.selected) const floatingMenu = this.renderFloatingMenu('value', meta.selected)
// const insertArea = this.renderInsertBeforeArea() // const insertArea = this.renderInsertBeforeArea()
@ -251,7 +216,7 @@ export default class JSONNode extends PureComponent {
key: 'insert', key: 'insert',
className: 'jsoneditor-insert jsoneditor-insert-before', className: 'jsoneditor-insert jsoneditor-insert-before',
title: 'Insert a new item or paste clipboard', 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', className: 'jsoneditor-node',
onKeyDown: this.handleKeyDownAppend onKeyDown: this.handleKeyDownAppend
}, [ }, [
this.renderPlaceholder('before'), this.renderPlaceholder('inside'),
this.renderReadonly(text) 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', { return h('div', {
key: 'readonly', key: 'readonly',
'data-area': dataArea, 'data-area': dataArea,
@ -419,7 +384,7 @@ export default class JSONNode extends PureComponent {
getContainerClassName (selected, hover) { getContainerClassName (selected, hover) {
let classNames = ['jsoneditor-node-container'] let classNames = ['jsoneditor-node-container']
if ((selected & SELECTED_BEFORE) !== 0) { if ((selected & SELECTED_INSIDE) !== 0) {
classNames.push('jsoneditor-selected-insert-before') classNames.push('jsoneditor-selected-insert-before')
} }
else if ((selected & SELECTED_AFTER) !== 0) { 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 ((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') classNames.push('jsoneditor-hover-insert-before')
} }
else if ((hover & SELECTED_AFTER) !== 0) { else if ((hover & SELECTED_AFTER) !== 0) {
@ -560,8 +525,10 @@ export default class JSONNode extends PureComponent {
) )
} }
renderFloatingMenu (items, selected) { renderFloatingMenu (type, selected) {
if ((selected & SELECTED_END) === 0) { if (((selected & SELECTED_END) === 0) &&
((selected & SELECTED_INSIDE) === 0) &&
((selected & SELECTED_AFTER) === 0)) {
return null return null
} }
@ -572,11 +539,68 @@ export default class JSONNode extends PureComponent {
key: 'floating-menu', key: 'floating-menu',
path: this.props.value[META].path, path: this.props.value[META].path,
emit: this.props.emit, emit: this.props.emit,
items, items: this.getFloatingMenuItems(type, selected),
position: isLastOfMultiple ? 'bottom' : 'top' 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) => { handleMouseOver = (event) => {
if (event.buttons === 0) { // no mouse button down, no dragging if (event.buttons === 0) { // no mouse button down, no dragging
event.stopPropagation() event.stopPropagation()

View File

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

View File

@ -174,7 +174,8 @@
.jsoneditor-node-end .jsoneditor-button-container:hover { .jsoneditor-node-end .jsoneditor-button-container:hover {
visibility: visible; } 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; } display: none; }
.jsoneditor-node > div { .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 { div.jsoneditor-node-container.jsoneditor-selected-insert-before > .jsoneditor-node-end > .jsoneditor-insert-before {
background-image: url("img/jsoneditor-icons.svg"); background-image: url("img/jsoneditor-icons.svg");
background-position: -2px -2px !important; } 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-container,
div.jsoneditor-node-container.jsoneditor-hover > .jsoneditor-node > .jsoneditor-button-placeholder { div.jsoneditor-node-container.jsoneditor-hover > .jsoneditor-node > .jsoneditor-button-placeholder {
background-color: #d3d3d3; } 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 {
display: none; > .jsoneditor-button-container,
> div.jsoneditor-insert {
display: none;
}
} }
.jsoneditor-node > div { .jsoneditor-node > div {
@ -702,6 +705,11 @@ div.jsoneditor-node-container {
background-image: url('img/jsoneditor-icons.svg'); background-image: url('img/jsoneditor-icons.svg');
background-position: -2px -2px !important; background-position: -2px -2px !important;
} }
> .jsoneditor-list {
border-top: 1px dashed $light-gray;
margin-top: -1px;
}
} }
&.jsoneditor-hover { &.jsoneditor-hover {

View File

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

View File

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