Use event emitter. Cleanup old action menu
This commit is contained in:
parent
9e0249f550
commit
1d4a5af82e
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue