Floating menu (WIP)
This commit is contained in:
parent
0910aa5a63
commit
cea4e2c101
|
@ -1,9 +1,10 @@
|
|||
// @flow weak
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
import { createElement as h, PureComponent } from 'react'
|
||||
import initial from 'lodash/initial'
|
||||
|
||||
import ActionMenu from './menu/ActionMenu'
|
||||
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'
|
||||
|
@ -11,7 +12,7 @@ import { compileJSONPointer } from '../eson'
|
|||
|
||||
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
|
||||
|
||||
export default class JSONNode extends Component {
|
||||
export default class JSONNode extends PureComponent {
|
||||
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
|
||||
|
||||
state = {
|
||||
|
@ -42,8 +43,16 @@ export default class JSONNode extends Component {
|
|||
className: 'jsoneditor-node jsoneditor-object'
|
||||
}, [
|
||||
this.renderExpandButton(),
|
||||
this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
|
||||
this.renderActionMenuButton(),
|
||||
// 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.renderError(data.error)
|
||||
|
@ -88,8 +97,16 @@ export default class JSONNode extends Component {
|
|||
className: 'jsoneditor-node jsoneditor-array'
|
||||
}, [
|
||||
this.renderExpandButton(),
|
||||
this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
|
||||
this.renderActionMenuButton(),
|
||||
// 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.renderError(data.error)
|
||||
|
@ -130,8 +147,16 @@ export default class JSONNode extends Component {
|
|||
className: 'jsoneditor-node'
|
||||
}, [
|
||||
this.renderPlaceholder(),
|
||||
this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
|
||||
this.renderActionMenuButton(),
|
||||
// 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),
|
||||
|
@ -151,8 +176,8 @@ export default class JSONNode extends Component {
|
|||
onKeyDown: this.handleKeyDownAppend
|
||||
}, [
|
||||
this.renderPlaceholder(),
|
||||
this.renderActionMenu('append', this.state.appendMenu, this.handleCloseAppendActionMenu),
|
||||
this.renderAppendActionMenuButton(),
|
||||
// this.renderActionMenu('append', this.state.appendMenu, this.handleCloseAppendActionMenu),
|
||||
// this.renderAppendActionMenuButton(),
|
||||
this.renderReadonly(text)
|
||||
])
|
||||
}
|
||||
|
@ -402,6 +427,15 @@ export default class JSONNode extends Component {
|
|||
])
|
||||
}
|
||||
|
||||
renderFloatingMenu (items) {
|
||||
return h(FloatingMenu, {
|
||||
key: 'menu',
|
||||
path: this.props.path,
|
||||
events: this.props.events,
|
||||
items
|
||||
})
|
||||
}
|
||||
|
||||
renderAppendActionMenuButton () {
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
((this.state.appendOpen) ? ' jsoneditor-visible' : '')
|
||||
|
@ -450,24 +484,6 @@ export default class JSONNode extends Component {
|
|||
this.setState({ appendMenu: null })
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
let prop
|
||||
|
||||
for (prop in nextProps) {
|
||||
if (nextProps.hasOwnProperty(prop) && this.props[prop] !== nextProps[prop]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for (prop in nextState) {
|
||||
if (nextState.hasOwnProperty(prop) && this.state[prop] !== nextState[prop]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
static getRootName (data, options) {
|
||||
return typeof options.name === 'string'
|
||||
? options.name
|
||||
|
|
|
@ -69,9 +69,9 @@ export default class TreeMode extends Component {
|
|||
'right': this.moveRight,
|
||||
'home': this.moveHome,
|
||||
'end': this.moveEnd,
|
||||
'cut': this.handleCut,
|
||||
'copy': this.handleCopy,
|
||||
'paste': this.handlePaste,
|
||||
'cut': this.handleKeyDownCut,
|
||||
'copy': this.handleKeyDownCopy,
|
||||
'paste': this.handleKeyDownPaste,
|
||||
'undo': this.handleUndo,
|
||||
'redo': this.handleRedo,
|
||||
'find': this.handleFocusFind,
|
||||
|
@ -95,6 +95,10 @@ export default class TreeMode extends Component {
|
|||
onRemove: this.handleRemove,
|
||||
onSort: this.handleSort,
|
||||
|
||||
onCut: this.handleMenuCut,
|
||||
onCopy: this.handleMenuCopy,
|
||||
onPaste: this.handleMenuPaste,
|
||||
|
||||
onExpand: this.handleExpand,
|
||||
|
||||
// TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events'
|
||||
|
@ -393,12 +397,57 @@ export default class TreeMode extends Component {
|
|||
moveEnd(event.target)
|
||||
}
|
||||
|
||||
handleCut = (event) => {
|
||||
const { data, selection } = this.state
|
||||
|
||||
handleKeyDownCut = (event) => {
|
||||
const { selection } = this.state
|
||||
if (selection) {
|
||||
event.preventDefault()
|
||||
}
|
||||
this.handleCut(selection)
|
||||
}
|
||||
|
||||
handleKeyDownCopy = (event) => {
|
||||
const { selection } = this.state
|
||||
if (selection) {
|
||||
event.preventDefault()
|
||||
}
|
||||
this.handleCopy(selection)
|
||||
}
|
||||
|
||||
handleKeyDownPaste = (event) => {
|
||||
const { clipboard, selection } = this.state
|
||||
if (clipboard && clipboard.length > 0) {
|
||||
event.preventDefault()
|
||||
if (selection) {
|
||||
this.handlePaste(clipboard, selection, null)
|
||||
}
|
||||
else {
|
||||
// no selection -> paste after current path
|
||||
const path = this.findDataPathFromElement(event.target)
|
||||
this.handlePaste(clipboard, null, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuCut = (path) => {
|
||||
const selection = { start: { path }, end: { path }}
|
||||
this.handleCut(selection)
|
||||
}
|
||||
|
||||
handleMenuCopy = (path) => {
|
||||
const selection = { start: { path }, end: { path }}
|
||||
this.handleCopy(selection)
|
||||
}
|
||||
|
||||
handleMenuPaste = (path) => {
|
||||
const { clipboard } = this.state
|
||||
if (clipboard && clipboard.length > 0) {
|
||||
this.handlePaste(clipboard, null, path)
|
||||
}
|
||||
}
|
||||
|
||||
handleCut = (selection: ESONSelection) => {
|
||||
if (selection && selection.start && selection.end) {
|
||||
const data = this.state.data
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const clipboard = contentsFromPaths(data, paths)
|
||||
|
||||
|
@ -415,12 +464,9 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleCopy = (event) => {
|
||||
const { data, selection } = this.state
|
||||
|
||||
if (selection) {
|
||||
event.preventDefault()
|
||||
|
||||
handleCopy = (selection: ESONSelection) => {
|
||||
if (selection && selection.start && selection.end) {
|
||||
const data = this.state.data
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const clipboard = contentsFromPaths(data, paths)
|
||||
|
||||
|
@ -432,15 +478,12 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handlePaste = (event) => {
|
||||
const { data, clipboard } = this.state
|
||||
handlePaste = (clipboard, selection: ESONSelection, path: JSONPath) => {
|
||||
const { data } = this.state
|
||||
|
||||
if (clipboard && clipboard.length > 0) {
|
||||
event.preventDefault()
|
||||
|
||||
// FIXME: handle pasting in an empty object or array
|
||||
|
||||
const path = this.findDataPathFromElement(event.target)
|
||||
if (path && path.length > 0) {
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(data, toEsonPath(data, parentPath))
|
||||
|
@ -468,6 +511,9 @@ export default class TreeMode extends Component {
|
|||
this.handlePatch(patch)
|
||||
}
|
||||
}
|
||||
else if (selection){
|
||||
console.log('TODO: replace selection')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -649,11 +695,12 @@ export default class TreeMode extends Component {
|
|||
const path = this.findDataPathFromElement(event.target.firstChild)
|
||||
if (path) {
|
||||
// TODO: implement a better solution to keep focus in the editor than selecting the action menu. Most also be solved for undo/redo for example
|
||||
const element = findNode(this.refs.contents, path)
|
||||
const actionMenuButton = element && element.querySelector('button.jsoneditor-actionmenu')
|
||||
if (actionMenuButton) {
|
||||
actionMenuButton.focus()
|
||||
}
|
||||
// --> focus to menu?
|
||||
// const element = findNode(this.refs.contents, path)
|
||||
// const actionMenuButton = element && element.querySelector('button.jsoneditor-actionmenu')
|
||||
// if (actionMenuButton) {
|
||||
// actionMenuButton.focus()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
// @flow weak
|
||||
|
||||
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'
|
||||
|
||||
// Array: Sort | Map | Filter | Duplicate | Cut | Copy | Remove
|
||||
// advanced sort (asc, desc, nested fields, custom comparator)
|
||||
// sort, map, filter, open a popup covering the editor (not the whole page)
|
||||
// (or if it's small, can be a dropdown)
|
||||
// Object: Sort | Duplicate | Cut | Copy | Remove
|
||||
// simple sort (asc/desc)
|
||||
// Value: [x] String | Duplicate | Cut | Copy | Remove
|
||||
// String is a checkmark
|
||||
// Between: Insert Structure | Insert Value | Insert Object | Insert Array | Paste
|
||||
// inserting (value selected): [field] [value]
|
||||
// inserting (array selected): (immediately show the "Between" menu to create the first item)
|
||||
// inserting (object selected): (immediately show the "Between" menu to create the first property)
|
||||
//
|
||||
// Selection: Duplicate | Cut | Copy | Paste | Remove
|
||||
//
|
||||
// menu must have vertical orientation on small screens?
|
||||
//
|
||||
// icons
|
||||
// cut
|
||||
// copy
|
||||
// paste
|
||||
// duplicate
|
||||
// remove
|
||||
// sort
|
||||
// transform ??? -> filter? cog?
|
||||
// undo
|
||||
// redo
|
||||
// expand ???
|
||||
// collapse ???
|
||||
// format/compact ???
|
||||
//
|
||||
// https://github.com/FortAwesome/Font-Awesome/wiki/Customize-Font-Awesome
|
||||
// http://fontastic.me/
|
||||
// --> have to create my own icons I guess :(
|
||||
|
||||
// TODO: show quick keys in the title of the menu items
|
||||
const CREATE_TYPE = {
|
||||
sort: (path, events) => h('button', {
|
||||
key: 'sort',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => events.onSort(path),
|
||||
title: 'Sort'
|
||||
}, 'Sort'),
|
||||
|
||||
duplicate: (path, events) => h('button', {
|
||||
key: 'duplicate',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => events.onDuplicate(path),
|
||||
title: 'Duplicate'
|
||||
}, 'Duplicate'),
|
||||
|
||||
cut: (path, events) => h('button', {
|
||||
key: 'cut',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => events.onCut(path),
|
||||
title: 'Cut'
|
||||
}, 'Cut'),
|
||||
|
||||
copy: (path, events) => h('button', {
|
||||
key: 'copy',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => events.onCopy(path),
|
||||
title: 'Copy'
|
||||
}, 'Copy'),
|
||||
|
||||
paste: (path, events) => h('button', {
|
||||
key: 'paste',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => events.onPaste(path),
|
||||
title: 'Paste'
|
||||
}, 'Paste'),
|
||||
|
||||
remove: (path, events) => h('button', {
|
||||
key: 'remove',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => events.onRemove(path),
|
||||
title: 'Remove'
|
||||
}, 'Remove'),
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
))
|
||||
}
|
||||
}
|
|
@ -5,6 +5,9 @@
|
|||
@black: #1A1A1A;
|
||||
@contentsMinHeight: 150px;
|
||||
@theme-color: #3883fa;
|
||||
@floating-menu-background: #4d4d4d;
|
||||
@floating-menu-color: #fff;
|
||||
@selectedColor: #e5e5e5;
|
||||
|
||||
.jsoneditor {
|
||||
border: 1px solid @theme-color;
|
||||
|
@ -257,7 +260,7 @@ div.jsoneditor-value.jsoneditor-empty::after {
|
|||
}
|
||||
|
||||
.jsoneditor-selected {
|
||||
background-color: #f0f0f0;
|
||||
background-color: @selectedColor;
|
||||
}
|
||||
|
||||
.jsoneditor-highlight {
|
||||
|
@ -556,6 +559,74 @@ button.jsoneditor-type-Array.jsoneditor-selected span.jsoneditor-icon {
|
|||
background-position: -96px 0;
|
||||
}
|
||||
|
||||
/******************************* Floatting Menu **********************************/
|
||||
|
||||
div.jsoneditor-node {
|
||||
|
||||
div.jsoneditor-floating-menu-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
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);
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
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%);
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @selectedColor;
|
||||
|
||||
div.jsoneditor-floating-menu-container {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/******************************* **********************************/
|
||||
div.jsoneditor-modes {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
|
Loading…
Reference in New Issue