Show floating menu on selection
This commit is contained in:
parent
cea4e2c101
commit
a2f7f61389
|
@ -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
|
||||
|
|
|
@ -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}}})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue