Floating menu starting to work (WIP)
This commit is contained in:
parent
a2f7f61389
commit
8425579718
|
@ -8,10 +8,25 @@ import FloatingMenu from './menu/FloatingMenu'
|
||||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||||
import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils'
|
import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils'
|
||||||
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
||||||
import { compileJSONPointer, SELECTED, SELECTED_END } from '../eson'
|
import { compileJSONPointer, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
|
||||||
|
|
||||||
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
|
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
|
||||||
|
|
||||||
|
// TODO: rename SELECTED, SELECTED_END, etc to AREA_*? It's used for both selection and hovering
|
||||||
|
const SELECTED_CLASS_NAMES = {
|
||||||
|
[SELECTED]: ' jsoneditor-selected',
|
||||||
|
[SELECTED_END]: ' jsoneditor-selected jsoneditor-selected-end',
|
||||||
|
[SELECTED_AFTER]: ' jsoneditor-selected jsoneditor-selected-after',
|
||||||
|
[SELECTED_BEFORE]: ' jsoneditor-selected jsoneditor-selected-before',
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOVERED_CLASS_NAMES = {
|
||||||
|
[SELECTED]: ' jsoneditor-hover',
|
||||||
|
[SELECTED_END]: ' jsoneditor-hover jsoneditor-hover-end',
|
||||||
|
[SELECTED_AFTER]: ' jsoneditor-hover jsoneditor-hover-after',
|
||||||
|
[SELECTED_BEFORE]: ' jsoneditor-hover jsoneditor-hover-before',
|
||||||
|
}
|
||||||
|
|
||||||
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'
|
||||||
|
|
||||||
|
@ -21,6 +36,12 @@ export default class JSONNode extends PureComponent {
|
||||||
hover: false
|
hover: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (hoveredNode === this) {
|
||||||
|
hoveredNode = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { props } = this
|
const { props } = this
|
||||||
|
|
||||||
|
@ -38,9 +59,8 @@ export default class JSONNode extends PureComponent {
|
||||||
renderJSONObject ({prop, index, data, options, events}) {
|
renderJSONObject ({prop, index, data, options, events}) {
|
||||||
const childCount = data.props.length
|
const childCount = data.props.length
|
||||||
const node = h('div', {
|
const node = h('div', {
|
||||||
'data-path': compileJSONPointer(this.props.path),
|
|
||||||
onKeyDown: this.handleKeyDown,
|
|
||||||
key: 'node',
|
key: 'node',
|
||||||
|
onKeyDown: this.handleKeyDown,
|
||||||
className: 'jsoneditor-node jsoneditor-object'
|
className: 'jsoneditor-node jsoneditor-object'
|
||||||
}, [
|
}, [
|
||||||
this.renderExpandButton(),
|
this.renderExpandButton(),
|
||||||
|
@ -55,60 +75,49 @@ export default class JSONNode extends PureComponent {
|
||||||
let childs
|
let childs
|
||||||
if (data.expanded) {
|
if (data.expanded) {
|
||||||
if (data.props.length > 0) {
|
if (data.props.length > 0) {
|
||||||
const props = data.props.map(prop => {
|
const props = data.props.map(prop => h(this.constructor, {
|
||||||
return h('li', { key: prop.id, className: JSONNode.selectedClassName(prop.value.selected) },
|
key: prop.id,
|
||||||
h(this.constructor, {
|
path: this.props.path.concat(prop.name),
|
||||||
path: this.props.path.concat(prop.name),
|
prop,
|
||||||
prop,
|
data: prop.value,
|
||||||
data: prop.value,
|
options,
|
||||||
options,
|
events
|
||||||
events
|
}))
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
childs = h('ul', {key: 'childs', className: 'jsoneditor-list'}, props)
|
childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, props)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
childs = h('ul', {key: 'childs', className: 'jsoneditor-list'},
|
childs = h('div', {key: 'childs', className: 'jsoneditor-list'},
|
||||||
h('li', {},
|
this.renderAppend('(empty object)')
|
||||||
this.renderAppend('(empty object)')
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const floatingMenu = this.renderFloatingMenu([
|
const floatingMenu = (data.selected === SELECTED_END)
|
||||||
{type: 'sort'},
|
? this.renderFloatingMenu([
|
||||||
{type: 'duplicate'},
|
{type: 'sort'},
|
||||||
{type: 'cut'},
|
{type: 'duplicate'},
|
||||||
{type: 'copy'},
|
{type: 'cut'},
|
||||||
{type: 'paste'},
|
{type: 'copy'},
|
||||||
{type: 'remove'}
|
{type: 'paste'},
|
||||||
])
|
{type: 'remove'}
|
||||||
|
])
|
||||||
|
: null
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''),
|
'data-path': compileJSONPointer(this.props.path),
|
||||||
|
className: this.getContainerClassName(data.selected, this.state.hover),
|
||||||
onMouseOver: this.handleMouseOver,
|
onMouseOver: this.handleMouseOver,
|
||||||
onMouseLeave: this.handleMouseLeave
|
onMouseLeave: this.handleMouseLeave
|
||||||
}, [node, floatingMenu, childs])
|
}, [node, floatingMenu, childs])
|
||||||
}
|
}
|
||||||
|
|
||||||
static selectedClassName(selected: number) {
|
|
||||||
return (selected === SELECTED)
|
|
||||||
? ' jsoneditor-selected'
|
|
||||||
: (selected === SELECTED_END)
|
|
||||||
? 'jsoneditor-selected jsoneditor-selected-end'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?)
|
// TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?)
|
||||||
renderJSONArray ({prop, index, data, options, events}) {
|
renderJSONArray ({prop, index, data, options, events}) {
|
||||||
const childCount = data.items.length
|
const childCount = data.items.length
|
||||||
const node = h('div', {
|
const node = h('div', {
|
||||||
'data-path': compileJSONPointer(this.props.path),
|
|
||||||
onKeyDown: this.handleKeyDown,
|
|
||||||
key: 'node',
|
key: 'node',
|
||||||
|
onKeyDown: this.handleKeyDown,
|
||||||
className: 'jsoneditor-node jsoneditor-array'
|
className: 'jsoneditor-node jsoneditor-array'
|
||||||
}, [
|
}, [
|
||||||
this.renderExpandButton(),
|
this.renderExpandButton(),
|
||||||
|
@ -123,39 +132,38 @@ export default class JSONNode extends PureComponent {
|
||||||
let childs
|
let childs
|
||||||
if (data.expanded) {
|
if (data.expanded) {
|
||||||
if (data.items.length > 0) {
|
if (data.items.length > 0) {
|
||||||
const items = data.items.map((item, index) => {
|
const items = data.items.map((item, index) => h(this.constructor, {
|
||||||
return h('li', { key : item.id, className: JSONNode.selectedClassName(prop.value.selected)},
|
key : item.id,
|
||||||
h(this.constructor, {
|
path: this.props.path.concat(String(index)),
|
||||||
path: this.props.path.concat(String(index)),
|
index,
|
||||||
index,
|
data: item.value,
|
||||||
data: item.value,
|
options,
|
||||||
options,
|
events
|
||||||
events
|
}))
|
||||||
})
|
|
||||||
)
|
childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, items)
|
||||||
})
|
|
||||||
childs = h('ul', {key: 'childs', className: 'jsoneditor-list'}, items)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
childs = h('ul', {key: 'childs', className: 'jsoneditor-list'},
|
childs = h('div', {key: 'childs', className: 'jsoneditor-list'},
|
||||||
h('li', {},
|
this.renderAppend('(empty array)')
|
||||||
this.renderAppend('(empty array)')
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const floatingMenu = this.renderFloatingMenu([
|
const floatingMenu = (data.selected === SELECTED_END)
|
||||||
{type: 'sort'},
|
? this.renderFloatingMenu([
|
||||||
{type: 'duplicate'},
|
{type: 'sort'},
|
||||||
{type: 'cut'},
|
{type: 'duplicate'},
|
||||||
{type: 'copy'},
|
{type: 'cut'},
|
||||||
{type: 'paste'},
|
{type: 'copy'},
|
||||||
{type: 'remove'}
|
{type: 'paste'},
|
||||||
])
|
{type: 'remove'}
|
||||||
|
])
|
||||||
|
: null
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''),
|
'data-path': compileJSONPointer(this.props.path),
|
||||||
|
className: this.getContainerClassName(data.selected, this.state.hover),
|
||||||
onMouseOver: this.handleMouseOver,
|
onMouseOver: this.handleMouseOver,
|
||||||
onMouseLeave: this.handleMouseLeave
|
onMouseLeave: this.handleMouseLeave
|
||||||
}, [node, floatingMenu, childs])
|
}, [node, floatingMenu, childs])
|
||||||
|
@ -163,8 +171,7 @@ export default class JSONNode extends PureComponent {
|
||||||
|
|
||||||
renderJSONValue ({prop, index, data, options}) {
|
renderJSONValue ({prop, index, data, options}) {
|
||||||
const node = h('div', {
|
const node = h('div', {
|
||||||
key: 'value',
|
key: 'node',
|
||||||
'data-path': compileJSONPointer(this.props.path),
|
|
||||||
onKeyDown: this.handleKeyDown,
|
onKeyDown: this.handleKeyDown,
|
||||||
className: 'jsoneditor-node'
|
className: 'jsoneditor-node'
|
||||||
}, [
|
}, [
|
||||||
|
@ -178,20 +185,43 @@ export default class JSONNode extends PureComponent {
|
||||||
this.renderError(data.error)
|
this.renderError(data.error)
|
||||||
])
|
])
|
||||||
|
|
||||||
const floatingMenu = this.renderFloatingMenu([
|
const floatingMenu = (data.selected === SELECTED_END)
|
||||||
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
|
? this.renderFloatingMenu([
|
||||||
{type: 'duplicate'},
|
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
|
||||||
{type: 'cut'},
|
{type: 'duplicate'},
|
||||||
{type: 'copy'},
|
{type: 'cut'},
|
||||||
{type: 'paste'},
|
{type: 'copy'},
|
||||||
{type: 'remove'}
|
{type: 'paste'},
|
||||||
])
|
{type: 'remove'}
|
||||||
|
])
|
||||||
|
: null
|
||||||
|
|
||||||
|
const insertArea = this.renderInsertArea()
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''),
|
'data-path': compileJSONPointer(this.props.path),
|
||||||
|
className: this.getContainerClassName(data.selected, this.state.hover),
|
||||||
onMouseOver: this.handleMouseOver,
|
onMouseOver: this.handleMouseOver,
|
||||||
onMouseLeave: this.handleMouseLeave
|
onMouseLeave: this.handleMouseLeave
|
||||||
}, [node, floatingMenu])
|
}, [node, floatingMenu, insertArea])
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInsertArea () {
|
||||||
|
const floatingMenu = (this.props.data.selected === SELECTED_AFTER)
|
||||||
|
? this.renderFloatingMenu([
|
||||||
|
{type: 'insertStructure'},
|
||||||
|
{type: 'insertValue'},
|
||||||
|
{type: 'insertObject'},
|
||||||
|
{type: 'insertArray'},
|
||||||
|
{type: 'paste'},
|
||||||
|
])
|
||||||
|
: null
|
||||||
|
|
||||||
|
return h('div', {
|
||||||
|
key: 'menu',
|
||||||
|
className: 'jsoneditor-insert-area',
|
||||||
|
'data-area': 'after'
|
||||||
|
}, [floatingMenu])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -311,6 +341,12 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContainerClassName (selected, hover) {
|
||||||
|
return 'jsoneditor-node-container' +
|
||||||
|
(hover ? (HOVERED_CLASS_NAMES[hover]) : '') +
|
||||||
|
(selected ? (SELECTED_CLASS_NAMES[selected]) : '')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the best position for the popover: right, above, below, or left
|
* Find the best position for the popover: right, above, below, or left
|
||||||
* from the warning icon.
|
* from the warning icon.
|
||||||
|
@ -473,7 +509,7 @@ export default class JSONNode extends PureComponent {
|
||||||
|
|
||||||
renderFloatingMenu (items) {
|
renderFloatingMenu (items) {
|
||||||
return h(FloatingMenu, {
|
return h(FloatingMenu, {
|
||||||
key: 'menu',
|
key: 'floating-menu',
|
||||||
path: this.props.path,
|
path: this.props.path,
|
||||||
events: this.props.events,
|
events: this.props.events,
|
||||||
items
|
items
|
||||||
|
@ -495,23 +531,31 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseOver = (event) => {
|
handleMouseOver = (event) => {
|
||||||
event.stopPropagation()
|
if (event.buttons === 0) { // no mouse button down, no dragging
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
if (hoveredNode !== this) {
|
const hover = (event.target.className.indexOf('jsoneditor-insert-area') !== -1)
|
||||||
|
? SELECTED_AFTER
|
||||||
|
: SELECTED
|
||||||
|
|
||||||
if (hoveredNode) {
|
if (hoveredNode && hoveredNode !== this) {
|
||||||
// FIXME: this may give issues when the hovered node doesn't exist anymore. check whether mounted
|
// FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted?
|
||||||
hoveredNode.setState({hover: false})
|
hoveredNode.setState({hover: null})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({hover: true})
|
if (hover !== this.state.hover) {
|
||||||
hoveredNode = this
|
this.setState({hover})
|
||||||
|
hoveredNode = this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseLeave = (event) => {
|
handleMouseLeave = (event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
this.setState({hover: false})
|
// FIXME: this gives issues when the hovered node doesn't exist anymore. check whether mounted?
|
||||||
|
hoveredNode.setState({hover: false})
|
||||||
|
|
||||||
|
this.setState({hover: null})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenActionMenu = (event) => {
|
handleOpenActionMenu = (event) => {
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||||
import { KEY_BINDINGS } from '../constants'
|
import { KEY_BINDINGS } from '../constants'
|
||||||
|
|
||||||
import type { ESON, ESONPatch, JSONPath, ESONSelection } from '../types'
|
import type { ESON, ESONPatch, JSONPath, ESONSelection, ESONPointer } from '../types'
|
||||||
|
|
||||||
const AJV_OPTIONS = {
|
const AJV_OPTIONS = {
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
|
@ -101,6 +101,8 @@ export default class TreeMode extends Component {
|
||||||
|
|
||||||
onExpand: this.handleExpand,
|
onExpand: this.handleExpand,
|
||||||
|
|
||||||
|
onSelect: this.handleSelect,
|
||||||
|
|
||||||
// TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events'
|
// TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events'
|
||||||
findKeyBinding: this.handleFindKeyBinding
|
findKeyBinding: this.handleFindKeyBinding
|
||||||
},
|
},
|
||||||
|
@ -205,12 +207,14 @@ export default class TreeMode extends Component {
|
||||||
h(Hammer, {
|
h(Hammer, {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
direction: 'DIRECTION_VERTICAL',
|
direction: 'DIRECTION_VERTICAL',
|
||||||
onTap: this.handleTap,
|
|
||||||
onPanStart: this.handlePanStart,
|
|
||||||
onPan: this.handlePan,
|
onPan: this.handlePan,
|
||||||
onPanEnd: this.handlePanEnd
|
onPanEnd: this.handlePanEnd
|
||||||
},
|
},
|
||||||
h('ul', {className: 'jsoneditor-list jsoneditor-root' + (data.selected ? ' jsoneditor-selected' : '')},
|
h('div', {
|
||||||
|
onMouseDown: this.handleTouchStart,
|
||||||
|
onTouchStart: this.handleTouchStart,
|
||||||
|
className: 'jsoneditor-list jsoneditor-root' +
|
||||||
|
(data.selected ? ' jsoneditor-selected' : '')},
|
||||||
h(Node, {
|
h(Node, {
|
||||||
data,
|
data,
|
||||||
events: state.events,
|
events: state.events,
|
||||||
|
@ -364,6 +368,7 @@ export default class TreeMode extends Component {
|
||||||
moveUp(fromElement, 'property')
|
moveUp(fromElement, 'property')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({ selection : null })
|
||||||
this.handlePatch(remove(path))
|
this.handlePatch(remove(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -535,6 +540,11 @@ export default class TreeMode extends Component {
|
||||||
this.handlePatch(sort(this.state.data, path, order))
|
this.handlePatch(sort(this.state.data, path, order))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSelect = (selection: ESONSelection) => {
|
||||||
|
console.log('handleSelect', selection)
|
||||||
|
this.setState({ selection })
|
||||||
|
}
|
||||||
|
|
||||||
handleExpand = (path, expanded, recurse) => {
|
handleExpand = (path, expanded, recurse) => {
|
||||||
if (recurse) {
|
if (recurse) {
|
||||||
const esonPath = toEsonPath(this.state.data, path)
|
const esonPath = toEsonPath(this.state.data, path)
|
||||||
|
@ -661,22 +671,13 @@ export default class TreeMode extends Component {
|
||||||
this.emitOnChange (actions, result.revert, result.data)
|
this.emitOnChange (actions, result.revert, result.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTap = (event) => {
|
handleTouchStart = (event) => {
|
||||||
const path = this.findDataPathFromElement(event.target.firstChild)
|
const pointer = this.findESONPointerFromElement(event.target)
|
||||||
if (this.state.selection) {
|
if (pointer) {
|
||||||
this.setState({ selection: {start: {path}, end: {path}}})
|
this.setState({ selection: {start: pointer, end: pointer}})
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
|
this.setState({ selection: null })
|
||||||
handlePanStart = (event) => {
|
|
||||||
const path = this.findDataPathFromElement(event.target.firstChild)
|
|
||||||
if (path) {
|
|
||||||
this.setState({
|
|
||||||
selection: {
|
|
||||||
start: {path},
|
|
||||||
end: {path}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,6 +714,13 @@ export default class TreeMode extends Component {
|
||||||
return attr ? parseJSONPointer(attr.replace(/\/-$/, '')) : null
|
return attr ? parseJSONPointer(attr.replace(/\/-$/, '')) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findESONPointerFromElement (element: Element) : ESONPointer {
|
||||||
|
const path = this.findDataPathFromElement(element)
|
||||||
|
const area = element && element.getAttribute && element.getAttribute('data-area') || null
|
||||||
|
|
||||||
|
return { path, area }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the window vertically to the node with given path
|
* Scroll the window vertically to the node with given path
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
|
|
|
@ -84,11 +84,49 @@ const CREATE_TYPE = {
|
||||||
onClick: () => events.onRemove(path),
|
onClick: () => events.onRemove(path),
|
||||||
title: 'Remove'
|
title: 'Remove'
|
||||||
}, 'Remove'),
|
}, 'Remove'),
|
||||||
|
|
||||||
|
insertStructure: (path, events) => h('button', {
|
||||||
|
key: 'insertStructure',
|
||||||
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
|
// onClick: () => events.onRemove(path),
|
||||||
|
title: 'Insert a new object with the same data structure as the item above'
|
||||||
|
}, 'Insert structure'),
|
||||||
|
|
||||||
|
insertValue: (path, events) => h('button', {
|
||||||
|
key: 'insertValue',
|
||||||
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
|
// onClick: () => events.onRemove(path),
|
||||||
|
title: 'Insert value'
|
||||||
|
}, 'Insert value'),
|
||||||
|
|
||||||
|
insertObject: (path, events) => h('button', {
|
||||||
|
key: 'insertObject',
|
||||||
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
|
// onClick: () => events.onRemove(path),
|
||||||
|
title: 'Insert Object'
|
||||||
|
}, 'Insert Object'),
|
||||||
|
|
||||||
|
insertArray: (path, events) => h('button', {
|
||||||
|
key: 'insertArray',
|
||||||
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
|
// onClick: () => events.onRemove(path),
|
||||||
|
title: 'Insert Array'
|
||||||
|
}, 'Insert Array'),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FloatingMenu extends PureComponent {
|
export default class FloatingMenu extends PureComponent {
|
||||||
|
componentDidMount () {
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstButton = this.refs.root && this.refs.root.querySelector('button')
|
||||||
|
if (firstButton) {
|
||||||
|
firstButton.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return h('div', {className: MENU_CLASS_NAME}, this.props.items.map(item => {
|
return h('div', {ref: 'root', className: MENU_CLASS_NAME}, 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) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ let lastInputName = null
|
||||||
// TODO: create a constants file with the CSS names that are used in domSelector
|
// TODO: create a constants file with the CSS names that are used in domSelector
|
||||||
const SEARCH_TEXT_CLASS_NAME = 'jsoneditor-search-text'
|
const SEARCH_TEXT_CLASS_NAME = 'jsoneditor-search-text'
|
||||||
const SEARCH_COMPONENT_CLASS_NAME = 'jsoneditor-search'
|
const SEARCH_COMPONENT_CLASS_NAME = 'jsoneditor-search'
|
||||||
const NODE_CONTAINER_CLASS_NAME = 'jsoneditor-node'
|
const NODE_CONTAINER_CLASS_NAME = 'jsoneditor-node-container'
|
||||||
const CONTENTS_CONTAINER_CLASS_NAME = 'jsoneditor-tree-contents'
|
const CONTENTS_CONTAINER_CLASS_NAME = 'jsoneditor-tree-contents'
|
||||||
const PROPERTY_CLASS_NAME = 'jsoneditor-property'
|
const PROPERTY_CLASS_NAME = 'jsoneditor-property'
|
||||||
const VALUE_CLASS_NAME = 'jsoneditor-value'
|
const VALUE_CLASS_NAME = 'jsoneditor-value'
|
||||||
|
|
14
src/eson.js
14
src/eson.js
|
@ -22,6 +22,8 @@ type RecurseCallback = (value: ESON, path: Path, root: ESON) => ESON
|
||||||
|
|
||||||
export const SELECTED = 1
|
export const SELECTED = 1
|
||||||
export const SELECTED_END = 2
|
export const SELECTED_END = 2
|
||||||
|
export const SELECTED_BEFORE = 3
|
||||||
|
export const SELECTED_AFTER = 4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand function which will expand all nodes
|
* Expand function which will expand all nodes
|
||||||
|
@ -263,7 +265,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
|
||||||
const parentPath = initial(path)
|
const parentPath = initial(path)
|
||||||
const parent = getIn(eson, toEsonPath(eson, parentPath))
|
const parent = getIn(eson, toEsonPath(eson, parentPath))
|
||||||
if (parent.type === 'Object') {
|
if (parent.type === 'Object') {
|
||||||
results.push({path, field: 'property'})
|
results.push({path, area: 'property'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -271,7 +273,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
|
||||||
// check value
|
// check value
|
||||||
if (value.type === 'value') {
|
if (value.type === 'value') {
|
||||||
if (containsCaseInsensitive(value.value, text)) {
|
if (containsCaseInsensitive(value.value, text)) {
|
||||||
results.push({path, field: 'value'})
|
results.push({path, area: 'value'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -335,13 +337,13 @@ export function applySearchResults (eson: ESON, searchResults: ESONPointer[], ac
|
||||||
let updatedEson = eson
|
let updatedEson = eson
|
||||||
|
|
||||||
searchResults.forEach(function (searchResult) {
|
searchResults.forEach(function (searchResult) {
|
||||||
if (searchResult.field === 'value') {
|
if (searchResult.area === 'value') {
|
||||||
const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult')
|
const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult')
|
||||||
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
||||||
updatedEson = setIn(updatedEson, esonPath, value)
|
updatedEson = setIn(updatedEson, esonPath, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchResult.field === 'property') {
|
if (searchResult.area === 'property') {
|
||||||
const esonPath = toEsonPath(updatedEson, searchResult.path)
|
const esonPath = toEsonPath(updatedEson, searchResult.path)
|
||||||
const propertyPath = initial(esonPath).concat('searchResult')
|
const propertyPath = initial(esonPath).concat('searchResult')
|
||||||
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
||||||
|
@ -366,8 +368,10 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
|
||||||
|
|
||||||
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
|
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
|
||||||
// select a single node
|
// select a single node
|
||||||
return setIn(eson, rootEsonPath.concat(['selected']), SELECTED_END)
|
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
|
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index
|
||||||
|
return setIn(eson, rootEsonPath.concat(['selected']), selectionType)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// select multiple childs of an object or array
|
// select multiple childs of an object or array
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
@floating-menu-color: #fff;
|
@floating-menu-color: #fff;
|
||||||
// @selectedColor: #e5e5e5;
|
// @selectedColor: #e5e5e5;
|
||||||
@selectedColor: #ffed99;
|
@selectedColor: #ffed99;
|
||||||
@hoverColor: rgba(10, 10, 10, 0.05);
|
@hoverColor: #f2f2f2;
|
||||||
|
@hoverAndSelectedColor: #F2E191;
|
||||||
|
|
||||||
.jsoneditor {
|
.jsoneditor {
|
||||||
border: 1px solid @theme-color;
|
border: 1px solid @theme-color;
|
||||||
|
@ -119,16 +120,17 @@
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.jsoneditor-list {
|
div.jsoneditor-list {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* no left padding for the root ul element */
|
/* no left padding for the root div element */
|
||||||
.jsoneditor-contents > ul.jsoneditor-list {
|
.jsoneditor-contents > div.jsoneditor-list {
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
|
padding-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor-property,
|
.jsoneditor-property,
|
||||||
|
@ -261,10 +263,6 @@ div.jsoneditor-value.jsoneditor-empty::after {
|
||||||
content: 'value';
|
content: 'value';
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsoneditor-selected {
|
|
||||||
background-color: @selectedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jsoneditor-highlight {
|
.jsoneditor-highlight {
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
}
|
}
|
||||||
|
@ -567,8 +565,90 @@ div.jsoneditor-node-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color 100ms ease-in;
|
transition: background-color 100ms ease-in;
|
||||||
|
|
||||||
|
// TODO: can the hover/select css be simplified?
|
||||||
|
|
||||||
|
&.jsoneditor-selected {
|
||||||
|
background-color: @selectedColor;
|
||||||
|
|
||||||
|
&.jsoneditor-hover {
|
||||||
|
background-color: @hoverAndSelectedColor;
|
||||||
|
|
||||||
|
&.jsoneditor-hover-after {
|
||||||
|
background-color: @selectedColor;
|
||||||
|
|
||||||
|
div.jsoneditor-insert-area {
|
||||||
|
border: 1px dashed gray;
|
||||||
|
background-color: @hoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.jsoneditor-selected-after {
|
||||||
|
div.jsoneditor-insert-area {
|
||||||
|
border: 1px dashed #f4af41;
|
||||||
|
background-color: @hoverAndSelectedColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.jsoneditor-selected-after {
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
&.jsoneditor-hover {
|
||||||
|
background-color: @hoverColor;
|
||||||
|
|
||||||
|
&.jsoneditor-hover-after {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.jsoneditor-insert-area {
|
||||||
|
border: 1px dashed #f4af41;
|
||||||
|
background: @selectedColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hovering nested elements
|
||||||
|
.jsoneditor-hover {
|
||||||
|
background-color: @hoverAndSelectedColor;
|
||||||
|
|
||||||
|
&.jsoneditor-hover-after {
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
div.jsoneditor-insert-area {
|
||||||
|
border: 1px dashed #f4af41;
|
||||||
|
background: @hoverAndSelectedColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.jsoneditor-hover {
|
||||||
|
background-color: @hoverColor;
|
||||||
|
|
||||||
|
&.jsoneditor-hover-after {
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
div.jsoneditor-insert-area {
|
||||||
|
border: 1px dashed gray;
|
||||||
|
background-color: @hoverColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.jsoneditor-insert-area {
|
||||||
|
@height: 8px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: @height;
|
||||||
|
left: 0;
|
||||||
|
bottom: -@height/2;
|
||||||
|
border: 1px transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1; // must be on top of next node, it overlaps a bit
|
||||||
|
}
|
||||||
|
|
||||||
div.jsoneditor-floating-menu {
|
div.jsoneditor-floating-menu {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -599,7 +679,9 @@ div.jsoneditor-node-container {
|
||||||
border-right: 1px solid lighten(@floating-menu-background, 10%);
|
border-right: 1px solid lighten(@floating-menu-background, 10%);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background: lighten(@floating-menu-background, 10%);
|
background: lighten(@floating-menu-background, 10%);
|
||||||
}
|
}
|
||||||
|
@ -616,14 +698,6 @@ div.jsoneditor-node-container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.jsoneditor-node-hover {
|
|
||||||
background-color: @hoverColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jsoneditor-selected-end > .jsoneditor-node-container > div.jsoneditor-floating-menu {
|
|
||||||
display: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************* **********************************/
|
/******************************* **********************************/
|
||||||
|
|
|
@ -33,7 +33,7 @@ export type JSONArrayType = JSONType[]
|
||||||
/********************** TYPES FOR THE ESON OBJECT MODEL *************************/
|
/********************** TYPES FOR THE ESON OBJECT MODEL *************************/
|
||||||
|
|
||||||
export type SearchResultStatus = 'normal' | 'active'
|
export type SearchResultStatus = 'normal' | 'active'
|
||||||
export type ESONPointerField = 'value' | 'property'
|
export type ESONPointerArea = 'value' | 'property' | 'before' | 'after'
|
||||||
|
|
||||||
export type ESONObjectProperty = {
|
export type ESONObjectProperty = {
|
||||||
id: number,
|
id: number,
|
||||||
|
@ -78,7 +78,7 @@ export type ESONPath = string[]
|
||||||
|
|
||||||
export type ESONPointer = {
|
export type ESONPointer = {
|
||||||
path: JSONPath, // TODO: change path to an ESONPath?
|
path: JSONPath, // TODO: change path to an ESONPath?
|
||||||
field?: ESONPointerField
|
area?: ESONPointerArea
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ESONSelection = {
|
export type ESONSelection = {
|
||||||
|
|
|
@ -194,12 +194,12 @@ test('search', t => {
|
||||||
// printJSON(searchResults)
|
// printJSON(searchResults)
|
||||||
|
|
||||||
t.deepEqual(searchResults, [
|
t.deepEqual(searchResults, [
|
||||||
{path: ['obj', 'arr', '2', 'last'], field: 'property'},
|
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
|
||||||
{path: ['str'], field: 'value'},
|
{path: ['str'], area: 'value'},
|
||||||
{path: ['nill'], field: 'property'},
|
{path: ['nill'], area: 'property'},
|
||||||
{path: ['nill'], field: 'value'},
|
{path: ['nill'], area: 'value'},
|
||||||
{path: ['bool'], field: 'property'},
|
{path: ['bool'], area: 'property'},
|
||||||
{path: ['bool'], field: 'value'}
|
{path: ['bool'], area: 'value'}
|
||||||
])
|
])
|
||||||
|
|
||||||
const activeSearchResult = searchResults[0]
|
const activeSearchResult = searchResults[0]
|
||||||
|
@ -219,30 +219,30 @@ test('search', t => {
|
||||||
|
|
||||||
test('nextSearchResult', t => {
|
test('nextSearchResult', t => {
|
||||||
const searchResults = [
|
const searchResults = [
|
||||||
{path: ['obj', 'arr', '2', 'last'], field: 'property'},
|
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
|
||||||
{path: ['str'], field: 'value'},
|
{path: ['str'], area: 'value'},
|
||||||
{path: ['nill'], field: 'property'},
|
{path: ['nill'], area: 'property'},
|
||||||
{path: ['nill'], field: 'value'},
|
{path: ['nill'], area: 'value'},
|
||||||
{path: ['bool'], field: 'property'},
|
{path: ['bool'], area: 'property'},
|
||||||
{path: ['bool'], field: 'value'}
|
{path: ['bool'], area: 'value'}
|
||||||
]
|
]
|
||||||
|
|
||||||
t.deepEqual(nextSearchResult(searchResults,
|
t.deepEqual(nextSearchResult(searchResults,
|
||||||
{path: ['nill'], field: 'property'}),
|
{path: ['nill'], area: 'property'}),
|
||||||
{path: ['nill'], field: 'value'})
|
{path: ['nill'], area: 'value'})
|
||||||
|
|
||||||
// wrap around
|
// wrap around
|
||||||
t.deepEqual(nextSearchResult(searchResults,
|
t.deepEqual(nextSearchResult(searchResults,
|
||||||
{path: ['bool'], field: 'value'}),
|
{path: ['bool'], area: 'value'}),
|
||||||
{path: ['obj', 'arr', '2', 'last'], field: 'property'})
|
{path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||||
|
|
||||||
// return first when current is not found
|
// return first when current is not found
|
||||||
t.deepEqual(nextSearchResult(searchResults,
|
t.deepEqual(nextSearchResult(searchResults,
|
||||||
{path: ['non', 'existing'], field: 'value'}),
|
{path: ['non', 'existing'], area: 'value'}),
|
||||||
{path: ['obj', 'arr', '2', 'last'], field: 'property'})
|
{path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||||
|
|
||||||
// return null when searchResults are empty
|
// return null when searchResults are empty
|
||||||
t.deepEqual(nextSearchResult([], {path: ['non', 'existing'], field: 'value'}), null)
|
t.deepEqual(nextSearchResult([], {path: ['non', 'existing'], area: 'value'}), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('previousSearchResult', t => {
|
test('previousSearchResult', t => {
|
||||||
|
|
Loading…
Reference in New Issue