Use event emitter. Cleanup old action menu

This commit is contained in:
jos 2017-12-27 16:58:52 +01:00
parent 9e0249f550
commit 1d4a5af82e
8 changed files with 109 additions and 633 deletions

8
package-lock.json generated
View File

@ -4364,8 +4364,7 @@
"jsbn": {
"version": "0.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"json-schema": {
"version": "0.2.3",
@ -7316,6 +7315,11 @@
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"mitt": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-1.1.3.tgz",
"integrity": "sha512-mUDCnVNsAi+eD6qA0HkRkwYczbLHJ49z17BGe2PYRhZL4wpZUFZGJHU7/5tmvohoma+Hdn0Vh/oJTiPEmgSruA=="
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",

View File

@ -23,6 +23,7 @@
"brace": "0.11.0",
"javascript-natural-sort": "0.7.1",
"lodash": "4.17.4",
"mitt": "1.1.3",
"prop-types": "15.6.0",
"react-hammerjs": "1.0.1"
},

View File

@ -2,7 +2,6 @@ import { createElement as h, PureComponent } from 'react'
import PropTypes from 'prop-types'
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'
@ -32,7 +31,8 @@ export default class JSONNode extends PureComponent {
index: PropTypes.number, // in case of an array item
eson: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
events: PropTypes.object.isRequired,
emit: PropTypes.func.isRequired,
findKeyBinding: PropTypes.func.isRequired,
// options
options: PropTypes.shape({
@ -68,7 +68,7 @@ export default class JSONNode extends PureComponent {
}
}
renderJSONObject ({prop, index, eson, options, events}) {
renderJSONObject ({prop, index, eson, options, emit, findKeyBinding}) {
const props = eson[META].props
const node = h('div', {
key: 'node',
@ -92,7 +92,8 @@ export default class JSONNode extends PureComponent {
// parent: this,
prop,
eson: eson[prop],
events,
emit,
findKeyBinding,
options
}))
@ -126,7 +127,7 @@ export default class JSONNode extends PureComponent {
}, [node, floatingMenu, insertArea, childs])
}
renderJSONArray ({prop, index, eson, options, events}) {
renderJSONArray ({prop, index, eson, options, emit, findKeyBinding}) {
const node = h('div', {
key: 'node',
onKeyDown: this.handleKeyDown,
@ -150,7 +151,8 @@ export default class JSONNode extends PureComponent {
index,
eson: item,
options,
events
emit,
findKeyBinding
}))
childs = h('div', {key: 'childs', className: 'jsoneditor-list'}, items)
@ -201,7 +203,7 @@ export default class JSONNode extends PureComponent {
const floatingMenu = (eson[META].selected === SELECTED_END)
? this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
// {text: 'String', onClick: this.props.emit('changeType', {type: 'checkbox', checked: false}}),
{type: 'duplicate'},
{type: 'cut'},
{type: 'copy'},
@ -484,78 +486,15 @@ export default class JSONNode extends PureComponent {
)
}
// TODO: simplify code for the two action menus
renderActionMenu (menuType, menuState, onClose) {
if (!menuState) {
return null
}
return h(ActionMenu, {
key: 'menu',
path: this.props.eson[META].path,
events: this.props.events,
type: this.props.eson[META].type, // TODO: fix type
menuType,
open: true,
anchor: menuState.anchor,
root: menuState.root,
onRequestClose: onClose
})
}
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
})
])
}
// 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: 'floating-menu',
path: this.props.eson[META].path,
events: this.props.events,
emit: this.props.emit,
items
})
}
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
})
])
}
handleMouseOver = (event) => {
if (event.buttons === 0) { // no mouse button down, no dragging
event.stopPropagation()
@ -631,16 +570,17 @@ export default class JSONNode extends PureComponent {
const newProp = unescapeHTML(getInnerText(event.target))
if (newProp !== oldProp) {
this.props.events.onChangeProperty(parentPath, oldProp, newProp)
this.props.emit('changeProperty', {parentPath, oldProp, newProp})
}
}
/** @private */
handleChangeValue = (event) => {
const value = this.getValueFromEvent(event)
const path = this.props.eson[META].path
if (value !== this.props.eson[META].value) {
this.props.events.onChangeValue(this.props.eson[META].path, value)
this.props.emit('changeValue', {path, value})
}
}
@ -653,28 +593,28 @@ export default class JSONNode extends PureComponent {
/** @private */
handleKeyDown = (event) => {
const keyBinding = this.props.events.findKeyBinding(event)
const keyBinding = this.props.findKeyBinding(event)
if (keyBinding === 'duplicate') {
event.preventDefault()
this.props.events.onDuplicate(this.props.eson[META].path)
this.props.emit('duplicate', {path: this.props.eson[META].path})
}
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onInsert(this.props.eson[META].path, 'value')
this.props.emit('insert', {path: this.props.eson[META].path, type: 'value'})
}
if (keyBinding === 'remove') {
event.preventDefault()
this.props.events.onRemove(this.props.eson[META].path)
this.props.emit('remove', {path: this.props.eson[META].path})
}
if (keyBinding === 'expand') {
event.preventDefault()
const recurse = false
const expanded = !this.props.eson[META].expanded
this.props.events.onExpand(this.props.eson[META].path, expanded, recurse)
this.props.emit('expand', {path: this.props.eson[META].path, expanded, recurse})
}
if (keyBinding === 'actionMenu') {
@ -685,11 +625,11 @@ export default class JSONNode extends PureComponent {
/** @private */
handleKeyDownAppend = (event) => {
const keyBinding = this.props.events.findKeyBinding(event)
const keyBinding = this.props.findKeyBinding(event)
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onAppend(this.props.eson[META].path, 'value')
this.props.emit('append', {path: this.props.eson[META].path, type: 'value'})
}
if (keyBinding === 'actionMenu') {
@ -700,7 +640,7 @@ export default class JSONNode extends PureComponent {
/** @private */
handleKeyDownValue = (event) => {
const keyBinding = this.props.events.findKeyBinding(event)
const keyBinding = this.props.findKeyBinding(event)
if (keyBinding === 'openUrl') {
this.openLinkIfUrl(event)
@ -713,7 +653,7 @@ export default class JSONNode extends PureComponent {
const path = this.props.eson[META].path
const expanded = !this.props.eson[META].expanded
this.props.events.onExpand(path, expanded, recurse)
this.props.emit('expand', {path, expanded, recurse})
}
/**

View File

@ -1,4 +1,5 @@
import { createElement as h, PureComponent } from 'react'
import mitt from 'mitt'
import isEqual from 'lodash/isEqual'
import reverse from 'lodash/reverse'
import initial from 'lodash/initial'
@ -73,6 +74,7 @@ export default class TreeMode extends PureComponent {
'copy': this.handleKeyDownCopy,
'paste': this.handleKeyDownPaste,
'duplicate': this.handleKeyDownDuplicate,
'remove': this.handleKeyDownRemove,
'undo': this.handleUndo,
'redo': this.handleRedo,
'find': this.handleFocusFind,
@ -80,6 +82,23 @@ export default class TreeMode extends PureComponent {
'findPrevious': this.handlePrevious
}
this.emitter = mitt()
this.emitter.on('changeProperty', this.handleChangeProperty)
this.emitter.on('changeValue', this.handleChangeValue)
this.emitter.on('changeType', this.handleChangeType)
this.emitter.on('insert', this.handleInsert)
this.emitter.on('insertStructure', this.handleInsertStructure)
this.emitter.on('append', this.handleAppend)
this.emitter.on('duplicate', this.handleDuplicate)
this.emitter.on('remove', this.handleRemove)
this.emitter.on('sort', this.handleSort)
this.emitter.on('cut', this.handleCut)
this.emitter.on('copy', this.handleCopy)
this.emitter.on('paste', this.handlePaste)
this.emitter.on('expand', this.handleExpand)
this.emitter.on('select', this.handleSelect)
this.state = {
json,
eson,
@ -88,28 +107,7 @@ export default class TreeMode extends PureComponent {
historyIndex: 0,
// TODO: use an event emitter instead? (like with vue.js)
events: {
onChangeProperty: this.handleChangeProperty,
onChangeValue: this.handleChangeValue,
onChangeType: this.handleChangeType,
onInsert: this.handleInsert,
onInsertStructure: this.handleInsertStructure,
onAppend: this.handleAppend,
onDuplicate: this.handleDuplicate,
onRemove: this.handleRemove,
onSort: this.handleSort,
onCut: this.handleCut,
onCopy: this.handleCopy,
onPaste: this.handlePaste,
onExpand: this.handleExpand,
onSelect: this.handleSelect,
// TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events'
findKeyBinding: this.handleFindKeyBinding
},
emit: this.emitter.emit,
options: {},
@ -236,7 +234,8 @@ export default class TreeMode extends PureComponent {
(eson[META].selected ? ' jsoneditor-selected' : '')},
h(Node, {
eson,
events: this.state.events,
emit: this.emitter.emit,
findKeyBinding: this.findKeyBinding,
options: this.state.options
})
)
@ -344,19 +343,19 @@ export default class TreeMode extends PureComponent {
}
}
handleChangeValue = (path, value) => {
handleChangeValue = ({path, value}) => {
this.handlePatch(changeValue(this.state.eson, path, value))
}
handleChangeProperty = (parentPath, oldProp, newProp) => {
handleChangeProperty = ({parentPath, oldProp, newProp}) => {
this.handlePatch(changeProperty(this.state.eson, parentPath, oldProp, newProp))
}
handleChangeType = (path, type) => {
handleChangeType = ({path, type}) => {
this.handlePatch(changeType(this.state.eson, path, type))
}
handleInsert = (path, type) => {
handleInsert = ({path, type}) => {
this.handlePatch(insertBefore(this.state.eson, path, [{
type,
name: '',
@ -369,13 +368,13 @@ export default class TreeMode extends PureComponent {
this.focusToPrevious(path)
}
handleInsertStructure = (path) => {
handleInsertStructure = ({path}) => {
// TODO: implement handleInsertStructure
console.log('TODO: handleInsertStructure', path)
alert('not yet implemented...')
}
handleAppend = (parentPath, type) => {
handleAppend = ({parentPath, type}) => {
this.handlePatch(append(this.state.eson, parentPath, type))
// apply focus to new node
@ -389,19 +388,8 @@ export default class TreeMode extends PureComponent {
}
}
handleRemove = (path) => {
if (path) {
// apply focus to next sibling element if existing, else to the previous element
const fromElement = findNode(this.refs.contents, path)
const success = moveDownSibling(fromElement, 'property')
if (!success) {
moveUp(fromElement, 'property')
}
this.setState({ selection : null })
this.handlePatch(remove(path))
}
else if (this.state.selection) {
handleRemove = () => {
if (this.state.selection) {
// remove selection
// TODO: select next property? (same as when removing a path?)
const paths = pathsFromSelection(this.state.eson, this.state.selection)
@ -476,6 +464,21 @@ export default class TreeMode extends PureComponent {
}
}
handleKeyDownRemove = (event) => {
const path = this.findDataPathFromElement(event.target)
if (path) {
// apply focus to next sibling element if existing, else to the previous element
const fromElement = findNode(this.refs.contents, path)
const success = moveDownSibling(fromElement, 'property')
if (!success) {
moveUp(fromElement, 'property')
}
this.setState({ selection : null })
this.handlePatch(remove(path))
}
}
handleCut = () => {
const selection = this.state.selection
if (selection && selection.start && selection.end) {
@ -560,11 +563,11 @@ export default class TreeMode extends PureComponent {
* Set selection
* @param {Selection} selection
*/
handleSelect = (selection) => {
handleSelect = ({selection}) => {
this.setState({ selection })
}
handleExpand = (path, expanded, recurse) => {
handleExpand = ({path, expanded, recurse}) => {
if (recurse) {
this.setState({
eson: updateIn(this.state.eson, path, function (child) {

View File

@ -1,53 +0,0 @@
import { createElement as h, Component } from 'react'
import Menu from './Menu'
import {
createChangeType, createSort,
createSeparator,
createInsert, createAppend, createDuplicate, createRemove
} from './items'
export default class ActionMenu extends Component {
/**
* props = {open, anchor, root, path, type, menuType, events, onRequestClose}
*/
render () {
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
items.push(createChangeType(props.path, props.type, props.events.onChangeType))
if (props.type === 'Array' || props.type === 'Object') {
// FIXME: get current sort order (to display correct icon)
const order = 'asc'
items.push(createSort(props.path, order, props.events.onSort))
}
const hasParent = props.path.length > 0
if (hasParent) {
items.push(createSeparator())
items.push(createInsert(props.path, props.events.onInsert))
items.push(createDuplicate(props.path, props.events.onDuplicate))
items.push(createRemove(props.path, props.events.onRemove))
}
return items
}
createAppendMenuItems () {
return [
createAppend(this.props.path, this.props.events.onAppend)
]
}
}

View File

@ -1,4 +1,5 @@
import { createElement as h, PureComponent } from 'react'
import PropTypes from 'prop-types'
const MENU_CLASS_NAME = 'jsoneditor-floating-menu'
const MENU_ITEM_CLASS_NAME = 'jsoneditor-floating-menu-item'
@ -40,73 +41,73 @@ const MENU_ITEM_CLASS_NAME = 'jsoneditor-floating-menu-item'
// TODO: show quick keys in the title of the menu items
const CREATE_TYPE = {
sort: (path, events) => h('button', {
sort: (path, emit) => h('button', {
key: 'sort',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onSort(path),
onClick: () => emit('sort', {path}),
title: 'Sort'
}, 'Sort'),
duplicate: (path, events) => h('button', {
duplicate: (path, emit) => h('button', {
key: 'duplicate',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onDuplicate(),
onClick: () => emit('duplicate'),
title: 'Duplicate'
}, 'Duplicate'),
cut: (path, events) => h('button', {
cut: (path, emit) => h('button', {
key: 'cut',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onCut(),
onClick: () => emit('cut'),
title: 'Cut'
}, 'Cut'),
copy: (path, events) => h('button', {
copy: (path, emit) => h('button', {
key: 'copy',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onCopy(),
onClick: () => emit('copy'),
title: 'Copy'
}, 'Copy'),
paste: (path, events) => h('button', {
paste: (path, emit) => h('button', {
key: 'paste',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onPaste(),
onClick: () => emit('paste'),
title: 'Paste'
}, 'Paste'),
remove: (path, events) => h('button', {
remove: (path, emit) => h('button', {
key: 'remove',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onRemove(),
onClick: () => emit('remove'),
title: 'Remove'
}, 'Remove'),
insertStructure: (path, events) => h('button', {
insertStructure: (path, emit) => h('button', {
key: 'insertStructure',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onInsertStructure(),
onClick: () => emit('insertStructure', {path}),
title: 'Insert a new object with the same data structure as the item above'
}, 'Insert structure'),
insertValue: (path, events) => h('button', {
insertValue: (path, emit) => h('button', {
key: 'insertValue',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onInsert(path, 'value'),
onClick: () => emit('insert', {path, type: 'value'}),
title: 'Insert value'
}, 'Insert value'),
insertObject: (path, events) => h('button', {
insertObject: (path, emit) => h('button', {
key: 'insertObject',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onInsert(path, 'Object'),
onClick: () => emit('insert', {path, type: 'Object'}),
title: 'Insert Object'
}, 'Insert Object'),
insertArray: (path, events) => h('button', {
insertArray: (path, emit) => h('button', {
key: 'insertArray',
className: MENU_ITEM_CLASS_NAME,
onClick: () => events.onInsert(path, 'Array'),
onClick: () => emit('insert', {path, type: 'Array'}),
title: 'Insert Array'
}, 'Insert Array'),
@ -125,12 +126,23 @@ export default class FloatingMenu extends PureComponent {
// })
// }
static propTypes = {
items: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.shape({
type: PropTypes.string.isRequired
})
]).isRequired
).isRequired
}
render () {
const items = 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)
return createType(this.props.path, this.props.emit)
}
else {
throw new Error('Unknown type of menu item for floating menu: ' + JSON.stringify(item))
@ -138,7 +150,6 @@ export default class FloatingMenu extends PureComponent {
})
return h('div', {
// ref: 'root',
className: MENU_CLASS_NAME,
onMouseDown: this.handleTouchStart,
onTouchStart: this.handleTouchStart,

View File

@ -1,261 +0,0 @@
import { createElement as h, Component } from 'react'
import { keyComboFromEvent } from '../../utils/keyBindings'
import { findParentWithClassName } from '../../utils/domUtils'
export let CONTEXT_MENU_HEIGHT = 240
const MENU_CLASS_NAME = 'jsoneditor-actionmenu'
const MENU_ITEM_CLASS_NAME = 'jsoneditor-menu-item'
export default class Menu extends Component {
/**
* @param {{open: boolean, items: Array, anchor, root, onRequestClose: function}} props
*/
constructor(props) {
super(props)
this.state = {
expanded: null, // menu index of expanded menu item
expanding: null, // menu index of expanding menu item
collapsing: null // menu index of collapsing menu item
}
}
render () {
if (!this.props.open) {
return null
}
// determine orientation
const anchorRect = this.props.anchor.getBoundingClientRect()
const rootRect = this.props.root.getBoundingClientRect()
const orientation = (rootRect.bottom - anchorRect.bottom < CONTEXT_MENU_HEIGHT &&
anchorRect.top - rootRect.top > CONTEXT_MENU_HEIGHT)
? 'top'
: 'bottom'
// TODO: create a non-visible button to set the focus to the menu
const className = MENU_CLASS_NAME + ' ' +
((orientation === 'top') ? 'jsoneditor-actionmenu-top' : 'jsoneditor-actionmenu-bottom')
return h('div', {
className: className,
ref: 'menu',
onKeyDown: this.handleKeyDown
},
this.props.items.map(this.renderMenuItem)
)
}
renderMenuItem = (item, index) => {
if (item.type === 'separator') {
return h('div', {key: index, className: 'jsoneditor-menu-separator'})
}
if (item.click && item.submenu) {
// FIXME: don't create functions in the render function
const onClick = (event) => {
item.click()
this.props.onRequestClose()
}
// two buttons: direct click and a small button to expand the submenu
return h('div', {key: index, className: 'jsoneditor-menu-item'}, [
h('button', {key: 'default', className: 'jsoneditor-menu-button jsoneditor-menu-default ' + item.className, title: item.title, onClick }, [
h('span', {key: 'icon', className: 'jsoneditor-icon'}),
h('span', {key: 'text', className: 'jsoneditor-text'}, item.text)
]),
h('button', {key: 'expand', className: 'jsoneditor-menu-button jsoneditor-menu-expand', onClick: this.createExpandHandler(index) },
h('span', {className: 'jsoneditor-icon jsoneditor-icon-expand'})
),
this.renderSubMenu(item.submenu, index)
])
}
else if (item.submenu) {
// button expands the submenu
return h('div', {key: index, className: 'jsoneditor-menu-item'}, [
h('button', {key: 'default', className: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick: this.createExpandHandler(index) }, [
h('span', {key: 'icon', className: 'jsoneditor-icon'}),
h('span', {key: 'text', className: 'jsoneditor-text'}, item.text),
h('span', {key: 'expand', className: 'jsoneditor-icon jsoneditor-icon-expand'}),
]),
this.renderSubMenu(item.submenu, index)
])
}
else {
// FIXME: don't create functions in the render function
const onClick = (event) => {
item.click()
this.props.onRequestClose()
}
// just a button (no submenu)
return h('div', {key: index, className: 'jsoneditor-menu-item'},
h('button', {className: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick }, [
h('span', {key: 'icon', className: 'jsoneditor-icon'}),
h('span', {key: 'text', className: 'jsoneditor-text'}, item.text)
]),
)
}
}
/**
* @param {Array} submenu
* @param {number} index
*/
renderSubMenu (submenu, index) {
const expanded = this.state.expanded === index
const collapsing = this.state.collapsing === index
const contents = submenu.map((item, index) => {
// FIXME: don't create functions in the render function
const onClick = () => {
item.click()
this.props.onRequestClose()
}
return h('div', {key: index, className: 'jsoneditor-menu-item'},
h('button', {className: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick }, [
h('span', {key: 'icon', className: 'jsoneditor-icon'}),
h('span', {key: 'text', className: 'jsoneditor-text'}, item.text)
]),
)
})
const className = 'jsoneditor-submenu ' +
(expanded ? ' jsoneditor-expanded' : '') +
(collapsing ? ' jsoneditor-collapsing' : '')
return h('div', {key: 'submenu', className: className}, contents)
}
createExpandHandler (index) {
return (event) => {
event.stopPropagation()
const prev = this.state.expanded
this.setState({
expanded: (prev === index) ? null : index,
collapsing: prev
})
// timeout after unit is collapsed
setTimeout(() => {
if (prev === this.state.collapsing) {
this.setState({
collapsing: null
})
}
}, 300)
}
}
componentDidMount () {
this.updateRequestCloseListener()
if (this.props.open) {
this.focusToFirstEntry ()
}
}
componentDidUpdate (prevProps, prevState) {
this.updateRequestCloseListener()
if (this.props.open && !prevProps.open) {
this.focusToFirstEntry ()
}
}
componentWillUnmount () {
// remove on next tick, since a listener can be created on next tick too
setTimeout(() => this.removeRequestCloseListener())
}
focusToFirstEntry () {
if (this.refs.menu) {
const firstButton = this.refs.menu.querySelector('button')
if (firstButton) {
firstButton.focus()
}
}
}
updateRequestCloseListener () {
if (this.props.open) {
this.addRequestCloseListener()
}
else {
this.removeRequestCloseListener()
}
}
addRequestCloseListener () {
// 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.handleRequestClose) {
this.handleRequestClose = (event) => {
this.props.onRequestClose()
}
window.addEventListener('click', this.handleRequestClose)
}
})
}
removeRequestCloseListener () {
if (this.handleRequestClose) {
window.removeEventListener('click', this.handleRequestClose)
this.handleRequestClose = null
}
}
handleKeyDown = (event) => {
const combo = keyComboFromEvent (event)
if (combo === 'Up') {
event.preventDefault()
const items = Menu.getItems (event.target)
const index = items.findIndex(item => item === event.target.parentNode)
const prev = items[index - 1]
if (prev) {
prev.querySelector('button').focus()
}
}
if (combo === 'Down') {
event.preventDefault()
const items = Menu.getItems (event.target)
const index = items.findIndex(item => item === event.target.parentNode)
const next = items[index + 1]
if (next) {
next.querySelector('button').focus()
}
}
if (combo === 'Left') {
const left = event.target.previousSibling
if (left && left.nodeName === 'BUTTON') {
left.focus()
}
}
if (combo === 'Right') {
const right = event.target.nextSibling
if (right && right.nodeName === 'BUTTON') {
right.focus()
}
}
}
static getItems (element) {
const menu = findParentWithClassName(element, MENU_CLASS_NAME)
return Array.from(menu.querySelectorAll('.' + MENU_ITEM_CLASS_NAME))
.filter(item => window.getComputedStyle(item).visibility === 'visible')
}
handleRequestClose = null
}

View File

@ -1,169 +0,0 @@
// This file contains functions to create menu entries
// TYPE_TITLES with explanation for the different types
const TYPE_TITLES = {
'value': 'Item type "value". ' +
'The item type is automatically determined from the value ' +
'and can be a string, number, boolean, or null.',
'Object': 'Item type "object". ' +
'An object contains an unordered set of key/value pairs.',
'Array': 'Item type "array". ' +
'An array contains an ordered collection of values.',
'string': 'Item type "string". ' +
'Item type is not determined from the value, ' +
'but always returned as string.'
}
export function createChangeType (path, type, onChangeType) {
return {
text: 'Type',
title: 'Change the type of this field',
className: 'jsoneditor-type-' + type,
submenu: [
{
text: 'Value',
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' : ''),
title: TYPE_TITLES.array,
click: () => onChangeType(path, 'Array')
},
{
text: 'Object',
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' : ''),
title: TYPE_TITLES.string,
click: () => onChangeType(path, 'string')
}
]
}
}
export function createSort (path, order, onSort) {
const direction = ((order === 'asc') ? 'desc': 'asc')
return {
text: 'Sort',
title: 'Sort the childs of this ' + TYPE_TITLES.type,
className: 'jsoneditor-sort-' + direction,
click: () => onSort(path),
submenu: [
{
text: 'Ascending',
className: 'jsoneditor-sort-asc',
title: 'Sort the childs of this ' + TYPE_TITLES.type + ' in ascending order',
click: () => onSort(path, 'asc')
},
{
text: 'Descending',
className: 'jsoneditor-sort-desc',
title: 'Sort the childs of this ' + TYPE_TITLES.type +' in descending order',
click: () => onSort(path, 'desc')
}
]
}
}
export function createInsert (path, onInsert) {
return {
text: 'Insert',
title: 'Insert a new item with type \'value\' after this item (Ctrl+Ins)',
submenuTitle: 'Select the type of the item to be inserted',
className: 'jsoneditor-insert',
click: () => onInsert(path, 'value'),
submenu: [
{
text: 'Value',
className: 'jsoneditor-type-value',
title: TYPE_TITLES.value,
click: () => onInsert(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-Array',
title: TYPE_TITLES.array,
click: () => onInsert(path, 'Array')
},
{
text: 'Object',
className: 'jsoneditor-type-Object',
title: TYPE_TITLES.object,
click: () => onInsert(path, 'Object')
},
{
text: 'String',
className: 'jsoneditor-type-string',
title: TYPE_TITLES.string,
click: () => onInsert(path, 'string')
}
]
}
}
export function createAppend (path, onAppend) {
return {
text: 'Insert',
title: 'Insert a new item with type \'value\' after this item (Ctrl+Ins)',
submenuTitle: 'Select the type of the item to be inserted',
className: 'jsoneditor-insert',
click: () => onAppend(path, 'value'),
submenu: [
{
text: 'Value',
className: 'jsoneditor-type-value',
title: TYPE_TITLES.value,
click: () => onAppend(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-Array',
title: TYPE_TITLES.array,
click: () => onAppend(path, 'Array')
},
{
text: 'Object',
className: 'jsoneditor-type-Object',
title: TYPE_TITLES.object,
click: () => onAppend(path, 'Object')
},
{
text: 'String',
className: 'jsoneditor-type-string',
title: TYPE_TITLES.string,
click: () => onAppend(path, 'string')
}
]
}
}
export function createDuplicate (path, onDuplicate) {
return {
text: 'Duplicate',
title: 'Duplicate this item (Ctrl+D)',
className: 'jsoneditor-duplicate',
click: () => onDuplicate(path)
}
}
export function createRemove (path, onRemove) {
return {
text: 'Remove',
title: 'Remove this item (Ctrl+Del)',
className: 'jsoneditor-remove',
click: () => onRemove(path)
}
}
export function createSeparator () {
return {
'type': 'separator'
}
}