Floating menu (WIP)

This commit is contained in:
jos 2017-10-13 13:27:33 +02:00
parent 0910aa5a63
commit cea4e2c101
4 changed files with 290 additions and 51 deletions

View File

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

View File

@ -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()
// }
}
}

View File

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

View File

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