Floating menu starting to work (WIP)

This commit is contained in:
jos 2017-11-03 14:23:04 +01:00
parent a2f7f61389
commit 8425579718
8 changed files with 316 additions and 148 deletions

View File

@ -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,30 +75,26 @@ 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)
? this.renderFloatingMenu([
{type: 'sort'}, {type: 'sort'},
{type: 'duplicate'}, {type: 'duplicate'},
{type: 'cut'}, {type: 'cut'},
@ -86,29 +102,22 @@ export default class JSONNode extends PureComponent {
{type: 'paste'}, {type: 'paste'},
{type: 'remove'} {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,29 +132,26 @@ 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)
? this.renderFloatingMenu([
{type: 'sort'}, {type: 'sort'},
{type: 'duplicate'}, {type: 'duplicate'},
{type: 'cut'}, {type: 'cut'},
@ -153,9 +159,11 @@ export default class JSONNode extends PureComponent {
{type: 'paste'}, {type: 'paste'},
{type: 'remove'} {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,7 +185,8 @@ export default class JSONNode extends PureComponent {
this.renderError(data.error) this.renderError(data.error)
]) ])
const floatingMenu = this.renderFloatingMenu([ const floatingMenu = (data.selected === SELECTED_END)
? this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false}, // {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
{type: 'duplicate'}, {type: 'duplicate'},
{type: 'cut'}, {type: 'cut'},
@ -186,12 +194,34 @@ export default class JSONNode extends PureComponent {
{type: 'paste'}, {type: 'paste'},
{type: 'remove'} {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) => {
if (event.buttons === 0) { // no mouse button down, no dragging
event.stopPropagation() 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) {
this.setState({hover})
hoveredNode = this 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) => {

View File

@ -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

View File

@ -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) {

View File

@ -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'

View File

@ -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

View File

@ -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;
} }
/******************************* **********************************/ /******************************* **********************************/

View File

@ -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 = {

View File

@ -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 => {