Show floating menu on selection

This commit is contained in:
jos 2017-10-14 21:57:59 +02:00
parent cea4e2c101
commit a2f7f61389
6 changed files with 174 additions and 102 deletions

View File

@ -8,7 +8,7 @@ import FloatingMenu from './menu/FloatingMenu'
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils'
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import { compileJSONPointer } from '../eson'
import { compileJSONPointer, SELECTED, SELECTED_END } from '../eson'
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
@ -18,6 +18,7 @@ export default class JSONNode extends PureComponent {
state = {
menu: null, // can contain object {anchor, root}
appendMenu: null, // can contain object {anchor, root}
hover: false
}
render () {
@ -45,16 +46,9 @@ export default class JSONNode extends PureComponent {
this.renderExpandButton(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]),
this.renderProperty(prop, index, data, options),
this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`),
// this.renderFloatingMenuButton(),
this.renderError(data.error)
])
@ -62,7 +56,7 @@ export default class JSONNode extends PureComponent {
if (data.expanded) {
if (data.props.length > 0) {
const props = data.props.map(prop => {
return h('li', { key: prop.id, className: (prop.value.selected ? ' jsoneditor-selected' : '') },
return h('li', { key: prop.id, className: JSONNode.selectedClassName(prop.value.selected) },
h(this.constructor, {
path: this.props.path.concat(prop.name),
prop,
@ -84,7 +78,28 @@ export default class JSONNode extends PureComponent {
}
}
return h('div', {}, [node, childs])
const floatingMenu = this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
])
return h('div', {
className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [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?)
@ -99,16 +114,9 @@ export default class JSONNode extends PureComponent {
this.renderExpandButton(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]),
this.renderProperty(prop, index, data, options),
this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`),
// this.renderFloatingMenuButton(),
this.renderError(data.error)
])
@ -116,7 +124,7 @@ export default class JSONNode extends PureComponent {
if (data.expanded) {
if (data.items.length > 0) {
const items = data.items.map((item, index) => {
return h('li', { key : item.id, className: (item.value.selected ? ' jsoneditor-selected' : '')},
return h('li', { key : item.id, className: JSONNode.selectedClassName(prop.value.selected)},
h(this.constructor, {
path: this.props.path.concat(String(index)),
index,
@ -137,11 +145,25 @@ export default class JSONNode extends PureComponent {
}
}
return h('div', {}, [node, childs])
const floatingMenu = this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
])
return h('div', {
className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, childs])
}
renderJSONValue ({prop, index, data, options}) {
return h('div', {
const node = h('div', {
key: 'value',
'data-path': compileJSONPointer(this.props.path),
onKeyDown: this.handleKeyDown,
className: 'jsoneditor-node'
@ -149,19 +171,27 @@ export default class JSONNode extends PureComponent {
this.renderPlaceholder(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
]),
this.renderProperty(prop, index, data, options),
this.renderSeparator(),
this.renderValue(data.value, data.searchResult, options),
// this.renderFloatingMenuButton(),
this.renderError(data.error)
])
const floatingMenu = this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
{type: 'paste'},
{type: 'remove'}
])
return h('div', {
className: 'jsoneditor-node-container ' + (this.state.hover ? ' jsoneditor-node-hover': ''),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu])
}
/**
@ -427,6 +457,20 @@ export default class JSONNode extends PureComponent {
])
}
// TODO: cleanup
renderFloatingMenuButton () {
const className = 'jsoneditor-button jsoneditor-floatingmenu' +
((this.state.open) ? ' jsoneditor-visible' : '')
return h('div', {className: 'jsoneditor-button-container', key: 'action'}, [
h('button', {
key: 'button',
className,
onClick: this.handleOpenActionMenu
})
])
}
renderFloatingMenu (items) {
return h(FloatingMenu, {
key: 'menu',
@ -450,6 +494,26 @@ export default class JSONNode extends PureComponent {
])
}
handleMouseOver = (event) => {
event.stopPropagation()
if (hoveredNode !== this) {
if (hoveredNode) {
// FIXME: this may give issues when the hovered node doesn't exist anymore. check whether mounted
hoveredNode.setState({hover: false})
}
this.setState({hover: true})
hoveredNode = this
}
}
handleMouseLeave = (event) => {
event.stopPropagation()
this.setState({hover: false})
}
handleOpenActionMenu = (event) => {
// TODO: don't use refs, find root purely via DOM?
const root = findParentWithAttribute(this.refs.actionMenuButton, 'data-jsoneditor', 'true')
@ -612,3 +676,6 @@ export default class JSONNode extends PureComponent {
: stringConvert(stringValue)
}
}
// singleton holding the node that's currently being hovered
let hoveredNode = null

View File

@ -662,8 +662,9 @@ export default class TreeMode extends Component {
}
handleTap = (event) => {
const path = this.findDataPathFromElement(event.target.firstChild)
if (this.state.selection) {
this.setState({ selection: null })
this.setState({ selection: {start: {path}, end: {path}}})
}
}

View File

@ -3,7 +3,6 @@
import { createElement as h, PureComponent } from 'react'
import { keyComboFromEvent } from '../../utils/keyBindings'
const MENU_CONTAINER_CLASS_NAME = 'jsoneditor-floating-menu-container'
const MENU_CLASS_NAME = 'jsoneditor-floating-menu'
const MENU_ITEM_CLASS_NAME = 'jsoneditor-floating-menu-item'
@ -89,17 +88,15 @@ const CREATE_TYPE = {
export default class FloatingMenu extends PureComponent {
render () {
return h('div', {className: MENU_CONTAINER_CLASS_NAME},
h('div', {className: MENU_CLASS_NAME}, this.props.items.map(item => {
const type = typeof item === 'string' ? item : item.type
const createType = CREATE_TYPE[type]
if (createType) {
return createType(this.props.path, this.props.events)
}
else {
throw new Error('Unknown type of menu item for floating menu: ' + JSON.stringify(item))
}
})
))
return h('div', {className: MENU_CLASS_NAME}, this.props.items.map(item => {
const type = typeof item === 'string' ? item : item.type
const createType = CREATE_TYPE[type]
if (createType) {
return createType(this.props.path, this.props.events)
}
else {
throw new Error('Unknown type of menu item for floating menu: ' + JSON.stringify(item))
}
}))
}
}

View File

@ -20,6 +20,9 @@ import type {
type RecurseCallback = (value: ESON, path: Path, root: ESON) => ESON
export const SELECTED = 1
export const SELECTED_END = 2
/**
* Expand function which will expand all nodes
* @param {Path} path
@ -363,7 +366,8 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
// select a single node
return setIn(eson, rootEsonPath.concat(['selected']), true)
return setIn(eson, rootEsonPath.concat(['selected']), SELECTED_END)
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index
}
else {
// select multiple childs of an object or array
@ -375,8 +379,9 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
const childsBefore = root[childsKey].slice(0, minIndex)
const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
.map(child => setIn(child, ['value', 'selected'], true))
.map((child, index) => setIn(child, ['value', 'selected'], index === 0 ? SELECTED_END : SELECTED))
const childsAfter = root[childsKey].slice(maxIndex)
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
})

View File

@ -7,7 +7,9 @@
@theme-color: #3883fa;
@floating-menu-background: #4d4d4d;
@floating-menu-color: #fff;
@selectedColor: #e5e5e5;
// @selectedColor: #e5e5e5;
@selectedColor: #ffed99;
@hoverColor: rgba(10, 10, 10, 0.05);
.jsoneditor {
border: 1px solid @theme-color;
@ -561,70 +563,68 @@ button.jsoneditor-type-Array.jsoneditor-selected span.jsoneditor-icon {
/******************************* Floatting Menu **********************************/
div.jsoneditor-node {
div.jsoneditor-node-container {
position: relative;
transition: background-color 100ms ease-in;
div.jsoneditor-floating-menu-container {
div.jsoneditor-floating-menu {
display: none;
position: absolute;
bottom: 100%;
left: 50%;
right: 0;
z-index: 999;
div.jsoneditor-floating-menu {
margin: 10px;
white-space: nowrap;
border-radius: 5px;
box-shadow: 0 2px 6px 0 rgba(0,0,0,.24);
margin: 10px;
white-space: nowrap;
border-radius: 5px;
box-shadow: 0 2px 6px 0 rgba(0,0,0,.24);
&:after {
content:'';
position: absolute;
top: 100%;
left: 10%;
margin-left: -10px;
margin-top: -10px;
width: 0;
height: 0;
border-top: solid 10px @floating-menu-background;
border-left: solid 10px transparent;
border-right: solid 10px transparent;
&:after {
content:'';
position: absolute;
top: 100%;
left: 35px;
margin-left: -10px;
width: 0;
height: 0;
border-top: solid 10px @floating-menu-background;
border-left: solid 10px transparent;
border-right: solid 10px transparent;
}
button.jsoneditor-floating-menu-item {
color: @floating-menu-color;
background: @floating-menu-background;
border: none;
border-right: 1px solid lighten(@floating-menu-background, 10%);
padding: 10px;
cursor: pointer;
&:hover {
background: lighten(@floating-menu-background, 10%);
}
button.jsoneditor-floating-menu-item {
color: @floating-menu-color;
background: @floating-menu-background;
border: none;
border-right: 1px solid lighten(@floating-menu-background, 10%);
padding: 10px;
cursor: pointer;
&:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
&:hover {
background: lighten(@floating-menu-background, 10%);
}
&:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
&:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
border-right: none;
}
&:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
border-right: none;
}
}
}
&:hover {
background-color: @selectedColor;
div.jsoneditor-floating-menu-container {
display: inherit;
}
&.jsoneditor-node-hover {
background-color: @hoverColor;
}
}
.jsoneditor-selected-end > .jsoneditor-node-container > div.jsoneditor-floating-menu {
display: inherit;
}
/******************************* **********************************/
div.jsoneditor-modes {

View File

@ -5,7 +5,8 @@ import {
jsonToEson, esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer,
expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection, pathsFromSelection
applySelection, pathsFromSelection,
SELECTED, SELECTED_END
} from '../src/eson'
const JSON1 = loadJSON('./resources/json1.json')
@ -281,9 +282,9 @@ test('selection (object)', t => {
const actual = applySelection(ESON1, selection)
let expected = ESON1
expected = setIn(expected, toEsonPath(ESON1, ['obj']).concat(['selected']), true)
expected = setIn(expected, toEsonPath(ESON1, ['str']).concat(['selected']), true)
expected = setIn(expected, toEsonPath(ESON1, ['nill']).concat(['selected']), true)
expected = setIn(expected, toEsonPath(ESON1, ['obj']).concat(['selected']), SELECTED_END)
expected = setIn(expected, toEsonPath(ESON1, ['str']).concat(['selected']), SELECTED)
expected = setIn(expected, toEsonPath(ESON1, ['nill']).concat(['selected']), SELECTED)
t.deepEqual(actual, expected)
})
@ -296,9 +297,10 @@ test('selection (array)', t => {
const actual = applySelection(ESON1, selection)
// FIXME: SELECTE_END should be selection.start, not the first
let expected = ESON1
expected = setIn(expected, toEsonPath(ESON1, ['obj', 'arr', '0']).concat(['selected']), true)
expected = setIn(expected, toEsonPath(ESON1, ['obj', 'arr', '1']).concat(['selected']), true)
expected = setIn(expected, toEsonPath(ESON1, ['obj', 'arr', '0']).concat(['selected']), SELECTED_END)
expected = setIn(expected, toEsonPath(ESON1, ['obj', 'arr', '1']).concat(['selected']), SELECTED)
t.deepEqual(actual, expected)
})
@ -310,7 +312,7 @@ test('selection (value)', t => {
}
const actual = applySelection(ESON1, selection)
const expected = setIn(ESON1, toEsonPath(ESON1, ['obj', 'arr', '2', 'first']).concat(['selected']), true)
const expected = setIn(ESON1, toEsonPath(ESON1, ['obj', 'arr', '2', 'first']).concat(['selected']), SELECTED_END)
t.deepEqual(actual, expected)
})
@ -321,7 +323,7 @@ test('selection (node)', t => {
}
const actual = applySelection(ESON1, selection)
const expected = setIn(ESON1, toEsonPath(ESON1, ['obj', 'arr']).concat(['selected']), true)
const expected = setIn(ESON1, toEsonPath(ESON1, ['obj', 'arr']).concat(['selected']), SELECTED_END)
t.deepEqual(actual, expected)
})