Refactored action menus, added quick keys Ctrl+M, Ctrl+E
This commit is contained in:
parent
bb8734707e
commit
94b7be90d3
|
@ -2,27 +2,20 @@
|
|||
|
||||
import { createElement as h, Component } from 'react'
|
||||
|
||||
import ActionButton from './menu/ActionButton'
|
||||
import AppendActionButton from './menu/AppendActionButton'
|
||||
import ActionMenu from './menu/ActionMenu'
|
||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||
import { getInnerText, insideRect } from '../utils/domUtils'
|
||||
import { getInnerText, insideRect, findParentNode } from '../utils/domUtils'
|
||||
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
||||
import { compileJSONPointer } from '../jsonData'
|
||||
|
||||
import type { PropertyData, JSONData, SearchResultStatus } from '../types'
|
||||
|
||||
/**
|
||||
* @type {JSONNode | null} activeContextMenu singleton holding the JSONNode having
|
||||
* the active (visible) context menu
|
||||
*/
|
||||
let activeContextMenu = null
|
||||
|
||||
export default class JSONNode extends Component {
|
||||
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
|
||||
|
||||
state = {
|
||||
menu: null, // context menu
|
||||
appendMenu: null, // append context menu (used in placeholder of empty object/array)
|
||||
menu: null, // can contain object {anchor, root}
|
||||
appendMenu: null, // can contain object {anchor, root}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -48,6 +41,7 @@ export default class JSONNode extends Component {
|
|||
className: 'jsoneditor-node jsoneditor-object'
|
||||
}, [
|
||||
this.renderExpandButton(),
|
||||
this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
|
||||
this.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`),
|
||||
|
@ -93,6 +87,7 @@ export default class JSONNode extends Component {
|
|||
className: 'jsoneditor-node jsoneditor-array'
|
||||
}, [
|
||||
this.renderExpandButton(),
|
||||
this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
|
||||
this.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`),
|
||||
|
@ -134,6 +129,7 @@ export default class JSONNode extends Component {
|
|||
className: 'jsoneditor-node'
|
||||
}, [
|
||||
this.renderPlaceholder(),
|
||||
this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
|
||||
this.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
this.renderSeparator(),
|
||||
|
@ -154,7 +150,8 @@ export default class JSONNode extends Component {
|
|||
onKeyDown: this.handleKeyDownAppend
|
||||
}, [
|
||||
this.renderPlaceholder(),
|
||||
this.renderAppendMenuButton(),
|
||||
this.renderActionMenu('append', this.state.appendMenu, this.handleCloseAppendActionMenu),
|
||||
this.renderAppendActionMenuButton(),
|
||||
this.renderReadonly(text)
|
||||
])
|
||||
}
|
||||
|
@ -167,6 +164,7 @@ export default class JSONNode extends Component {
|
|||
return h('div', {key: 'readonly', className: 'jsoneditor-readonly', title}, text)
|
||||
}
|
||||
|
||||
// TODO: simplify the method renderProperty
|
||||
renderProperty (prop: ?PropertyData, index: ?number, data: JSONData, options: {escapeUnicode: boolean, isPropertyEditable: (path: string) => boolean}) {
|
||||
const isIndex = typeof index === 'number'
|
||||
|
||||
|
@ -368,23 +366,89 @@ export default class JSONNode extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
renderActionMenuButton () {
|
||||
return h(ActionButton, {
|
||||
key: 'action',
|
||||
// TODO: simplify code for the two action menus
|
||||
renderActionMenu (menuType, menuState, onClose) {
|
||||
if (!menuState) {
|
||||
return null
|
||||
}
|
||||
|
||||
return h(ActionMenu, {
|
||||
key: 'menu',
|
||||
path: this.props.path,
|
||||
events: this.props.events,
|
||||
type: this.props.data.type,
|
||||
events: this.props.events
|
||||
|
||||
menuType,
|
||||
open: true,
|
||||
anchor: menuState.anchor,
|
||||
root: menuState.root,
|
||||
|
||||
onRequestClose: onClose
|
||||
})
|
||||
}
|
||||
|
||||
renderAppendMenuButton () {
|
||||
return h(AppendActionButton, {
|
||||
key: 'append',
|
||||
path: this.props.path,
|
||||
events: this.props.events
|
||||
renderActionMenuButton () {
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
((this.state.open) ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {className: 'jsoneditor-button-container', key: 'action'}, [
|
||||
h('button', {
|
||||
key: 'button',
|
||||
ref: 'actionMenuButton',
|
||||
className,
|
||||
onClick: this.handleOpenActionMenu
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
renderAppendActionMenuButton () {
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
((this.state.appendOpen) ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {className: 'jsoneditor-button-container', key: 'action'}, [
|
||||
h('button', {
|
||||
key: 'button',
|
||||
ref: 'appendActionMenuButton',
|
||||
className,
|
||||
onClick: this.handleOpenAppendActionMenu
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
handleOpenActionMenu = (event) => {
|
||||
// TODO: don't use refs, find root purely via DOM?
|
||||
const root = findParentNode(this.refs.actionMenuButton, 'data-jsoneditor', 'true')
|
||||
|
||||
this.setState({
|
||||
menu: {
|
||||
open: true,
|
||||
anchor: this.refs.actionMenuButton,
|
||||
root
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleCloseActionMenu = () => {
|
||||
this.setState({ menu: null })
|
||||
}
|
||||
|
||||
handleOpenAppendActionMenu = (event) => {
|
||||
// TODO: don't use refs, find root purely via DOM?
|
||||
const root = findParentNode(this.refs.appendActionMenuButton, 'data-jsoneditor', 'true')
|
||||
|
||||
this.setState({
|
||||
appendMenu: {
|
||||
open: true,
|
||||
anchor: this.refs.actionMenuButton,
|
||||
root
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleCloseAppendActionMenu = () => {
|
||||
this.setState({ appendMenu: null })
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
let prop
|
||||
|
||||
|
@ -456,6 +520,18 @@ export default class JSONNode extends Component {
|
|||
event.preventDefault()
|
||||
this.props.events.onRemove(this.props.path)
|
||||
}
|
||||
|
||||
if (keyBinding === 'expand') {
|
||||
event.preventDefault()
|
||||
const recurse = false
|
||||
const expanded = !this.props.data.expanded
|
||||
this.props.events.onExpand(this.props.path, expanded, recurse)
|
||||
}
|
||||
|
||||
if (keyBinding === 'actionMenu') {
|
||||
event.preventDefault()
|
||||
this.handleOpenActionMenu(event)
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
|
@ -466,6 +542,11 @@ export default class JSONNode extends Component {
|
|||
event.preventDefault()
|
||||
this.props.events.onAppend(this.props.path, 'value')
|
||||
}
|
||||
|
||||
if (keyBinding === 'actionMenu') {
|
||||
event.preventDefault()
|
||||
this.handleOpenAppendActionMenu(event)
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
|
@ -485,20 +566,6 @@ export default class JSONNode extends Component {
|
|||
this.props.events.onExpand(this.props.path, expanded, recurse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton function to hide the currently visible context menu if any.
|
||||
* @protected
|
||||
*/
|
||||
static hideActionMenu () {
|
||||
if (activeContextMenu) {
|
||||
activeContextMenu.setState({
|
||||
menu: null,
|
||||
appendMenu: null
|
||||
})
|
||||
activeContextMenu = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When this JSONNode holds an URL as value, open this URL in a new browser tab
|
||||
* @param event
|
||||
|
|
|
@ -38,6 +38,28 @@ const AJV_OPTIONS = {
|
|||
const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept in memory
|
||||
const SEARCH_DEBOUNCE = 300 // milliseconds
|
||||
|
||||
// TODO: make key bindings configurable
|
||||
const KEY_BINDINGS = {
|
||||
'duplicate': ['Ctrl+D', 'Command+D'],
|
||||
'insert': ['Ctrl+Insert', 'Command+Insert'],
|
||||
'remove': ['Ctrl+Delete', 'Command+Delete'],
|
||||
'expand': ['Ctrl+E', 'Command+E'],
|
||||
'actionMenu':['Ctrl+M', 'Command+M'],
|
||||
'up': ['Alt+Up', 'Option+Up'],
|
||||
'down': ['Alt+Down', 'Option+Down'],
|
||||
'left': ['Alt+Left', 'Option+Left'],
|
||||
'right': ['Alt+Right', 'Option+Right'],
|
||||
'openUrl': ['Ctrl+Enter', 'Command+Enter']
|
||||
// TODO: implement all quick keys
|
||||
// Ctrl+Shift+Arrow Up/Down Select multiple fields
|
||||
// Shift+Alt+Arrows Move current field or selected fields up/down/left/right
|
||||
// Ctrl+F Find
|
||||
// F3, Ctrl+G Find next
|
||||
// Shift+F3, Ctrl+Shift+G Find previous
|
||||
// Ctrl+Z Undo last action
|
||||
// Ctrl+Shift+Z Redo
|
||||
}
|
||||
|
||||
export default class TreeMode extends Component {
|
||||
id: number
|
||||
state: Object
|
||||
|
@ -49,30 +71,6 @@ export default class TreeMode extends Component {
|
|||
|
||||
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
|
||||
|
||||
// TODO: make key bindings configurable
|
||||
const keyBindings = {
|
||||
'duplicate': ['Ctrl+D', 'Command+D'],
|
||||
'insert': ['Ctrl+Insert', 'Command+Insert'],
|
||||
'remove': ['Ctrl+Delete', 'Command+Delete'],
|
||||
'up': ['Alt+Up', 'Option+Up'],
|
||||
'down': ['Alt+Down', 'Option+Down'],
|
||||
'left': ['Alt+Left', 'Option+Left'],
|
||||
'right': ['Alt+Right', 'Option+Right'],
|
||||
'openUrl': ['Ctrl+Enter', 'Command+Enter']
|
||||
// TODO: implement all quick keys
|
||||
// Ctrl+Shift+Arrow Up/Down Select multiple fields
|
||||
// Shift+Alt+Arrows Move current field or selected fields up/down/left/right
|
||||
// Ctrl+Ins Insert a new field with type auto
|
||||
// Ctrl+Shift+Ins Append a new field with type auto
|
||||
// Ctrl+E Expand or collapse field
|
||||
// Ctrl+F Find
|
||||
// F3, Ctrl+G Find next
|
||||
// Shift+F3, Ctrl+Shift+G Find previous
|
||||
// Ctrl+M Show actions menu
|
||||
// Ctrl+Z Undo last action
|
||||
// Ctrl+Shift+Z Redo
|
||||
}
|
||||
|
||||
this.state = {
|
||||
data,
|
||||
|
||||
|
@ -100,7 +98,7 @@ export default class TreeMode extends Component {
|
|||
active: null // active search result
|
||||
},
|
||||
|
||||
keyCombos: this.bindingsByCombos (keyBindings)
|
||||
keyCombos: this.bindingsByCombos (KEY_BINDINGS)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +173,7 @@ export default class TreeMode extends Component {
|
|||
key: 'contents',
|
||||
ref: 'contents',
|
||||
className: 'jsoneditor-contents jsoneditor-tree-contents',
|
||||
onClick: this.handleHideMenus, id: this.id
|
||||
id: this.id
|
||||
},
|
||||
h('ul', {className: 'jsoneditor-list jsoneditor-root'},
|
||||
h(Node, {
|
||||
|
@ -322,11 +320,6 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleHideMenus = () => {
|
||||
JSONNode.hideActionMenu()
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleChangeValue = (path, value) => {
|
||||
this.handlePatch(changeValue(this.state.data, path, value))
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { createElement as h, Component } from 'react'
|
||||
import ActionMenu from './ActionMenu'
|
||||
import { findParentNode } from '../../utils/domUtils'
|
||||
|
||||
export default class ActionButton extends Component {
|
||||
constructor (props) {
|
||||
super (props)
|
||||
|
||||
this.state = {
|
||||
open: false, // whether the menu is open or not
|
||||
anchor: null,
|
||||
root: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{path, type, events}} props
|
||||
* @param state
|
||||
* @return {*}
|
||||
*/
|
||||
render () {
|
||||
const { props, state} = this
|
||||
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
(this.state.open ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {className: 'jsoneditor-button-container'}, [
|
||||
h(ActionMenu, {
|
||||
key: 'menu',
|
||||
...props, // path, type, events
|
||||
...state, // open, anchor, root
|
||||
onRequestClose: this.handleRequestClose
|
||||
}),
|
||||
h('button', {
|
||||
key: 'button',
|
||||
className,
|
||||
onClick: this.handleOpen
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
handleOpen = (event) => {
|
||||
this.setState({
|
||||
open: true,
|
||||
anchor: event.target,
|
||||
root: findParentNode(event.target, 'data-jsoneditor', 'true')
|
||||
})
|
||||
}
|
||||
|
||||
handleRequestClose = () => {
|
||||
this.setState({open: false})
|
||||
}
|
||||
}
|
|
@ -3,17 +3,26 @@ import Menu from './Menu'
|
|||
import {
|
||||
createChangeType, createSort,
|
||||
createSeparator,
|
||||
createInsert, createDuplicate, createRemove
|
||||
} from './entries'
|
||||
createInsert, createAppend, createDuplicate, createRemove
|
||||
} from './items'
|
||||
|
||||
export default class ActionMenu extends Component {
|
||||
/**
|
||||
* @param {{open, anchor, root, path, type, events, onRequestClose}} props
|
||||
* @param state
|
||||
* @return {JSX.Element}
|
||||
* props = {open, anchor, root, path, type, menuType, events, onRequestClose}
|
||||
*/
|
||||
|
||||
render () {
|
||||
const { props, state} = this
|
||||
const items = this.props.menuType === 'append' // update or append
|
||||
? this.createAppendMenuItems()
|
||||
: this.createActionMenuItems()
|
||||
|
||||
// TODO: implement a hook to adjust the action menu items
|
||||
|
||||
return h(Menu, { ...this.props, items })
|
||||
}
|
||||
|
||||
createActionMenuItems () {
|
||||
const props = this.props
|
||||
|
||||
let items = [] // array with menu items
|
||||
|
||||
|
@ -33,11 +42,12 @@ export default class ActionMenu extends Component {
|
|||
items.push(createRemove(props.path, props.events.onRemove))
|
||||
}
|
||||
|
||||
// TODO: implement a hook to adjust the action menu
|
||||
return items
|
||||
}
|
||||
|
||||
return h(Menu, {
|
||||
...props,
|
||||
items
|
||||
})
|
||||
createAppendMenuItems () {
|
||||
return [
|
||||
createAppend(this.props.path, this.props.events.onAppend)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { createElement as h, Component } from 'react'
|
||||
import AppendActionMenu from './AppendActionMenu'
|
||||
import { findParentNode } from '../../utils/domUtils'
|
||||
|
||||
export default class AppendActionButton extends Component {
|
||||
constructor (props) {
|
||||
super (props)
|
||||
|
||||
this.state = {
|
||||
open: false, // whether the menu is open or not
|
||||
anchor: null,
|
||||
root: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{path, events}} props
|
||||
* @param state
|
||||
* @return {*}
|
||||
*/
|
||||
render () {
|
||||
const { props, state} = this
|
||||
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
(this.state.open ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {className: 'jsoneditor-button-container'}, [
|
||||
h(AppendActionMenu, {
|
||||
key: 'menu',
|
||||
...props, // path, events
|
||||
...state, // open, anchor, root
|
||||
onRequestClose: this.handleRequestClose
|
||||
}),
|
||||
h('button', {
|
||||
key: 'button',
|
||||
className: className,
|
||||
onClick: this.handleOpen
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
handleOpen = (event) => {
|
||||
this.setState({
|
||||
open: true,
|
||||
anchor: event.target,
|
||||
root: findParentNode(event.target, 'data-jsoneditor', 'true')
|
||||
})
|
||||
}
|
||||
|
||||
handleRequestClose = () => {
|
||||
this.setState({open: false})
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { createElement as h, Component } from 'react'
|
||||
import Menu from './Menu'
|
||||
import { createAppend } from './entries'
|
||||
|
||||
export default class AppendActionMenu extends Component {
|
||||
/**
|
||||
* @param {{anchor, root, path, events}} props
|
||||
* @param state
|
||||
* @return {JSX.Element}
|
||||
*/
|
||||
render () {
|
||||
const { props, state} = this
|
||||
|
||||
const items = [
|
||||
createAppend(props.path, props.events.onAppend)
|
||||
]
|
||||
|
||||
// TODO: implement a hook to adjust the action menu
|
||||
|
||||
return h(Menu, {
|
||||
...props,
|
||||
items
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4,6 +4,10 @@ import { findParentNode } from '../../utils/domUtils'
|
|||
export let CONTEXT_MENU_HEIGHT = 240
|
||||
|
||||
export default class Menu extends Component {
|
||||
|
||||
/**
|
||||
* @param {{open: boolean, items: Array, anchor, root, onRequestClose: function}} props
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -14,11 +18,6 @@ export default class Menu extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{open: boolean, items: Array, anchor, root, onRequestClose: function}} props
|
||||
* @param state
|
||||
* @return {*}
|
||||
*/
|
||||
render () {
|
||||
if (!this.props.open) {
|
||||
return null
|
||||
|
@ -159,7 +158,8 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeRequestCloseListener()
|
||||
// remove on next tick, since a listener can be created on next tick too
|
||||
setTimeout(() => this.removeRequestCloseListener())
|
||||
}
|
||||
|
||||
updateRequestCloseListener () {
|
||||
|
@ -172,18 +172,14 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
addRequestCloseListener () {
|
||||
if (!this.handleRequestClose) {
|
||||
// Attach event listener on next tick, else the current click to open
|
||||
// the menu will immediately result in requestClose event as well
|
||||
setTimeout(() => {
|
||||
this.handleRequestClose = (event) => {
|
||||
if (!findParentNode(event.target, 'data-menu', 'true')) {
|
||||
this.props.onRequestClose()
|
||||
}
|
||||
}
|
||||
// Attach event listener on next tick, else the current click to open
|
||||
// the menu will immediately result in requestClose event as well
|
||||
setTimeout(() => {
|
||||
if (this.props.open && !this.handleRequestClose) {
|
||||
this.handleRequestClose = (event) => this.props.onRequestClose()
|
||||
window.addEventListener('click', this.handleRequestClose)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
removeRequestCloseListener () {
|
||||
|
|
|
@ -22,25 +22,25 @@ export function createChangeType (path, type, onChangeType) {
|
|||
submenu: [
|
||||
{
|
||||
text: 'Value',
|
||||
className: 'jsoneditor-type-value' + (type == 'value' ? ' jsoneditor-selected' : ''),
|
||||
className: 'jsoneditor-type-value' + (type === 'value' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.value,
|
||||
click: () => onChangeType(path, 'value')
|
||||
},
|
||||
{
|
||||
text: 'Array',
|
||||
className: 'jsoneditor-type-Array' + (type == 'Array' ? ' jsoneditor-selected' : ''),
|
||||
className: 'jsoneditor-type-Array' + (type === 'Array' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.array,
|
||||
click: () => onChangeType(path, 'Array')
|
||||
},
|
||||
{
|
||||
text: 'Object',
|
||||
className: 'jsoneditor-type-Object' + (type == 'Object' ? ' jsoneditor-selected' : ''),
|
||||
className: 'jsoneditor-type-Object' + (type === 'Object' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.object,
|
||||
click: () => onChangeType(path, 'Object')
|
||||
},
|
||||
{
|
||||
text: 'String',
|
||||
className: 'jsoneditor-type-string' + (type == 'string' ? ' jsoneditor-selected' : ''),
|
||||
className: 'jsoneditor-type-string' + (type === 'string' ? ' jsoneditor-selected' : ''),
|
||||
title: TYPE_TITLES.string,
|
||||
click: () => onChangeType(path, 'string')
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export function createChangeType (path, type, onChangeType) {
|
|||
}
|
||||
|
||||
export function createSort (path, order, onSort) {
|
||||
var direction = ((order == 'asc') ? 'desc': 'asc')
|
||||
const direction = ((order === 'asc') ? 'desc': 'asc')
|
||||
return {
|
||||
text: 'Sort',
|
||||
title: 'Sort the childs of this ' + TYPE_TITLES.type,
|
|
@ -1,4 +1,4 @@
|
|||
import { selectContentEditable, getSelection as getDOMSelection } from '../../utils/domUtils'
|
||||
import { selectContentEditable } from '../../utils/domUtils'
|
||||
|
||||
// singleton
|
||||
let lastInputName = null
|
||||
|
|
Loading…
Reference in New Issue