From 1d4a5af82ea204b8c7f2fee53584490bd6849627 Mon Sep 17 00:00:00 2001 From: jos Date: Wed, 27 Dec 2017 16:58:52 +0100 Subject: [PATCH] Use event emitter. Cleanup old action menu --- package-lock.json | 8 +- package.json | 1 + src/jsoneditor/components/JSONNode.js | 104 ++----- src/jsoneditor/components/TreeMode.js | 91 +++--- src/jsoneditor/components/menu/ActionMenu.js | 53 ---- .../components/menu/FloatingMenu.js | 55 ++-- src/jsoneditor/components/menu/Menu.js | 261 ------------------ src/jsoneditor/components/menu/items.js | 169 ------------ 8 files changed, 109 insertions(+), 633 deletions(-) delete mode 100644 src/jsoneditor/components/menu/ActionMenu.js delete mode 100644 src/jsoneditor/components/menu/Menu.js delete mode 100644 src/jsoneditor/components/menu/items.js diff --git a/package-lock.json b/package-lock.json index 0511695..216074b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c03eeae..9808756 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/jsoneditor/components/JSONNode.js b/src/jsoneditor/components/JSONNode.js index 811e242..207ccf8 100644 --- a/src/jsoneditor/components/JSONNode.js +++ b/src/jsoneditor/components/JSONNode.js @@ -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}) } /** diff --git a/src/jsoneditor/components/TreeMode.js b/src/jsoneditor/components/TreeMode.js index 6064ef9..123794b 100644 --- a/src/jsoneditor/components/TreeMode.js +++ b/src/jsoneditor/components/TreeMode.js @@ -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) { diff --git a/src/jsoneditor/components/menu/ActionMenu.js b/src/jsoneditor/components/menu/ActionMenu.js deleted file mode 100644 index d81b0cc..0000000 --- a/src/jsoneditor/components/menu/ActionMenu.js +++ /dev/null @@ -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) - ] - } -} diff --git a/src/jsoneditor/components/menu/FloatingMenu.js b/src/jsoneditor/components/menu/FloatingMenu.js index 199e2ed..e8d47bf 100644 --- a/src/jsoneditor/components/menu/FloatingMenu.js +++ b/src/jsoneditor/components/menu/FloatingMenu.js @@ -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, diff --git a/src/jsoneditor/components/menu/Menu.js b/src/jsoneditor/components/menu/Menu.js deleted file mode 100644 index 7456e4f..0000000 --- a/src/jsoneditor/components/menu/Menu.js +++ /dev/null @@ -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 -} diff --git a/src/jsoneditor/components/menu/items.js b/src/jsoneditor/components/menu/items.js deleted file mode 100644 index a3c5631..0000000 --- a/src/jsoneditor/components/menu/items.js +++ /dev/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' - } -}